I recently started to use signals in my Django project (v. 1.3) and they all work fine except that
I just can't figure out why the m2m_changed signal never gets triggered on my model. The Section instance is edited by adding/deleting PageChild inline instances on an django admin form.
I tried to register the callback function either way as described in the documentation, but don't get any result.
Excerpt from my models.py
from django.db import models
from django.db.models.signals import m2m_changed
class Section(models.Model):
name = models.CharField(unique = True, max_length = 100)
pages = models.ManyToManyField(Page, through = 'PageChild')
class PageChild(models.Model):
section = models.ForeignKey(Section)
page = models.ForeignKey(Page, limit_choices_to = Q(is_template = False, is_background = False))
#receiver(m2m_changed, sender = Section.pages.through)
def m2m(sender, **kwargs):
print "m2m changed!"
m2m_changed.connect(m2m, sender = Section.pages.through, dispatch_uid = 'foo', weak = False)
Am I missing something obvious?
This is an open bug: https://code.djangoproject.com/ticket/16073
I wasted hours on it this week.
You are connecting it twice, once with m2m_changed.connect and the other time with receiver decorator.
Not sure if it will help, but the following is working for me:
class Flow(models.Model):
datalist = models.ManyToManyField(Data)
from django.db.models.signals import post_save, pre_delete, m2m_changed
def handle_flow(sender, instance, *args, **kwargs):
logger.debug("Signal catched !")
m2m_changed.connect(handle_flow, sender=Flow.datalist.through)
I'm not sure if this will help, but are you sure that you should use Sender.pages.through for this special case? perhaps if you tried #reciever(m2m_changed, sender=PageChild)
Note: if you have #reciever, you do not need m2_changed.connect(...) as #reciever already performs the connect operation.
Related
I've implemented django signals for a model. It's only working fine while debug is True. Any idea how to fix this issue, so that post_save signal works when debug is False in production.
Here is my code in models.py
#receiver(post_save, sender=PackageSubscription)
def update_balance(sender, instance, **kwargs):
current_balance = Balance.objects.get(user=instance.user)
last_balance = current_balance.last_balance
get_package = Package.objects.filter(title=instance.package)
for i in get_package:
get_coin = i.coin
update_current_balance = last_balance + get_coin
if instance.payment_status is True:
Balance.objects.filter(user=instance.user).update(last_balance=update_current_balance, updated_at=timezone.now())
FYI - I'm using django 3.2.4
There is a similar question here but that is from 2011 and Django 1.2. Many things have changed since then. I'm using Django 1.9.
I'm wondering how should I do this correctly, without any risks of messing up the filesystem. I have an ImageField in a model:
class CartaMagicPy(models.Model):
imagen = models.ImageField(null=True, upload_to=ubicar_magicpy)
On a first moment, the image is saved. But then, the user crops the image and I get a new image assigned to the same ImageField.
The old image is not deleted from the filesystem. How can I delete it? The problem is that model_object.imagen.path after being updated contains the path to the new image. And if I delete it before I update it with the new image, I risk that maybe the save could fail and then I end up with no image at all. Is there a combination of post-save signals I could use? Any advice will help.
As seen in this answer, "there's an app for that": django-cleanup.
If you prefer to implement it yourself, the accepted answer to the question #raphv linked in the comment gives some good pointers.
A good solution for updated change followup is to use model signals; I tend to use post_init and post_save, in this case to delete the old image file after the change has been made. Something like so:
from django.db.models.signals import post_init, post_save
from django.dispatch import receiver
from myapp.models import CartaMagicPy
#receiver(post_init, sender= CartaMagicPy)
def backup_image_path(sender, instance, **kwargs):
instance._current_imagen_file = instance.imagen
#receiver(post_save, sender= CartaMagicPy)
def delete_old_image(sender, instance, **kwargs):
if hasattr(instance, '_current_imagen_file'):
if instance._current_imagen_file != instance.imagen.path:
instance._current_imagen_file.delete(save=False)
This also seems to be generally what the linked package above does, plus some additional safety checks and cleanup.
We could optimize if the image accepts null values with the following code.
#receiver(post_init, sender= CartaMagicPy)
def backup_image_path(sender, instance, **kwargs):
instance._current_imagen_file = instance.imagen
#receiver(post_save, sender=CartaMagicPy)
def delete_old_image(sender, instance, **kwargs):
if hasattr(instance, '_current_imagen_file'):
if instance.imagen:
if instance._current_imagen_file != instance.imagen.path:
instance._current_imagen_file.delete(save=False)
else:
if instance._current_imagen_file:
instance._current_imagen_file.delete()
I am not sure, but you can try this
use Django-cleanup
pip install django-cleanup
settings.py
INSTALLED_APPS = (
...
'django_cleanup', # should go after your apps
...
)
I'm trying to create a simple signal which prints something after a new object of the Staff model is saved in the Django-admin. The MVC python files live in AppName. Here is the code in each file:
models.py
from django.db import models
from django.db.models import signals
from django.dispatch import Signal
from django.contrib.auth.models import User
from AppName.signals import printfunction
from django.db.models.signals import post_save
class Staff(User):
class Meta:
proxy = True
app_label = 'auth'
verbose_name_plural = 'Users - Staff'
Signal.connect(printfunction, signal=signals.post_save, sender=Staff)
signals.py
def printfunction(sender, instance, signal, *args, **kwargs):
print ("signal alpha!")
However it is raising the following exception:
TypeError: connect() got an unexpected keyword argument 'signal'
I followed the 1.8 django documentation on signals. Why is this error occurring and how to fix it?
Signal.connect(receiver[, sender=None, weak=True, dispatch_uid=None])
This is a very common notation for documentation. It is not literal code that can be used as-is. The arguments in between [ and ] are optional, if you leave them out they will use the default values. connect is a method on the class Signal. Unless otherwise specified, you can assume it is an instance method. Instead of literally calling Signal.connect(), you should call signal_instance.connect(), where signal_instance is of course an instance of the Signal class.
In this case, signals.post_save is an instance of Signal, and it's the instance to which you want to connect your function. The receiver argument is required, and in this case it is your function printfunction. sender, weak and dispatch_uid are all optional. In your example you're only passing in Staff as the sender, and you leave the other arguments as their default values. So, your final function call should look like this:
signals.post_save.connect(printfunction, sender=Staff)
Here's its the right way to do it:
#receiver(post_save, sender=Staff)
def printfunction(sender, instance, signal, *args, **kwargs):
print ("signal alpha!")
So here's something I'm trying to figure out. I've got a method that is triggered by post_save
for this "Story" model. Works fine. What I need to do is figure out how to mock out the test, so I can fake the call and make assertions on my returns. I think I need to patch it somehow, but I've tried a couple different ways without much success. Best i can get is a object instance, but it ignores values I pass in.
I've commented in my test where my confusion lies. Any help would be welcome.
Here's my test:
from django.test import TestCase
from django.test.client import Client
from marketing.blog.models import Post, Tag
from unittest.mock import patch, Mock
class BlogTestCase(TestCase):
fixtures = [
'auth-test.json',
'blog-test.json',
]
def setUp(self):
self.client = Client()
def test_list(self):
# verify that we can load the list page
r = self.client.get('/blog/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, "<h1>The Latest from Our Blog</h1>")
self.assertContains(r, 'Simple JavaScript Date Formatting')
self.assertContains(r, 'Page 1 of 2')
# loading a page out of range should redirect to last page
r = self.client.get('/blog/5/', follow=True)
self.assertEqual(r.redirect_chain, [
('http://testserver/blog/2/', 302)
])
self.assertContains(r, 'Page 2 of 2')
# verify that unpublished posts are not displayed
with patch('requests') as mock_requests:
# my futile attempt at mocking.
# creates <MagicMock> object but not able to call return_values
mock_requests.post.return_value = mock_response = Mock()
# this doesn't get to the magic mock object. Why?
mock_response.status_code = 201
p = Post.objects.get(id=5)
p.published = False
# post_save signal runs here and requests is called.
# Needs to be mocked.
p.save()
r = self.client.get('/blog/')
self.assertNotContains(r, 'Simple JavaScript Date Formatting')
Here's the model:
from django.db import models
from django.conf import settings
from django.db.models import signals
import requests
def update_console(sender, instance, raw, created, **kwargs):
# ignoring raw so that test fixture data can load without
# hitting this method.
if not raw:
update = instance
json_obj = {
'author': {
'alias': 'the_dude',
'token': 'the_dude'
},
'text': update.description,
}
headers = {'content-type': 'application/json'}
path = 'http://testserver.com:80/content/add/'
request = requests(path, 'POST',
json_obj, headers=headers,
)
if request.status_code < 299:
story_id = request.json().get('id')
if story_id:
# disconnect and reconnect signal so
# we don't enter recursion-land
signals.post_save.disconnect(
update_console,
sender = Story, )
update.story_id = story_id
update.save()
signals.post_save.connect(
update_console,
sender = Story, )
else:
raise AttributeError('Error Saving to console, '+ request.text)
class Story(models.Model):
"""Lets tell a story"""
story_id = models.CharField(
blank=True,
max_length=10,
help_text="This maps to the id of the post"
)
slug = models.SlugField(
unique=True,
help_text="This is used in URL and in code references.",
)
description = models.TextField(
help_text='2-3 short paragraphs about the story.',
)
def __str__(self):
return self.short_headline
# add/update this record as a custom update in console
signals.post_save.connect(update_console, sender = Story)
You need to patch requests in the module where it is actually used, i.e.
with patch('path.to.your.models.requests') as mock_requests:
mock_requests.return_value.status_code = 200
mock_requests.return_value.json.return_value = {'id': story_id'}
...
The documentation offers more detailed explanations on where to patch:
patch works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Here, you need to patch the name requests inside the models module, hence the need to provide its full path.
Is it possible to get object with comments related to it? Right now django comment framework creates query for every object which has related comments and another queries for comments owners. Can I somehow avoid this? I use django 1.4 so prefetch_related is allowed.
You could create a function that caches the count:
from django.contrib.contenttypes.models import ContentType
from django.contrib import comments
def get_comment_count_key(model):
content_type = ContentType.objects.get_for_model(model)
return 'comment_count_%s_%s' % (content_type.pk, model.pk)
def get_comment_count(model):
key = get_comment_count_key(model)
value = cache.get(key)
if value is None:
value = comments.get_model().objects.filter(
content_type = ContentType.objects.get_for_model(model),
object_pk = model.pk,
site__pk = settings.SITE_ID
).count()
cache.set(key, value)
return value
You could extend the Comment model and add get_comment_count there. Or put get_comment_count as a template filter. It doesn't matter.
Of course, you would also need cache invalidation when a new comment is posted:
from django.db.models import signals
from django.contrib import comments
def refresh_comment_count(sender, instance, **kwargs):
cache.delete(get_comment_count_key(instance.content_object))
get_comment_count(instance.content_object)
post_save.connect(refresh_comment_count, sender=comments.get_model())
post_delete.connect(refresh_comment_count, sender=comments.get_model())
You could improve this last snippet, by using cache.incr() on comment_was_posted, and cache.decr() on post_delete but that's left as an exercise for you :)