Django annotate query set with a count on subquery - django

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.

Related

django-activity-stream with aggregation

I'm using django-activity-stream to create a feed of goals. A simplified version of Goal object is as follows:
class Goal(models.Model):
user = models.ForeignKey(User, related_name="goals", on_delete=models.CASCADE)
liked_by = models.ManyToManyField(User, related_name="goals_cheered")
title = models.CharField(_("title"), max_length=80)
There are 2 actions in place: the goal should appear in the feed once it's created and once it has been completed. Then users can like the goal, and here's where I'm stuck: adding the 'likes' count on the feed.
This was the (failed) attempt that made the most sense to me so far:
from django.db.models import Prefetch
goal_qs = Goal.objects.annotate(likes=Count("liked_by"))
prefetch = [Prefetch("action_object", queryset=goal_qs)]
# in a `group` feed:
qs = group.target_actions.all().prefetch_related("actor__profile").prefetch_related(*prefetch)
This gives me a ValueError: "Custom queryset can't be used for this lookup."
Maybe it's because django-activity-stream uses GFKs?
How can I get a count of likes? Can I limit the content_type somehow?
Update
Managed to get is work by using the {{ goal.liked_by.all|length }} on the template. Not really comfortable with that solution, especially in a ListView. It felt super unoptimized.
This is indeed sub-optimal. But with generic foreign keys I'm afraid you can't do much better. However, {{ goal.liked_by.count }} would still be much more efficient than {{ goal.liked_by.all|length }}.

Is it possible to use Model manager to filter directly from template?

I have created a model manager to be able to filter data
class BOMVersion_default_active_Manager(models.Manager):
def get_queryset(self):
return super(BOMVersion_default_active, self).get_queryset().filter(is_default=True,is_active=True)
#with_author
class BOMVersion(models.Model):
version = IntegerVersionField( )
name = models.CharField(max_length=200,null=True, blank=True)
description = models.TextField(null=True, blank=True)
material = models.ForeignKey(Material)
default_active_objects = BOMVersion_default_active_Manager()
I try to use it in my nested for loop from template ( since I cant filter directly in template and this is how i decided to overcome this limitation)
{% for bomversion in soproduct.product.material.bomversion.default_active_objects_set.all %}
But I am not getting any output. What could be the problem? Can I do it in general?
Using a custom Manager is not the right way to go. You need to carefully read the whole Django Managers article.
In this situation what you need is a custom QuerySet. Something like this:
class BOMVersionQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True)
def default(self):
return self.filter(is_default=True)
#with_author
class BOMVersion(models.Model):
version = IntegerVersionField( )
name = models.CharField(max_length=200,null=True, blank=True)
description = models.TextField(null=True, blank=True)
material = models.ForeignKey(Material)
objects = BOMVersionQuerySet.as_manager()
Now you can use .active() and .default() methods to filter any BOMVersion-QuerySet. This is actually what you got when you use the reverse relation from Material model, material.bomversion_set is a BOMVersion-QuerySet and thus you don't have access to BOMVersion.objects, but since its a BOMVersion-QuerySet you can use .active() and .default()
{% for bomversion in soproduct.product.material.bomversion_set.default.active %}
However, again this is completely wrong and my advice is not to do it in the template. Use the view to build you querysets, and only iterate them in the template.
Why its bad in the template? Because right now you are making 1 query per material object, and there is no way to optimize it unless you use prefetch_related, but guess what? You can't use prefetch_related in the django template system. (Its on purpose, and the reason is to not do stuff like that in the template), so the correct approach is to make something similar to this:
#in the view:
soproduct = SoProduct.objects.select_related('product__material').prefetch_related(
Prefetch(
'product__material__bomversion_set',
queryset=BOMVersion.objects.default().active()
to_attr='default_active_bomversions'
)
).get(pk=soproduct_id)
#in the template:
{% for bomversion in soproduct.product.material.default_active_bomversions %}
Now you are gonna make 1 query for the soproduct with its related product and material data and 1 more query for the requested default_active_bomversions.

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.

Django three way join using Foreign Keys

I recently started evaluating Django for migrating our archaic web application written 10 years ago. I have been reading up Django documentation for the last few days, but haven't been able to figure out the best way to achieve a multi table database join in my case:
Model:
class Product(models.Model):
productid = models.IntegerField(primary_key=True, db_column='ProductId')
productname = models.CharField(max_length=120, db_column='ProductName')
class Testcases(models.Model):
testcaseid = models.IntegerField(primary_key=True, db_column='TestCaseId')
testcasename = models.CharField(max_length=240, db_column='TestCaseName')
class Testmatrix(models.Model):
testmatrixid = models.IntegerField(primary_key=True, db_column='TestMatrixId')
productid = models.ForeignKey(Product, db_column='ProductId')
testcaseid = models.ForeignKey(Testcases, db_column='TestCaseId')
class Status(models.Model):
testmatrixid = models.ForeignKey(Testmatrix, db_column='TestMatrixId')
title = models.CharField(max_length=240, db_column='Title', blank=True)
(Note that model was generated by inspectdb and I'd prefer not to modify it at this point in time)
View:
from django.shortcuts import render_to_response
from mysite.testmatrix.models import Product, Testcases, Testmatrix, Status
def get_products(request):
tm = list(Testmatrix.objects.filter(productid='abc'))
return render_to_response('products.html', {'tm': tm})
template is designed to be minimal at this point to help focus on the real issue in (views/model).
Template: (products.html)
{% extends "main.html" %}
{% block body %}
<table>
{% for tm in tm %}
<tr>
<td>{{ tm.testmatrixid }}</td>
<td>{{ tm.testcaseid.testcasename }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
Problem:
Although I'm able to join Testmatrix and Testcase models, I am unable to generate an equivalent queryset by joining all of TestMatrix, TestCase, Status records on say productid='abc'
I tried the following:
1) Use select_related between Testmatrix and Testcases and Product tables and was able to access attributes across all three models (testmatrixid, productid, productname, testcaseid, testcasename). However I'm not sure how to extend this auto foreign key referencing to Status model. This would have been easier if all Foreign Keys were defined within Testmatrix itself. But Status has a Foreign Key to TestMatrix.
2) I tried using something like: entries = Status.objects.filter(testmatrixid__productid=pid). This again gave me a queryset as a result of joining Testmatrix and Status, but not Testcases.
Pardon any blaring mistakes or bloopers. This is my very first post!
So you need to access a related_object. It is very simple.
First, add related_name here:
class Status(models.Model):
testmatrixid = models.ForeignKey(Testmatrix, db_column='TestMatrixId', related_name='statuses')
Now you can get all the statuses for desired Testmatrix like
test_matrix.statuses.all()
If you don't want to hit DB, when you access statuses, don't forget to use select_related.
Without any specific error messages, it is hard to diagnose the cause of said errors. However, in your example, in views.get_products: tm = list(Testmatrix.objects.filter(productid='abc')) will not work, because 'abc' is a string and your productid is actually a Product object (not just an integer, even though the field is an integer foreign key to your reference table's pk); you can do tm = list(Testmatrix.objects.filter(productid=Product.objects.get(product_name='abc')), assuming that 'abc' is the product name of the product record. When you set a field to models.ForeignKey(...), you address that reference record as an object, not an id.
Other than that, nothing blaring, your template looks solid and your models look fine to me. I would suggest creating some test cases to see where the errors lie: Django Testing; Also, this is also a great tutorial to understand TDD and unit testing with Django. Using unittests, you can verify every step of your application and make future updates with assurance.

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