Accessing foreign key values in django ListView of GCBV - django

I have two models related by a Foreign key as follows. (Only shown important fields here.)
in model:
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
description = models.TextField()
...
class Price(models.Model):
category = models.ForeignKey(Category) # referred to above model
sub_type = models.CharField(max_length=4, choices=CHOICE_SUB_TYPE)
price = models.DecimalField()
...
I'm going to display Categories in a ListView along with related pricing details. In order do that, I need to set related pricing objects for each Category object. What is the best and efficient way to do this?

Your code is fine as it is, in templates, you have access to related models. When listing categories in your template you can:
<ul>
{% for cat in categories %}
<li>{{ cat.name }}</li>
<ul>
{% for price in cat.price_set.all %}
{{ price.price }}
{% endfor %}
</ul>
{% endfor %}
</ul>
For displaying categories when showing details for a price, you might want to Use a Single object Mixin with a ListView.
Recommended: Related object reference.

Related

Django PostgreSQL – Efficiently fetch recursive category structure

I have a model which looks like this:
class Category(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField()
parent = models.ForeignKey(
'categories.Category',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='categories'
)
basically, in the parent field, it references itself. If a parent is set to None, it's the root category.
I use it to build a hierarchy of categories.
What would be the most efficient way to:
fetch all the objects through the hierarchy
display them in a template?
For some reason, select_related does not seem to lead to performance improvements here.
I also found this: How to recursively query in django efficiently?
But had a really hard time applying it to my example, because I still don't really understand what's going on. This was my result:
WITH RECURSIVE hierarchy(slug, parent_id) AS (
SELECT slug, parent_id
FROM categories_category
WHERE parent_id = '18000'
UNION ALL
SELECT sm.slug, sm.parent_id
FROM categories_category AS sm, hierarchy AS h
WHERE sm.parent_id = h.slug
)
SELECT * FROM hierarchy
Would appreciate any help.
Thanks!
One possible solution can be using https://django-mptt.readthedocs.io/en/latest/overview.html#what-is-django-mptt
MPTT is a technique for storing hierarchical data in a database. The
aim is to make retrieval operations very efficient.
The trade-off for this efficiency is that performing inserts and moving items around the tree is more involved, as there’s some extra work required to keep the tree structure in a good state at all times.
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
name = models.CharField(max_length=50)
slug = models.SlugField()
parent = TreeForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='children'
)
class MPTTMeta:
order_insertion_by = ['name']
You can use the django-mptt template tag as this:
{% load mptt_tags %}
<ul>
{% recursetree categories %}
<li>
{{ node.name }}
{% if not node.is_leaf_node %}
<ul class="children">
{{ children }}
</ul>
{% endif %}
</li>
{% endrecursetree %}
</ul>
There is a tutorial and more information int library docs.
I had the same problem and ended up creating the following function that hits the database once, then sorts out the heirarchy and returns a dict:
def get_category_tree():
categories = Category.objects.order_by('name')
itemtree = {}
# Add 'children' attribute to each category; populate dict
for category in categories:
category.children = {}
itemtree[category.pk] = category
# Add categories to 'children'
for key,value in itemtree.items():
if value.parent_id:
itemtree[value.parent_id].children[key] = value
# Return top-level items
return {k:v for k,v in itemtree.items() if not v.parent_id}
Each value of the returned dict is a top-level Category object which has a children attribute.
You can render it in the template by looping through the dict values. The following example will handle three levels of heirarchy:
<ul>
{% for level1 in category_tree.values %}
<li>
{{ level1.name }}
{% if level1.children %}
<ul>
{for level2 in level1.children.values %}
<li>{{ level2.name }}
{% if level2.children %}
<ul>
{for level3 in level2.children.values %}
<li>{{ level3.name }}</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
If you need to render many levels of the heirarchy, you could consider using template recursion. Have a read of the following question and answers to determine if that might be suitable: Represent a tree of objects in Django template

How to group a model data based on another model and paginate in django?

