Selecting filtered related objects in django - python

I've got two models like this (simplified):
class Post(models.Model):
content = models.TextField('content')
#...
class Vote(models.Model):
user = ForeignKey(User)
status = models.TextField(choices = some_choices, default = 'likes')
post = models.ForeignKey(Post)
#...
What i want to do is to select (using one query) posts using some filter with one particular (let's say current) user's votes on this posts (it's okay if he didn't vote for it), so I can then output all posts and the user can see which ones he liked, which ones he didn't and which ones he didn't vote at all.
select_related for Vote model will not help here, because related objects cannot be filtered, so I guess I should do something with extra, but I cannot figure out what arguments should I pass.
So I guess, it should be something like:
Post.objects.filter(content__contains="test").extra(tables="app_vote", where={'my_vote_status': 'something here perhaps?'})
Could you please help me to understand how make a query like this?
UPD: schacki provided a good solution, the only problem is that I want to access votes by different users from the template, somtehing like Post.vote_by_me and Post.vote_by_this_user or Post.vote_by_top_user

Well, If you proper want answers, sometimes you should look for them yourself :)
Here's how I've solved my problem:
posts = Post.objects.filter(content__contains="test"
).extra(select={
#extra selects vote status here for current user
"my_vote_status":"""SELECT status FROM blog_vote as vt
WHERE vt.user_id = %s
AND vt.post_id = blog_posts.id
""" % (request.user.pk) #
}, tables=['blog_vote'])
UPD: probably would work without tables argument

If I understand your requirements correctly, you will need two objects to pass into context. Try like this, where me and other_user must be valid user objects.
posts.vote_by_me=Post.objects.filter(content__contains="test",vote_set__status="something here perhaps?",vote_set__user=me)
posts.vote_by_other_user=Post.objects.filter(content__contains="test",vote_set__status="something here perhaps?",vote_set__user=other_user)

It's very difficult to understand what you want, but here is another attempt. First, get your posts:
posts = Post.objects.filter(whatever)
Now you want all the votes by a group of users on the posts, correct?
votes = Vote.objects.filter(post__in=posts, user__in=users)
Now all you have to do is associate the votes to the posts based on, say, the user id:
votes_by_user_by_post = defaultdict(lambda: defaultdict(list))
for v in votes:
votes_by_user_by_post[post.id][v.user_id].append(v)
for post in posts:
post.votes_by_user = votes_by_user_by_post[post.id]
Performance-wise, it's fine to do this in two queries plus some scripting. They aren't complicated queries and the scripting part is just two for-loops.

Related

Django Count Aggregation with Filter on Related Field Not Working

