Order the related set in a many-to-many relation - django

Lets say I have a setup like this:
class Product(models.Model):
name = models.CharField()
category = models.CharField()
class Shop(models.Model):
name = models.CharField()
website = models.URLField()
class ProductPrices(model.Model):
product = models.ForeignKey(Product)
shop = models.ForeignKey(Shop)
price = models.FloatField()
updated = models.DateTimeField()
and then I have a page on which I would like to list all products, which shops they can be found at and to what prices. Is it possible to have the prices for all (or just some) products sorted, without using:
class Meta:
ordering = ['-price']
on ProductPrices?
That is, in my view I would want to do something like this (sort the productprice_set, on the price column):
def get_queryset(self):
queryset = Product.objects.all()
for product in queryset:
product.productprice_set.order_by('-price')
# Have also tried the following, without any result
product.productprice_set = product.productprice_set.order_by('-price')
return queryset
To clarify: I want to sort the prices for each product, not the products based on the prices. But I want to do this by going "through" the products way.
I want 'productprice_set' ordered, and not 'product'.

That syntax does not somehow modify the relationship so that subsequent calls return an ordered queryset: it returns the ordered qs then and there. Subsequent calls will have the default ordering.
The way to do this is to use the order_by clause when you're retrieving the objects. Since you can't pass arguments to methods in the template, the way to do it is to define a method on Product which returns the ordered prices:
class Product(models.Model):
...
def ordered_prices (self):
return self.productprices_set.order_by('-price')
and call it in the template:
{% for product in products %}
{{ product.name }}
{% for price in product.ordered_prices %}
{% price.price %}
{% endfor %}
{% endfor %}

I have found a solution now in which I don't have to modify any model, nor make any more database queries.
In my view I have now:
def get_queryset(self):
queryset = Product.objects.all().prefetch_related('price_set')
for product in queryset:
product.prices = sorted(product.price_set.all(), key=lambda x: x.price, reverse=true)
return queryset
And then in my template:
{% for product in products %}
{{ product.name }}
{% for price in product.prices %}
{% price.price %}
{% endfor %}
{% endfor %}
Since I'm new to using Django, I wonder if there's anything "wrong" or bad by solving it like this?

Related

Writing on-request filtering for related objects

Suppose that we have following models
class Category(models.Model):
name = models.CharField(max_length=254)
class Item(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="items")
name = models.CharField(max_length=254)
state = models.ForeignKey(State, on_delete=models.CASCADE)
Categories and their items are listed like so
def view(request):
categories = Category.objects.all()
pass
{% for category in categories %}
{{ category.name }}
{% for item in category.items.all %}
{{ item.name }}
{% endfor %}
{% endfor %}
In this structure, I want to write on-request filtering for listed 'items'.
def view(request):
...
queryset = ???
state = request.GET.get('state')
if state:
queryset = queryset.filter(state__name=state)
The problem is defining 'queryset'. Because, Items are listed as related objects of category.
Can it be done properly? or Do I need to change design?
You can take a look at my design to be more clear.
Low fidelity design
Your related_name in the Item object for category field should be named "items". Then category.items.all will give you the list of items in that category. Look at the example in the documentation.
class Tag(models.Model):
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name="tags",
)
name = models.CharField(max_length=255)
To filter items depending on category you can pass the PKs of the category in the request and filter according to those specific categories.
views.py
def your_view(request):
...
list_of_your_category_ids = [1,4] #get them as a list from frontend
list_of_your_state_names = [1,2]
queryset = Item.objects.filter(
category__in=list_of_your_category_ids,
state__code__in=list_of_your_category_names
)
This will give you the queryset you wanted. Now all you need is to regroup this queryset with their categories.
Django provides a regroup template tag that does exactly this.
{% regroup queryset by category as categories_list %}
<ul>
{% for category in categories_list %}
<li>
{{category.grouper}}
<ul>
{% for item in category.list %}
<li>
{{item}}: state - {{item.state.code}}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
https://i.stack.imgur.com/TbTpz.png

