I have created a decorator in my Django project to inject parameter values to the decorated method's parameters.
I do this by using inspect.getargspec to check which parameters are present in the method and place them in kwargs. Otherwise I get an error due to the incorrect number of parameters in the method.
While this works properly in individual view methods, it fails when it comes to Django's class based views.
I believe this might be because the decorators are applied using #method_decorator at the class level to the dispatch method instead of the individual get and post methods.
I'm a python newbie and might be overlooking something obvious here.
Is there a better way to do what I'm doing? Is it possible to get the method parameter names in a class based view?
I'm using Python 2.7 and Django 1.11
The Decorator
def need_jwt_verification(decorated_function):
#wraps(decorated_function)
def decorator(*args, **kwargs):
request = args[0]
if not isinstance(request, HttpRequest):
raise RuntimeError(
"This decorator can only work with django view methods accepting a HTTPRequest as the first parameter")
if AUTHORIZATION_HEADER_NAME not in request.META:
return HttpResponse("Missing authentication header", status=401)
jwt_token = request.META[AUTHORIZATION_HEADER_NAME].replace(BEARER_METHOD_TEXT, "")
try:
decoded_payload = jwt_service.verify_token(jwt_token)
parameter_names = inspect.getargspec(decorated_function).args
if "phone_number" in parameter_names or "phone_number" in parameter_names:
kwargs["phone_number"] = decoded_payload["phone"]
if "user_id" in parameter_names:
kwargs["user_id"] = decoded_payload["user_id"]
if "email" in parameter_names:
kwargs["email"] = decoded_payload["email"]
return decorated_function(*args, **kwargs)
except JWTError as e:
return HttpResponse("Incorrect or expired authentication header", status=401)
return decorator
A class based view
#method_decorator([csrf_exempt, need_jwt_verification], name="dispatch")
class EMController(View):
def get(self, request, phone_number, event_id):
data = get_data()
return JsonResponse(data, safe=False)
def post(self, request, phone_number, event_id):
return JsonResponse("Operation successful", safe=False)
EDIT:
The obvious solution of applying the decorator at the method level, doesn't work with Django's class based views. You need apply the decorator at the url configuration or apply the decorator to the dispatch method.
EDIT:
I've posted code that was related to a workaround I was exploring, passing the parameter names as an argument into the decorator.
I found this post: Function decorators with parameters on a class based view in Django
which may provide the answer to your problem:
If you want to pass a decorator with parameters, you only need to:
Evaluate the parameters in the decorator-creator function.
Pass the evaluated value to #method_decorator.
The above mentioned and the code provided in the linked answer taken under consideration, you should:
injectables=[inject_1, inject_2, ..., inject_n]
decorators = [csrf_exempt, need_jwt_verification(injectables)]
#method_decorator(decorators, name="dispatch")
class EMController(View):
...
Leaving my previous mistaken answer here for legacy reasons, don't try this at home (or anywhere, in django, for that matter!!)
If we observe the "decorating a class" docs, we can see the following:
Or, more succinctly, you can decorate the class instead and pass the name of the method to be decorated as the keyword argument name:
so you have to change the name argument of your #method_decorator to match the method that will apply to:
decorators = [csrf_exempt, need_jwt_verification(injectables=[])]
#method_decorator(decorators, name='get')
#method_decorator(decorators, name='post')
class EMController(View):
Personally I prefer to place my decorators on top of the specific method they will apply to:
class EMController(View):
#method_decorator(decorators)
def get(self, request, phone_number, event_id):
...
#method_decorator(decorators)
def post(self, request, phone_number, event_id):
...
What I want seemed impossible in the current state of the libraries. So here's what I finally went with.
parameter_names = inspect.getargspec(decorated_function).args
if "phone_number" in parameter_names or "phone_number" in injectables:
kwargs["phone_number"] = decoded_payload["phone"]
if "user_id" in parameter_names:
kwargs["user_id"] = decoded_payload["user_id"]
if "email" in parameter_names:
kwargs["email"] = decoded_payload["email"]
request.__setattr__("JWT", {})
request.JWT["phone_number"] = decoded_payload["phone"]
request.JWT["user_id"] = decoded_payload["user_id"]
request.JWT["email"] = decoded_payload["email"]
This decorator will automatically populate parameters in method based views as intended.
But it will also inject an JWT attribute to the request object for the class based views to use. Like request.GET and request.POST.
Related
For many - but not all - of my views I have to do some validation to make sure the user that is logged in has access to the object they are trying to access. For 30+ views I have this code:
def whatever_view_name(request, id, access_id):
check = Access.objects.filter(user=request.user, id=access_id)
if check:
access_object = check[0]
else:
return redirect(reverse("create_new_access_object"))
.... and now my view-specific code will follow ...
So I need to check if a particular database record (Access) exists for this particular user. This code is repeated a lot, which does not seem to be right. I've been thinking about using middleware, but there are two problems: a) I need to use this object in the view (see variable access_object so I fear I'd have to query it twice if I put it in the middleware), and b) I don't need to do this ALWAYS so I wonder how to only run it for some views and not all if this is middleware.
Any thoughts?
You can write a decorator for this:
from functools import wraps
def check_access(function):
#wraps(function)
def wrap(request, id, access_id, *args, **kwargs):
check = Access.objects.filter(user=request.user, id=access_id)
if check.exists():
return function(request, id, access_id, *args, **kwargs)
else:
return redirect(reverse("create_new_access_object"))
return wrap
# usage
#check_access
def whatever_view_name(request, id, access_id):
return ...
One way that I can think of is using inheritance. We can refactor out the common stuff into a super view class and then extend the same in child view classes.
Something like this :
We can have a super class like this
class AccessVerifiedView(View):
def get(self, request, *args, **kwargs):
check = Access.objects.filter(user=request.user, id=kwargs["access_id"])
if check:
access_object = check[0]
self.verified_get(access_object)
else:
return redirect(reverse("create_new_access_object"))
def verified_get(self, access_object):
raise NotImplementedError
Then we can extend that class and the use in our views.
class MyView(AccessVerifiedView):
def verified_get(self, access_object):
return access_object
This approach see bit more readable. Anyone seeing the code can see the super class and understand the code flow.
Other few ways to do it is
Decorator : We can have a decorator which will do the same thing. And then we can decorate the view which we want to verify.
As per DRF documentation DRF Validators
My method should be like
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
I want to pass an additional argument to validate_title, so it should look like
Here id is some value that I want to pass while calling the Serializer.
def validate_title(self, value, id):
"""
Check that the blog post is about Django.
"""
# Use <id> here
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
I am not able to understand how to achieve this, any help?
I am not sure if I understand your question correctly. To answer your question, I guess you want to pass extra parameters to validate_{} method. You can always use *args and **kwargs to pass extra parameters to custom model validate method. Then extract values from the above parameters which should help you get the parameter.
def validate_title(self, *args, **kwargs):
Use context. Pass context to serializer constructor (Serializer(data=data, context={'id': id}), then get id from context in valid_title method(id = self.context.get('id')).
I'm need opportunity to add http-header(X-Accel-Expires) for each add_view.
And for add_static_view.
Ideally would be pass parameter, something like add_view(..., x_accel_expires=100), add_static_view(..., x_accel_expires=100), but the pyramid can't this.
I can do Base View, where add http-header X-Accel-Expires.
I will only need to add an attribute in each view, something like: add_headers = (('X-Accel-Expires', '100'),).
But how can add this header for add_static_view?
For the case of add_view you can use the decorator argument as documented by view configuration parameters:
A dotted Python name to a function (or the function itself) which will be used to decorate the registered view callable. The decorator function will be called with the view callable as a single argument. The view callable it is passed will accept (context, request). The decorator must return a replacement view callable which also accepts (context, request). The decorator may also be an iterable of decorators, in which case they will be applied one after the other to the view, in reverse order.
This is the example given in the documentation:
#view_config(..., decorator=(decorator2, decorator1))
def myview(request):
pass
Is similar to doing:
#view_config(...)
#decorator2
#decorator1
def myview(request):
pass
This would allow you to write the following for example:
def accel_headers_factory(expires=100):
def add_accel_headers(view):
def wrapped_view(context, request):
resp = view(context, request)
resp.headers.append(('X-Accel-Expires', expires))
return wrapped_view
return add_accel_headers
Then use:
#view_config(..., decorator=(accel_headers_factory(500),))
def myview(request):
return {}
This would then always add the X-Accel-Expires header to the response as returned from the view.
Unfortunately it doesn't look like add_static_view allows you to pass it a decorator argument.
I have code like this in my pyramid project:
class SomeViews(object):
#view_config(...)
def view_a(request):
return {...}
#view_config(...)
def view_b(request):
return {...}
I would like to decorate the view methods to modify the returned dictionary. It's possible to apply an decorator to a view, if it's the first one before view_config. Otherwise Pyramid is still using the original function, due to the nature of Venusian.
Because I would apply the same decorator to all methods in a class, I would prefer to use a class decorator. So instead of doing
#view_config(...)
#my_decorator("some_meta_info")
def view_b(request):
return {...}
for each method, I would like to do
#my_decorator("some_meta_info")
class SomeViews(object):
...
But because the class decorator is executed after the view_config calls, again it does not work with Venusian. I had a look at the view_defaults implementation, to get a hint how to solve my problem, but I did not figured out how it works.
Any hint how to do that? What I want to do, is just to modify the result dictionary of a set of view methods. I also thought about using the BeforeRender event, but I found no way to inject the required meta data in a way that I can access it in the event handler. Using decorators would anyway be the more natural and pythonic way in my opinion.
import functools
def my_decorator(value):
def _dec(f):
#functools.wraps(f)
def wrapper(context, request):
print 'hey look!', value
return f(context, request)
return wrapper
return _dec
#view_defaults(decorator=my_decorator('some meta info'))
class SomeViews(object):
def __init__(self, request):
self.request = request
#view_config(..., renderer='string')
def view_a(self):
return 'foo'
Think of view_defaults as default options passed to every view_config on the class. If you add a decorator to the view_config though, the defaults are overridden and your default decorator would be dropped.
I'm trying to adopt the Django documentation example on using class based views with mixins in order to be able to make a simple way of downloading the objects in a list view in CSV format, but I am failing miserably and do not really see what I am doing wrong.
I have a view defined as:
class MyObjectList(CSVResponseMixin,
MultipleObjectTemplateResponseMixin,
BaseListView
):
object_type = None
def get_context_data(self, **kwargs):
object_type = self.object_type
...some code...
return context
def render_to_response(self, context, **kwargs):
if self.request.GET.get('format', '') == 'csv':
return CSVReponseMixin.render_to_response(self, context, **kwargs)
else:
return MultipleObjectTemplateResponseMixin.render_to_response(self, context, **kwargs)
the mixin is:
class CSVResponseMixin(object):
def render_to_response(self, ctx, **kwargs):
return self.HttpResponse.render_to_response(self.convert_to_csv(ctx), **kwargs)
def conver_to_csv(ctx):
return do_csv_magic(ctx)
and in urls.py the view is called as:
url(r'^list/foos/$',
MyObjectList.as_view(object_type="someobject", model=models.MyModel),
name="myobjects",
)
However when I try to access the view without the ?format=csv query, I get a TypeError
Exception Value: __init__() got an unexpected keyword argument 'request'
Exception Location: /usr/lib/python2.6/site-packages/django/views/generic/base.py in render_to_response, line 97
EDIT: I added some details to the question and ended up implementing this with a different approach, but I still want to know what I was doing wrong.
In short, you're overdoing it. I'm not sure what is your intention here, but I've learned that the best approach is to find the closest generic view to what you're trying to do and simply extend it in views.py. Examples are many, but I invite you to check my code at https://bitbucket.org/BerislavLopac/resume/src/d7cfcf9c370b/resume_app/myproject/web/views.py.
According to the docs, render_to_response only takes the following arguments: template_name, dictionary, context_instance, mimetype
Therefore within FooResponseMixin when you're calling:
self.HttpResponse.render_to_response(self.mutilate_context(ctx), **kwargs)
You're passing in extra arguments within kwargs that render_to_response doesn't accept. Either remove the **kwargs or assign only what you need from it to variables to pass in to the accepted arguments.