Suppose I've got 2 models Coupon and UserRedemption (and of course a user model). They look like:
Coupon(models.Model):
offer = models.TextField() # not important to this task
limited_use = models.BooleanField(default=False)
max_uses = models.IntegerField(default=0)
UserRedemption(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
coupon = models.ForeignKey(Coupon)
so pretty basic. There are coupons in my app and each coupon has a list of times it's been redeemed by various users. Some coupons are "limited use", ie they can only be used a certain number of times by a given user.
My goal: return a list of coupons excluding the coupons which have been used the maximum number of times by the user making the request.
I'm attempting to do this by annotating the coupons with the number of times it's been redeemed by the current user (n_user_redemptions) and filtering the list for coupons where limited_use = False OR max_uses > n_user_redemptions. Here is my attempted method on the Coupon class:
#classmethod
def get_available_user_coupons(cls, user):
coupon_redemption_count = Count('userredemption', filter=Q(userredemption__user=user))
return cls.objects.annotate(n_user_redemptions=coupon_redemption_count)
.filter(Q(limited_use=False) | Q(max_uses__gt=F('n_user_redemptions')))
However, it's not filtering properly for the user passed in from the request. It always sets n_user_redemptions to the count of all redemptions of that coupon by all users, not just the ones for the given user. I've tested that it returns coupons correctly if limited_use is set to False and if that coupon has been redeemed less times total (by all users) than the max_uses. And I have confirmed that I am passing in the user correctly.
I'm not sure what I'm missing here. I've gone through the django aggregation docs pretty extensively and it seems like this should work. Any help would be appreciated.
Just a further update:
I inspected the generated SQL and notice that the queries are identical whether I set:
coupon_redemption_count = Count('userredemption', filter=Q(userredemption__user=user))
OR
coupon_redemption_count = Count('userredemption')
Not sure what the behavior of the filter argument is supposed to be in Count.
The relevant SQL:
SELECT "coupon_coupon"."id", "coupon_coupon"."offer", "coupon_coupon"."limited_use", "coupon_coupon"."max_uses",
COUNT("userredemption_userredemption"."id") AS "n_user_redemptions"
FROM "coupon_coupon"
LEFT OUTER JOIN "userredemption_userredemption"
ON ("coupon_coupon"."id" = "userredemption_userredemption"."coupon_id")
GROUP BY "coupon_coupon"."id"
ORDER BY "coupon_coupon"."id" DESC
No mention of the user anywhere?
Can't you just do it like so:
def return_list_of_coupons(user):
list_of_coupons = [coupon for coupon in Coupon.objects.all()
if UserRedemption.objects.filter(user=user.pk, coupon=coupon.pk).count() < coupon.max_uses]
return list_of_coupons
It's simpler and a bit crude, but it does the job just fine.
So I never got an answer or figured out why the aggregate filter wasn’t working but i did figure out a less pretty workaround using a sub query which at least isn’t raw sql which would present a maintenance nightmare and is not forcing me to evaluate multiple queries. I’d prefer to use the aggregate because that uses joins under the hood rather than a sub query which would be the more efficient approach but this will service for now and I will log with the Django team to see what’s up with the aggregate filter.
Here is the work around:
coupon_redemption_count = Count(Subquery(UserRedemption.objects.filter(user=user, coupon=OuterRef(‘pk’)).values_list(‘id’, flat=True)))
Then everything else is the same.
Leaving this question open in case anyone finds it and can tell me why the aggregate filter isn’t working.

django- how would I create a sort for a query to put one specific element first, then remaining by some field?

Specifically, I am looking to get a query of users. My User model has a first_name and last_name field. What I need to do is order the request.user at the top of the results, and the remaining users in alphabetical order by last_name, first_name. The last part is easy:
q = User.objects.all().order_by('last_name', 'first_name')
However I am not sure how to ensure that the request.user is the first result in the query. This is all being done for a django rest framework view, and thus (I believe) I need to have it done through a query which is passed on to the serializer.
First, it might be better design to not do this. Have some other endpoint that returns your own user object if you need it, and in the list view treat yourself no differently. But if you really neeed to.
You probably could use an annotation.
User.objects.annotate(is_me=Case(
When(pk=request.user.pk, then=Value(True)),
When(pk__ne=request.user.pk, then=Value(False)),
output_field=BooleanField())
).order_by('-is_me')
If you don't really need the result to be a queryset you can try the following:
import itertools
me = request.user
others = User.objects.exclude(id=me.pk)
users_list = itertools.chain([me], others)

Django 'likes' - ManyToManyField vs new model

I'm implementing likes on profiles for my website and I'm not sure which would be the best practice, a ManyToManyField like so:
class MyUser(AbstractBaseUser):
...
likes = models.ManyToManyField('self', symmetrical = False, null = True)
...
or just creating a class Like, like so:
class Like(models.Model):
liker = models.ForeignKey(MyUser, related_name='liker')
liked = models.ForeignKey(MyUser, related_name='liked')
Is one of them a better choice than the other? If so, why?
thanks
The first option should be preffered. If you need some additional fields to describe the likes, you can still use through="Likes" in your ManyToManyField and define the model Likes.
Manipulating the data entries would be also somewhat more pythonic:
# returns an object collection
likes_for_me = MyUser.objects.filter(pk=1).likes
instead of:
me = MyUser.objects.filter(pk=1)
likes_for_me = Like.objects.filter(liked=me)
The second option is basically what is done internally: a new table is created, which is used to create the links between the entities.
For the first option, you let django do the job for you.
The choice is certainly more about how you want to do the requests. On the second options, you would have to query the Like models that match you model, while on the first one, you only have to request the MyUser, from which you can access the connections.
Second option is more flexible and extensible. For example, you'll probably want to track when like was created (just add Like.date_created field). Also you'll probably want to send notification to content author when content was liked. But at first like only (add Like.cancelled boolead field and wrap it with some logic...).
So I'll go with separate model.
I think the one you choose totally depends on the one you find easier to implement or better. I tend to always use the first approach, as it is more straightforward and logical, at least to me. I also disagree with Igor on that it's not flexible and extensible, you can also initiate notifications when it happens. If you are going to use the Django rest framework, then I totally suggest using the first method, as the second could be a pain.
class Post(models.Model):
like = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='post_like')
Then in your view, you just do this.
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def like(request, id):
signed_in = request.user
post = Post.objects.get(id=id)
if signed_in and post:
post.like.add(signed_in)
# For unlike, remove instead of add
return Response("Successful")
else:
return Response("Unsuccessful", status.HTTP_404_NOT_FOUND)
Then you can use the response however you like on the front end.

Unsure about what type of Django form I should use

