Search code examples
pythondjangodjango-modelsdjango-viewsdjango-templates

Django - Autocomplete Search Throws Error For Logged Out Users?


I’m making some updates to the autocomplete portion of the search functionality in my application and for some reason i’m getting an error for logged out users that says TypeError: Field 'id' expected a number but got <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x1088a5e20>>.

This is only happening for logged out users. When users are logged in the autocomplete works as built so I know I’m missing something but I just don’t know what.

What needs to get changed to fix this issue?

I’m building a gaming application where users have access to play certain games based on their rank in our application. So when a user is logged in I’d like the autocomplete functionality to reflect the games they have unlocked. So let’s say if a user has a rank of 100 then all the games with a game_rank of 100 and below will be displayed. For logged out users I would like all games to be shown.

Made some notes in my views.py code from what I tested and added the JavaScript to the search functionality just in case.

Below is my code.

Any help is gladly appreciated!

All My Best!

models.py

class Game_Info(models.Model):
    id = models.IntegerField(primary_key=True, unique=True, blank=True, editable=False)
    game_title = models.CharField(max_length=100, null=True)
    game_rank = models.IntegerField(default=1)
    game_image = models.ImageField(default='default.png', upload_to='game_covers', null=True, blank=True)


class User_Info(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    image = models.ImageField(default='/profile_pics/default.png', upload_to='profile_pics', null=True, blank=True)
    user = models.OneToOneField(settings.AUTH_USER_MODEL,blank=True, null=True, on_delete=models.CASCADE)
    rank = models.IntegerField(default=1)   

views.py

def search_results_view(request):
    if request.headers.get('x-requested-with') == 'XMLHttpRequest': 
        res = None
        game = request.POST.get('game')
        print(game)

        ## This works for both logged in and logged out users but inlcudes all games. Would like to have this for logged out users. Commenting this out to test below. 
        # qs = Game_Info.objects.filter(game_title__icontains=game).order_by('game_title')[:4]

        ## This only works for when users are logged in. 
        user_profile_game_obj = User_Info.objects.get(user=request.user)
        user_profile_rank = int(user_profile_game_obj.rank)
        qs = Game_Info.objects.filter(game_title__icontains=game, game_rank__lte=user_profile_rank).order_by('game_title')[:4]

        if len(qs) > 0 and len(game) > 0:
            data = []
            for pos in qs:
                item ={
                    'pk': pos.pk,
                    'name': pos.game_title,
                    'game_provider': pos.game_provider,
                    'image': str(pos.game_image.url),
                    'url': reverse('detail', args=[pos.pk]),
                }
                data.append(item)
            res = data
        else:
            res = 'No games found...'

        return JsonResponse({'data': res})
    return JsonResponse({})

custom.js

// Live Search Functionalty 
$(function () {
    const url = window.location.href
    const searchForm = document.getElementById("search-form");
    const searchInput = document.getElementById("search_input_field");
    const resultsBox = document.getElementById("search-results");
    const csrf = document.getElementsByName('csrfmiddlewaretoken')[0].value

    const sendSearchData = (game) =>{
        $.ajax ({
            type: 'POST',
            url: '/games/',

            data: {
                'csrfmiddlewaretoken': csrf,
                'game' : game,
            },
            success: (res)=> {
                console.log(res.data)
                const data = res.data
                if (Array.isArray(data)) {
                    resultsBox.innerHTML = `<div class="search_heading"><h1>Recommended Games</h1></div>`
                    data.forEach(game=> {
                        resultsBox.innerHTML += `                    
                            <a href="${game.url}" class="item" >                          
                                <div class="row">
                                    <div class="search-cover-container">
                                        <img src="${game.image}" class="game-img">
                                    </div>
                                    <div class="search-title-container">
                                        <p>${game.name}</p>
                                        <span class="publisher_title">${game.game_provider}</span>
                                    </div>
                                    <div class="search-icon-container">
                                        <i class="material-icons">trending_up</i>
                                    </div>
                                </div>
                            </a>
                        `
                    })
                } else {
                   if (searchInput.value.length > 0) {
                    resultsBox.innerHTML = `<h2>${data}</h2>`
                   } else {
                    resultsBox.classList.add('not_visible')
                   }
                }
            },
            error: (err)=> {
                console.log(err)
            }
        })
    }

    searchInput.addEventListener('keyup', e=> {
        sendSearchData(e.target.value)
    })

});

base.html

            <form method="POST" autocomplete="off" id="search-form" action="{% url 'search_results' %}">
                {% csrf_token %}
                <div class="input-group">
                    <input id="search_input_field" type="text" name="q" autocomplete="off" class="form-control gs-search-bar" placeholder="Search Games..." value="">
                    <div id="search-results" class="results-container not_visible"></div>
                    <span class="search-clear">x</span>
                    <button id="search-btn" type="submit" class="btn btn-primary search-button" disabled>
                        <span class="input-group-addon">
                            <i class="zmdi zmdi-search"></i>
                        </span> 
                    </button>
                </div>
            </form>

Solution

  • Based on the query you're showing, logged out users should not even be able to navigate to this view. I'd decorate the view with @login_required.

    The problem you're seeing is on this line:

    user_profile_game_obj = User_Info.objects.get(user=request.user)
    

    When a user isn't logged in, request.user is an AnonymousUser instance, which doesn't have an ID. When that query is made, the underlying framework grabs the ID of the passed instance, which in this case isn't resolvable. You can't associate an AnonymousUser with a record, they don't have an identifier, and cannot be used in queries.

    Alternatively, you can change the behavior of the view by using user.is_authenticated()


    Edit: Given the requirements, I'd refactor your view to the following.

    from http import HTTPStatus
    
    
    def search_results_view(request):
        if not request.headers.get("x-requested-with") == "XMLHttpRequest":
            # Give an error response when something unexpected happens
            return JsonResponse(
                {"error": "Invalid request"}, status_code=HTTPStatus.BAD_REQUEST
            )
    
        # Create the primary queryset, then additionally filter it if the user is logged in
        game = request.POST.get("game")
        games = Game_Info.objects.filter(game_title__icontains=game).order_by("game_title")
        if request.user.is_authenticated():
            user_info = User_Info.objects.get(user=request.user)
            games.filter(game_rank__lte=user_info.rank)
    
        # Return the JSON response with a list comprehension, limiting the queryset to 4 items
        return JsonResponse(
            {
                "data": [
                    {
                        "pk": game.pk,
                        "name": game.game_title,
                        "game_provider": game.game_provider,
                        "image": game.game_image.url,
                        "url": reverse("detail", args=[game.pk]),
                    }
                    for game in games.limit(4)
                ]
            }
        )
    

    With that, I would refactor your frontend code to handle receiving an empty response and an error response.

    Note: There's a lot more I would do here.

    • You aren't actually using Django's templating system, so you'd likely be much better served by writing a DRF application.
    • There are issues with your naming conventions. According to PEP8 guidelines, class names should be in UpperCamelCase, so your models should be GameInfo and UserInfo.
    • I would use a ListView class for this instead of a method view, customizing it to return a JsonResponse. Not totally necessary, but why do extra work when ListView is designed for exactly this purpose

    I could go on and probably add 3-4 more things, but I'm pretty opinionated so I won't.