Displaying a sorted list with ManyToMany relation - django

I have a certain number ob Subjects and Users. An user can vote for one or more subjects. I'd like to make a view displaying the top 10 Subjects sorted by count of votes in descanding order.
In fact, I found a working way, but I'm asking myself if there is more suitable way to do that.
I started with models:
class Subject(models.Model):
text = models.TextField()
class User(models.Model):
name = models.CharField(max_length=64)
subject = models.ManyToManyField(Subject)
now I can type:
Subject.user_set.all()
and I will get those users who has voted for the subject.
Now I would like to make a list view, where top 10 subjects would be displayed sorted by the number of votes.
So I added a class method to the Subject class:
class Subject(models.Model):
text = models.TextField()
#staticmethod
def by_votes():
myList = list(Subject.objects.all())
return sorted(myList, key=lambda s: s.user_set.count(), reverse=True)[:10]
and defined that class based view:
class SubjectListView(ListView):
model = Subject
template_name = "subject_list.html"
context_object_name = "subject_list"
queryset = Subject.by_votes()
Which actually works well.
But I saw in the docs Manager and QuerySet classes, unfortunately I did not quite understand how to use them (defining custom ones) to get what I'm looking for (without dealing with raw SQL queries).
I'm little bit afraid, because I used a list I could run into memory problems having large number of subjects.
What would you say, would it be more suitable to use custom Manager oder QuerySet for that, wouldn't?
If yes, how to do it?
Any other alternative ideas?
Thanks!
Peter

