Django left join m2m field - django

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 %}

Related

Django how to select with less queries in a many to many relationship

I have the following models,
class TblMaterials(models.Model):
name = ...
....
class TblCategoris(models.Model):
name = ...
....
class TblMaterialCategories(models.Model):
tbl_categories = models.ForeignKey(TblCategories, blank=True, null=True)
tbl_materials = models.ForeignKey('TblMaterials', blank=True, null=True)
and in my home page i want to print all the materials and related material category within. Obviously there could be some materials without any categories.
in my view i try something like:
TblMaterials.objects.all().select_related('tblmaterialcategories_set')
and in templates:
{%for mat in materials%}
{{mat.name }}
{%for cat in mat.tblmaterialcategories_set.all %}
{{cat.tbl_categories.name}} ,
{%endfor%}
{%endfor%}
I dont think select_related works in that _set item.
I want to achieve something like that without making queries for each item. If I can add another field to material query set and access it like mat.categories and for loop in it, it is also appriciated.
What could be the best way to display all materials and their categories?
Thanks.
Select related can be used for the foreign keys which are in the current model. For an example:
class TblMaterials(models.Model):
name = ...
....
Foreign Key: TblMaterialCategories
While calling the TblMaterial model , if you want to use select related with TblMaterialCategories
TblMaterials.objects.all().select_related('tblmaterialcategories')
It will work.

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.

m2m through : accessing intermediate table from templates

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')

Django annotate query set with a count on subquery

This doesn't seem to work in django 1.1 (I believe this will require a subquery, therefore comes the title)
qs.annotate(interest_level= \
Count(Q(tags__favoritedtag_set__user=request.user))
)
There are items in my query set which are tagged and tags can be favorited by users, I would like to calculate how many times a user had favorited each item in the set via tags.
is there a way to construct a query like this without using extra()?
Thanks.
Looking at the add_aggregate function within django/db/models/sql/query.py, query objects will not be accepted as input values.
Unfortunately, there is currently no direct way within Django to aggregate/annotate on what amounts to a queryset, especially not one that is additionally filtered somehow.
Assuming the following models:
class Item(models.Model):
name = models.CharField(max_length=32)
class Tag(models.Model):
itemfk = models.ForeignKey(Item, related_name='tags')
name = models.CharField(max_length=32)
class FavoritedTag(models.Model):
user = models.ForeignKey(User)
tag = models.ForeignKey(Tag)
Also, you cannot annotate a queryset on fields defined via .extra().
One could drop into SQL in views.py like so:
from testing.models import Item, Tag, FavoritedTag
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from django.utils.datastructures import SortedDict
#login_required
def interest_level(request):
ruid = request.user.id
qs = Item.objects.extra(
select = SortedDict([
('interest_level', 'SELECT COUNT(*) FROM testing_favoritedtag, testing_tag \
WHERE testing_favoritedtag.user_id = %s \
AND testing_favoritedtag.tag_id = testing_tag.id \
AND testing_tag.itemfk_id = testing_item.id'),
]),
select_params = (str(ruid),)
)
return render_to_response('testing/interest_level.html', {'qs': qs})
Template:
{% for item in qs %}
name: {{ item.name }}, level: {{ item.interest_level }}<br>
{% endfor %}
I tested this using MySQL5. Since I'm no SQL expert though, I'd be curious as to how to optimize here, or if there is another way to "lessen" the amount of SQL. Maybe there is some interesting way to utilize the related_name feature here directly within SQL?
If you want to avoid dropping to raw SQL, another way to skin this cat would be to use a model method, which will then give you a new attribute on the model to use in your templates. Untested, but something like this on your Tags model should work:
class Tag(models.Model):
itemfk = models.ForeignKey(Item, related_name='tags')
name = models.CharField(max_length=32)
def get_favetag_count(self):
"""
Calculate the number of times the current user has favorited a particular tag
"""
favetag_count = FavoritedTag.objects.filter(tag=self,user=request.user).count()
return favetag_count
Then in your template you can use something like :
{{tag}} ({{tag.get_favetag_count}})
The downside of this approach is that it could hit the database more if you're in a big loop or something. But in general it works well and gets around the inability of annotate to do queries on related models. And avoids having to use raw SQL.