55

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?

Anto
  • 6,806
  • 8
  • 43
  • 65
Mo J. Mughrabi
  • 6,747
  • 16
  • 85
  • 143
  • 1
    There is a reason why django does not want you to access request object in models and signals. Its philosophy of separation of concerns will be violated. – shreyas Jun 27 '19 at 08:52

13 Answers13

40

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.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • hmm It make sense, I just reformatted my question to find out a way to do the job using alternative methods – Mo J. Mughrabi Jan 18 '11 at 15:13
  • http://stackoverflow.com/questions/4725685/django-auto-filling-some-data-based-on-model-attribute – Mo J. Mughrabi Jan 18 '11 at 15:14
  • 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. – radtek Oct 02 '14 at 23:21
  • 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. – MiniGunnR Apr 05 '16 at 10:49
11

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")
Mahmood Garibov
  • 314
  • 2
  • 13
10

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
PaulR
  • 610
  • 6
  • 15
9

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.

Filip Dupanović
  • 32,650
  • 13
  • 84
  • 114
7

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
firebird631
  • 569
  • 7
  • 10
  • Sounds clean approach. I want to find out how expensive is this. – varnothing Oct 17 '16 at 07:44
  • 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. – Daniel Dror Jul 10 '17 at 07:41
  • @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. – Agey Nov 27 '17 at 14:39
  • I used it, works badly. Probably it's because one thread can process several requests. – Mark Mishyn Nov 04 '19 at 06:39
  • @MarkMishyn do you imply that `current_user` can be overwritten by another request while the original request hasn't finished yet? – Eugene Kovalev Apr 05 '20 at 13:54
  • 1
    @EugeneKovalev Yes, I imply this scenario. – Mark Mishyn Apr 06 '20 at 13:05
6

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))
Ian
  • 172
  • 1
  • 13
  • 2
    This is terribly prone to race conditions. Which might be ok for a lot of use-cases, but it should be mentioned. – Julien Mar 25 '17 at 12:35
6

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

bjorn
  • 120
  • 1
  • 5
4

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.

  • 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). – SaturnFromTitan Mar 19 '19 at 19:12
  • 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. – Yaroslav Varkhol Mar 20 '19 at 06:07
2

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)

kiril
  • 4,914
  • 1
  • 30
  • 40
  • Could you elaborate? How come:"all the instances I create had a reference to the user that creates them"? – dmitri Jun 12 '14 at 06:39
  • 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. – kiril Jul 03 '14 at 13:41
0

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 ?

yassine
  • 117
  • 3
0

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]
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
SaimumIslam27
  • 971
  • 1
  • 8
  • 14
  • 2
    While this code snippet may be the solution, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Shawn Hemelstrand Jan 24 '23 at 08:00
-4
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',
)
  • 4
    A description of what you did there would be helpful. – JoelC Nov 18 '14 at 13:11
  • 6
    This is a very bad idea, never do it this way. With most cache configurations this is gonna lead to race condition. – marxin Aug 10 '16 at 10:36
  • 3
    This is totally WRONG. cache is not request specific – sha256 Jan 25 '17 at 10:09
  • 1
    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 Jul 10 '17 at 07:34