8

Question:

I'm trying to access an attribute of the view instance in the middleware layer.

For example, given a class-based view like this:

# views.py
class MyView(View):
    my_attribute = 'something'

I'd love to be able to get a handle on my_attribute in the middleware by doing something like this:

# middleware.py
def process_view(self, request, view_func, view_args, view_kwargs):
    my_attribute = request.view.my_attribute

Of course, this does not work because Django doesn't expose the view instance through the request object. Is there a way to get this accomplished?

Thanks!


My first attempt:

I initially figured that the process_view() method might be a good place to do this. Unfortunately, the view_func argument it receives contains a function -- the output of MyView.as_view() -- rather than the view instance itself. From the Django docs:

process_view(self, request, view_func, view_args, view_kwargs)

...view_func is the Python function that Django is about to use. (It’s the actual function object, not the name of the function as a string.)...


My second attempt:

A handle to the view instance is available in process_template_response() method, but it's pretty awkward, and, in any case, I'd like to be able to work with my_attribute at an earlier point in the middleware stack. But this does work:

def process_template_response(self, request, response):
    my_attribute = response.context_data['view'].my_attribute
tino
  • 4,780
  • 5
  • 24
  • 30
  • What's the problem you are trying to solve? – Burhan Khalid Dec 21 '13 at 05:42
  • @burhan-khalid: The goal is to place some data in the template context depending on the value of the view attribute. I would do this with a context processor but haven't found a way of accessing the view instance in a context processor either. My current approach is to use a mixin that overrides `get_context_data()`. This gets the job done, but this functionality is required for every request, so I'd like to avoid having to inherit from the mixin in every single view in the application. – tino Dec 21 '13 at 07:19
  • I can think of a number of other applications as well. just one example: an easy way to handle view access control. I know there are other ways of handling access control, but this seems like a particularly straightforward method, and I'd love to know whether it's possible. – tino Dec 27 '13 at 20:19

4 Answers4

4

Using decorators, there are quite some ways to achieve the desired behavior.

1. If you only want to mark a class for the middleware to do something

from django.utils.decorators import classonlymethod

def special_marker(class_view):
    def as_view(cls, **initkwargs):
        view = super(cls, cls).as_view(**initkwargs)
        view.special_marker = True
        return view
    return type(class_view.__name__, (class_view,), {
        'as_view': classonlymethod(as_view),
    })


@special_marker
class MyView(View):
    pass


class MyMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        special_marker = getattr(view_func, 'special_marker', False)
        if special_marker:
            # Do something

2. If you want to pass some data to the middleware that you don't need in the view

from django.utils.decorators import classonlymethod

def tell_middleware(**kwargs):
    def wrapper(class_view):
        def as_view(cls, **initkwargs):
            view = super(cls, cls).as_view(**initkwargs)
            for k, v in kwargs.items():
                setattr(view, k, v)
            return view
        return type(class_view.__name__, (class_view,), {
            'as_view': classonlymethod(as_view),
        })
    return wrapper


@tell_middleware(my_attribute='something')
class MyView(View):
    pass


class MyMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        my_attribute = getattr(view_func, 'my_attribute', 'default value')
        if my_attribute == 'something':
            # Do something

3. If you want to expose some view attributes to the middleware

from django.utils.decorators import classonlymethod

def expose_to_middleware(*args):
    def wrapper(class_view):
        def as_view(cls, **initkwargs):
            view = super(cls, cls).as_view(**initkwargs)
            for attr in args:
                setattr(view, attr, getattr(class_view, attr)
            return view
        return type(class_view.__name__, (class_view,), {
            'as_view': classonlymethod(as_view),
        })
    return wrapper


@expose_to_middleware('my_attribute', 'my_other_attribute')
class MyView(View):
    my_attribute = 'something'
    my_other_attribute = 'else'
    unexposed_attribute = 'foobar'


class MyMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        my_attribute = getattr(view_func, 'my_attribute', 'default value')
        if my_attribute == 'something':
            # Do something

4. If you want to expose the whole class based view to the middleware

from django.utils.decorators import classonlymethod

def expose_cbv_to_middleware(class_view):
    def as_view(cls, **initkwargs):
        view = super(cls, cls).as_view(**initkwargs)
        view.cbv = class_view
        return view
    return type(class_view.__name__, (class_view,), {
        'as_view': classonlymethod(as_view),
    })


@expose_cbv_to_middleware
class MyView(View):
    my_attribute = 'something'


class MyMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        cbv = getattr(view_func, 'cbv', None)
        if cbv:
            if hasattr(cbv, 'my_attribute'):
                print(cbv.my_attribute)
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
3

There is no built-in way to do this, but here is a solution given to me by a kindly user on the django-users mailing list. I'm reposting his suggestion here in case anyone else is trying to do the same thing.

This is useful if:

  1. you want to identify properties of the current view in your middleware and perform processing accordingly, and;
  2. for various reasons you don't want to use mixins or decorators to accomplish similar results.

This inspects the view_func object passed to the process_view() middleware hook and determines and imports the the appropriate view class.

# middleware.py
from myutils import get_class

def process_view(self, request, view_func, view_args, view_kwargs):
        view = get_class(view_func.__module__, view_func.__name__)
        view.my_attribute

Then your get_class() definition:

# myutils.py
from django.utils import importlib

def get_class(module_name, cls_name):
    try:
        module = importlib.import_module(module_name)
    except ImportError:
        raise ImportError('Invalid class path: {}'.format(module_name))
    try:
        cls = getattr(module, cls_name)
    except AttributeError:
        raise ImportError('Invalid class name: {}'.format(cls_name))
    else:
        return cls
tino
  • 4,780
  • 5
  • 24
  • 30
1

Another solution could be to create a new View class:

from django.views.generic.base import View
class AddClassView(View):
    @classonlymethod
    def as_view(cls, **initkwargs):
        view = super(AddClassView, cls).as_view(**initkwargs)
        view.cls = cls
        return view

And use this in your class based view:

# views.py
class MyView(AddClassView):
    my_attribute = 'something'

Then you do the following in the middleware:

# middleware.py
def process_view(self, request, view_func, view_args, view_kwargs):
    view_func.cls.my_attribute  # 'something'

This method is used in the Django REST Framework(https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/views.py#L94-L104)

sgillis
  • 242
  • 2
  • 6
0

If it depends on the view, it should probably be a mixin of that view. If you're doing something like a menu that depends on the active view, I'd do a reverse lookup of the current URL name:

see a previous answer about using URL name lookup of the current URL

Community
  • 1
  • 1
Kevin Stone
  • 8,831
  • 41
  • 29
  • Those are definitely potential workarounds in some cases. But Mixins must be declared in the inheritance hierarchy of every view, which can become an issue if you need the mixin essentially everywhere. Isn't that a situation better handled by middleware? And if the path is available through `request.path_info` why not make the view instance itself available at `request.view`? Even more surprising: the view instance is available in the _template context_: I can grab the view attribute simply with `{{ view.my_attribute }}`... but to process it, I need to use a template tag. Odd. – tino Dec 21 '13 at 10:38
  • Sorry for the tangent, but wouldn't it be a great extension to class-based views to be able to work with the view instance directly? It would let you do some things across the board that you can currently only accomplish with mixins on a case-by-case basis. Or with decorators, which are a fairly inaccessible feature to beginners. In any case, it seems kind of silly to expose the view instance in templates but not through the request object. And but I could also just be missing something totally fundamental here. :) – tino Dec 21 '13 at 10:40