Dan Abramov
Dan Abramov

Reputation: 268225

How to add parameters to Django class based generic view decorators?

I have written a decorator to display success message on object creation:

from django.contrib import messages

def success_message(klass):
    def form_valid(self, form):
        response = super(klass, self).form_valid(form)
        messages.success(self.request, 'Object added successfully')
        return response

    klass.form_valid = form_valid
    return klass

and use it to decorate class based generic view:

@success_message
class BandCreateView(CreateView):
    model = Band

Now I want to parameterize the decorator so this is possible:

@success_message('Band created successfully.')
class BandCreateView(CreateView):
    model = Band

How do I do it? I tried adding message parameter to success_message but the compiler complained about parameter count mismatch so I figure there must be another way.

Upvotes: 1

Views: 1223

Answers (3)

Daniel Swarbrick
Daniel Swarbrick

Reputation: 11

I previously did this kind of thing as a mixin, but a decorator makes more sense (and is a bit less of a hassle to add to classes). You might like to get the form instance's verbose name like the following, to eliminate the need to pass a message to the decorator:

from django.utils.translation import ugettext_lazy as _

def ucfirst(value):
    return value[0].upper() + value[1:]

def success_message(klass):
    __orig_form_valid = klass.form_valid
    def form_valid(self, form):
        response = __orig_form_valid(self, form)
        messages.success(self.request, _("%(object)s \"%(object_name)s\" was saved successfully.") %
                         {'object': ucfirst(form.instance._meta.verbose_name), 'object_name': unicode(form.instance)})
        return response

    klass.form_valid = form_valid
    return klass

This will produce a success message something along the lines of:

Customer "ACME Inc." was saved successfully.

Upvotes: 1

Guard
Guard

Reputation: 6955

Looks like you have to use closure:

def decorator(arg):
    def wrap(klass): ...
    return wrap

because your call is evaluated to

class BandCreateView(CreateView): ...
BandCreateView = @success_message('Band created successfully.')(BandCreateView)

note double call

Upvotes: 3

Dan Abramov
Dan Abramov

Reputation: 268225

TL;DR

Parameterless decorator is declared as a class -> class function.
Decorator with parameters is declared as a higher order args -> (class -> class) function.

It takes parameters and returns a function that will take class and return class (i.e. the “simple” decorator).

Explanation

An answer from a related thread by t.dubrownik literally saved me.

Anyway, the syntax for decorators with arguments is a bit different—the decorator with arguments should return a function that will take a function and return another function. So it should really return a normal decorator.

Although he is talking about function decorators, the concept is applicable to class decorators as well.
Here is what I got:

from django.contrib import messages

def success_message(message):
    def actual_decorator(klass):
        def form_valid(self, form):
            response = super(klass, self).form_valid(form)
            messages.success(self.request, message)
            return response

        klass.form_valid = form_valid
        return klass

    return actual_decorator

Note how actual_decorator itself repeats the parameterless version of success_message and how success_message is now a higher order function because its return value is function itself.

Upvotes: 0

Related Questions