No need for custom managers or raw SQL here, just aggregation.
from django.db.models import Count
Subject.objects.annotate(user_count=Count('user').order_by('-user_count')[:10]

I'm fairly new to this but I solved a similar problem with this:
Subject.objects.all().annotate(count=Count('user')).order_by('-count')[:10]
Aggregation Doc

Related

dynamiclly count vs database record

I have a post model as below, now I use number_of_likes to record the liked post number. If so, I have to manually maintain the number_of_likes field.
Now, I add this field in post mainly two reasons, and I would like to hear your advice.
it is easy to write serialisation using declarative syntax(every post need this)
I don't need to filter and count on model Like, which is more expensive than just get this value from field
class Post(models.Model):
...
number_of_likes = models.IntegerField()
class Like(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
I would like to know which method is better, using Like.objects.filter(user=user).count() or maintain a new field such as number_of_likes.If choose later, what is the best way to maintain this field
As #WillemVanOnsem suggested, best way to display this data is by annotation. For example:
from django.db.models import Count
posts = Post.objects.annotate(num_of_likes=Count('like'))
# usage
for post in posts:
print(post.num_of_likes)
# or
posts.values('pk', 'num_of_likes')

Filter and count with django

Suppose I have a Post and Vote tables.
Each post can be either liked or disliked (this is the post_type).
class Post(models.Model):
author = models.ForeignKey(User)
title = models.CharField(verbose_name=_("title"), max_length=100, null=True, blank=True)
content = models.TextField(verbose_name=_("content"), unique=True)
ip = models.CharField(verbose_name=_("ip"), max_length=15)
class Vote(models.Model):
user = models.ForeignKey(User)
post = models.ForeignKey(Post)
post_type = models.PositiveSmallIntegerField(_('post_type'))
I want to get posts and annotate each post with number of likes.
What is the best way to do this?
You should make a function in Post model and call this whenever you need the count.
class Post(models.Model):
...
def likes_count(self):
return self.vote_set.filter(post_type=1).count()
Use it like this:
p = Post.objects.get(pk=1)
print p.likes_count()
One approach is to add a method to the Post class that fetches this count, as shown by #sachin-gupta. However this will generate one extra query for every post that you fetch. If you are fetching posts and their counts in bulk, this is not desirable.
You could annotate the posts in bulk but I don't think your current model structure will allow it, because you cannot filter within an annotation. You could consider changing your structure as follows:
class Vote(models.Model):
"""
An abstract vote model.
"""
user = models.ForeignKey(User)
post = models.ForeignKey(Post)
class Meta:
abstract = True
class LikeVote(Vote)
pass
class DislikeVote(Vote)
pass
i.e., instead of storing likes and dislikes in one model, you have a separate model for each. Now, you can annotate your posts in bulk, in a single query:
from django.db.models import Count
posts = Post.objects.all().annotate(Count('likevote_set'))
for post in posts:
print post.likevote__count
Of course, whether or not this is feasible depends on the architecture of the rest of your app, and how many "vote types" you are planning to have. However if you are going to be querying the vote counts of posts frequently then you will need to try and avoid a large number of database queries.

Django many-to-many lookup from different models

I have some models that represents some companies and their structure. Also all models can generate some Notifications (Notes). User can see own Notes, and, of course, can't see others.
class Note(models.Model):
text = models.CharField(...)
class Company(models.Model):
user = models.ForeignKey(User)
note = models.ManyToManyField(Note, blank='True', null='True')
class Department(models.Model):
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
class Worker(models.Model):
department = models.ForeignKey(Department)
note = models.ManyToManyField(Note, blank='True', null='True')
class Document(models.Model)
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
The question is how I can collect all Notes for particular user to show them?
I can do:
Note.objects.filter(worker__company__user=2)
But its only for Notes that was generated by Workers. What about another? I can try hardcoded all existing models, but if do so dozen of kittens will die!
I also tried to use backward lookups but got "do not support nested lookups". May be I did something wrong.
EDIT:
As I mentioned above I know how to do this by enumerating all models (Company, Worker, etc. ). But if I will create a new model (in another App for example) that also can generate Notes, I have to change code in the View in another App, and that's not good.
You can get the Notes of a user by using the following query:
For example let us think that a user's id is 1 and we want to keep it in variable x so that we can use it in query. So the code will be like this:
>>x = 1
>>Note.objects.filter(Q(**{'%s_id' % 'worker__department__company__user' : x})|Q(**{'%s_id' % 'document__company__user' : x})|Q(**{'%s_id' % 'company__user' : x})|Q(**{'%s_id' % 'department__company__user' : x})).distinct()
Here I am running OR operation using Q and distinct() at the end of the query to remove duplicates.
EDIT:
As I mentioned above I know how to do this by enumerating all models
(Company, Worker, etc. ). But if I will create a new model (in another
App for example) that also can generate Notes, I have to change code
in the View in another App, and that's not good.
In my opinion, if you write another model, how are you suppose to get the notes from that model without adding new query? Here each class (ie. Department, Worker) are separately connected to Company and each of the classes has its own m2m relation with Note and there is no straight connection to User with Note's of other classes(except Company). Another way could be using through but for that you have change the existing model definitions.
Another Solution:
As you have mentioned in comments, you are willing to change the model structure if it makes your query easier, then you can try the following solution:
class BaseModel(models.Model):
user = models.Foreignkey(User)
note = models.ManyToManyField(Note)
reports_to = models.ForeignKey('self', null=True, default=None)
class Company(BaseModel):
class Meta:
proxy = True
class Document(BaseModel):
class Meta:
proxy = True
#And so on.....
Advantages: No need to create separate table for document/company etc.
object creation:
>>c= Company.objects.create(user_id=1)
>>c.note.add(Note.objects.create(text='Hello'))
>>d = Document.objects.create(user_id=1, related_to=c)
>>d.note.add(Note.objects.create(text='Hello World'))

Model "Behaviors" in Django

I'm new to Django, moved from PHP with Propel ORM engine.
So here is what I am currently doing in Django. My website has several models like
Book,
Publisher,
Comment,
Article and so on (it's not the point)
Each of them can can be
liked or disliked by a user (only once) changing the model's rating by +1 or -1.
In terms of PHP i would create a behavior, for ex. "Rateable" which would add some fields and methods to the original model and query class (like get_rating(), order_by_rating(), etc) and create a separate table for each model, for ex. book_rating_history which would hold all of the ratings for each object, to determine if the user can or can't change the rating (or show all object's ratings, if necessary). So all I would need to do is to specify the "Rateable" behavior in the model declaration, and that's all. Everything else is done automatically.
The question is - how to solve this in Django? How to model correctly? Which techniques do you use in similar cases?
You'll want to store ratings and books separately, something like this (untested).
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Sum
class BookRating(models.Model):
user = models.ForeignKey(User)
book = models.ForeignKey('Book')
# you'll want to enforce that this is only ever +1 or -1
rating = models.IntegerField()
class Meta:
unique_together = (('user', 'book'),)
class Book(models.Model):
title = models.CharField(max_length = 50)
def rating(self):
return BookRating.objects.filter(book = self).aggregate(Sum('rating'))
unique_together enforces that each user can only rate a given book once.
You can then use this something like:
book = Book.objects.get(pk = 1)
rating = book.rating()
Add a comment if you have problems with this - I've not tested it, but hopefully this is enough to get you started.
You can probably avoid each object (books, publishers, comments, articles) having a separate rating object using content types, if you want.
Alternatively, you might consider looking at existing reusable apps that handle liking, like django-likes or phileo.
You can define special methods (for example vote, get_rating, etc.) only onсe in abstract model and then create your "Rateable" models using this one.
class Rateable(models.Model):
class Meta:
abstract = True
def vote(self, *args, **kwargs):
...
def rating(self, *args, **kwargs):
...
class Book(Rateable):
...
Also it is better to use single model for storing rating data witch content types as noticed Dominic Rodger

How to fetch the top two products for each product type?

Let's say I have two models looking like this:
class ProductType(models.Model):
product_type = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
product_type = models.ForeignKey(ProductType)
score = models.PositiveIntegerField(default=0)
Now I want to fetch the top two products (the two with highest score) from each ProductType.
So if I have Phones, Computers, TVs as ProductTypes I want the two best Phones, Computers, TVs.
As I don't even know how to do this in MySQL I've tried searching for the MySQL-solution but the ones I find is extremely complex and this doesn't feel like an extremely complex thing to do.
I am leaning towards making an own model for the top products and have a cronjob to fix this, but I'd like to see if there's an easier solution to this.
Well, you could make a method inside of the ProductType class that returns the top two results for its products:
class ProductType(models.Model):
product_type = models.CharField(max_length=100)
def TopTwoProducts(self):
return self.product_set.order_by('-score')[0:2]
Then you would just do something like:
for type in ProductType.objects.all():
type.TopTwoProducts()
While adam's solution is correct, a more django-ish way would be to use a manager.
See Managers versus class methods on James Bennett's blog
Among other advantages :
a manager carries all query-specific code, while avoiding to clutter the model class
the same manager class can be shared among several classes
the manager can be used directly on a model class, or via a one-to-many or m2m relation
Thus, for the above question :
from django.db import models
from django.db.models.manager import Manager
class ProductScoreManager(Manager):
use_for_related_field = True
def top_score(self, limit=2):
return self.get_query_set().order_by('-score')[:limit]
Then add this manager class as default manager for Product :
class Product(models.Model):
...
objects = ProductScoreManager()
...
Now, thanks to objects overriding the default manager, and use_for_related_field allowing its use in relation queries, the top_score method can be used in any model related to products.
myproducttype = ProductType.objects.get(...)
myproducttype.products.top_score() # return top 2 products
This allows a more consistent syntax : the related products is always accessed via products, top_score acting as a filter. Additionally, ProductType class is no more cluttered with Product's query logic.
Just filter out product type from Product model slice them like this -
product_type_list = ProductType.objects.value("id")
for product_type in product_type_list:
Product.objects.filter(
product_type=product_type
).order_by("-score")[0:2]