How to display sum of disctinct items in django?

I would like to display sum all the distinct categories of the products that belongs to the user. I searched on the web, but all the things that I tried doensn't work. You may find the models,view and template below. It doesn't give me anything at the html.
Model:
class Product(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE,related_name='products')
category = models.CharField(max_length=120)
brand = models.CharField(max_length=120)
product = models.CharField(max_length=120)
price = models.DecimalField(decimal_places=2,max_digits=100)
class Comp_Product(models.Model):
product = models.ForeignKey(Product,on_delete=models.CASCADE, related_name="comp_products")
competitor = models.URLField()
price = models.DecimalField(decimal_places=2,max_digits=100)
change = models.FloatField()
stock = models.BooleanField()
last_update = models.DateField(auto_now_add=True)
View:
class DashboardList(ListView):
template_name='dashboard_by_user.html'
def get_queryset(self):
return Product.objects.filter(user=self.request.user).annotate(count_category=Count('category',distinct=True)).aggregate(sum_category=Sum('count_category'))
template:
{% for product in product_list %}
{{product.sum_category}}
{%endfor%}
welcome to stackoverflow,
I'm assuming that you want to count the number of distinct categories for the given user. This can be done like this:
views.py:
from django.views.generic.list import ListView
from .models import Product
class DashboardList(ListView):
template_name = 'dashboard_by_user.html'
def get_queryset(self):
return Product.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Products = context['object_list']
context['distinct_category_count'] = Products.values(
'category').distinct().count()
return context
dashboard_by_user.html:
<h1>Products</h1>
<ul>
{% for product in object_list %}
<li>{{ product.brand }} - {{ product.product }} - {{ product.category }}</li>
{% empty %}
<li>No Producs yet.</li>
{% endfor %}
</ul>
<p> Number of distinct categories: {{ distinct_category_count }} </p>
This should give you an output similar to this one:

Unable to display parent table with child table in a for loop with template tagging in Django