I have an Author model with 200k instances in a MySQL database. I want to have the user search for an Author's unique ID (which is a string) and then select an ID which will then produce a table and small graphic about that author.
Do I want to use a charfield model form? Also, is there a built in search function?
I don't think Django has a builtin function for searching. You will have to use one of its extensions for this purpose (django-search or haystack).
They may seem too complicated for your case so I would go with simplier solution (and I would give up using form):
from django.views.generic import ListView
from django.db.models import Q
from .models import Author
def SearchAuthorView(ListView):
def get_queryset(self):
name = self.request.GET['author_name']
name_tokens = name.split(' ')
matched_authors = []
authors = Author.objects.all()
for author in authors:
for name_token in author.name.split(' '):
if name_token in name_tokens:
matched_authors.append(author)
break
return matched_authors
With 200k you may hit performance problems so if you do, you should use an optimized, raw MySql query.
You may also like to order your results somehow. For example give lastname's match a bigger priority than firstname's match.
Honestly I don't understand the question. You have a table called Author with 200k instances and you want to have possibility to find one of them. This can be done by simply function in views.py
def search_author(request, author_id):
author = Author.objects.get(id=author_id)
return render_to_response('your/path/to/template', {'author': author}, RequestContext(request))
Then in your template you just simply display the informations:
<div class="author">
<p>{{author.name}}</p>
<p>{{author.image}}</p>
</div>
Of course if your models.py looks like this:
class Author(models.Model):
name = models.CharField(max_number=100)
image ....

Fetching data from parent table in Django in database hierarchy

Following this answer, I tried to split my SQL Story table into parent/children - with the children holding the specific user data, the parent more generic data. Now I've run into a problem that betrays my lack of experience in Django. My user page attempts to show a list of all the stories that a user has written. Before, when my user page was only pulling data from the story table, it worked fine. Now I need to pull data from two tables with linked info and I just can't work out how to do it.
Here's my user_page view before attempts to pull data from the parent story table too:
def user_page(request, username):
user = get_object_or_404(User, username=username)
userstories = user.userstory_set.order_by('-id')
variables = RequestContext(request, {
'username': username,
'userstories': userstories,
'show_tags': True
})
return render_to_response('user_page.html', variables)
Here is my models.py:
class story(models.Model):
title = models.CharField(max_length=400)
thetext = models.TextField()
class userstory(models.Model):
main = models.ForeignKey(story)
date = models.DateTimeField()
user = models.ForeignKey(User)
I don't really know where to start in terms of looking up the appropriate information in the parent table too and assinging it to a variable. What I need to do is follow the 'main' Key of the userstory table into the story table and assign the story table as a variable. But I just can't see how to implement that in the definition.
EDIT: I've tried story = userstory.objects.get(user=user) but I get 'userstory matching query does not exist.'
Reading through your previous question that you linked to, I've discovered where the confusion lies. I was under the impression that a Story may have many UserStorys associated with it. Note that I'm using Capital for the class name, which is common Python practise. I've made this assumption because your model structure is allowing this to happen with the use of a Foreign Key in your UserStory model. Your model structure should look like this instead:
class Story(models.Model):
title = models.CharField(max_length=400)
thetext = models.TextField()
class UserStory(models.Model):
story = models.OneToOneField(Story) # renamed field to story as convention suggests
date = models.DateTimeField()
user = models.ForeignKey(User)
class ClassicStory(models.Model)
story = models.OneToOneField(Story)
date = models.DateTimeField()
author = models.CharField(max_length=200)
See the use of OneToOne relationships here. A OneToOne field denotes a 1-to-1 relationship, meaning that a Story has one, and only one, UserStory. This also means that a UserStory is related to exactly one Story. This is the "parent-child" relationship, with the extra constraint that a parent has only a single child. Your use of a ForeignKey before means that a Story has multiple UserStories associated with it, which is wrong for your use case.
Now your queries (and attribute accessors) will behave like you expected.
# get all of the users UserStories:
user = request.user
stories = UserStory.objects.filter(user=user).select_related('story')
# print all of the stories:
for s in stories:
print s.story.title
print s.story.thetext
Note that select_related will create a SQL join, so you're not executing another query each time you print out the story text. Read up on this, it is very very very important!
Your previous question mentions that you have another table, ClassicStories. It should also have a OneToOneField, just like the UserStories. Using OneToOne fields in this way makes it very difficult to iterate over the Story model, as it may be a "ClassicStory" but it might be a "UserStory" instead:
# iterate over ALL stories
allstories = Story.objects.all()
for s in allstories:
print s.title
print s.thetext
print s.userstory # this might error!
print s.classicstory # this might error!
See the issue? You don't know what kind of story it is. You need to check the type of story it is before accessing the fields in the sub-table. There are projects that help manage this kind of inheritance around, an example is django-model-utils InheritanceManager, but that's a little advanved. If you never need to iterate over the Story model and access it's sub tables, you don't need to worry though. As long as you only access Story from ClassicStories or UserStories, you will be fine.

Categories