Create query between ManyToMany relationship models in Django ORM - django

class Review(models.Model):
student = models.ForeignKey(UserDetail)
text = models.TextField()
created_at = models.DateTimeField(auto_now=True)
vote_content = models.FloatField()
vote_knowledge = models.FloatField()
vote_assignment = models.FloatField()
vote_classroom = models.FloatField()
vote_instructor = models.FloatField()
class UserDetail(models.Model):
username = models.CharField(max_length=100)
email = models.CharField(max_length=255)
...
class Course(models.Model):
title = models.CharField(max_length=255)
studentlist = models.ManyToManyField(UserDetail, related_name='course_studentlist', blank=True)
reviewlist = models.ManyToManyField(Review,related_name='course_reviewlist', blank=True)
...
In the above model structure, the Course model has a relationship with UserDetail and Review with ManyToMany.
The review is based on the average of the 5 votes. (content, knowledge etc.)
A review of a course is the average of the votes of the students who take the course.
I would like to make a search and sort according to Course's review, for example, list bigger than 3 votes.
Thanks for your help.

The easiest and probably the cleanest solution is to create additional field for storing average score in Review and calculate it on save().
Is there a reason why you keep course reviews as m2m field? Do you allow the same review, with the same text etc. to be used in many courses? Maybe you need ForeignKey in this case. Then you could just do:
class Review(models.Model):
...
vote_avg = models.FloatField()
course = models.ForeignKey('Course')
...
def save(self, *args, **kwargs):
self.voce_avg = (self.vote_content + ...) / 5
super(Review, self).save(*args, **kwargs)
def foo():
return Course.objects.prefetch_related('review_set').annotate(
avg_reviews=Avg('review__vote_avg')
).filter(avg_reviews__gt=3).order_by('avg_reviews')

Try this:
from django.db.models import Count
Course.objects.annotate(reviews_count=Count('reviewlist')).filter(reviews_count__gt=3)

Related

Django Many To Many Ordering

I have two tables Subjectlist and Day. Subject list is m2m in Day. So my problem is I'm creating school timetable. So for each days different subjects to be shown, when i add subjects on each days the order of subject is same.
#Models.py
class SubjectList(models.Model):
subject_name = models.CharField(max_length=25)
def __str__(self):
return self.subject_name
class Day(models.Model):
day_name = models.CharField(max_length=15)
subject_name = models.ManyToManyField(SubjectList)
class_number = models.ForeignKey(AddClass, on_delete=models.CASCADE, null=True, blank=True)
start_time = models.TimeField(null=True, blank=True)
end_time = models.TimeField(null=True, blank=True)
def __str__(self):
return self.class_number.class_number
#Views.py
class TimeTableView(APIView):
def get(self, request, id):
class_number = AddClass.objects.get(id=id)
day = Day.objects.filter(class_number=class_number.id)
print(day)
serializer = DaySerializer(day, many=True)
return Response(serializer.data)
I want to do like this
Monday - English, maths, science, Social Science
Tuesady - Maths, Social Science, Englih, Math's
but i get like this
Monday - English, maths, science, Social Science
Tuesday- English, maths, science, Social Science
both are in same order even if add subjects in different order.
You can add more fields to the M2M table by declaring a model and assigning it to the M2M relation with the through parameter
class DaySubjectList(models.Model):
day = models.ForeignKey(Day, on_delete=models.CASCADE, related_name="day_subject_lists")
subject_list = models.ForeignKey(SubjectList, on_delete=models.CASCADE, related_name="day_subject_lists")
order = models.IntegerField(default=0)
class Day(models.Model):
day_name = models.CharField(max_length=15)
subject_name = models.ManyToManyField(SubjectList, through=DaySubjectList)
class_number = models.ForeignKey(AddClass, on_delete=models.CASCADE, null=True, blank=True)
start_time = models.TimeField(null=True, blank=True)
end_time = models.TimeField(null=True, blank=True)
def __str__(self):
return self.class_number.class_number
You can set the order field using the through_defaults parameter of the related manager
my_day.subject_name.add(my_subject, through_defaults={"position": my_position})
You can order using related_name
Day.objects.filter(class_number=class_number.id).prefetch_related(Prefetch("day_subject_lists", queryset=DaySubjectList.objects.order_by("position")))
Records will only be ordered if you access my_day.day_subject_lists.all() not my_day.subject_name.all()
You can store those records under another name if thats more convenient
Day.objects.filter(class_number=class_number.id).prefetch_related(Prefetch("day_subject_lists", queryset=DaySubjectList.objects.order_by("position"), to_attr="my_prefered_name"))
be aware that my_day.my_prefered_name is a list not a queryet so don't use .all() to access it.
Use you have any issue with your models referencing each other you can use string synthax
class DaySubjectList(models.Model):
day = models.ForeignKey("myapp.Day", on_delete=models.CASCADE, related_name="day_subject_lists")
subject_list = models.ForeignKey("myapp.SubjectList", on_delete=models.CASCADE, related_name="day_subject_lists")
order = models.IntegerField(default=0)
You need to specify an order for the m2m relation [Django-doc]:
class Day(models.Model):
# ...
subject_name = models.ManyToManyField(
SubjectList,
ordered=True
)
# ...
Now if you query a Day object, the related SubjectList objects will be ordered in the order you added these.
You can further alter the order with .move(...), or .reorder(...).
Note: You should rename the subject_name field to subjects, since it relates to multiple SubjectList objects. A ForeignKey or OneToOneField indeed often uses the _name suffix, since these relate to at most one other object, but a ManyToManyField does not have this limitation.
Note: You should not use ForeignKey(.., null=True)s. A foreign key represents a relation, and a relation can not be "NULL". You should make these fields not nullable, and make the related model have a nullable parent relation (with null=True), or make the field optional with blank=True.

