Model field value does not get updated m2m_changed(Django) - python

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.

Related

How to Update value of a Model with another Model's value in Django

class Trade(models.Model):
pips = models.FloatField(default=0)
direction = models.CharField(max_length=30)
new_balance = FloatField(default=0.0)
...
class Summary(models.Model):
winning_trades = models.IntegerField(default=0)
account_balance = FloatField(default=0.0)
...
When a user post a request he/she will populate the Trade model, this will update the summary model and send back to the user new summary data. How can I do this in an elegant way?
You're most likely looking for Django Signals. You'd want your Trade model's create event to trigger a post_save signal that a listener will receive and process.
Assuming you have saved your models in a file models.py,
Create a file signals.py with the following:
# code
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Trade, Summary
#receiver(post_save, sender=Trade)
def update_summary(sender, instance, created, **kwargs):
if created:
# query to update Summary as needed
You'll have to add the signals to your app config.
in your apps.py of the relevant app, add the following:
from django.apps import AppConfig
class AppnameConfig(AppConfig):
name = 'appname'
**def ready(self):
import appname.signals**
First, I will encourage you to create a transaction to perform these two actions. If the second one fails, your database will remain consistent.
Then, Django allows you to override the model methods such as save. You should try that with something like the following:
class Trade():
...
def save(self, *args, **kwargs):
with transaction.atomic():
super.save()
update_summary()
Then, in the view, you could query for the recently updated Summary and return it.
class TradeViewSet():
def create(self, request, *args, **kwargs):
user = request.user
trade = Trade.create(request.data)
updated_summary = get_summary(user.id)
response = SummarySerializer(updated_summary)
return Response(response)

Django model delete method not overriding

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

Correct way to write Django post_save signal configuration

I've been trying to learn how to use Signals, and unfortunately, my code below doesn't work. In addition, I've come across a dozen posts that all show different ways to do this.
My goal with this post is to clarify and come to a consensus on writing signals in the best way possible in 2019 for scalability. In this example using post_save.
model.py
from django.db import models
from metrics.models import InventoryProduct
from product.models.product import Product
class PurchasedOrder(models.Model):
__quantity_sold = None
product = models.ForeignKey(Product, on_delete=models.CASCADE)
purchase_quantity = models.IntegerField()
quantity_sold = models.IntegerField(default=0)
def __init__(self, *args, **kwargs):
super(PurchasedOrder, self).__init__(*args, **kwargs)
self.__original_quantity_sold = self.quantity_sold
def save(self, *args, **kwargs):
# If the quantity sold field has changed run the update_sold_out method
if self.quantity_sold != self.__original_quantity_sold:
PurchasedOrderLogic.update_sold_out(self)
super(PurchasedOrder, self).save(*args, **kwargs)
self.__original_quantity_sold = self.quantity_sold
def __str__(self):
return self.product.name
signals.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
from purchasing.models import PurchasedOrder
from purchasing.services.models_logic import PurchasedOrderLogic
#receiver(post_save, sender=PurchasedOrder)
def update_inventory_product_total_qty_purchased(sender, instance):
PurchasedOrderLogic.update_inventory_product_total_qty_purchased(
sender, instance.product.id, instance.purchase_quantity
)
This is where my logic resides. When I update the PurchasedOrder purchase_quantity field, it should update also the InventoryProduct's total_qty_purchased field.
purchasing.services.models_logic.py
class PurchasedOrderLogic(object):
#static_method
def update_inventory_product_total_qty_purchased(purchased_order_class, product_id):
inventory_product = InventoryProduct.objects.get(pk=product_id)
total_qty_purchased = purchased_order_class.objects.filter(product_id=product_id).aggregate(
Sum('purchase_quantity')
).get('purchase_quantity__sum')
inventory_product.total_qty_purchased = total_qty_purchased
inventory_product.save()
apps.py
from django.apps import AppConfig
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from purchasing.models import PurchasedOrder
from purchasing.signals import update_inventory_product_total_qty_purchased
class PurchasingConfig(AppConfig):
name = 'purchasing'
verbose_name = _('purchasing')
def ready(self):
post_save.connect(update_inventory_product_total_qty_purchased, sender=PurchasedOrder)
What am missing? I'm not sure even if the signal is running?
I'm triggering .save() by manually modifying any field on the model.
Personally, I've been changing the purchase_quantity field which should print this value as it's called inside the update_inventory_product_total_qty_purchased method

How to tell if a model instance is new or not when using UUIDField as a Primary Key

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

Django M2M save and add relationship for each relative

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)

Categories

Resources