Django change foreign key data and save - django

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()

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.

How to add data in table dynamically in django?

I have following two models in my models.py.
class Book(models.Model):
book_id = models.AutoField
book_name = models.CharField(max_length=50)
book_author = models.CharField(max_length=50)
book_category = models.CharField(max_length=50)
class Author(models.Model):
author_id = models.AutoField
author_name = models.CharField(max_length=50)
author_description = models.CharField(max_length=500)
I want whenever I add new row in Book table, it automatically do entry in Author table ( Only author_id and author_name, author_description i will add manually ) for every unique author.
How to do it?
You can do it, in some different way.I suggest you you these ways:
Overwrite Save method of Book model. (Using Save method)
Send Signal to Author model for create Author object.
Now, I implement second way:
#receiver(post_save, sender=Book)
def create_author(sender, instance, created, **kwargs):
if created:
author = Author.objects.create(id=your_id,author_name=your_author_name,author_description=your_author_description)
Book.objects.objects.filter(pk=instance.id).update(author=author)
#receiver(post_save, sender=Book)
def save_author(sender, instance, **kwargs):
instance.author.save()

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 model ForeignKey with subset of data

Is it possible to define a foreign key or OneToOne relation in django model with only subset of data?
For example :
I have 2 models.
#with_author
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
and
#with_author
class UOM(models.Model):
uomname = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
so I want in my Product model only to allow UOM values that have same material value as in product.
Is it possible on model level or any other place and not to display non relevant values in the dropdown?
You can enforce this constraint by adding some validation to the model's clean() method. Something like:
from django.core.exceptions import ValidationError
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
def clean(self):
if not self.material == self.UOM.material:
# This will cause the model not to be saved and report an error
raise ValidationError('Material does not match UOM material')
If you are using a ModelForm to handle edits to your models, then clean() will be called automatically as part of the form validation. If you are modifying models directly in your code, then you need to call it yourself before saving the model. The documentation explains this in detail.
If you want to be doubly sure, you can also override the save() method:
def save(self, *args, **kwargs):
if not self.material == self.UOM.material:
return # Model is not saved
super(Product, self).save(*args, **kwargs)
This will not report any errors - it will just not save the model. Hence you should also use the clean() method above.

Django foreign key on_delete: how to pass argument to callable when using models.SET

This is my models.py:
def set_image(instance):
return Image.objects.filter(user=instance.user)[0]
class User(models.Model):
user = models.CharField(max_length=50)
class Image(models.Model):
user = models.ForeignKey(User)
image = models.ImageField(upload_to='media')
class Item(models.Model):
user = models.ForeignKey(User)
image = models.ForeignKey(
Image,
blank=True,
null=True,
on_delete=models.SET(set_image)
)
When I delete an 'Image', it's related 'Item' calls set_image, which returns an error, because models.SET doesn't pass the instance to the callable. How can I change this behavior? Should I override models.SET or is there any other way around it?
Override the Image delete method to remove the Item ForeignKey
The following can be used as an intermediary model base class to add this functionality to all of your models:
from django.db import models
class Model(models.Model):
"""
Intermediate model base class.
"""
class Meta:
abstract = True
def delete(self, *args, **kwargs):
self.clear_nullable_related()
super(Model, self).delete(*args, **kwargs)
def clear_nullable_related(self):
"""
Recursively clears any nullable foreign key fields on related objects.
Django is hard-wired for cascading deletes, which is very dangerous for
us. This simulates ON DELETE SET NULL behavior manually.
"""
for related in self._meta.get_all_related_objects():
accessor = related.get_accessor_name()
related_set = getattr(self, accessor)
if related.field.null:
related_set.clear()
else:
for related_object in related_set.all():
related_object.clear_nullable_related()
source