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

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

Related

how to allow users to rate a product only once,

a Reviews model linked to Users and Product, nothing special,
users can vote as much as they want which is working,
but i'm stuck restricting the rating to just once
the view so far:
class ReviewCreateView(CreateView):
model = Review
form_class = ReviewForm
template_name = "form.html"
success_url = reverse_lazy('product_list')
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
model
class Review(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT, null=True,related_name='reviews')
user = models.ForeignKey(User, on_delete=models.PROTECT, null=True,related_name='reviewers')
rating = models.IntegerField(null=True, blank=True, default=0)
comment = models.TextField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
the other models wouldn't help that muc ,Product just describe a product ,and default (regular) User model
the form is ModelForm based on the Review Model itself
any suggestion is welcome, even if i have to redo the model, thank you very much
set db constraint like so:
class Meta:
constraints = [
UniqueConstraint([fields=['user', 'product'], name='product_user_unique'),
]
the condition for your views would be like this:
def get(self, request, product):
if (Review.objects.filter(user=request.user)
.filter(product_id=product).exists():
# probably, you'd want to pass some
# conditional data to the template's context here
do_something()
That could be it. You restrict non-unique reviews by UI, but if it's somehow bypassed, user will get a server error. Of course, you could also apply validation in your CreateView (or analogous).
From architectural standpoint, you can always create a review_user table which will have the Product ID, and the ID of the users who voted on that product.. and upon second vote, you check if there's already a vote in the table.
Once you have this understanding, I am sure that you will find a way to incorporate the logic within your code.

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.

Update datetimefiled of all related models when model is updated

I have two models (Post and Display). Both have Datetime-auto fields. My problem is that i want to update all display objects related to a post, once a post is updated.
I have read here that you could override one models save method, but all the examples are About updating the model with the foreign key in it and then call the save method of the other model. In my case it's the other way arround. How can i do this ?
class Post(models.Model):
title = models.CharField(max_length=40)
content = models.TextField(max_length=300)
date_posted = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
rooms = models.ManyToManyField(Room, related_name='roomposts', through='Display')
def __str__(self):
return self.title
def get_absolute_url(self):
return "/post/{}/".format(self.pk)
class Display(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
room = models.ForeignKey(Room, on_delete=models.CASCADE)
isdisplayed = models.BooleanField(default=0)
date_posted = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.isdisplayed)
i want to update the date_posted of all related Display-objects once their related post is changed. I do not know if overriding the save-method works here.
in this case you should have a look at django's reverse foreign key documentation
https://docs.djangoproject.com/en/2.2/topics/db/queries/#following-relationships-backward
in your case you can override the save method on your Post model
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
#either: this excutes many sql statments
for display in self.display_set.all():
display.save()
#or faster: this excute only one sql statements,
#but note that this does not call Display.save
self.display_set.all().update(date_posted=self.date_posted)
The name display_set can be changed using the related_name option
in Display, you can change it:
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='displays')
Then, instead of using self.display_set in your code, you can use self.displays
Overriding save method works, but that's not were you should go, imo.
What you need is signals:
#receiver(post_save, sender=Post)
def update_displays_on_post_save(sender, instance, **kwargs):
if kwargs.get('created') is False: # This means you have updated the post
# do smth with instance.display_set
Usually it goes into signals.py.
Also you need to include this in you AppConfig
def ready(self):
from . import signals # noqa

Django form save or model save with a select field?

So I'm still new to Django. I have a single field in my form. And I was just wondering whether or not I need a form save function or a model save function? When is it appropriate to use either or?
For instance: My form:
class OpinionStatusForm(forms.Form):
choices = (('0', "Your Status"), ('1', "This"), ('2', "That"), ('3', "The Other"))
status = forms.CharField(max_length=2, widget=forms.Select(choices=choices, attrs={'class':'status_dropdown','onchange': 'this.form.submit();'}), required=False)
def save(self, opinion_status):
opinion_status.status = self.cleaned_data['status']
My model:
class OptionStatus(models.Model):
user = models.ForeignKey(User, null=True, unique=True)
status = models.CharField(max_length=2, choices=opinion_statuses)
opinion = models.ForeignKey(Opinion, null=True, blank=True)
def __unicode__(self):
return self.status
def save(self, *args, **kwargs):
super(OpinionStatus, self).save(*args, **kwargs)
I'm going to be ajax-ing the form. I don't know if that makes a difference or not. Thanks!
What you actually need, is a ModelForm. In your example you're working with a standard forms.Form. This is not bound to a model instance. As a result, there's also no need for a save method. The best examples are really given inside the Django docs:
https://docs.djangoproject.com/en/dev/topics/forms/modelforms/
Go step by step over the code examples and you'll understand. It would be too much to explain it all in one Stackoverflow answer - and the Django docs are incredible thorough.

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.