Django get related object's QuerySet inside annotate - python

My Models
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
#Unnecessary
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_comments")
post = models.ForeignKey(Post, on_delete=models.CASCADE,related_name="post_comments")
liked_users = models.ManyToManyField(User, blank=True, related_name="liked_comments")
disliked_users = models.ManyToManyField(User, blank=True related_name="disliked_comments")
#Unnecessary
What I Have
Normally, i use this code to make my comment objects have the attribute "voting" and order them by that attribute.
comments = models.Comment.objects.filter(post=post).annotate(voting=Count("liked_users")-Count("disliked_users")).order_by("-voting")
But when I want to query posts, I can't reach their comments:
What I Want
I want my QuerySet "posts" to have an attribute for every single post, called "best_3_comments". Which is a QuerySet of comments of them ordered by their voting's.
Can I achieve this without querying all of the comments everytime I query posts?
posts = models.Post.objects.annotate(
best_3_comments = get_query_of_the_posts_comments("post_comments").annotate(
voting=Count("liked_users")-Count("disliked_users")
).order_by("-voting")[:3]
)
What function(s) can i use to get query of the post's comments, and how should i approach this ? I'm completely open to new ideas of ways to do what I want to achieve, I couldn't find which function and how to use for it.
Thank you in advance.

I found a solution. I would like to share it for those in future looking for a way to solve their problems similar to mine.
posts = models.Post.objects.all() # The QuerySet You Want To Use
for post in posts:
post.best_3_comments = models.Comment.objects.filter(post=post).annotate(voting=Count("liked_users")-Count("disliked_users")).order_by("-voting")[:3]
The solution is iterating over our QuerySet, assigning an attribute for all of them which contains a QuerySet.

Related

Django Rest Framework many-to-many relation create the link

Since there are a few questions about m2m and DRF I'll try narrow down what specifically I'm interested in. Let's call the two models 'article' and 'publication'. Assume that:
The 'publication' object already exists.
The 'article' object may or may not exist. specifically:
a) If a previous publication contained the article, then it will already be
there.
b) If not, then the article will need to be created.
I want to send a post http request with the article data in the body
and the publication id available from the url which will:
a) if the article already exists, link it to the publication
b) if the article does not exist, create it, and then link it to the publication
Going for the 'default' strategy below did not work out. I can think of two ways to approach this problem:
Overriding the create method on the article serializer. However I'm scepticle of doing that since this seems like a problem that should be common and have a non-custom solution.
Creating an endpoint to directly work with the 'through' model. I could then split up the process into two steps (and 2 requests) where I first get_or_create the article, and then post to the through model endpoint to create the link.
Are there any other approaches or built-in DRF solutions to this problem?
Here's where I'm at currently:
models.py
class Publication(models.Model):
name = models.CharField(max_length=255, unique=True)
collection = models.CharField(max_length=255)
class Article(models.Model):
major = models.IntegerField()
minor = models.IntegerField()
publication = models.ManyToManyField(Publication)
class Meta:
constraints = [models.UniqueConstraint(fields=['major', 'minor'], name='unique_article')]
views.py
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
serializers.py
class ArticleSerializer(serializers.ModelSerializer):
publication = serializers.SlugRelatedField(slug_field='name', queryset=Publication.objects.all()), many=True)
class Meta:
model = Article
fields = '__all__'
When posting to this endpoint I'll get a 'duplicate entry' integrity error if the article does already exist, instead of the article then just being linked.
This is the way I have handled this issue in the past. If your using the Primary keys these calls are not very expensive.
pub = Publications.objects.get(id=1)
article, created = Articles.objects.get_or_create(
id=1,
defaults= {other_params:'value', param : 'value'},
)
pub.articles.add(article)

Using tastypie to retrieve data from foreign key

These are two models in my Django app :
models.py
class Posts(models.Model):
title = models.CharField(max_length=200, blank=True)
author = models.ForeignKey(user,on_delete=models.CASCADE,default=None, blank=True)
content = models.TextField()
class Unposted(models.Model):
article = models.ForeignKey(Posts, on_delete=models.CASCADE)
upload_at = models.DateTimeField(default=datetime.now, blank=True)
I'm trying to retrieve data from Posts using an API request to Unposted.
Here's what I have until now but I'm unsure how to get data from the Posts model. Right now I just get a JSON response with only the upload_at field.
resources.py
class UnpostedResource(ModelResource):
class Meta:
queryset = Unposted.objects.all()
resource_name = 'unposted'
If I'm not wrong, u can just import your Posts model and then just by for loop make an array with posts models using foreign key from unposted to filter your posts =) Sounds weird and I'm not sure about effectiveness, but looks pretty nice. It will look smth like:
queryset = Posts.objects.filter(article_in=[get(i.article) for i in Unposted.objects.all()])
In the case, Posts is a foreignkey of Unposted, thus you need to define foreignkey field in the resource for the corresponding field in model, this tutorial maybe can help you.