Django Sum in Annotate

Good afternoon,
I am really struggling with getting a sum using Annotate in DJango.
I am using User object and the following models:
class Depts(models.Model):
dept_name = models.CharField(max_length=55)
dept_description = models.CharField(max_length=255)
isBranch = models.BooleanField(default=False)
def __str__(self):
return "{}".format(self.dept_name)
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile')
title = models.CharField(max_length=75)
dept = models.ForeignKey(Depts, on_delete=models.CASCADE, related_name="dept", null=True)
class ActivityLog(models.Model):
activity_datetime = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='activity_user')
activity_category = models.ForeignKey(ActivityCategory, on_delete=models.CASCADE, null=True, related_name='activity_cat')
activity_description = models.CharField(max_length=100, default="Misc Activity")
class ActivityCategory(models.Model):
activity_name = models.CharField(max_length=40)
activity_description = models.CharField(max_length=150)
pts = models.IntegerField()
def __str__(self):
return '%s' % (self.activity_name)
What I need to do is get a group of departments with aggregating the sum of the pts earned by all the users activitylogs.
So a user is part of department, they do activities, each activity is of a type activity_category and has associated points. How can I query using the ORM to get a sum of points for everyone in each department?
Thank you, I cannot seem to wrap my mind around it.
You annotate the departments with the sum:
from django.db.models import Sum
Depts.objects.annotate(
total_pts=Sum('dept__user__activity_user__activity_category__pts')
)
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the Depts model to the UserProfile
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the dept relation to userprofiles.
After setting the related_name='userprofiles', the query is:
from django.db.models import Sum
Depts.objects.annotate(
total_pts=Sum('userprofiles__user__activity_user__activity_category__pts')
)

Django - counting foreign keys in models

I know how I can count things with annotate (in my view) but I would like to do the same in model (so it would be more reusable).
For example (lets take an example from django documentation) I have this model:
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
and I can use in view this:
from django.db.models import Count
pubs = Publisher.objects.annotate(num_books=Count('book'))
But how I do that in model?
I know this question is pretty basic (probably) but I'm pretty much beginner in django.
Thanks for answers!
You can use custom managers:
Django docs: Managers
class BookManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(num_books=Count('book'))
class Publisher(models.Model):
name = models.CharField(max_length=300)
books = BookManager()
Now you can call it like this:
pubs = Publisher.books.all()
And you will have num_books with your objects.
You can use classmethod for this.
class Publisher(models.Model):
...
#classmethod
def get_book_count(cls):
return cls.objects.annotate(num_books=Count('book'))
You can call this method as
pubs = Publisher.get_book_count()
Edit - Also check out the answer by #Navid2zp which might be a better solution for you.

Which approach is better in Django Jsonfield or ForeignKey?

Such a question, I want to create a price comparison site, there are two ideas how to implement a list of prices from different stores.
First via ForeignKey
class Price(models.Model):
price = models.DecimalField()
shop = models.CharField()
class Product(models.Model):
name = models.CharField(max_length=255)
prices = models.ForeignKey()
JSONfield second method
class Product(models.Model):
name = models.CharField(max_length=255)
data = JSONField()
""" Product.objects.create(name='product', data={'price': 999,
'shop': 'Amazon.com'}
def __str__(self):
return self.name
If anyone has experience, what works faster and more reliable if a large number of goods? Thanks for the early ones.

Django counting related objects in model layer

It is possible to do something like this working:
class Book(models.Model):
voters = models.ManyToManyField(User, blank=True)
vote = models.IntegerField() # summary of all votes
def average_vote(self):
return int(vote/self.annotate(Count('voters')))
Maybe something like this?
class Book(models.Model):
voters = models.ManyToManyField(User, blank=True)
vote = models.IntegerField() # summary of all votes
def average_vote(self):
return int(self.vote/self.voters.all().count())
Let me know if that works. I haven't tested it.
Just override the default manager to make it always return an annotated queryset:
class BookUserManager(models.Manager):
def get_query_set(self, *args, **kwargs):
return super(BookUserManager, self).get_query_set(*args, **kwargs).annotate(average_vote=models.Avg('books__vote'))
class BookUser(User):
objects = BookUserManager()
class Meta:
proxy = True
class Book(models.Model):
# Next line has been changed to use proxy model. This *will* affect the m2m table name.
voters = models.ManyToManyField(BookUser, blank=True, related_name='books')
vote = models.IntegerField() # summary of all votes
objects = BookManager()
Then, you can get at the value like any other attribute on your the user model:
user = BookUser.objects.get(username='joe')
print user.average_vote
Update: Sorry... got that all wrong. That's what I get for reading the question too quickly. You'd actually need to annotate User not Book, but since User is coming from django.contrib.auth (I'm assuming) that's not going to be possible, or at least it requires more steps. Code above has been updated.