Django: Populate user ID when saving a model
Asked Answered
I

13

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?

Illumination answered 14/5, 2009 at 10:2 Comment(3)
This blog entry discusses exactly that.Cabal
This post is pretty old. Is there an easy way to achieve this now?Cufic
@Illumination What's the answer to this?Cufic
D
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.

Deedradeeds answered 14/2, 2014 at 18:23 Comment(5)
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'Yangyangtze
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.Deedradeeds
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 or they can be considered as equivalent solutions?Yangyangtze
@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.Deedradeeds
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. +1Fairish
R
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
Reinforce answered 14/5, 2009 at 11:43 Comment(8)
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?Deliver
If using class based views it is very important to see Monster and Florentin's answers below, they are essential steps.Bakerman
but what about having acces to it in pre_save of model? #25305686Tefillin
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)Grudging
way late...but how would this be modified for a modelformset in views.py?Teeter
the syntax of super(MyModelForm, self) changed in python3, just call super()Auriga
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)Auriga
Please add the comment of @Grudging to the answerLotetgaronne
B
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()
Bosh answered 7/7, 2011 at 17:36 Comment(1)
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?Apartment
S
14

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
Skiplane answered 19/10, 2012 at 15:37 Comment(4)
The best answer. I ended up using getattr(instance, 'pk') to determine whether my models were being created.Icosahedron
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.Deedradeeds
This code is indeed not thread safe; probably better to avoid in production environmentsKurr
@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: pypi.org/project/django-crumDeedradeeds
M
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)
Misadvise answered 21/3, 2013 at 5:25 Comment(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)Tombac
B
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()

Bench answered 2/3, 2012 at 1:49 Comment(0)
A
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.

Auriga answered 7/7, 2018 at 4:14 Comment(0)
O
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.

On answered 11/7, 2015 at 23:53 Comment(0)
R
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

Register answered 4/8, 2017 at 1:1 Comment(1)
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?Glantz
L
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)
Labio answered 18/6, 2019 at 21:13 Comment(0)
T
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.

Twickenham answered 4/8, 2010 at 17:56 Comment(0)
H
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
Hamite answered 20/12, 2014 at 14:46 Comment(0)
W
-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)
Wet answered 5/1, 2014 at 23:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.