Get absolute_url with Django signal - python

I would like to get the absolute url from my saved object. My model has a method named get_absolute_url and I would like to call this method with my django post_save signal.
I receive a post_save signal when a new entry is added inside a specific table named Thread. This post_save signal executes my Celery task.
My Thread model is :
class Thread(models.Model):
""" A thread with a title """
topic = models.ForeignKey('Topic')
title = models.CharField(max_length=200)
sticky = models.BooleanField(default=False)
slug = models.SlugField()
time_created = models.DateTimeField(default=timezone.now)
time_last_activity = models.DateTimeField(default=timezone.now)
def __init__(self, *args, **kwargs):
""" Initialize 'time_last_activity' to 'time_created' """
super(Thread, self).__init__(*args, **kwargs)
self.time_last_activity = self.time_created
def __str__(self):
""" Return the thread's title """
return self.title
def get_absolute_url(self):
""" Return the url of the instance's detail view """
url_kwargs = {
'topic_pk': self.topic.pk,
'topic_slug': self.topic.slug,
'thread_pk': self.pk,
'thread_slug': self.slug,
}
return reverse('simple-forums:thread-detail', kwargs=url_kwargs)
In my model, I have a celery.py file :
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db.models.signals import post_save
from django.dispatch import receiver
from simple_forums.models import Thread
from ..tasks import thread_notification
#receiver(post_save, sender=Thread)
def get_new_thread(sender, instance, **kwargs):
""" Post_save method which start Celery task to notify forum subscribers that a new thread has been created """
url = Thread.get_absolute_url()
print(url)
thread_title = instance.title
thread_id = instance.id
topic_id = instance.topic_id
topic_slug = instance.topic.slug
topic_title = instance.topic.title
thread_notification.delay(thread_id=thread_id, thread_title=thread_title, topic_id=topic_id, topic_slug=topic_slug,
topic_title=topic_title)
And in my tasks.py file :
# -*- coding: utf-8 -*-
from celery import shared_task
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
User = get_user_model()
#shared_task(bind=True, time_limit=3600, soft_time_limit=3600)
def thread_notification(self):
print('Celery task executed')
return ['success_message', _('Celery task ended')]
I would like to get the absolute_url in order to send an email with the new Thread path.
My question is : How I can pick up get_absolute_url or use request.build_absolute_uri if I don't have a specific view (not necessary) ?