Create a Blog which support multiple type of post

I am a new user of Django, and I am trying to figure out how to created a model which can support many kind (type) of elements.
This is the plot : I want to create a Blog module on my application.
To do this, I created a model Page, which describe a Blog Page. And a model PageElement, which describe a Post on the blog. Each Page can contain many PageElement.
A PageElement can have many types, because I want my users could post like just a short text, or just a video, or just a picture. I also would like (for example) the user could just post a reference to another model (like a reference to an user). Depending of the kind of content the user posted, the HTML page will display each PageElement in a different way.
But I don't know what is the right way to declare the PageElement class in order to support all these cases :(
Here is my Page model :
class Page(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
# Basical informations
title = models.CharField(max_length=150)
description = models.TextField(blank=True)
# Foreign links
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='pages_as_user'
)
created_at = models.DateTimeField(default=timezone.now)
# Other fields ....
class Meta:
indexes = [
models.Index(fields=['uuid']),
models.Index(fields=['user', 'artist'])
]
For now, I have two solutions, the first one use inheritance : When you create a new post on the blog, you create an Element which inherit from PageElement model. Here are my different Models for each cases :
class PageElement(models.Model):
page = models.ForeignKey(
Page,
on_delete=models.CASCADE,
related_name='%(class)s_elements'
)
updated_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(default=timezone.now)
class PageImageElement(PageElement):
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
class PageVideoElement(PageElement):
video = models.FileField(null=True)
video_url = models.URLField(null=True)
class PageTextElement(PageElement):
text = models.TextField(null=True)
class PageUserElement(PageElement):
user = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
related_name='elements'
)
This solution would be the one I have choosen if I had to work with "pure" Python. Because I could stored each PageElement in a dictionnary and filter them by class. And this solution could be easily extended in the futur with new type of content.
But with Django models. It seems that is not the best solution. Because it will be really difficult to get all PageElement children from the database (I can't just write "page.elements" to get all elements of all types, I need to get all %(class)s_elements elements manually and concatenate them :/). I have thinked about a solution like below (I don't have tried it yet), but it seems overkilled for this problem (and for the database which will have to deal with a large number of request):
class Page(models.Model):
# ...
def get_elements(self):
# Retrieve all PageElements children linked to the current Page
R = []
fields = self._meta.get_fields(include_hidden=True)
for f in fields:
try:
if '_elements' in f.name:
R += getattr(self, f.name)
except TypeError as e:
continue
return R
My second "solution" use an unique class which contains all fields I need. Depending of the kind of PageElement I want to create, I would put type field to the correct value, put the values in the corresponding fields, and put to NULL all other unused fields :
class PageElement(models.Model):
page = models.OneToOneField(
Page,
on_delete=models.CASCADE,
related_name='elements'
)
updated_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(default=timezone.now)
TYPES_CHOICE = (
('img', 'Image'),
('vid', 'Video'),
('txt', 'Text'),
('usr', 'User'),
)
type = models.CharField(max_length=60, choices=TYPES_CHOICE)
# For type Image
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
# For type Video
video = models.FileField(null=True)
video_url = models.URLField(null=True)
# For type Text
text = models.TextField(null=True)
# For type User
user = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
related_name='elements',
null=True
)
With this solution, I can retrieve all elements in a single request with "page.elements". But it is less extendable than the previous one (I need to modify my entire table structure to add a new field or a new kind of Element).
To be honnest, I have absolutly no idea of which solution is the best. And I am sure other (better) solutions exist, but my poor Oriented-Object skills don't give me the ability to think about them ( :( )...
I want a solution which can be easily modified in the future (if for example, I want to add a new Type "calendar" on the Blog, which reference a DateTime). And which would be easy to use in my application if I want to retrieve all Elements related to a Page...
Thanks for your attention :)
I'm not sure it fits your problem but using GenericForeignKeys/ContentType framework may be appropriate in this case. It's quite powerful when one grasps the concept.
Example construct:
class Page(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
page_element = GenericForeignKey('content_type', 'object_id')
...
You can now connect any model object by the GenericFK to the Page model. So adding a new type (as a new model), at a later stage, is not intrusive.
Update:
As a comment pointed out this construct doesn't support many PageElements in a good way for a Page.
To elaborate, one way to solve that problem, still taking advantage of the GenericFK...
class PageElement(models.Model):
class Meta:
unique_together=('page', 'content_type', 'object_id') # Solve the unique per page
page = models.ForeignKey(Page, related_name='page_elements')
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
A Page can have many "abstract" PageElements and content_object is the "concrete PageElement model/implementation".
Easy to retrieve all elements for a specific page and allows inspection of the ContentType to check the type of element etc.
Just one way of many to solve this particular problem.
To establish the relationship between Page and PageElement in Django you would rather use Foreign Key relationship, than inheritance.
class PageImageElement(PageElement):
page = models.ForeignKey(Page,
on_delete=models.CASCADE,
related_name='images')
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
Every user's post would create an instance of Page. Every addition of image to the Page would create an instance of PageImageElement and you could query for them using the related name. This way would be really easy to access all video, image, text modules of a single Page.
On a related note, I would say that PageElement class could be abstract see the docs and if you declare fields as possibly containing null values as in video = models.FileField(null=True) then it might be worth declaring blank=True as well, otherwise there will be errors when creating the object with these fields undefined. Discussed, for example, here: differentiate null=True, blank=True in django
I can't just write "page.elements" to get all elements of all types
Well actually, you can if you use multi-table inheritance. The problem is that all records returned are instances of PageElement, meaning you lose all information of the subclass type and the additional data these child objects may hold.
There are quite a lot of packages that tackle this polymorphism problem:
django packages: Model inheritance

django 2 - Nested query based off multiple models

Update: See bottom.
I'm trying to wrap my head around how to achieve a nested/chained query based on my needs. There might be a better way to get the results I need so please let me know.
Trying to get the authenticated user and get a list of friends, which I have working and I get a queryset object of friends. I would like to pass the queryset object of friends into another query that searches the Post model, matches the username found Friend.users to the Post.creator so I get back another queryset which will have all the Posts of all my friends which I can display in the template.
class Friend(models.Model):
users = models.ManyToManyField(User, blank=True)
owner = models.ForeignKey(User, related_name='owner_friend', on_delete=models.CASCADE, null=True, blank=True)
class Post(models.Model):
creator = models.ForeignKey(User, on_delete=None, null=True)
EDIT:
I'm trying to do this with my code
f = Friend.objects.all().filter(owner__username='admin').filter(users__username='jeff')
output
<QuerySet [<Friend: Friend object (1)>]>
p = Post.objects.all().filter(creator__username__in=f)
ValueError: Cannot use QuerySet for "Friend": Use a QuerySet for "User".
Thanks for the help.
Update 1:
I've changed my models to use ForeignKeys instead. I've been using the shell to test and I'm getting an unexpected result, see below.
class Friend(models.Model):
users = models.ForeignKey(User, related_name='user_friend', on_delete=models.CASCADE, null=True, blank=True)
owner = models.ForeignKey(User, related_name='owner_friend', on_delete=models.CASCADE, null=True, blank=True)
f = Friend.objects.filter(owner__username='admin').values_list('users__username', flat=True)
Output
<QuerySet ['jeff', 'sam']>
Post.objects.filter(creator__username__in=list(f))
Output
<QuerySet []>
If I put in the list manually
Post.objects.filter(creator__username__in=['admin', 'jeff'])
Output
<QuerySet [<Post: Post Title 1>]>
I think if I can get the __in=list(f) to work this should fix the issue.
Update 2
list(f) is actually working, I didn't have a Post linked for the user accounts for the friends being pulled through. Once I created the post for the friends, I'm now getting a queryset.
I'm not sure what I'm doing is the best way but it's working.
For future readers I hope this helps.
The Friend model is a bit strange and is causing you some difficulty. Really, Friend should be the through table in the many-to-many relationship between User and itself. Presumably you are using the built-in User model, which is why you have used an external model; but you can still simulate a through table with two foreign keys:
class Friend(models.Model):
from_user = models.ForeignKey(User, related_name='users_from', on_delete=models.CASCADE)
to_user = models.ForeignKey(User, related_name='users_to', on_delete=models.CASCADE)
Now your query can be:
Post.objects.filter(creator__users_to__from_user__username='admin')
that is, give me all Posts whose creator is on the "to_user" side of the friend relationship where the from_user's username is "admin".

Django: How to order comments of a post directly under each original post object?

I’m creating a django app that allows posting something on each user’s page and then allowing people to comment on each of those posts. I’m trying to get the associated comments for each post and display them right under each post in timestamp order. I have figured out how to do this for the original posts of each profile using allpageposts = username.newpost_set.all().order_by('-postdate'), but can’t seem to figure out what the best way to do it for comments even though it seems at first it’d be the same type of logic as the original posts. The problem I am having is that I need to keep track of which comments are for which specific ‘newpost’ in a user’s page which can have many ‘newposts’ on their page. What is a good way that I can capture each page's newpost's comments and display them in a template? Thanks for any tips or hints. Here are my models:
class newpost(models.Model):
newlinktag = models.ForeignKey('username')
postcontent = models.CharField(max_length=1024)
postdate = models.DateTimeField()
postlikes = models.IntegerField(null=False, default=0)
def __unicode__(self):
return self.postcontent
class postcomment(models.Model):
comment = models.CharField(max_length=1024, null=False)
commenttag = models.ForeignKey('newpost')
postcommentdate = models.DateTimeField()
commentlikes = models.IntegerField(null=False, default=0)
def __unicode__(self):
return self.comment
If I've understand you correct, you need something like this:
for post in username.newpost_set.all():
comments = postcomment.objects.filter(commenttag=post).order_by('-postcommentdate')
# your code here

Categories

Resources