Order a queryset by a field in another queryset - django

In my django blog app, I have a model for Bookmark, which includes fields pointing to Post and User, and a field for when the bookmark was created.
# models
class Post(models.Model):
# ...
title = models.CharField(max_length=70)
body = RichTextField()
author = models.ForeignKey(
UserProfile,
on_delete=models.CASCADE,
related_name='posts')
created_on = models.DateTimeField(default=timezone.now)
# ...
class Meta:
ordering = ['created_on']
class Bookmark(models.Model):
post = models.ForeignKey(
Post, on_delete=CASCADE,
related_name="bookmarked_post"
)
user = models.ForeignKey(
User, on_delete=CASCADE,
related_name="bookmarks"
)
created_on = models.DateTimeField(default=timezone.now)
class Meta:
ordering = ['created_on']
I have a view to display my users' bookmarks. I want to order the posts queryset in the order they were bookmarked, with the recently bookmarked posts first.
# views.py
#login_required
def bookmarks_view(request):
bookmarks = Bookmark.objects.filter(user=request.user)
bookmarked_posts = Post.objects.filter(
bookmarked_post__in=bookmarks).order_by(???)
return render(request, 'bookmarks.html', {'posts': bookmarked_posts})
I'm struggling to figure out how to order my Posts queryset by the created_on field in the Bookmark model. To add additional complexity, both the Post and Bookmark model have a created_on field.
Can anyone point me in the right direction with this? I've scoured the django documentation and SO and not finding what I need, possibly I just don't know the right keywords... Or have I set up my models incorrectly to achieve what I want?

Something like this should work:
bookmarks = Bookmark.objects.filter(user=u0)
bookmarked_posts = Post.objects.filter(
bookmarked_post__in=bookmarks
).order_by("-bookmarked_post__created_on")
You can drop the __created_on at the end - Django will then use the default ordering specified on the related model. The - at the beginning means descending, so newer dates come first.
See also the documentation for order_by:
To order by a field in a different model, use the same syntax as when you are querying across model relations. That is, the name of the field, followed by a double underscore (__), followed by the name of the field in the new model, and so on for as many models as you want to join.
Because you specified the related name as bookmarked_post, that's also the name of the field you need to specify, from the perspective of Post, just like you used it in the filter.
Without the related_name, it would look like this:
bookmarks = Bookmark.objects.filter(user=u0)
bookmarked_posts = Post.objects.filter(
bookmark__in=bookmarks
).order_by("-bookmark__created_on")
(Note that you may want to specify a unique_together constraint for post and user on the Bookmark model, to make sure that a post can only be bookmarked once per user)

Related

Django ValueError: Cannot use QuerySet for "": Use a QuerySet for "User"

