m2m through : accessing intermediate table from templates - django

When a ManyToMany relationship has extra data via a through table, how can you get to the data in a template? From a view I can get the data if I supply parameters:
class Category(models.Model):
title = models.CharField(max_length=1024,null=True,blank=True)
entry = models.ManyToManyField(Entry,null=True,blank=True,
related_name='category_entry',
through='CategoryEntry',
)
class CategoryEntry(models.Model):
category = models.ForeignKey(Category)
entry = models.ForeignKey(Entry)
votes = models.IntegerField(null=False, default=0)
def category_detail(request, pk):
category = models.Category.objects.select_related().get(pk=pk)
entries = category.entry.order_by('-temp_sort_order').filter(temp_sort_order__gte=0)
for entry in entries:
assert isinstance(entry, models.Entry)
ce = models.CategoryEntry.objects.get(entry=entry, category=category)
pprint('Show votes as a test: ' + ce.votes) #OK
pprint('entry title: ' + entry.title) #OK
pprint('entry votes: ' + str(entry.category_entry.votes)) #BAD
pprint('entry votes: ' + str(entry.entry.votes)) #BAD
....
But templates can't supply parameters to methods.
The documentation at https://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships is silent on templates. Using using for entry in category.category_entry_set.all gives 'Category' object has no attribute 'category_entry_set'. category.category_entry.all does not work either.
Ultimately I want to display the extra data in a template:
{% for entry in entries %}
<ul>
<li>Title: {{ entry.title }} Votes: {{ entry.category_entry.votes }} {{ entry.entry.votes }}</li>
</ul>
{% endfor %}