I want to display an unordered list of NBA teams from my teams table database. Within each team, I want an ordered list of players on that team. So basically I want to produce a list of all objects from my parent table and for each object from the parent table I want a list of all objects from the child table related the parent table. I understand that the problem is methods are not allowed in template tagging, so 'team.players_set.all()' would not be allowed. How else can I get the results that I want?
class Teams(models.Model):
name = models.CharField(max_length=20, unique=True)
num_of_plyrs = models.IntegerField()
def __str__(self):
return f"{self.name} have {self.num_of_plyrs} players."
class Meta:
verbose_name_plural = "Teams"
class Players(models.Model):
name = models.CharField(max_length=20)
team = models.ForeignKey(Teams, on_delete=models.CASCADE)
def __str__(self):
return f"{self.name} plays for {self.team}"
class Meta:
verbose_name_plural = 'Players'
__________________________________________________________________________________
def teams(request):
teams = Teams.objects.all()
context = {'teams': teams}
return render(request, 'one_app/teams.html', context)
__________________________________________________________________________________
<h3>NBA Teams</h3>
<ul>
{% for team in teams %}
<li>
<p>Team: {{ team.name }}</p>
<p>Number of players: {{ team.num_of_plyrs }}</p>
<ol>
{% for plyr in team.players_set.all() %}
<li>{{ plyr.name }}</li>
{% endfor %}
</ol>
</li>
{% empty %}
<li>There are no teams listed.</li>
{% endfor %}
</ul>
{% for player in team.players_set.all %}
table name should be singular
you can set related_name and use players instead players_set
team = models.ForeignKey(Teams, related_name='players',on_delete=models.CASCADE)
make use of select_related
One way to achieve this is:
class PlayersView(ListView):
model = Team
...
def get_queryset(self, **kwargs):
teams = []
queryset = super().get_queryset()
for team in queryset:
players = team.player_set.all()
for player in players:
teams.append([team, (player.name, ...)]
return teams
And in your template:
{% for team, players in teams %}
...
{% for player in players %}
...
{% endfor %}
{% endfor %}
ALTERNATIVE
If you know this is something you might be using a lot and to simplify your view, you can also create a manager:
from your_app.managers import TeamManager
class Team(models.Model):
...
objects = Manager()
team_manager = managers.TeamManager.as_manager()
...
In a file manager.py or anything you want:
from django.db.models import QuerySet
class TeamManager(QuerySet):
def table_queryset(self):
queryset = self.all()
teams = []
for team in queryset:
players = team.player_set.all()
for player in players:
teams.append([team, (player.name, ...)]
return teams
And then in your view:
class PlayersView(ListView):
model = Team
queryset = Team.player_manager.table_queryset()

Django - get in template reverse related many to many field

I have 3 models, Entry model and Category model, and I have created intermediate model CategoryEntry.
class Entry(models.Model):
entry_text = models.TextField()
user = models.ForeignKey(User)
class Category(models.Model):
user = models.ForeignKey(User)
category_text = models.CharField(max_length=200)
entries = models.ManyToManyField(Entry, through='CategoryEntry')
class CategoryEntry(models.Model):
category = models.ForeignKey(Category)
entry = models.ForeignKey(Entry)
viewed = models.BooleanField(default=False)
I have created a View with get_queryset method like this
def get_queryset(self):
order_by = self.request.GET.get('order_by')
if not order_by:
order_by = '-pub_date'
return Entry.objects.filter(category__id=self.kwargs.get('category_id', None), category__user__id=self.request.user.id).order_by(order_by)[:].select_related('user')
I have category ID from kwargs. The problem is - how to get every entries related field "viewed", so I can show its value in template. For example related User field I can get like
{% for entry in entries %}
{{ entry.entry_text }}
{{ entry.user.name }}
{% endfor %}
What I need is to access "viewed" field something like "entry.categoryentry.viewed"
Tried prefetch_related, but doesn't seem to work or don't get how to know right name to access field in template
Entry.objects.filter(category__id=self.kwargs.get('category_id', None), category__user__id=self.request.user.id).order_by(order_by)[:].select_related('user').prefetch_related(Prefetch("categoryentry_set", queryset=CategoryEntry.objects.filter(category__id=self.kwargs.get('category_id', None))))
Thanks!
You would do:
{% for entry in entries %}
{{ entry.entry_text }}
{{ entry.user.name }}
{% for ce in entry.catalogentry_set.all %}
{{ce.category.category_text}}
{# or whatever.. #}
{% endfor %}
{% endfor %}
Basically, if no related_name is present, you could access the reverse lookup element by lowercase model name + _set - which returns a queryset manager object.

many-to-many select_related in django class-based list view

We have an example model:
#models.py
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
name = models.CharField(max_length=255)
author = models.ManyToManyField(Author, blank=True)
def get_authors(self):
return self.authors.all().order_by('id').values_list('name')
#views.py
class BooksView(ListView):
model = Book
def get_queryset(self):
q = Book.select_related('authors').all()
#template
{% for book in books %}
{{ book.name }} ({%for author in book.get_authors %} {{ author }} {% endfor %}
{% endfor %}
When I try to get data from template using get_authors function, I see multiple SQL queries that dramatically reduce performance (SQL works about 5sec). Is it possible to reduce queries? Now I see SQL query for each author in cycle.
M2M uses prefetch_related not select_related.
Fix the Model (there are different ways to do what you want to do):
Your Model:
class Book(models.Model):
name = models.CharField(max_length=255)
author = models.ManyToManyField(Author, blank=True)
def get_authors(self):
if self.authors:
return '%s' % " / ".join([author.name for author in self.authors.all()])
Fix your view:
class BooksView(ListView):
"""
Note that default Django views use object_list for the context,
in order to use books, you need to define context_object_name.
"""
context_object_name = "books"
"""
You don't need to override the queryset for this kind of operation.
Just define the queryset attribute of the CBV.
"""
queryset = Book.objects.prefetch_related('authors')
Then in your template:
#template
{% for book in books %}
{{ book.name }} {{ book.get_authors }}
{% endfor %}