I'm working on a project in CS50w where I have to show the posts of the user I'm following and I'm getting the following error
ValueError: Cannot use QuerySet for "Following": Use a QuerySet for "User".
models.py:
class Post(models.Model):
"""Tracks all the posts"""
text = models.TextField(max_length=256)
user = models.ForeignKey(User, on_delete=models.CASCADE)
date_posted = models.DateTimeField(auto_now_add=True)
class Following(models.Model):
"""Tracks the following of a user"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
following = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followers")
And this how I'm trying to retrieve the posts of the users I'm following:
views.py
# Gets all the posts from the users the current user is following
followings_usernames = Following.objects.filter(user=request.user)
posts = Post.objects.filter(user=followings_usernames)
Any help is appreciated.
You can filter based on a field (Following.user) through a reverse relation (followers) through the Post.user field:
posts = Post.objects.filter(user__followers__user=request.user)
See the Django documentation for lookups that span relationships and the usage of double underscores to separate models and fields.
Try using this, it might help
# Gets all the posts from the users the current user is following
followings_usernames = list(Following.objects.filter(user=request.user).values_list('user', flat=True))
posts = Post.objects.filter(user__in=followings_usernames)

How can I add a field to a many-to-many relationship in Django?

This is a question about how to add a field to a many-to-many relationship in Django.
I have a model LandingPage and a model Product. (Code below). In my project, LandingPages can have many Products listed on them and those same Products can appear on multiple different LandingPages.
Product is connected to LandingPage via a ManyToManyField.
My Goal:
I am trying to figure out how to add a field so that I can set the order (1 through 10) for Products on their associated LandingPages. Reminder, Product instances can appear on multiple LandingPages, so each instance will need to have a different order attribute.
Ideally, I'd like to expose this functionality via the built-in Django admin. Right now it shows the relationships table, but not the order field as it does not yet exist. (Screenshots/mockups below).
My Code:
models.py
class LandingPage(models.Model):
"""Stores a single LandingPage and metadata.
"""
name = models.CharField(max_length=200, help_text="The name is only used internally. It is not visible to the public.")
slug = models.SlugField(default="", editable=False, max_length=150, null=False, verbose_name="Slug", help_text="This is not editable.")
# Additional fields that I do not believe are relevant
class Product(models.Model):
"""Stores a single Product and metadata.
"""
name = models.CharField(max_length=200, help_text="Used internally. Not visible to the public.")
active = models.BooleanField(default=False, verbose_name="Product is Live on Landing Pages", help_text="Determines whether the product should be visible on the assocaited landing page or not.")
landing_page = models.ManyToManyField(
LandingPage,
verbose_name="Landing Page",
help_text="The landing page or pages that this product is assocaited with.",
)
# Additional fields that I do not believe are relevant
admin.py
# Inline configuration used by LandingPageAdmin
class ProductInline(admin.TabularInline):
"""Creates Inline table format for displaying Product data."""
model = Product.landing_page.through
extra = 0
class LandingPageAdmin(admin.ModelAdmin):
"""Specifies LandingPage data in Admin."""
readonly_fields=('slug',)
inlines = [ProductInline]
save_as = True
# Inline configuration used by Product Admin
class LandingPageInline(admin.TabularInline):
"""Creates Inline table format for displaying LandingPage data."""
model = LandingPage.product_set.through
extra = 0
class ProductAdmin(admin.ModelAdmin):
"""Specifies Product data in Admin."""
inlines = [LandingPageInline]
save_as = True
Mockups (for clarity):
Current State
Desired State
(I added the desired functionality in red for clarity. The order integers should be editable so that the order can be re-arranged.)
My Question
How can I accomplish this goal of adding an editable order field to this pre-existing relationship?
Should I manually add an order field to the product-landingpage join table that was automatically created by Django? If I do that, is there a way to have the Django admin show that added field?
Or should I go about it a totally different way?
Thank you in advance!
I found the answer to this.
The solution is create an intermediary model and connect it using "through". Example below:
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Official docs are here: https://docs.djangoproject.com/en/3.1/topics/db/models/#intermediary-manytomany
Others in my situation may find it useful to read this question/answer as it does a good job of explaining various solutions: Define an order for ManyToManyField with Django

Filtering one model based on another model's field

I have the following models:
class User(AbstractUser):
pass
class Post(models.Model):
post_user = models.ForeignKey(User, on_delete=models.CASCADE)
post_content = models.TextField()
post_date = models.DateTimeField(default=timezone.now)
class Follow(models.Model):
follow_user = models.ForeignKey(User, on_delete=models.CASCADE)
follow_target = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follow_target')
I'm trying to create a queryset of all posts that were created by users I follow (me being the currently logged in user). My queryset currently looks like this but I keep getting NameError: field not found:
postresult = Post.objects.all().filter(post_user=follow__follow_target, follow__follow_user=request.user)
Any help will be greatly appreciated!
You filter with:
postresult = Post.objects.filter(
post_user__follow_target__follow_user=request.user
)
The post_user will follow the ForeignKey from Post to User. Then we use the related_name='follow_target' to follow the relation to the Follow object, and then we use follow_user to retrieve the following user.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the User model to the Follow
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the follow_target relation to followers.
In case you rename the related name, the query is thus:
postresult = Post.objects.filter(
post_user__followers__follow_user=request.user
)

Get List of Categories on the basis of foreign model in django

I've two models as below.
class Category(models.Model):
title = models.CharField(max_length=55)
class Meta:
verbose_name = 'Food Category'
verbose_name_plural = 'Food Categories'
def __str__(self):
return self.title
class FoodItem(TimeStampWithCreator):
CATEGORY_CHOICES = (
('takeway', 'Takeaway'),
('dine_in', 'Dine In'),
('function', 'Function'),
)
type_menu_select = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='takeway')
category = models.ForeignKey(FoodCategory, on_delete=models.CASCADE)
i want to filter all the categories containing takeaway, I've no idea how to achieve this
You've included category choices in your FoodItem model but the model also has a ForeignKey to a Category model, this isn't needed if the only category objects you have are those three choices (the category field must refer to one of those anyway as it's the ForeignKey). To filter the items by category you would need to use a queryset filter.
https://docs.djangoproject.com/en/3.0/topics/db/queries/#retrieving-specific-objects-with-filters
FoodItem.objects.filter(category=YourCategory)
This is usually the kind of thing I'd like to test in the shell as I don't do it very often. If what you want is actually all Category with a FoodItem that have type_menu_select set to 'takeway' then the following should work (but I haven't tested it):
Category.objects.filter(fooditem__type_menu_select='takeway')
This is using the "reverse" relationship on the ForeignKey and there's more information in the Django docs (search for 'reverse').

Retrieving all database objects and its related objects Django

I am currently learning Django, and I am finding it a bit difficult wrapping my head around the ManyToMany fields. I am using an intermediate model to manage my relationships.
I have three models; Ticket, User, and TicketUserRelation.
I want to be able to query the ticket model, and retrieve both its corresponding user objects and the ticket object. How would I go about doing this?
In Laravel I would do something along the lines of
Ticket::where('id', '1')->with('contributors')
But I can't really figure out how to do this in Django
The models:
class User(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Ticket(models.Model):
contributors = models.ManyToManyField(User, through=TicketUserRelation, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
class TicketUserRelation(models.Model):
id = models.AutoField(primary_key=True, db_column='relation_id')
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
EDIT: I am using an intermediate model so that I can easily add things like join date later.
You don't need the TicketUserRelation model when using Django ORM. You could simply use a ForeignKey in the Ticket model, or use the ManyToManyField already defined, if one ticket can be assigned to multiple users.
class Ticket(models.Model):
# For one user, use ForeignKey
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tickets')
# For multiple users, use ManyToManyField
contributors = models.ManyToManyField(User, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
You can then get all tickets for a user u with:
u.tickets.all()
Figured it out myself, using prefetch_related. I was having trouble understanding how prefetch_related works. For those that are confused too, from my understanding it works like this:
Ticket.objects.all().prefetch_related('contributors')
This returns a queryset, something along the lines of this
<QuerySet [<Ticket: Testing ticket one>, <Ticket: Testing ticket two>, <Ticket: Testing ticket three'>, <Ticket: Testing ticket four>]>
When you then access the elements in the queryset, you can then call .contributors on the object, like so:
# Get the queryset
tickets_with_contribs = Ticket.objects.all().prefetch_related('contributors')
# Print the contributors of the first ticket returned
print(tickets_with_contribs[0].contributors)
# Print the contributors of each ticket
for ticket in tickets_with_contribs:
print(ticket.contributors)
Looking back at it this should have been pretty self explanatory, but oh well.