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>
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.
UpperCamelCase
, so your models should be GameInfo
and UserInfo
.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 purposeI could go on and probably add 3-4 more things, but I'm pretty opinionated so I won't.