post_delete/pre_delete signals not firing for specific sender - django

I´ve got a model "Comment" and a signal to take actions when a comment is deleted.
The signal executes when deleting the comment in the admin, but not when deleted through django-rest-framework.
#receiver(post_delete, sender=Comment, dispatch_uid=str(uuid.uuid1())) # I tried also removing dispatch_uid
def comment_post_delete(sender, instance, *args, **kwargs):
I´m not really sure if this is related to django-rest-framework but that´s how my app works.
Other thing to note is that many other signals are working just fine.
All the signals are declared in a separate file signals.py and I import it at the end of models.py with a simple import signals
The only difference with other delete operations is that I´m overriding the "destroy" method of the viewset:
class CommentViewSet(mixins.CreateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = CommentSerializer
def destroy(self, request, *args, **kwargs):
# only the comment author or the media owner are allowed to delete
instance = self.get_object()
if request.user != instance.user and request.user != instance.media.owner:
error = {'detail': 'No tienes permiso para borrar este comentario'}
return Response(data=error, status=status.HTTP_403_FORBIDDEN)
return super(CommentViewSet, self).destroy(request, args, kwargs)

post_delete and pre_delete will not be fired if the sender parameter does not match the model you are expecting.
To check the sender, create a receiver without sender parameter:
#receiver(post_delete)
def comment_post_delete(sender, instance, *args, **kwargs):
if sender == Comment:
# do something
Why could a signal get dispatched with a different model if the model being saved was "Comment"?
This can happen when django automatically set a deferred model, so when I was expecting a "Comment" I was getting something like "Comment_deferred_somefield".
Django automatic deferring can happen for example when the query is using Model.objects.only('field1', 'field2', 'etc') and there are some missing fields in the only() method

Related

DRF PUT on Viewset/Serializer does not fire post_save signal on model instance

I have the following DRF viewset:
class MyViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet, CreateModelMixin, DestroyModelMixin):
serializer_class = MySerializer
queryset = My.objects.all()
lookup_field = "uuid"
The following Serializer:
class MySerializer(serializers.ModelSerializer):
class Meta:
model = My
fields = [
'uuid',
'name'
]
I have a signal stored in signal.py in the app
#receiver(post_save, sender=My)
def my_updated(sender, instance, created, **kwargs):
if not created:
print('MySignal Fired')
The signal is imported in apps.py.
The signal is working when I open a terminal and run the .save() method on a instance.
The app is also declared in the django config.
Any ideas?
BTW. when executing the .PUT on the API it saves everything nicely in the DB. So the api is doing it's work. Not sure why the signal is not firing though.
post_save not work on put and patch request..
The new functions to hook on generic views are:
perform_create(self, serializer) - Called by CreateModelMixin when saving a new object instance.
perform_update(self, serializer) - Called by UpdateModelMixin when saving an existing object instance.
perform_destroy(self, instance) - Called by DestroyModelMixin when deleting an object instance.
See the docs : Read this
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def perform_update(self, serializer):
instance = serializer.save()

Django REST API: run operations after put request