If you have a category instance in template:
category.entry.all -> list of entries
If you have an entry instance in template:
entry.category_entry.all -> list of categories
You should call M2M fields in plural form,
then you will have a more readable code
category.entries.all
%model%_set syntax (or related name, if you've specified it) is using to access to model trough a backward relationship.
https://docs.djangoproject.com/en/1.4/topics/db/queries/#following-relationships-backward
But how do I get the 'votes' associated with the m2m instance? – Bryce
I suggest you the following way:
class Category(models.Model):
title = models.CharField(max_length=1024,null=True,blank=True)
entries = models.ManyToManyField(Entry,null=True,blank=True,
related_name='categories',
through='CategoryEntry',
)
class CategoryEntry(models.Model):
category = models.ForeignKey(Category, related_name='category_entries')
entry = models.ForeignKey(Entry)
votes = models.IntegerField(null=False, default=0)
def category_detail(request, pk):
category = models.Category.objects.select_related().get(pk=pk)
category_entries = category.category_entries.filter(entry__temp_sort_order__gte=0).order_by('-entry__temp_sort_order')
for category_entry in category_entries:
# category_entry is an instance of the model CategoryEntry
pprint('category entry votes: ' + str(category_entry.votes))
pprint('entry title: ' + category_entry.entry.title)
....
HOW TO
entry = Entry.objects.get(pk=1)
entry.categories.all() # list of categories (here we work through related name of the field entries)
category = Category.objects.get(pk=1)
category.entries.all() # list of entries (here we work through m2m field entries)
category.category_entries.all() # list of CategoryEntry objects (through related name category_entries of the field category in model CategoryEntry)

Updating my answer, i mistakenly put related manager on wrong model, in your case, like Andrey said, the correct way to get entries from category is:
category.entry.all()
Now, to address your iteration and ordering question. In python it will look like this:
for ce in category.categoryentry_set.order_by('-votes'):
print ce.entry, ce.votes
This will give you entries in each category ordered by votes. To get this to template you can just save a queryset category.categoryentry_set.order_by('-votes') into variable and iterate over it.

Here's an ugly ugly hack that works. After the filter and sort, process the list and append the extra model fields. The templates now have easy access:
entries = category.entry.order_by('-temp_sort_order').filter(temp_sort_order__gte=0)
for entry in entries:
assert isinstance(entry, models.Entry)
ce = models.CategoryEntry.objects.get(entry=entry, category=category)
entry.xxx_votes = mark_safe(ce.votes) # use {{ entry.xxx_votes to access }}
entry.xxx_ce = ce # Use {{ entry.ce.votes to access }}
return render_to_response('category.html')
Hopefully someone can provide a better answer, or suggest an improvement to django itself. This solution does not allow me to sort: category.entry.order_by('-category_entry.votes')

Related

Prefetch related starting from a single object - geting first in second prefetch and count and order

I have 3 Models Product,Company Categories.
class Product(Meta):
categories = models.ManyToManyField(Category)
company = models.ForeignKey(Company, related_name='products', on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now_add=False, auto_now=True)
I need:
to get all the products of a company
show the product first category
count the number products per company and show
order products by reverse updated_at
I start from:
1. Company.objects.get(pk=company_pk).prefetch_related('products')
will give me an error, because get returns an object:
class CompanyProductListView(ListView):
model = Company
template_name_suffix = '_company_list'
def get_queryset(self):
company_pk = self.kwargs.get('pk')
return Company.objects.get(pk=company_pk).prefetch_related('products')
get without prefetch works.
return Company.objects.filter(pk=company_pk).prefetch_related('products')
there is no error, but in template:
{% for company in company_list %}
{{ company.name}}
{% endfor %}
I loop even is one, but doesn't show me anything.
Besides that I need to attach first category to each product, and count the number of products
I'm thinking on access something like this:
{{company.name}}
{% for product in company.products %}
{{ product.name }}
{{ product.category }}
This query will get a little complicated, but should help you solve your issue.
PS: I haven't tested this but should mostly work. Will take a deeper look once I get some more time.
First we get the company we want:
company = Company.objects.get(pk=company_pk)
Then we fetch all the first categories for all products, it can be done by using this question as a guide:
first_categories = Category.objects.order_by('product__id', '-id').distinct('product__id')
Now we use the first_categories to use to limit the amount of data we prefetch (giving this a different perspective, we will query the Product model instead of the Company model)
product_list = Products.objects.filter(company=company).prefetch_related(
Prefetch('categories', queryset=first_categories)
)
def get_queryset():
company_pk = ...
company = ...
first_categories = ...
product_list = ...
return product_list

Django 1.7.7 get object count in template through double relation

I have 3 models, Entry model and Category model, and I have created intermediate model CategoryEntry.
class Entry(models.Model):
entry_text = models.TextField()
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, related_name="related_entry_categories")
entry = models.ForeignKey(Entry)
viewed = models.BooleanField(default=False)
How can I get in template Users total Entry count.
For example I can get total users Category count with
{{ user.category_set.count }}
So I tried many different ways, but don't get how to follow next relation
{{ user.category_set.entries.count}}
{{ user.category_set.categoryentry_set.count}}
{{ user.category_set.all.categoryentry_set.count}}
{{ user.category_set.related_entry_categories.count }}
Is this even possible (good thing to do) to count in template? Or is there better way?
Thanks!
your queries don't make sense because category_set is a collection of objects rather than a single object, so you cannot simply ask for category_set.entries.count
first you have to think about what you want... do you want:
individual count of entries for each category in category_set?
or total count of entries across all categories in category_set?
For the former you need to annotate the queryset. this will have to be done in the view rather than template because the method needs arguments:
from django.db.models import Count
user_categories = user.category_set.annotate(entry_count=Count('entries'))
# then pass the user_categories queryset into your template along with user
you can then iterate over user_categories in the template to display individual counts:
{% for category in user_categories %}
No. of entries: {{ category.entry_count }}
{% endfor %}
For the latter you can use aggregate, again in the view:
from django.db.models import Count
total = user.category_set.aggregate(entry_count=Count('entries'))
# note that aggregate method returns a dict:
print total['entry_count']
# then pass total['entry_count'] as a value into your template along with user

Django - show in template related class count filtered by parameter

