Thihara
Thihara

Reputation: 6969

Check view method parameter name in Django class based views

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.

Upvotes: 13

Views: 1957

Answers (2)

John Moutafis
John Moutafis

Reputation: 23134

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):
        ...

Upvotes: 5

Thihara
Thihara

Reputation: 6969

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.

Upvotes: 1

Related Questions