Here:
#receiver(post_save, sender=Thread)
def get_new_thread(sender, instance, **kwargs):
url = Thread.get_absolute_url()
the saved Thread instance is (suprise, surprise) your instance argument, so you want:
url = instance.get_absolute_url()
calling an instance method on a class makes no sense (nb: except for a couple specific corner cases, and then you have to pass the instance as first argument, but let's not get further with this, when you'll need it you'll know how it works).
Now since you're in the same app, using a signal here makes no sense either and is actually an antipattern. The point of signals is to allow an app to react to events emitted by other apps. Here, your code should quite simply be in Thread.save().

Related

How can I implement post_save signals in Django?

I am currently building a geolocation app, and I'm somewhat stuck somewhere. I'm trying to implement a post_save Django signals in this code, but I can't figure out what exactly I need to do. any help here would be appreciate. Here's my code:
from ipaddress import ip_address
from django.contrib.auth import get_user_model
from celery import shared_task
from apps.users.abstractapi import AbstractAPI
User = get_user_model()
#shared_task
def enrich_user(user_pk):
user = User.objects.get(pk=user_pk)
api = AbstractAPI()
location_details = api.get_geolocation_details(ip_address=user.ip_address)
if location_details is not None:
user.country = location_details.get("country")
user.country_code = location_details.get("country_code")
user.country_geoname_id = location_details.details.get("country_geoname_id")
user.longitude = location_details.get("longitude")
user.latitude = location_details.get("latitude")
user.save(update_fields=("country", "country_code", "country_geoname_id", "longitude", "latitude"))
holiday_details = api.get_holiday_details(
country_code=user.country_code,
day=user.date_joined.day,
month=user.date_joined.month,
year=user.date_joined.year,
)
if holiday_details is not None and any(holiday_details):
user.joined_on_holiday = True
user.save(update_fields=("joined_on_holiday",))
A post_save signal in Django looks like this:
from django.dispatch import receiver
#receiver(models.signals.post_save, sender=User)
def your_function(sender, instance, using, **kwargs):
# your code that you want to run
instance.save()
Be careful with saving the instance - that will itself cause the post_save signal to run again. You should put a condition in place that will only evaluate once before the instance is saved. Something like:
if instance.joined_on_holiday == False:
instance.joined_on_holiday = True
instance.save()

Celery (4.2): how to get task id for current task

AIM
For the purposes of an alarm clock, I am attempting to 'get' the specific Alarm created by the User in the SetAlarmForm.
From the other answers on the same topic (Q1, Q2, Q3), I am attempting the line: objx = Alarm.objects.get(id=run_alarm.request.id). Perhaps, I am missing something obvious or the version of Celery has been updated?
ERROR
[ERROR/ForkPoolWorker-2] raised unexpected: DoesNotExist('Alarm matching query does not exist')
CODE
Models.py
class Alarm(models.Model):
""" Model representing each Alarm """
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Views.py
class AlarmCreateView(LoginRequiredMixin, CreateView):
""" CreateView for User to create the Alarm object """
model = Alarm
form_class = SetAlarmForm
template_name = 'weather_alarm/set_alarm.html'
login_url = "/login/"
def form_valid(self, form):
self.create_alarm_object(self.request, form)
run_alarm.delay()
return HttpResponseRedirect(self.get_success_url())
Tasks.py
import time
from celery import Celery, shared_task, current_task
from datetime import datetime
from .models import Alarm
#shared_task
def run_alarm():
""" Function to organise the steps for the alarm using Celery """
objx = Alarm.objects.get(id=run_alarm.request.id)
second_countdown = objx.get_alarm_length() # get the length of the alarm, in seconds
time.sleep(second_countdown) # wait for the alarm time
conditions_satisfied = objx.check_conditionals() # check if conditionals are satisfied
if conditions_satisfied == True: # conditions are satified
print("Ring ring!")
return True
else: # conditions aren't satisfied
print("I'm only sleeping!")
return True
The simplest way to fix this would be to make alarmID an argument passed to your task:
Tasks.py
#shared_task
def run_alarm(alarmID):
objx = Alarm.objects.get(id = alarmID)
You'll need to pass this ID when calling your task in your view:
Views.py
...
def form_valid(self, form):
#making an assumption about what create_alarm_object returns here; you get the idea
newAlarm = self.create_alarm_object(self.request, form)
run_alarm.delay(newAlarm.id)
Notice how you can pass the argument to run_alarm by giving it to delay here. Further reading: http://docs.celeryproject.org/en/latest/userguide/calling.html#example
The reason you are encountering your error is that request.id is going to point at the task ID of the individual asynchronous celery task being run, not at the ID of the alarm object. Further reading: http://docs.celeryproject.org/en/latest/userguide/tasks.html?highlight=request#task-request

Sending and receiving signals in django models

I am using django 2.0.8 and Python 3.5. I want to be able to send and receive custom signals when an object is saved to the database.
I have followed the Django documentation on listening to signals and also the core signals bundled with Django - however, I am unable to get my example to work.
This is what I have so far:
myapp/models.py
from django.db import models
import django.dispatch
my_signal = django.dispatch.Signal(providing_args=["name"])
class Foo(models.Model):
name = models.CharField(max_length=16)
def save(self, *args, **kwargs):
try:
# Call the "real" save() method.
super().save(*args, **kwargs)
# Fire off signal
my_signal.send(sender=self.__class__, name=self.name)
except Exception as e:
print ('Exception:', e)
#pass
myapp/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Foo
#receiver(post_save, sender=Foo)
def foo_handler(sender, **kwargs):
print('Foo Signal Recieved!')
print(kwargs)
myapp/app.py
class MyappConfig(AppConfig):
name = 'myapp'
label = 'myapp'
def ready(self):
import myapp.signals
Sample usage
from myapp.models import Foo
foo = Foo(name='Homer Simpson')
foo.save() # Object is saved, but event is not fired!
Can anyone explain why the signal is not being fired?
It seems you need two features supplied by Django. signal and contenttypes.
So read the doc first
The model Activity is related to contenttypes,seems you miss object_id Field, which indicate which model instance is being crud.
For every crud action, An Activity instance is being created.This part is just the code written in signal.py
signal: signal have to connect each concrete model. Fortunately,See the source code of decorator receiver.
We have a signal list [post_save,post_delete] and a model list (FoodooChile, FooBarChile) to connect .
In post_save,argument created indicate the action is create or update.
At last, Usually we import the signal file in urls.py, maybe not the best practice.
It is also related to your settings.py. use 'myapp.apps.MyappConfig' replace myapp in settings.py,or define default_app_config = 'myapp.apps.MyappConfig' in myapp/__init__.py. The link above in comments describe this in detail
In the myapp.signals you have a receiver that handels the post_save signal (#receiver(post_save, sender=Foo)) it doesn't connect to your signal.
Make sure you are using your app config in the __init__.py of you application default_app_config = 'myapp.apps.MyappConfig'
To connect to the signal you created try this in your signals.py file:
#receiver(my_signal)
def my_handler(name, **kwargs):
print(name)
You are reinventing the wheel, but only putting it on one side of the cart, so to speak.
the post_save signal is always sent on save, so defining your own signal is overkill. I know you got the argument there, but the receiver has the sender argument already, which is the saved object, so you can just do sender.name and you got the value you need.
Apart from that, you have a syntax error, your custom save function for your model is not indented. I don't know if this is a formatting error in your question or if that is how it looks in your code. Either way, should work if you just drop your custom signal.
Model
from django.db import models
import django.dispatch
class Foo(models.Model):
name = models.CharField(max_length=16)
Signals
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Foo
#receiver(post_save, sender=Foo)
def foo_handler(sender, **kwargs):
print(sender.name)
App
class MyappConfig(AppConfig):
name = 'myapp'
label = 'myapp'
def ready(self):
import myapp.signals
Sample
from myapp.models import Foo
foo = Foo(name='Homer Simpson')
foo.save()

How to use a post_save receiver with django-polymorphic?

I have a django-polymorphic model and want to implement a post_save signal to automatically create a related model that is also polymorphic.
It's something like the code below, the relevant piece of non-working code being the #receiver decorated work_post_save method. The problem is the instance is a ctype_id and not an object.
from django.db import models
from django.db.models import signals
from django.dispatch import receiver
from polymorphic.models import PolymorphicModel
from mygallery.models import Album
# Work Parent PolymorphicModel
class Work(PolymorphicModel):
title = models.CharField(blank=True,max_length=256)
slug = models.SlugField(max_length=256)
#receiver(signals.post_save, sender=Work)
def work_post_save(sender, instance, signal, created, **kwargs):
album, new = Album.objects.get_or_create(title=instance.title + ' Stills', slug=instance.slug + '-stills')
work_album, new = WorkAlbum.objects.get_or_create(work=instance, album=album, is_key=True)
class ArtProject(Work):
manifesto = models.CharField(blank=True,max_length=256)
class CodeProject(Work):
code = models.CharField(blank=True,max_length=256)
# Content Parent PolymorphicModel
class WorkContent(PolymorphicModel):
is_key = models.BooleanField(default=False, unique=True, default=False)
class WorkAlbum(WorkContent):
work = models.ForeignKey(Work, related_name='work_albums')
album = models.ForeignKey(Album, related_name='album_works')
I've only fiddled around with this for a bit so I'm not 100% sure what the correct way of handling this is.
What I ended up doing is to not declaring the sender in the #receiver annotation. This has the effect that the callback is triggered by every post_save signal. Then in the callback I check isinstance() with my parent model (in your case Work), so that the callback is only executed after a model I'm interested in is saved. When the callback is executed, the instance parameter is a child model (in your case ArtProject or CodeProject).
#receiver(signals.post_save)
def work_post_save(sender, instance, signal, created, **kwargs):
if isinstance(instance, Work):
# instance is either ArtProject or CodeProject
album, new = Album.objects.get_or_create(title=instance.title + ' Stills', slug=instance.slug + '-stills')
work_album, new = WorkAlbum.objects.get_or_create(work=instance, album=album, is_key=True)
Triggering directly on the parent save() is apparently not supported.

Accessing the user's request in a post_save signal

I have done the below post_save signal in my project.
from django.db.models.signals import post_save
from django.contrib.auth.models import User
# CORE - SIGNALS
# Core Signals will operate based on post
def after_save_handler_attr_audit_obj(sender, **kwargs):
print User.get_profile()
if hasattr(kwargs['instance'], 'audit_obj'):
if kwargs['created']:
kwargs['instance'].audit_obj.create(operation="INSERT", operation_by=**USER.ID**).save()
else:
kwargs['instance'].audit_obj.create(operation="UPDATE").save()
# Connect the handler with the post save signal - Django 1.2
post_save.connect(after_save_handler_attr_audit_obj, dispatch_uid="core.models.audit.new")
The operation_by column, I want to get the user_id and store it. Any idea how can do that?
Can't be done. The current user is only available via the request, which is not available when using purely model functionality. Access the user in the view somehow.
I was able to do it by inspecting the stack and looking for the view then looking at the local variables for the view to get the request. It feels like a bit of a hack, but it worked.
import inspect, os
#receiver(post_save, sender=MyModel)
def get_user_in_signal(sender, **kwargs):
for entry in reversed(inspect.stack()):
if os.path.dirname(__file__) + '/views.py' == entry[1]:
try:
user = entry[0].f_locals['request'].user
except:
user = None
break
if user:
# do stuff with the user variable
Ignacio is right. Django's model signals are intended to notify other system components about events associated with instances and their respected data, so I guess it's valid that you cannot, say, access request data from a model post_save signal, unless that request data was stored on or associated with the instance.
I guess there are lots of ways to handle it, ranging from worse to better, but I'd say this is a prime example for creating class-based/function-based generic views that will automatically handle this for you.
Have your views that inherit from CreateView, UpdateView or DeleteView additionally inherit from your AuditMixin class if they handle verbs that operate on models that need to be audited. The AuditMixin can then hook into the views that successfully create\update\delete objects and create an entry in the database.
Makes perfect sense, very clean, easily pluggable and gives birth to happy ponies. Flipside? You'll either have to be on the soon-to-be-released Django 1.3 release or you'll have to spend some time fiddlebending the function-based generic views and providing new ones for each auditing operation.
You can do that with the help of middleware. Create get_request.py in your app. Then
from threading import current_thread
from django.utils.deprecation import MiddlewareMixin
_requests = {}
def current_request():
return _requests.get(current_thread().ident, None)
class RequestMiddleware(MiddlewareMixin):
def process_request(self, request):
_requests[current_thread().ident] = request
def process_response(self, request, response):
# when response is ready, request should be flushed
_requests.pop(current_thread().ident, None)
return response
def process_exception(self, request, exception):
# if an exception has happened, request should be flushed too
_requests.pop(current_thread().ident, None)
Then add this middleware to your settings:
MIDDLEWARE = [
....
'<your_app>.get_request.RequestMiddleware',
]
Then add import to your signals:
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from <your_app>.get_request import current_request
# CORE - SIGNALS
# Core Signals will operate based on post
def after_save_handler_attr_audit_obj(sender, **kwargs):
print(Current User, current_request().user)
print User.get_profile()
if hasattr(kwargs['instance'], 'audit_obj'):
if kwargs['created']:
kwargs['instance'].audit_obj.create(operation="INSERT", operation_by=**USER.ID**).save()
else:
kwargs['instance'].audit_obj.create(operation="UPDATE").save()
# Connect the handler with the post save signal - Django 1.2
post_save.connect(after_save_handler_attr_audit_obj, dispatch_uid="core.models.audit.new")
Why not adding a middleware with something like this :
class RequestMiddleware(object):
thread_local = threading.local()
def process_request(self, request):
RequestMiddleware.thread_local.current_user = request.user
and later in your code (specially in a signal in that topic) :
thread_local = RequestMiddleware.thread_local
if hasattr(thread_local, 'current_user'):
user = thread_local.current_user
else:
user = None
For traceability add two attributes to your Model(created_by and updated_by), in "updated_by" save the last user who modified the record. Then in your signal you have the user:
models.py:
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
created_by = models. (max_length=100)
updated_by = models. (max_length=100)
views.py
p = Question.objects.get(pk=1)
p.question_text = 'some new text'
p.updated_by = request.user
p.save()
signals.py
#receiver(pre_save, sender=Question)
def do_something(sender, instance, **kwargs):
try:
obj = Question.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass
else:
if not obj.user == instance.user: # Field has changed
# do something
print('change: user, old=%s new=%s' % (obj.user, instance.user))
You could also use django-reversion for this purpose, e.g.
from reversion.signals import post_revision_commit
import reversion
#receiver(post_save)
def post_revision_commit(sender, **kwargs):
if reversion.is_active():
print(reversion.get_user())
Read more on their API https://django-reversion.readthedocs.io/en/stable/api.html#revision-api
You can do a small hack by overriding you model save() method and setting the user on the saved instance as additional parameter. To get the user I used get_current_authenticated_user() from django_currentuser.middleware.ThreadLocalUserMiddleware (see https://pypi.org/project/django-currentuser/).
In your models.py:
from django_currentuser.middleware import get_current_authenticated_user
class YourModel(models.Model):
...
...
def save(self, *args, **kwargs):
# Hack to pass the user to post save signal.
self.current_authenticated_user = get_current_authenticated_user()
super(YourModel, self).save(*args, **kwargs)
In your signals.py:
#receiver(post_save, sender=YourModel)
def your_model_saved(sender, instance, **kwargs):
user = getattr(instance, 'current_authenticated_user', None)
PS: Don't forget to add 'django_currentuser.middleware.ThreadLocalUserMiddleware' to your MIDDLEWARE_CLASSES.
I imagine you would have figured this out, but I had the same problem and I realised that all the instances I create had a reference to the user that creates them (which is what you are looking for)
it's possible i guess.
in models.py
class _M(models.Model):
user = models.ForeignKey(...)
in views.py
def _f(request):
_M.objects.create(user=request.user)
in signals.py
#receiver(post_save, sender=_M)
def _p(sender, instance, created, **kwargs):
user = instance.user
No ?
Request object can be obtained from frame record by inspecting.
import inspect
request = [
frame_record[0].f_locals["request"]
for frame_record in inspect.stack()
if frame_record[3] == "get_response"
][0]
def get_requested_user():
import inspect
for frame_record in inspect.stack():
if frame_record[3] == 'get_response':
request = frame_record[0].f_locals['request']
return request.user
else:
return None
context_processors.py
from django.core.cache import cache
def global_variables(request):
cache.set('user', request.user)
----------------------------------
in you model
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.core.cache import cache
from news.models import News
#receiver(pre_delete, sender=News)
def news_delete(sender, instance, **kwargs):
user = cache.get('user')
in settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
'web.context_processors.global_variables',
)

Categories