A bit of fun with method decorators inside a CBV.
I do this all the time:
class ModelView(TemplateView):
def get_objects(self):
return Model.objects.filter(some_id=self.kwargs["some_id"])
def get_other_objects(self):
return Model.objects.all()
def current_datetime(self):
return timezone.now()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["objects"] = self.get_objects()
context["other_objects"] = self.get_other_objects()
context["current_datetime"] = self.get_current_datetime()
return context
There has to be an easier way to do this, right? (Yes, this is a super contrived example). I figure what I want to do is get rid of the get_context_data
override part and somehow have the output of the methods to automatically be added into the context data. I would mostly like the key of the context to be the method name, but sometimes I don't. I figure decorators would be a neat way to do this.
Ultimately, what I want is:
class ModelView(TemplateView):
@context_data(key="objects")
def get_objects(self):
return Model.objects.filter(some_id=self.kwargs["some_id"])
@context_data(key="other_objects")
def get_other_objects(self):
return Model.objects.all()
@context_data
def current_datetime(self):
return timezone.now()
The decorator needs to basically "register" the methods and flag them as the ones we want to use for context data. We can do this:
def context_data(_func=None, *, key=None):
def _context_data(func):
def wrapper(self):
return func(self, self.request)
wrapper._is_context_data = True
wrapper._context_key = key or func.__name__
return wrapper
if callable(_func):
return _context_data(_func)
else:
return _context_data
The actual implementation here is not super crazy, but we basically allow for the context key to be passed (or not). When its not passed, the context key will be the method name. In addition, I thought it would be good if the context methods also took the request argument. Sure we could get self.request
inside the context methods but this will keep them nice and isolated for potential unit tests down the line.
Next, let's make a mixin we can use:
class ContextDataMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
if getattr(method, "_is_context_data", False) is True:
key = getattr(method, "_context_key", name)
context[key] = method()
return context
Again, nothing crazy here. Just loop through the methods in the class instance and see if the _is_context_data
property is True. If that is the case, get the context key property and resolve the method. You may notice here the method is not invoked with request - we don't need to. At this point the method is the actual "wrapper" of the decorator, which itself calls the inner method with the request arg.
So there I have it, the final implementation:
class ModelView(ContextDataMixin, TemplateView):
@context_data(key="objects")
def get_objects(self, request):
return Model.objects.filter(some_id=self.kwargs["some_id"])
@context_data(key="other_objects")
def get_other_objects(self, request):
return Model.objects.all()
@context_data
def current_datetime(self, request):
return timezone.now()
@context_data(key="request_method")
def get_request_method(self, request):
return request.method
It could probably be optimised somewhere along the line (and I will add unit tests, maybe chuck it on pypi), but it does what I need it to for now. It's also sparked the chrysalis of an idea to register other view actions/flows with a decorator too.
Take a look at PEP 318 for more about python decorators.