是否可以自动将请求注入模型方法?

Is it possible to automatically inject the request into model methods?

"Why do you need this for?",你可能会问。 嗯,在Django中是很常见的场景。

遵循规则 "thin views, fat models",非常鼓励将与模型相关的所有逻辑置于模型中。因此,在模型中放置所有这些方法以检查当前用户是否有权对模型的给定实例执行某些操作是很常见的。

这可能会给你敲响警钟:

class SomeModel(models.Model):
    ...
    ...
    def allow_editing(self, user):
        ...

    def allow_super_awkward_action(self, user):
        ...

所有这一切都希望能够在模板中做这样的事情:

{% if object.allow_super_awkward_action %}
   place some link to go to the super awkward action
{% endif %}

问题是:如果 Django 不允许您这样做,您如何将当前用户传递给该方法调用?此代码将无可救药地失败,并且最终将遵循备用路径之一:

那么,如果可以在调用函数时自动将 request.user 注入到函数中会怎样呢?或更好!如果您可以注入整个 request 对象会怎样?这可能吗?

这是可能的,它以装饰器的形式出现在我面前。 这有点模糊,我知道,但我们在 Python,并且 Python 允许一点点黑暗 ;)

这里的这个在堆栈帧中搜索 HttpRequest 的实例,如果找到它,它将把它注入到装饰函数中。 (如果您想知道,断言可以帮助您进行防御性编码:可能找不到请求对象,或者如果额外参数没有默认值等,函数调用仍然可能在模板中失败,等等)

import inspect
from django.http import HttpRequest
from django.utils import six

def inject_request(func):
    argspec = inspect.getargspec(func)
    assert 'request' in argspec.args, \
        "There must be a 'request' argument in the function."
    index = argspec.args.index('request')
    assert index in [0, 1], \
        "The 'request' argument must be, at most, the second positional argument."
    assert len(argspec.args) - len(argspec.defaults or []) == index, \
        "All arguments after (and including) 'request' must have default values."
    def wrapper(*args, **kwargs):
        if (index < len(args) and args[index]) or kwargs.get('request', None):
            return func(*args, **kwargs)
        request = None
        frame = inspect.currentframe().f_back
        while frame and not request:
            for v_name, v_object in six.iteritems(frame.f_locals):
                if isinstance(v_object, HttpRequest):
                    request = v_object
                    break
            frame = frame.f_back
        if request:
            kwargs.setdefault('request', request)
        return func(*args, **kwargs)
    return wrapper

在您的模型中,您可以这样做:

class SomeModel(models.Model):
    ...
    ...
    @inject_request
    def allow_editing(self, request=None):
        ...
    @inject_request
    def allow_super_awkward_action(self, request=None):
        ...

我们又回到了快乐的状态,使用模板中的普通方法调用:

{% if object.allow_super_awkward_action %}
    place link to the action
{% endif %}

这行得通!