59

I have a model with a created_by field that is linked to the standard Django User model. I need to automatically populate this with the ID of the current User when the model is saved. I can't do this at the Admin layer, as most parts of the site will not use the built-in Admin. Can anyone advise on how I should go about this?

Matt Ball
  • 354,903
  • 100
  • 647
  • 710

13 Answers13

40

UPDATE 2020-01-02
⚠ The following answer was never updated to the latest Python and Django versions. Since writing this a few years ago packages have been released to solve this problem. Nowadays I highly recommend using django-crum which implements the same technique but has tests and is updated regularly: https://pypi.org/project/django-crum/

The least obstrusive way is to use a CurrentUserMiddleware to store the current user in a thread local object:

current_user.py

from threading import local

_user = local()

class CurrentUserMiddleware(object):
    def process_request(self, request):
        _user.value = request.user

def get_current_user():
    return _user.value

Now you only need to add this middleware to your MIDDLEWARE_CLASSES after the authentication middleware.

settings.py

MIDDLEWARE_CLASSES = (
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
    'current_user.CurrentUserMiddleware',
    ...
)

Your model can now use the get_current_user function to access the user without having to pass the request object around.

models.py

from django.db import models
from current_user import get_current_user

class MyModel(models.Model):
    created_by = models.ForeignKey('auth.User', default=get_current_user)

Hint:

If you are using Django CMS you do not even need to define your own CurrentUserMiddleware but can use cms.middleware.user.CurrentUserMiddleware and the cms.utils.permissions.get_current_user function to retrieve the current user.

