Django counting related objects in model layer - django

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.

Related

Django referencing a specific object in a many-to-one relationship

Let's say I have the following models:
from django.db import models
class Reporter(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField()
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
I'd like to add a favorite_article field to my Reporter model that will reference a specific Article from reporter.articles.
One option is put the information into the Article model instead:
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
is_favorite = models.BooleanField()
But this doesn't seem like a very clean solution. Is there a better method to do this?
The approach you've suggested will work, however in its current form it allows for multiple Articles to be the favorite of one Reporter. With a bit of extra processing you can ensure that only one (at most) Article per Reporter is the favorite.
Making a few modifications to a couple of the answers to the question Unique BooleanField value in Django? we can restrict one True value per Reporter rather than one True value for the entire Article model. The approach is to check for other favorite Articles for the same Reporter and set them to not be favorites when saving an instance (rather than using a validation restriction).
I'd also suggest using a single transaction in the save method so that if saving the instance fails the other instances are not modified.
Here's an example:
from django.db import transaction
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
is_favorite = models.BooleanField(default=False)
def save(self, *args, **kwargs):
with transaction.atomic():
if self.is_favorite:
reporter_id = self.reporter.id if self.reporter is not None else self.reporter_id
other_favorites = Article.objects.filter(is_favorite=True, reporter_id=reporter_id)
if self.pk is not None: # is None when creating a new instance
other_favorites.exclude(pk=self.pk)
other_favorites.update(is_favorite=False)
return super().save(*args, **kwargs)
I've also changed the approach to use a filter rather than a get just in case.
Then to get the favorite article for a reporter, you can use:
try:
favorite_article = reporter.articles.get(is_favorite=True)
except Article.DoesNotExist:
favorite_article = None
which you could wrap into a method/property of the Reporter class.

Django - Update integer field value in one model by change in other

Here is a project I've created to practice, in my models.py,
class Post(models.Model):
title = models.CharField(max_length = 140)
author = models.ForeignKey(User, on_delete=models.CASCADE)
votes = models.BigIntegerField(default=0, blank=True)
class Vote(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='voter')
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='vpost')
#receiver(post_save, sender=Vote)
def update_votes(sender, **kwargs):
# # ??
Here I have a Voteform with that user can vote any particular post. That part works well.
Here is my question, whenever a user votes a particular post, I want votes field in Post model to increase as well.
I know I can show it with {{post.vpost.count}} in my html. But I want that increment here.
Other way I have tried,
class Vote(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='voter')
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='vpost')
def update_votes(self):
p = self.post
p.votes += 1
p.save()
This one only works once, not working from second time, so I want to use signal method. So how can I update the vote field in Post model using signal?
Nearly there. I would rename Post.votes to Post.votes_count as votes indicates a reverse relationship.
#receiver(post_save, sender=Vote)
def update_votes(sender, instance, **kwargs):
post = instance.post
post.votes_count += 1
post.save()
Although you might want to make sure that the count is correct, by introducing another query:
#receiver(post_save, sender=Vote)
def update_votes(sender, instance, **kwargs):
post = instance.post
post.votes_count = post.votes_set.all().count()
post.save()
You might also want to do this when/if a Vote is deleted to make sure the count is correct.
Bear in mind you could also just do this in the Vote's save method instead of needing signals.
You could also do this as a cronjob or task depending on your circumstances

Retrieving all database objects and its related objects Django

I am currently learning Django, and I am finding it a bit difficult wrapping my head around the ManyToMany fields. I am using an intermediate model to manage my relationships.
I have three models; Ticket, User, and TicketUserRelation.
I want to be able to query the ticket model, and retrieve both its corresponding user objects and the ticket object. How would I go about doing this?
In Laravel I would do something along the lines of
Ticket::where('id', '1')->with('contributors')
But I can't really figure out how to do this in Django
The models:
class User(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Ticket(models.Model):
contributors = models.ManyToManyField(User, through=TicketUserRelation, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
class TicketUserRelation(models.Model):
id = models.AutoField(primary_key=True, db_column='relation_id')
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
EDIT: I am using an intermediate model so that I can easily add things like join date later.
You don't need the TicketUserRelation model when using Django ORM. You could simply use a ForeignKey in the Ticket model, or use the ManyToManyField already defined, if one ticket can be assigned to multiple users.
class Ticket(models.Model):
# For one user, use ForeignKey
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tickets')
# For multiple users, use ManyToManyField
contributors = models.ManyToManyField(User, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
You can then get all tickets for a user u with:
u.tickets.all()
Figured it out myself, using prefetch_related. I was having trouble understanding how prefetch_related works. For those that are confused too, from my understanding it works like this:
Ticket.objects.all().prefetch_related('contributors')
This returns a queryset, something along the lines of this
<QuerySet [<Ticket: Testing ticket one>, <Ticket: Testing ticket two>, <Ticket: Testing ticket three'>, <Ticket: Testing ticket four>]>
When you then access the elements in the queryset, you can then call .contributors on the object, like so:
# Get the queryset
tickets_with_contribs = Ticket.objects.all().prefetch_related('contributors')
# Print the contributors of the first ticket returned
print(tickets_with_contribs[0].contributors)
# Print the contributors of each ticket
for ticket in tickets_with_contribs:
print(ticket.contributors)
Looking back at it this should have been pretty self explanatory, but oh well.

Create query between ManyToMany relationship models in Django ORM

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)

Django - Inserting data into a many to many relationship model with unique constraint

I have a models.py:
class Skill(models.Model):
title = models.CharField(max_length=255, unique=True)
category = models.ForeignKey(
SkillCategory, default=None, null=True, blank=True
)
def __unicode__(self):
return self.title
class UserProfile(models.Model):
user = models.OneToOneField(User)
skill = models.ManyToManyField(Skill)
def __unicode__(self):
return self.user.username
And in my views I am basically getting a set of skills which I use to populate the UserProfile and Skills models after login:
#login_required
def UpdateUserSkills(request):
cleaned_skills = get_skill_list(user=request.user)
user_profile, created = UserProfile.objects.get_or_create(
user=request.user
)
for s in cleaned_skills:
user_profile.skill.get_or_create(title=s)
return HttpResponseRedirect(reverse('show_user_profile'))
My question is - does this method of adding skills respect the unique constraint of the model, and if it does, will it NOT populate those skills for a given user if they already exist? Would it perhaps be better to do something like this:
for s in cleaned_skills:
skill = Skill.objects.get_or_create(title=s)
user_profile.skill.get_or_create(title=skill)
Though this seems as though it will generate double the number of DB queries.
Perhaps there is another, better way?
You should do something like this:
for s in cleaned_skills:
skill = Skill.objects.get_or_create(title=s)
user_profile.skill.add(skill)
That will only create the skill if a skill with that same title does not already exist. It then adds the skill to the user.
EDIT: you could save a few queries, if you do a bulk_create. Something like this:
skill_titles = Skill.objects.values_list('title', flat=True)
new_skills = Skill.objects.bulk_create([Skill(title=s) for s in cleaned_skills if s not in skill_titles])
user_profile.skill.add(*new_skills)