Django:在保存模型时填充用户ID

59

我有一个模型,它有一个连接到标准的Django用户模型的created_by字段。当保存模型时,我需要自动填充这个字段为当前用户的ID。由于大多数网站部分不使用内置的Admin,因此我无法在Admin层面实现这一点。请问有哪位能提出建议,我该怎么做?


4
这篇博客文章正是讨论这个问题的。 - Farinha
这篇文章相当老了。现在有简单的方法实现吗? - user201788
1
@seanf 这个问题的答案是什么? - user201788
13个回答

40

更新于2020-01-02
⚠以下答案从未更新到最新版本的Python和Django。自几年前编写以来,已发布了解决此问题的软件包。现在我强烈建议使用django-crum,它实现了相同的技术,但具有测试并定期更新:https://pypi.org/project/django-crum/

最不明显的方法是使用CurrentUserMiddleware将当前用户存储在线程本地对象中:

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

现在,您只需要将此中间件添加到MIDDLEWARE_CLASSES中的身份验证中间件之后即可。

settings.py

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

您的模型现在可以使用get_current_user函数访问用户,而无需传递请求对象。

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)

提示:

如果您使用Django CMS,则无需定义自己的CurrentUserMiddleware,而可以使用cms.middleware.user.CurrentUserMiddlewarecms.utils.permissions.get_current_user函数来检索当前用户。


2
看起来很全面,但在Django 1.6.2和Python 3.3.2中,当我使用south时,执行./manage.py schemamigration app --initial时会收到以下错误提取:return _user.value AttributeError: '_thread._local' object has no attribute 'value' - raratiru
3
对的,因为中间件从未被调用。为了使South(以及可能有很多其他管理命令)工作,您需要在get_current_user中捕获AttributeError并返回None - bikeshedder
谢谢!您的模型现在可以使用get_current_user函数来访问用户,而无需传递请求对象。这是为什么使用中间件类比使用此片段更好的原因吗?还是它们可以被视为等效的解决方案? - raratiru
2
@rara_tiru 这取决于你的需求。通过使用线程本地对象和中间件,任何在请求期间保存的经过身份验证的用户对象都将填充created_by字段。该代码片段仅修补管理员。 - bikeshedder
1
由于某些原因,这似乎对我来说不太干净,可能只是因为我还没有习惯,但我喜欢这可以做到的事实。我开始有印象,在模型函数中无法访问任意变量是完全不可能的,事实上,有些人已经明确表示了这一点。感谢分享这个。+1 - Teekin

36

如果你想要一个既可以在管理员页面中工作,也可以在其他地方使用的解决方案,你应该使用一个自定义模型表单。基本思路是覆盖__init__方法,将额外的参数 - 请求(request) - 存储为表单的一个属性,然后还要覆盖保存方法,在保存到数据库之前设置用户ID。

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

2
我无法弄清如何让管理员使用“请求”对象初始化MyModelForm。如果不修改contrib.admin代码本身,这是否可能? - jholster
如果使用基于类的视图,则非常重要查看Monster和Florentin的答案,它们是必要的步骤。 - Mark Stahler
但是,在模型的pre_save中访问它怎么办?http://stackoverflow.com/questions/25305186/accessing-in-passing-to-django-models-signal-methods-like-pre-save-pre-delete - andilabs
5
为了能够在__init__中访问request对象,必须在视图中初始化表单时传递它。例如:myform = MyForm(request.POST, request=request) - vincebodi
请在答案中添加@vincebodi的评论。 - Yacine
显示剩余3条评论

27

Daniel的答案不能直接适用于管理员,因为您需要传递请求对象。您可以通过在ModelAdmin类中覆盖get_form方法来完成此操作,但最好避免表单自定义并只覆盖ModelAdmin中的save_model

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,你好。如果在admin.py中定义save_model时尝试创建一个新条目(即一个类的模型对象,并且该类具有m2m字段),则会出现错误:“'MyClass'实例需要在使用多对多关系之前具有主键值。” 请问您有什么建议应该添加什么? - ted

15

整个方法让我感到很烦恼。我想只说一遍,所以我在中间件中实现了它。只需在身份验证中间件之后添加 WhodidMiddleware。

如果你的 created_by 和 modified_by 字段设置为 editable = False,那么你将无需更改任何表单。

"""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

最佳答案。我最终使用getattr(instance,'pk')来确定我的模型是否正在创建。 - Fábio Santos
2
此代码不是线程安全的。如果您正在使用使用线程的WSGI容器(例如Apache2 + mod_wsgi,uWSGI等),则可能会存储错误的用户。 - bikeshedder
这段代码确实不是线程安全的;在生产环境中最好避免使用。 - acidjunk
@giantas 这段代码的概念是错误的。你需要使用一个 thread local 对象才能使其正常工作。如果有疑问,只需使用 django-crum,它可以正常工作且正确:https://pypi.org/project/django-crum/ - bikeshedder

10

这是我使用通用视图的方法:

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)

1
save() 方法中存在依赖提交的自定义操作时(无论如何都会调用模型实例上的 save() 方法),请再次调用 form.save() 而不是 object.save() - trybik

7

如果您正在使用基于类的视图,那么丹尼尔的答案需要更多内容。请添加以下内容,以确保请求对象在您的ModelForm对象中可用。

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

此外,如前所述,你需要在ModelForm.save()方法的末尾返回该对象。

4
使用类似以下代码会有什么问题:
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

现在在视图中使用myform.save(request.user)进行调用。

这里是ModelForm的save函数,它只有一个commit参数。


3

对于未来的参考,我找到了关于这个主题最好的解决方案:

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

该库由一些中间件组成。 设置完该库后,只需重写模型的保存方法并执行以下操作:

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)

如果您创建一个抽象模型并从中继承所有模型,您将获得自动填充的created_by和changed_by字段。

3

基于bikeshedder的回答,我找到了一个解决方案,因为他的方法对我实际上并没有起作用。

  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)
    
我正在使用Python 3.5和Django 1.11.3。

在运行./manage.py migrate时,我遇到了以下错误:AttributeError: '_thread._local' object has no attribute 'value'。这很有道理,因为请求中间件在迁移期间未加载。或者是我漏掉了什么?您能否澄清一下? - freerunner

2
从Django文档《模型和请求用户》中得知:

“如果要跟踪使用CreateView创建对象的用户,可以使用自定义ModelForm。在视图中,确保您不包含[用户字段]在要编辑的字段列表中,并覆盖form_valid()以添加用户:

最初的回答:从Django文档《模型和请求用户》中得知:要跟踪使用CreateView创建对象的用户,可以使用自定义ModelForm。在视图中,确保您不包含用户字段在要编辑的字段列表中,并覆盖form_valid()以添加用户。
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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接