bikeshedder
  • 7,337
  • 1
  • 23
  • 29
  • 2
    Seems thorough but in django 1.6.2 and Python 3.3.2 I receive the following error extract when I use south ```./manage.py schemamigration app --initial``` :```return _user.value AttributeError: '_thread._local' object has no attribute 'value'``` – raratiru Mar 22 '14 at 21:01
  • 3
    That is right as the middleware is never called. For South (and probably a lot of other management commands) to work you need to catch the `AttributeError` in `get_current_user` and return `None`. – bikeshedder Mar 23 '14 at 18:58
  • Thank you! `Your model can now use the get_current_user function to access the user without having to pass the request object around.`. Is this the reason why using a middleware class is better practice than using [this snippet](https://code.djangoproject.com/wiki/CookBookNewformsAdminAndUser) or they can be considered as equivalent solutions? – raratiru Mar 24 '14 at 20:33
  • 2
    @rara_tiru It depends on your needs. By using a thread local object and a middleware ANY object saved during the request with an authenticated user will fill the `created_by` field. The snippet only patches the admin. – bikeshedder Mar 25 '14 at 22:22
  • 1
    For some reason this doesn't seem too clean to me, maybe just because I haven't gotten used to it yet, but I like the fact that this can be done. I was starting to get the impression that it was entirely impossible to reach arbitrary variables in a model function, in fact some people have outright stated as much. Thanks for sharing this. +1 – Teekin Nov 16 '14 at 17:44
36

If you want something that will work both in the admin and elsewhere, you should use a custom modelform. The basic idea is to override the __init__ method to take an extra parameter - request - and store it as an attribute of the form, then also override the save method to set the user id before saving to the database.

class MyModelForm(forms.ModelForm):

   def __init__(self, *args, **kwargs):
       self.request = kwargs.pop('request', None)
       return super(MyModelForm, self).__init__(*args, **kwargs)


   def save(self, *args, **kwargs):
       kwargs['commit']=False
       obj = super(MyModelForm, self).save(*args, **kwargs)
       if self.request:
           obj.user = self.request.user
       obj.save()
       return obj
Dmitrii Mikhailov
  • 5,053
  • 7
  • 43
  • 69
Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • 2
    I'm unable to figure out how to make the admin to initialize MyModelForm with 'request' object. Is it even possible without modifying the contrib.admin code itself? – jholster Aug 23 '10 at 09:26
  • If using class based views it is very important to see Monster and Florentin's answers below, they are essential steps. – Mark Stahler Mar 06 '13 at 15:16
  • but what about having acces to it in `pre_save` of model? http://stackoverflow.com/questions/25305186/accessing-in-passing-to-django-models-signal-methods-like-pre-save-pre-delete – andilabs Aug 14 '14 at 10:02
  • 5
    just to complete the answer. To be able to access `request` object in `__init__` , it must be passed when initialising your form in your view. Ex: `myform = MyForm(request.POST, request=request)` – vincebodi Jul 04 '15 at 10:06
  • way late...but how would this be modified for a modelformset in views.py? – OverflowingTheGlass Nov 07 '17 at 20:37
  • the syntax of `super(MyModelForm, self)` [changed in python3](https://docs.python.org/3/library/functions.html#super), just call `super()` – suhailvs Jul 07 '18 at 02:47
  • what is the problem with using something like: ```def save(self, user): obj = super().save(commit = False) obj.user = user obj.save() return stuff```. and then call it like `myform.save(request.user)` – suhailvs Jul 07 '18 at 04:03
  • Please add the comment of @vincebodi to the answer – Yacine Mar 27 '20 at 14:25
27

Daniel's answer won't work directly for the admin because you need to pass in the request object. You might be able to do this by overriding the get_form method in your ModelAdmin class but it's probably easier to stay away from the form customisation and just override save_model in your ModelAdmin.

def save_model(self, request, obj, form, change):
    """When creating a new object, set the creator field.
    """
    if not change:
        obj.creator = request.user
    obj.save()
Tim Fletcher
  • 7,062
  • 1
  • 35
  • 33
  • Tim Fletcher, hello, i get an error if save_model is defined in admin.py when trying to create a new entry(model object of a class and that class has m2m field): "'MyClass' instance needs to have a primary key value before a many-to-many relationship can be used.". Can you advice what should be added? – ted Feb 24 '12 at 18:28
15

This whole approach bugged the heck out of me. I wanted to say it exactly once, so I implemented it in middleware. Just add WhodidMiddleware after your authentication middleware.

If your created_by & modified_by fields are set to editable = False then you will not have to change any of your forms at all.

"""Add user created_by and modified_by foreign key refs to any model automatically.
   Almost entirely taken from https://github.com/Atomidata/django-audit-log/blob/master/audit_log/middleware.py"""
from django.db.models import signals
from django.utils.functional import curry

class WhodidMiddleware(object):
    def process_request(self, request):
        if not request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            if hasattr(request, 'user') and request.user.is_authenticated():
                user = request.user
            else:
                user = None

            mark_whodid = curry(self.mark_whodid, user)
            signals.pre_save.connect(mark_whodid,  dispatch_uid = (self.__class__, request,), weak = False)

    def process_response(self, request, response):
        signals.pre_save.disconnect(dispatch_uid =  (self.__class__, request,))
        return response

    def mark_whodid(self, user, sender, instance, **kwargs):
        if 'created_by' in instance._meta.fields and not instance.created_by:
            instance.created_by = user
        if 'modified_by' in instance._meta.fields:
            instance.modified_by = user
mindlace
  • 313
  • 2
  • 8
  • The best answer. I ended up using getattr(instance, 'pk') to determine whether my models were being created. – Fábio Santos Nov 22 '12 at 16:45
  • 2
    This code is not thread safe. If you are using a WSGI container that uses threads (e.g. Apache2 + mod_wsgi, uWSGI, etc.) you can end up storing the wrong user. – bikeshedder Feb 14 '14 at 18:22
  • This code is indeed not thread safe; probably better to avoid in production environments – acidjunk Jul 20 '16 at 14:20
  • @giantas this code is conceptually wrong. You need to use a `thread local` object for this to work properly. In doubt just use `django-crum` which works and is correct: https://pypi.org/project/django-crum/ – bikeshedder Jan 02 '20 at 10:09
10

here's how I do it with generic views:

class MyView(CreateView):
    model = MyModel

    def form_valid(self, form):
        object = form.save(commit=False)
        object.owner = self.request.user
        object.save()
        return super(MyView, self).form_valid(form)
yretuta
  • 7,963
  • 17
  • 80
  • 151
  • 1
    call `form.save()` again, not `object.save()`, in case there are commit-dependent customizations in the `save()` method (which anyway will call `save()` on the model instance) – trybik Mar 19 '18 at 11:13
7

If you are using class based views Daniel's answer needs more. Add the following to ensure that the request object is available for us in your ModelForm object

class BaseCreateView(CreateView):
    def get_form_kwargs(self):
        """
        Returns the keyword arguments for instanciating the form.
        """
        kwargs = {'initial': self.get_initial()}
        if self.request.method in ('POST', 'PUT'):
            kwargs.update({
                'data': self.request.POST,
                'files': self.request.FILES,
                'request': self.request})
        return kwargs

Also, as already mentioned, you need to return the obj at the end of ModelForm.save()

M Somerville
  • 4,499
  • 30
  • 38
Monster
  • 71
  • 1
  • 1
4

what is the problem with using something like:

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
        exclude = ['created_by']

    def save(self, user):
        obj = super().save(commit = False)
        obj.created_by = user
        obj.save()
        return obj

Now call it like myform.save(request.user) in the views.

here is ModelForm's save function, which has only a commit parameter.

suhailvs
  • 20,182
  • 14
  • 100
  • 98
3

For future references, best solution I found about this subject:

https://pypi.python.org/pypi/django-crum/0.6.1

This library consist of some middleware. After setting up this libary, simply override the save method of model and do the following,

from crum import get_current_user        


def save(self, *args, **kwargs):
    user = get_current_user()
    if not self.pk:
        self.created_by = user
    else:
        self.changed_by = user
    super(Foomodel, self).save(*args, **kwargs)

if you create and abstract model and inherit from it for all your model, you get your auto populated created_by and changed_by fields.

durdenk
  • 1,590
  • 1
  • 14
  • 36
3

Based on bikeshedder's answer, I found a solution since his did not actually work for me.

  1. app/middleware/current_user.py

    from threading import local
    
    _user = local()
    
    class CurrentUserMiddleware(object):
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        _user.value = request.user
        return self.get_response(request)
    
    def get_current_user():
        return _user.value
    
  2. settings.py

    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    
        'common.middleware.current_user.CurrentUserMiddleware',
    ]
    
  3. model.py

    from common.middleware import current_user
    created_by = models.ForeignKey(User, blank=False, related_name='created_by', editable=False, default=current_user.get_current_user)
    

I'm using python 3.5 and django 1.11.3

Rob
  • 26,989
  • 16
  • 82
  • 98
leila
  • 461
  • 1
  • 7
  • 21
  • while running ./manage.py migrate, I get the following error: `AttributeError: '_thread._local' object has no attribute 'value'` which makes sense as the request middle ware is not loaded during migration. Or did I missing something here? Could you please clarify? – radiopassive Apr 04 '19 at 10:11
2

From the Django documentation Models and request.user:

" To track the user that created an object using a CreateView, you can use a custom ModelForm. In the view, ensure that you don’t include [the user field] in the list of fields to edit, and override form_valid() to add the user:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from myapp.models import Author

class AuthorCreate(LoginRequiredMixin, CreateView):
    model = Author
    fields = ['name']

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)
Martin CR
  • 1,250
  • 13
  • 25
1

I don't believe Daniel's answer is the best there is since it changes the default behaviour of a model form by always saving the object.

The code I would use:

forms.py

from django import forms

class MyModelForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super(MyModelForm, self).__init__(*args, **kwargs)

    def save(self, commit=True):
        obj = super(MyModelForm, self).save(commit=False)

        if obj.created_by_id is None:
            obj.created_by = self.user

        if commit:
            obj.save()
        return obj
jgadelange
  • 2,482
  • 1
  • 13
  • 10
1

The 'save' method from forms.ModelForm returns the saved instanced.

You should add one last line to MyModelForm:
...
return obj

This change is necessary if you are using create_object or update_object generic views.
They use the saved object to do the redirect.

Florentin
  • 651
  • 8
  • 8
-5

Note sure if you were looking for this, but adding the following

user = models.ForeignKey('auth.User')

to a model will work to add the user id to the model.

In the following, each hierarchy belongs to a user.

class Hierarchy(models.Model):
    user = models.ForeignKey('auth.User')
    name = models.CharField(max_length=200)
    desc = models.CharField(max_length=1500)