I am trying to find a clean way to process some task after successfully completes the PUT request for REST API. I am using post_update() function but its never being called. Here is my code
class portfolio_crud(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'id'
serializer_class = user_ticker_portfolio_serializer
def get_queryset(self):
return user_ticker_portfolio.objects.filter(user = self.request.user)
def put(self, request, *args, **kwargs):
print("got the put request to update portfolio")
return self.update(request, *args, **kwargs)
def post_update(self, serializer):
print("got the post save call") #never executed
Depends on what you want to do, but I usually use django's post_save hook as opposed to something on the viewset or serializer. Something like this:
from django.db.models.signals import post_save
#receiver(post_save, sender=YourPortolioClass)
def portfolio_post_save(sender, created, instance, raw, **kwargs):
""" We need to do something after updating a portfolio
"""
if created or raw:
return
# do your update stuff here.
Not sure if this will be much help to you (but I hope so) - I just use multithreading and create another thread before returning.
I don't see anything calling that function... but I am curious

Get current user log in signal in Django

I am just using the admin site in Django. I have 2 Django signals (pre_save and post_save). I would like to have the username of the current user. How would I do that? It does not seem I can send a request Or I did not understand it.
Thanks
Being reluctant to mess around with thread-local state, I decided to try a different approach. As far as I can tell, the post_save and pre_save signal handlers are called synchronously in the thread that calls save(). If we are in the normal request handling loop, then we can just walk up the stack to find the request object as a local variable somewhere. e.g.
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save)
def my_callback(sender, **kwargs):
import inspect
for frame_record in inspect.stack():
if frame_record[3]=='get_response':
request = frame_record[0].f_locals['request']
break
else:
request = None
...
If there's a current request, you can grab the user attribute from it.
Note: like it says in the inspect module docs,
This function relies on Python stack frame support in the interpreter, which isn’t
guaranteed to exist in all implementations of Python.
If you are using the admin site why not use a custom model admin
class MyModelAdmin( admin.ModelAdmin ):
def save_model( self, request, obj, form, change ):
#pre save stuff here
obj.save()
#post save stuff here
admin.site.register( MyModel, MyModelAdmin )
A signal is something that is fired every time the object is saved regardless of if it is being done by the admin or some process that isn't tied to a request and isn't really an appropriate place to be doing request based actions
You can use a middleware to store the current user: http://djangosnippets.org/snippets/2179/
Then you would be able to get the user with get_current_user()
We can solve this problem using middleware classes.
Create singleton class in where will be storing user variable.
class Singleton(type):
'''
Singleton pattern requires for GetUser class
'''
def __init__(cls, name, bases, dicts):
cls.instance = None
def __call__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
return cls.instance
class NotLoggedInUserException(Exception):
'''
'''
def __init__(self, val='No users have been logged in'):
self.val = val
super(NotLoggedInUser, self).__init__()
def __str__(self):
return self.val
class LoggedInUser(object):
__metaclass__ = Singleton
user = None
def set_user(self, request):
if request.user.is_authenticated():
self.user = request.user
#property
def current_user(self):
'''
Return current user or raise Exception
'''
if self.user is None:
raise NotLoggedInUserException()
return self.user
#property
def have_user(self):
return not user is None
Create own middleware class that will be setting user for LoggedInUser instance,and insert out middleware after 'django.contrib.auth.middleware.AuthenticationMiddleware' in settings.py
from useranytimeaccess import LoggedInUser
class LoggedInUserMiddleware(object):
'''
Insert this middleware after django.contrib.auth.middleware.AuthenticationMiddleware
'''
def process_request(self, request):
'''
Returned None for continue request
'''
logged_in_user = LoggedInUser()
logged_in_user.set_user(request)
return None
In signals import LoggedInUser class and get current user
logged_in = LoggedInUser()
user = logged_in.user
In both signals, signal send three arguments,
Sender
Instance
Using
What you need is the Instant being modified...
def signal_catcher(sender, instance, **kwargs):
uname = instance.username
http://docs.djangoproject.com/en/dev/ref/signals/#pre-save

Django model: delete() not triggered