I will give my models first and then write description.
class Entry(models.Model):
entry_text = models.TextField()
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)
So I have Entry model and Category model, and I have created intermediate model CategoryEntry as descriebed here https://docs.djangoproject.com/en/1.7/topics/db/models/#extra-fields-on-many-to-many-relationships because I need one extra field "viewed" (marked as True when user for the first time opens specific Entry link).
So I have created generic.ListView view, where I show all these categories that user has created for himself. What I want, is to show next to every category name, how many entries there are and how many entries he hasn't viewed yet.
Like:
Category Total Not_viewed
AAA 126 5
BBB 17 15
I have managed to show total entries in template by
{% for category in categories %}
{{ category.text }}
{{ category.entries.count }}
{% endfor %}
In my view I have get_queryset like
def get_queryset(self):
categories = Category.objects.filter(user=self.request.user.id)[:]
return categories
As I understand, then the best way would somehow add this extra info about every categories entries viewed count in get_queryset. I have searched around but didn't found anything what works. Have tried some things with select_related, prefetch_related, annotate but don't get whats the right way to do this.
Know that it's not right, but tried something like that and some other things.
categories = Category.objects.filter(user=self.request.user.id).select_related('categoryentry').filter(categoryentry__viewed=False).count()
categories = Category.objects.filter(user=self.request.user.id).annotate(not_viewed_count=Count('categoryentry')).filter(not_viewed_count__viewed=False)
Hope you get my idea what I wan't to achieve.
In your CategoryEntry model, use related_name in the category field like so:
category = models.ForeignKey(Category, related_name="related_entry_categories")
Now you can use this related name when querying the Category model. For example:
from itertools import chain
categories_not_viewed = Category.objects.filter(user=self.request.user.id, related_entry_categories__viewed=False).annotate(num_not_viewed=Count('related_en‌​try_categories'))
categories_viewed = Category.objects.filter(user=self.request.user.id, related_entry_categories__viewed=True).extra(select={'num_not_viewed': 0})
categories = chain(list(categories_not_viewed), list(categories_viewed))
At end I came up with this solution:
categories = Category.objects.filter(user=self.request.user.id).extra(select = {
"num_not_viewed" : """
SELECT COUNT(*)
FROM app_categoryentry
WHERE app_categoryentry.category_id = app_category.id
AND app_categoryentry.viewed = %d """ % 0,
})
Based on the solution from this resource http://timmyomahony.com/blog/filtering-annotations-django/
If anyone have other solution how the get the same result with only Django ORM, I would like to know.

How do I get the foreign key values of a form in a template?

So Im trying to create a similar panel like django admin and while I have my tables set up with query information and filters, Im trying to copy the "editable" option by creating fields that can be edited in the table. My problem is lets say I have models:
class model1(model.Models):
name = CharField(max_length = 20)
phone = IntegerField(max_length = 20)
def __unicode__(self):
return (#I Just return the name and phone number as a string here)
class model2(model.Models):
name = ForeignKeyFeild(model1)
team = CharField(max_length = 20)
If I set up a modelformset factory like so in a view:
qset = model2.objects.all()
fset = modelformset_factory(model2)
form = fset(queryset = qset)
In templates how can I show the value of 'phone' from model1?
When I render the form in the template:
{% for f in form %}
{{f.team.value}} # this gives me the value without an input field
{{f.name.phone.value}} # this however renders nothing and that's what I'm trying to find out
{%endfor%}
Also How can I make it so that {{f.name.value}} shows me the unicode string and not the actual numerical value (ID)?
How can I make the {{f.name.phone}} which is information from the table where it holds a foreign_key show?
If not is there a better way of doing this?
The original model object is available as the instance attribute of the form:
{% for f in form %}
{{ f.instance.team }}
{{ f.instance.name.phone }}
{% endfor %}

Django left join m2m field

Here's my Model:
class User(models.Model):
pass
class Item(models.Model):
pass
class ItemVote(models.Model):
user = models.ForeignKey(User)
item = models.ForeignKey(Item)
vote = models.BooleanField()
I want to retrieve a list of Items, and I want to know if the current user has voted for each Item. How do I alter my query object so that it will generate sql similar to:
SELECT ...
FROM items
LEFT OUTER JOIN item_votes ON (item_votes.user_id = ? AND
item_votes.item_id = items.id)
You cannot in plain django queries. This is a many to many relation ship, you can even specify it more clearly by doing something like this:
class Item(models.Model):
votes = models.ManyToManyField(User, through='ItemVote', related='votedItems')
In a Many To Many relationship, we can speak of related sets (because there are multiple objects). While django can filter on related sets, something like:
Item.objects.filter(votes=request.user)
this will return you all Items that a user has voted for. However when you will list the .votes attribute in those objects, you will get all users that have voted for that item. the filtering is for the original object, never for the related set.
In a purely django way, you can expand my previous Item class to:
class Item(models.Model):
votes = models.ManyToManyField(User, through='ItemVote', related='votedItems')
def markVoted(self, user):
self.voted = user in self.votes
And then call this method for every object. This will however create an additional query to the votes set for every object (and no, select_related does not work for many to many relationships).
The only way to solve this is to add some SQL to your queryset using the extra method, like this:
Item.objects.extra('voted' : 'SELECT COUNT(*) FROM app_itemvote WHERE app_itemvote.item_id = app_item.id AND app_itemvote.user_id=%d'%request.user.pk)
Return ItemVote items:
items = ItemVote.objects.filter(user=user)
use in Django template:
{% for i in items %}
item: {{ i.item }}
voted: {{ i.voted }}
{% endfor %}