I have two models:
Category - Zero or multiple books can be in one category
Book - A book can have zero or one category
if I do Book.objects.all() I'll get something like [book11, book10, book9, ...] normally but I don't want that.
What I want is something like:
[
[book11, book2, book1],
[book10],
[book8, book6],
[book7],
[book4, book3],
...
]
Where
Books are grouped according to their category. The books that don't have a category will also include in the list as a single element group
Ordering would be according to book's creation in reverse
For better understanding here is my model structure:
class Category(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
name = models.CharField(max_length=128)
category = models.ForeignKey(Category, on_delete=models.CASCADE,related_name='books', null=True, blank=True)
To do this grouping in Python code you can use itertools.groupby
from itertools import groupby
qs = Book.objects.order_by('category', '-created_at')
grouped_books = groupby(qs, lambda book: book.category)
for category, books in grouped_books:
print(category)
print(list(books))
You can also do this in a template using the regroup tag
In your view pass this queryset to your template context
books = Book.objects.order_by('category', '-created_at')
In your template
{% regroup books by category as category_list %}
<ul>
{% for category in category_list %}
<li>{{ category.grouper }}
<ul>
{% for book in category.list %}
<li>{{ book }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>

Django manytomany field filter list

I am printing a list of beers matching some filters, and the bars where each is on tap. These are in a manytomany relationship. I need to filter this list of bars to only show those in a given state.
I can achieve this using if statements in the template, but then am unable to format the list to use commas with an 'and' before the final item (like https://stackoverflow.com/a/3649002/6180992), as I do not know the length of the list.
I have thought of three ways this might be possible, but cannot get any to work:
filtering the bars related field as well as the beers in the views
assembling the list in the template before looping through again to print it
filtering the bars related field in the template
Here are the relevant sections of code:
models.py
class Bar(models.Model):
bar = models.CharField(max_length=200, default='FinshnPig')
state = models.CharField(max_length=200,default='NY')
def __str__(self):
return self.bar
class Meta:
ordering = ('bar','region')
class Tap(models.Model):
bar = models.ManyToManyField(Bar,default='FinshnPig')
brewery = models.CharField(max_length=200)
beer = models.CharField(max_length=200)
state = models.CharField(max_length=200, default='NY')
def __str__(self):
return self.beer
views.py
f = TapFilter(request.GET, queryset=Tap.objects.filter(state="VIC"))
template:
{% for tap in filter %}
<li>
<b>{{ tap.beer }}</b>
<em>{{ tap.brewery }}</em>
#{% for bar in tap.bar.all %}{% if bar.state == "VIC" %}{{ bar.bar }}</b>{% endif %}{% include "taplists/comma.html" %}{% endfor %}
</li>
{% endfor %}
You can use prefetch for querying only the related bars before sending it to template, like so:
prefetch = Prefetch(
'bar',
queryset=Bar.objects.filter(state=CHOSEN_STATE),
to_attr='selected_states'
)
filter = Tap.objects.filter(state=CHOSEN_STATE).prefetch_related(prefetch)
Now inside your template use the custom attribute you assigned:
{% for tap in filter %}
# Now selected_bars only contains the bars in the state you want
{% for bar in tap.selected_bars.all %}
...
{% endfor %}
{% endfor %}
Additional info on Django docs for prefetch_related
Note: Using prefetch for ManyToMany relations will also increase the performance, as there won't be as many database lookups.

Django regroup many to many field

I'm having some trouble making this work on the template, wonder if I could get some help:
I have a table with plants in it, and the plant table has a many-to-many relationship with the category table. Such that a plant can be in one or more categories.
I'm using Django 1.5, here are the models:
class Plant(models.Model):
scientific_name = models.CharField(max_length=128, unique=True)
category = models.ManyToManyField(Category)
...
class Category(models.Model):
category = models.CharField(max_length=128)
...
And the view:
class PlantListView(ListView):
context_object_name='plant_list'
template_name='plants/index.html'
model = Plant
def get_queryset(self):
return self.model.objects.all().order_by('category')
I've tried this:
{% regroup plant_list by category as category_list %}
<ul>
{% for category in category_list %}
<li>{{ category.grouper }}
<ul>
{% for plant in plant_list.list %}
<li>{{ plant.scientific_name }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
But all I get is:
<django.db.models.fields.related.ManyRelatedManager object at 0x1053bfb90>
<django.db.models.fields.related.ManyRelatedManager object at 0x1053bfbd0>
<django.db.models.fields.related.ManyRelatedManager object at 0x1053bfd50>
Of course, what I really want is:
Annuals
Ageratum houstonianum
Abutilon hybridum
Acalypha hispida
Bulbs Perennial
Ageratum houstonianum
Allium giganteum
Allium karataviense
... etc ...
Note that the same plant can exist in multiple categories, and should be listed in each.
What am I doing wrong?
Thanks in advance!!
Well, turned out it was easier (and probably better) to make a Dict of all the results, with category as the index.

django: child categories and their parents

I've built a website that categorizes products in categories and children (sub) categories.
These are my models:
class Category(models.Model):
name = models.CharField(max_length=250)
parent = models.ForeignKey('self', related_name='children')
...
class Product(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=250)
...
On the view that renders the categories, I have this:
def some_view(request, category):
category_list = Category.objects.filter(parent__isnull=True)
product_list = Product.objects.filter(category=category)
My template shows everything correctly:
<ul>
{% for category in category_list %}
<li>
{{ category.name }}
<ul>
{% for child in category.children.all %}
<li>{{ child.name }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
I can successfully display the categories and their children. My problem is that I'm using sub-categories to further filter down products, but they should also belong to the main category. For example:
Books
--- Comics
--- Sci-Fi
--- ...
Music
--- Classical
--- Pop
--- ...
If I classify a product in "Books > Comics", I will get that product if I select "Comics" in the category listing in my template. But, selecting "Books" should also list that product because it is the top category, but it doesn't show any products unless I categorize them as the parent category "Books".
I'm not sure how to explain this in a better way, but I basically want to be able to show all products that belong to a sub-category, but when I select the main category, that product should be there as well, and I can't seem to make it work. Any suggestions, please?
Off the top off my head, the simplest to implement would be,
change the foreign key from product to category to many-to-many, then override your model save such that it automatically assigns the parent category to the product for every category.
But a better solution would be (i read your comment, im still saying this), implement django-mptt and use south [http://south.aeracode.org/docs/tutorial/part3.html] to handle the datamigration.