I'm currently working on a Django blog app where I have a Post connecting to a Category. The Category has both name and times_used fields. times_used will be incremented by 1 after a Post is created and I have achieved this overriding the save() method on the Post model. I'm trying to override the delete() method to decrement the current post's category by 1 but it's not actually reaching the overridden delete() method it seems, because the category still has the incremented value even after the post got deleted. See my code below...
model.py
class Category(models.Model):
name = models.CharField(max_length=150, unique=True)
times_used = models.IntegerField(default=0)
class Post(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
# This increments this category by 1 then saves the post - Works well!
def save(self, *args, **kwargs):
self.category.times_used += 1
self.category.save()
super(Post, self).save(*args, **kwargs)
# This decrements this category by 1 then deletes the post - Which is not working...
def delete(self, *args, **kwargs):
self.category.times_used -= 1
self.category.save()
super(Post, self).delete(*args, **kwargs)
Any suggestions how to accomplish this?
I read about Django signals just now and have accomplished what I wanted to. According to the Django documentation... the delete() method doesn't get called when we override it to delete objects in bulk using a QuerySet. So it's best to use a signal to get these types of job done.
In my models.py I've pass the Post as an argument to a function which I have a created in signals.py
models.py
class Post(models.Model):
....
delete_post_signal(Post)
Within my signals.py I have the following code...
from django.dispatch import receiver
from django.db.models.signals import pre_delete
def delete_post_signal(Post):
#receiver(pre_delete, sender=Post)
def decrement_category_usage(sender, instance, **kwargs):
instance.category.times_used -= 1
instance.category.save()
This worked for me perfectly...
You can do it with pre_delete signal like this
#receiver(pre_delete, sender=Post)
def my_handler(sender, instance, **kwargs):
instance.category.times_used -= 1
instance.category.save()
Related
I have two Django models:
from django.db import models
class Policy(models.Model):
status = models.IntegerField()
def save(self, *args, **kwargs):
quote = self.documents.get(document_type=DocumentType.quote)
if self.status == 0:
quote.delete()
elif self.status == 1:
new_quote_content = create_new_quote()
quote.s3_file.save(quote.name, File(new_quote_content))
super().save(*args, *kwargs)
class Document(models.Model):
policy = models.ForeignKey(
to=Policy,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="documents",
)
s3_file = models.FileField(
storage=S3Storage(aws_s3_bucket_name="policy-documents"),
upload_to=get_document_s3_key,
max_length=255,
)
I want to delete/update the document when the policy status is updated and I've overriden the save() method in Policy to do it. However, neither the doc deletion nor the doc's FieldFile update works in the save() method. If I move them to outside the save() method, everything works.
Does someone understand what's the issue here?
It is not calling the super method to save the model. To override a model it has to be something like this as given in the documentation of Django.
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, *args, **kwargs):
do_something()
super().save(*args, **kwargs) # Call the "real" save() method.
do_something_else()
The save() was being called from a Policy ModelAdmin with a inlined Document form set. After it was run, Django executed the ModelAdmin's save_related() method, which saved the Document form set data, overwriting the Document changes I had just saved in save(). I solved it by overriding save_related() and deleting/updating the document after form.save_m2m() and form.save_formset().
I have been searching for an answer for hours, however every answer I found does not work. Also trying to find a bug on my own didn't bring me any results.
I have created a receiver function which should update model's total_likes attribute(based on number of users_like attribute) every time user click on the like button of the specific image. (This is a part of 'Django by Example` book). But the field's value stays always the same and is equal to default value of 0. Even if I try to assign the value to the field manually, in the django's shell, it does not change(code example in 'Update' section).
Can someone please have a look at the code and point me in the right directions if I am doing something wrong?
I am using Django 1.9.
# models.py
class Image(models.Model):
...
users_like = models.ManyToManyField(settings.AUTH_USER_MODEL,
related_name='images_liked',
blank=True)
total_likes = models.PositiveIntegerField(default=5)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Image, self).save(*args, **kwargs)
# signals.py
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Image
#receiver(m2m_changed, sender=Image.users_like.through)
def users_like_changed(sender, instance, **kwargs):
instance.total_likes = instance.users_like.count()
instance.save()
# apps.py
from django.apps import AppConfig
class ImagesConfig(AppConfig):
name = 'images'
verbose_name = 'Image bookmarks'
def ready(self):
# import signal handlers
import images.signals
# __init__.py
default_app_config = 'images.apps.ImagesConfig'
Update:
When I run code below from django shell, this does change the total_likes value, but it looks like it do just temporary:
from images.models import Image
for image in Image.objects.all():
print(image.total_likes)
image.total_likes = image.users_like.count()
print(image.total_likes)
image.save()
print(image.total_likes)
Output from code above:
0 #initial/default value of 0
3 #current number of users who like the picture
3
Because when I run the for loop code again, to see the results(or even check the field value in admin interface) i still get the initial/default value of 0.
Can someone see the problem why the field does not get updated?
Ok, so the problem was with the custom save() method on the model class.
I needed to call the save() method of the parent class like this:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Image, self).save(*args, **kwargs)
and it made it work.
I have a model that requires some post-processing (I generate an MD5 of the body field).
models.py
class MyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
body = models.TextField()
md5 = models.CharField(max_length=32)
...
def save(self, *args, **kwargs):
if self.pk is None: # Only for create (edit disabled)
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
The problem is that the final block won't execute because I don't see a way to check if the instance is new or not: self.pk is never None because a UUID is populated before saving.
I'd like to know what the best practice is for handling this.
Thanks in advance.
Update:
The only solution I can think of is to call the database directly and:
Check if the id exists
Compare the modified and created fields to tell if it's an edit
EDIT
self.pk is never None because a UUID is populated before saving.
Instead of setting a default for id, use a method to set id for the new instance.
class MyModel(...):
id = models.UUIDField(primary_key=True, default=None,...)
def set_pk(self):
self.pk = uuid.uuid4()
def save(self, *args, **kwargs):
if self.pk is None:
self.set_pk()
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
Looks like the cleanest approach to this is to make sure that all your models have a created date on them by inheriting from an Abstract model, then you simply check if created has a value:
models.py
class BaseModel(models.Model):
"""
Base model which all other models can inherit from.
"""
id = fields.CustomUUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
# Abstract models are not created in the DB
abstract = True
class MyModel(BaseModel):
my_field = models.CharField(max_length=50)
def save(self, *args, **kwargs):
if self.created:
# Do stuff
pass
super(MyModel, self).save(*args, **kwargs)
I just got the same issue in my project and found out that you can check the internal state of the model's instance:
def save(self, *args, **kwargs):
if self._state.adding: # Only for create (edit disabled)
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
But this solution relies on internal implementation and may stop working after Django is updated
As I've answered here as well, the cleanest solution I've found that doesn't require any additional datetime fields or similar tinkering is to plug into the Django's post_save signal. Add this to your models.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=MyModel)
def mymodel_saved(sender, instance, created, **kwargs):
if created:
# do extra work on your instance
self.md5 = get_md5(self.body)
This callback will block the save method, so you can do things like trigger notifications or update the model further before your response is sent back over the wire, whether you're using forms or the Django REST framework for AJAX calls. Of course, use responsibly and offload heavy tasks to a job queue instead of keeping your users waiting :)
i have one silly problem with Django M2M saving.
I don't use Django Admin (use my own custom templates)
So, I have simple relationship:
# models
class News(models.Model):
title = models.CharField(max_length=256)
class Webmaster(AbstractUser):
...
news = models.ManyToManyField(News)
I need after saving every news mark it as new to every webmaster. So i figure out something like this:
# models
class News(models.Model):
title = models.CharField(max_length=256)
def save(self, *args, **kwargs):
if self.id:
news = News.objects.all()[0]
self.webmasters.add(news)
super(News, self).save(*args, **kwargs)
But i try like 6-7 different ways, and my code still don't work, can you help? Thanks!
You have to use post_save () signal, add this code in models.py
def Webmaster_add(sender, instance, **kwargs):
#you can put here some condition stuff
w = Webmasters.objects.all()
for obj in w:
obj.news.add(instance)
# register the signal
post_save.connect(webmaster_add, sender=News)
I am new to Django and I'm not able to understand how to work with Django signals. Can anyone please explain "Django signals" with simple examples?
Thanks in advance.
You can find very good content about django signals over Internet by doing very small research.
Here i will explain you very brief about Django signals.
What are Django signals?
Signals allow certain senders to notify a set of receivers that some action has taken place
Actions :
model's save() method is called.
django.db.models.signals.pre_save | post_save
model's delete() method is called.
django.db.models.signals.pre_delete | post_delete
ManyToManyField on a model is changed.
django.db.models.signals.m2m_changed
Django starts or finishes an HTTP request.
django.core.signals.request_started | request_finished
All signals are django.dispatch.Signal instances.
very basic example :
models.py
from django.db import models
from django.db.models import signals
def create_customer(sender, instance, created, **kwargs):
print "Save is called"
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
signals.post_save.connect(receiver=create_customer, sender=Customer)
Shell
In [1]: obj = Customer(name='foo', description='foo in detail')
In [2]: obj.save()
Save is called
Apart from the explanation given by Prashant, you can also use receiver decorator present in django.dispatch module.
e.g.
from django.db import models
from django.db.models import signals
from django.dispatch import receiver
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
#receiver(signals.pre_save, sender=Customer)
def create_customer(sender, instance, created, **kwargs):
print "customer created"
For more information, refer to this link.
In the signals.post_save.connect(receiver=create_customer, sender=Customer)... sender will always be the model which we are defining... or we can use the User as well in the sender.
Signals are used to perform any action on modification of a model instance. The signals are utilities that help us to connect events with actions. We can develop a function that will run when a signal calls it. In other words, Signals are used to perform some action on modification/creation of a particular entry in Database. For example, One would want to create a profile instance, as soon as a new user instance is created in Database
There are 3 types of signal.
pre_save/post_save: This signal works before/after the method save().
pre_delete/post_delete: This signal works before after delete a model’s instance (method delete()) this signal is thrown.
pre_init/post_init: This signal is thrown before/after instantiating a model (init() method).
One of the example, if we want to create a profile of a user as soon as the user is created using post_save signal.
For code example, I found the Geeks for Geeks, which explains is very simple way, and easy to understand.
https://www.geeksforgeeks.org/how-to-create-and-use-signals-in-django/
You can add signals to your models.py file
here is an example for adding an auto slug, if you use a SlugField:
this is the stuff you need to import
from django.utils.text import slugify
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
Add the #receiver to the bottom of your class, included the def
If you add the def __str__(self): under the receiver, you will get an error
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(pre_save, sender=Store)
def store_pre_save(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
or you can use post_save
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(post_save, sender=Store)
def store_post_save(sender, instance, created, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
instance.save()
I found this example from this tutorial