I have a model:
class MyModel(models.Model):
...
def save(self):
print "saving"
...
def delete(self):
print "deleting"
...
The save()-Method is triggered, but the delete() is not. I use the latest svn-Version (Django version 1.2 pre-alpha SVN-11593), and concerning the documentation at http://www.djangoproject.com/documentation/models/save_delete_hooks/ this should work.
Any ideas?
I think you're probably using the admin's bulk delete feature, and are running into the fact that the admin's bulk delete method doesn't call delete() (see the related ticket).
I've got round this in the past by writing a custom admin action for deleting models.
If you're not using the admin's bulk delete method (e.g. you're clicking the delete button on the object's edit page) then something else is going on.
See the warning here:
The “delete selected objects” action
uses QuerySet.delete() for efficiency
reasons, which has an important
caveat: your model’s delete() method
will not be called.
If you wish to override this behavior,
simply write a custom action which
accomplishes deletion in your
preferred manner – for example, by
calling Model.delete() for each of the
selected items.
For more background on bulk deletion,
see the documentation on object
deletion.
My custom admin model looks like this:
from photoblog.models import PhotoBlogEntry
from django.contrib import admin
class PhotoBlogEntryAdmin(admin.ModelAdmin):
actions=['really_delete_selected']
def get_actions(self, request):
actions = super(PhotoBlogEntryAdmin, self).get_actions(request)
del actions['delete_selected']
return actions
def really_delete_selected(self, request, queryset):
for obj in queryset:
obj.delete()
if queryset.count() == 1:
message_bit = "1 photoblog entry was"
else:
message_bit = "%s photoblog entries were" % queryset.count()
self.message_user(request, "%s successfully deleted." % message_bit)
really_delete_selected.short_description = "Delete selected entries"
admin.site.register(PhotoBlogEntry, PhotoBlogEntryAdmin)
I know this question is ancient, but I just ran into this again and wanted to add that you can always move your code to a pre_delete or post_delete signal like so:
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
#receiver(pre_delete, sender=MyModel)
def _mymodel_delete(sender, instance, **kwargs):
print("deleting")
It works with the admin's bulk delete action (at least as of 1.3.1).
The bulk action of the admin calls queryset.delete().
You could override the .delete() method of the queryset,
so it always does a 1-by-1 deletion of objects. For example:
in managers.py:
from django.db import models
from django.db.models.query import QuerySet
class PhotoQuerySet(QuerySet):
""" Methods that appear both in the manager and queryset. """
def delete(self):
# Use individual queries to the attachment is removed.
for photo in self.all():
photo.delete()
In models.py:
from django.db import models
class Photo(models.Model):
image = models.ImageField(upload_to='images')
objects = PhotoQuerySet.as_manager()
def delete(self, *args, **kwargs):
# Note this is a simple example. it only handles delete(),
# and not replacing images in .save()
super(Photo, self).delete(*args, **kwargs)
self.image.delete()
Using django v2.2.2, I solved this problem with the following code
models.py
class MyModel(models.Model):
file = models.FileField(upload_to=<path>)
def save(self, *args, **kwargs):
if self.pk is not None:
old_file = MyModel.objects.get(pk=self.pk).file
if old_file.path != self.file.path:
self.file.storage.delete(old_file.path)
return super(MyModel, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
ret = super(MyModel, self).delete(*args, **kwargs)
self.file.storage.delete(self.file.path)
return ret
admin.py
class MyModelAdmin(admin.ModelAdmin):
def delete_queryset(self, request, queryset):
for obj in queryset:
obj.delete()
For the DefaultAdminSite the delete_queryset is called if the user has the correct permissions, the only difference is that the original function calls queryset.delete() which doesn't trigger the model delete method. This is less efficient since is not a bulk operation anymore, but it keeps the filesystem clean =)
The main issue is that the Django admin's bulk delete uses SQL, not instance.delete(), as noted elsewhere. For an admin-only solution, the following solution preserves the Django admin's "do you really want to delete these" interstitial. vdboor's solution is the most general, however.
from django.contrib.admin.actions import delete_selected
class BulkDeleteMixin(object):
class SafeDeleteQuerysetWrapper(object):
def __init__(self, wrapped_queryset):
self.wrapped_queryset = wrapped_queryset
def _safe_delete(self):
for obj in self.wrapped_queryset:
obj.delete()
def __getattr__(self, attr):
if attr == 'delete':
return self._safe_delete
else:
return getattr(self.wrapped_queryset, attr)
def __iter__(self):
for obj in self.wrapped_queryset:
yield obj
def __getitem__(self, index):
return self.wrapped_queryset[index]
def __len__(self):
return len(self.wrapped_queryset)
def get_actions(self, request):
actions = super(BulkDeleteMixin, self).get_actions(request)
actions['delete_selected'] = (BulkDeleteMixin.action_safe_bulk_delete, 'delete_selected', ugettext_lazy("Delete selected %(verbose_name_plural)s"))
return actions
def action_safe_bulk_delete(self, request, queryset):
wrapped_queryset = BulkDeleteMixin.SafeDeleteQuerysetWrapper(queryset)
return delete_selected(self, request, wrapped_queryset)
class SomeAdmin(BulkDeleteMixin, ModelAdmin):
...

Showing custom model validation exceptions in the Django admin site

I have a booking model that needs to check if the item being booked out is available. I would like to have the logic behind figuring out if the item is available centralised so that no matter where I save the instance this code validates that it can be saved.
At the moment I have this code in a custom save function of my model class:
def save(self):
if self.is_available(): # my custom check availability function
super(MyObj, self).save()
else:
# this is the bit I'm stuck with..
raise forms.ValidationError('Item already booked for those dates')
This works fine - the error is raised if the item is unavailable, and my item is not saved. I can capture the exception from my front end form code, but what about the Django admin site? How can I get my exception to be displayed like any other validation error in the admin site?
In django 1.2, model validation has been added.
You can now add a "clean" method to your models which raise ValidationError exceptions, and it will be called automatically when using the django admin.
The clean() method is called when using the django admin, but NOT called on save().
If you need to use the clean() method outside of the admin, you will need to explicitly call clean() yourself.
http://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#validating-objects
So your clean method could be something like this:
from django.core.exceptions import ValidationError
class MyModel(models.Model):
def is_available(self):
#do check here
return result
def clean(self):
if not self.is_available():
raise ValidationError('Item already booked for those dates')
I haven't made use of it extensively, but seems like much less code than having to create a ModelForm, and then link that form in the admin.py file for use in django admin.
Pretty old post, but I think "use custom cleaning" is still the accepted answer. But it is not satisfactory. You can do as much pre checking as you want you still may get an exception in Model.save(), and you may want to show a message to the user in a fashion consistent with a form validation error.
The solution I found was to override ModelAdmin.changeform_view(). In this case I'm catching an integrity error generated somewhere down in the SQL driver:
from django.contrib import messages
from django.http import HttpResponseRedirect
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
try:
return super(MyModelAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except IntegrityError as e:
self.message_user(request, e, level=messages.ERROR)
return HttpResponseRedirect(form_url)
The best way is put the validation one field is use the ModelForm... [ forms.py]
class FormProduct(forms.ModelForm):
class Meta:
model = Product
def clean_photo(self):
if self.cleaned_data["photo"] is None:
raise forms.ValidationError(u"You need set some imagem.")
And set the FORM that you create in respective model admin [ admin.py ]
class ProductAdmin(admin.ModelAdmin):
form = FormProduct
I've also tried to solve this and there is my solution- in my case i needed to deny any changes in related_objects if the main_object is locked for editing.
1) custom Exception
class Error(Exception):
"""Base class for errors in this module."""
pass
class EditNotAllowedError(Error):
def __init__(self, msg):
Exception.__init__(self, msg)
2) metaclass with custom save method- all my related_data models will be based on this:
class RelatedModel(models.Model):
main_object = models.ForeignKey("Main")
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.main_object.is_editable():
super(RelatedModel, self).save(*args, **kwargs)
else:
raise EditNotAllowedError, "Closed for editing"
3) metaform - all my related_data admin forms will be based on this (it will ensure that admin interface will inform user without admin interface error):
from django.forms import ModelForm, ValidationError
...
class RelatedModelForm(ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if not cleaned_data.get("main_object")
raise ValidationError("Closed for editing")
super(RelatedModelForm, self).clean() # important- let admin do its work on data!
return cleaned_data
To my mind it is not so much overhead and still pretty straightforward and maintainable.
from django.db import models
from django.core.exceptions import ValidationError
class Post(models.Model):
is_cleaned = False
title = models.CharField(max_length=255)
def clean(self):
self.is_cleaned = True
if something():
raise ValidationError("my error message")
super(Post, self).clean()
def save(self, *args, **kwargs):
if not self.is_cleaned:
self.full_clean()
super(Post, self).save(*args, **kwargs)