Accessing the user's request in a post_save signal

36,230

Solution 1

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.

Solution 2

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

Solution 3

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.

Solution 4

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

Solution 5

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))
Share:
36,230
Mo J. Mughrabi
Author by

Mo J. Mughrabi

Updated on February 17, 2022

Comments

  • Mo J. Mughrabi
    Mo J. Mughrabi over 2 years

    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?

  • Mo J. Mughrabi
    Mo J. Mughrabi over 13 years
    hmm It make sense, I just reformatted my question to find out a way to do the job using alternative methods
  • Mo J. Mughrabi
    Mo J. Mughrabi over 13 years
  • dmitri
    dmitri about 10 years
    Could you elaborate? How come:"all the instances I create had a reference to the user that creates them"?
  • kiril
    kiril almost 10 years
    What I mean is that all the signals I use has reference to an instance (in the kwargs) that has a field 'created_by' or something similar, that refers to the user.
  • radtek
    radtek over 9 years
    You can do it when loading your view. In your post_save save the status you want to flag in the model. When loading the model in the view, check the status flag and do what you will with it before response. I do an api call post status, then set a model field called api_status with the appropriate status code. I check the code in my get and use the messages framework to notify the user of the status received from the call.
  • JoelC
    JoelC over 9 years
    A description of what you did there would be helpful.
  • MiniGunnR
    MiniGunnR about 8 years
    I am using post_save. The model which is saving the data has a field called user. I am using instance.user to target the user. However, the condition over here is that no user can edit other user's instances. Thus, the logged in user is equal to the instance.user value.
  • marxin
    marxin almost 8 years
    This is a very bad idea, never do it this way. With most cache configurations this is gonna lead to race condition.
  • varnothing
    varnothing over 7 years
    Sounds clean approach. I want to find out how expensive is this.
  • sha256
    sha256 over 7 years
    This is totally WRONG. cache is not request specific
  • Julien
    Julien over 7 years
    This is terribly prone to race conditions. Which might be ok for a lot of use-cases, but it should be mentioned.
  • Daniel Dror
    Daniel Dror almost 7 years
    Just a note, this is not necessarily a bad idea if you give a unique id as a key. My use-case is to add meta data from the request to an instance that is created via a signal, but needs data from the request. I dislike the idea of coupling this with a specific process. I would use the users PK field as user_{pk} and access it's metadata. this shouldn't create a race condition, and gives the intended result.
  • Daniel Dror
    Daniel Dror almost 7 years
    I would advise against using threading for this. This is feel very hacky, and is not application level code, since you don't have much control of those threads. I would use the cache as a communication backend here using a unique ID.
  • Agey
    Agey over 6 years
    @electropoet did you find out how expensive is it? I'm thinking of implementing it as well, and so far i can not think of any side effects.
  • Scott Jungwirth
    Scott Jungwirth over 6 years
    worked great for me, I didn't know what the view would be so I removed that check and it still worked fine.
  • digofreitas
    digofreitas over 5 years
    It's not "right" but it works great, so I think this is the right answer.
  • SaturnFromTitan
    SaturnFromTitan over 5 years
    That feels too hacky and will appear like black magic in the other app that's consuming the signal (accessing a field that doesn't exist in the model definition).
  • Yaroslav Varkhol
    Yaroslav Varkhol over 5 years
    That are the words from which the post starts: "You can do a small hack". Hack is hack, it can solve certain problem. Just be careful with it or find better and safer solution.
  • Mark Mishyn
    Mark Mishyn over 4 years
    I used it, works badly. Probably it's because one thread can process several requests.
  • Eugene Kovalev
    Eugene Kovalev about 4 years
    @MarkMishyn do you imply that current_user can be overwritten by another request while the original request hasn't finished yet?
  • Mark Mishyn
    Mark Mishyn about 4 years
    @EugeneKovalev Yes, I imply this scenario.
  • Ricardo
    Ricardo about 4 years
    very hacky solution, downvote because it's a very bad practice
  • codemastermind
    codemastermind about 4 years
    This does't work for me. Getting KeyError at /api/ 'request'
  • damd
    damd over 3 years
    It might work sometimes, but it's an extreme hack and not meant to be used: downvote.
  • scorpionipx
    scorpionipx over 2 years
    Any downside to this method? I use it and everything seems fine.
  • Cl0ud-l3ss
    Cl0ud-l3ss over 2 years
    Timeless answer
  • logicOnAbstractions
    logicOnAbstractions over 2 years
    DJango is under no obligation to maintain the stack trace compatible with this approach. This code could break at any update really. Nice try, but downvoted as well.