I have the following two models:
class Blog(TimeStampedModel):
summary = models.TextField()
status = models.CharField(max_length=255)
class Entry(TimeStampedModel):
author = models.CharField(max_length=255)
text = models.TextField()
blog = models.ForeignKey(Blog, models.CASCADE, related_name='entries')
Both models sublass a common meta-model that defines a timestamp for when each model was last updated:
class TimeStampedModel(models.Model):
last_changed = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
This works fine when saving each model individually. However, in my use case, when an Entry is updated, it should also reflect in the update of the last_changed timestamp of the associated Blog.
Is there any easy way to tell Django to also bump the timestamps of related models?
I admit that this is hacky, but you can override the save method of Entry model:
def save(self, *args, **kwargs):
self.blog.save()
super().save(*args, **kwargs)
Related
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.
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
I have two models like
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, on_delete=models.CASCADE)
Now for an object of Article lets say
a=Article.objects.filter(id=1)
a=a[0]
I try to change the headline and the email of the author who has written this article so I do
a.heagline = "Saving foreign key does not work"
a.reporter.email = "changed#email.com"
a.save()
This saves the Article object but does not modify the Reporter.
I explicitly have to do
a.reporter.save()
to see the changes to the reporter object. As this is a Many to One relation it should also modify the Foreign key on saving
How can I save the parent Model too by just calling the childs save method
You could override the save method or just create a custom method.
class Article(models.Model):
...
# Overriding save
def save(self, *args, **kwargs):
self.reporter.save()
super(Article, self).save(*args, **kwargs)
# Creating a custom method
def save_related(self):
self.reporter.save()
self.save()
I suggest you create a custom method because it doesn't introduce unexpected behavior in save()
Accordingly to the Django docs I can do the following:
class Article(models.Model):
headline = models.CharField(max_length=200, null=True, blank=True,
help_text="Use puns liberally")
content = models.TextField()
class ArticleForm(ModelForm):
headline = MyFormField(max_length=200, required=False,
help_text="Use puns liberally")
class Meta:
model = Article
In my case I would like "headline" not to be displayed at all as an option in a subclass. What is the best method to do that? I already tried "exclude"
class ArticleForm(ModelForm):
class Meta:
model = Article
exclude = ["headline"]
But since it is declared in the parent it is rendered anyways. Also I tried declaring it as headline = "" but same result.
Solution:
def __init__(self, *args, **kwargs):
super(NameOfSubclassedForm, self).__init__(*args, **kwargs)
del self.fields['headline'] # field that needs removing
TIA
*update: in my original post I placed exclude outside meta by accident
*update2: bug already reported: https://code.djangoproject.com/ticket/13971
*update3: Added Solution
I believe you simply have put your exclude in the wrong place. Try it like this:
class ArticleForm(ModelForm):
class Meta:
model = Article
exclude = ("headline",)
Check out the docs.
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.