How to avoid repeated code in views in Django?

Viewed 311

I have several function based views in a django project, and I noticed that they have some repeated code, in fact, they all do the same thing:

def my_view(request):
   form = MyForm(request.POST or None)
   if requqest.POST:
      if form.is_valid():
         do_somethin()
         and_something_else()
         return redirect('another:page')
   return render(request, 'my/tempalate.html', {'form': form})

The only thing that is different is the url where the user is redirected to in case of a succcesful form validation, the template, and maybe the form in the future.

Is it a good idea to use something like this to avoid that repetition?:

def a_view(request, success_redirect_url, template):
    form = MyForm(request.POST or None)
    if request.POST:
        if form.is_valid():
           do_something()
           and_something_else()
           return redirect(success_redirect_url)
    return render(request, template, {'form': form})

and then reuse it in other views that have the repeated code? like:

def my_view1(request, url='another_page', template='my/template.html'):
    return a_view(request, url, template)
2 Answers

Typically such patterns are defined in class-based views where one can use inheritance to override only some parts of the code flow.

Class-based views

Your view for example looks quite similar to a FormView [Django-doc]. Indeed, we can define a view with:

# app_name/views.py

from django.urls import reverse_lazy
from django.views.generic import FormView

class MyView(FormView):
    template_name = 'my/template.html'
    success_url = reverse_lazy('another_page')
    form_class = MyForm

    def form_valid(self, form):
        do_somethin()
        and_something_else()
        return super().form_valid(form)

Here the class-based view has implemented a handler for a POST request that will first construct the form, then check if that form is valid, and if it is invalid rerender the template with the form that now contains the errors. It thus minimizes the amount of code that one has to write to handle a simple form.

Such views also explain clearly what they are doing. A CreateView for example explains by its name that it is used to create new objects, it will thus trigger the .save() method of the form it is operating on. A DeleteView on the other hand makes it clear that a POST request to that view will delete an object.

You can also override the attributes in the urls.py by specifying a value in the .as_view(…) [Django-doc] call:

# app_name/urls.py

from app_name.views import MyView
from django.urls import path

urlpatterns = [
    # ⋮,
    path('some/path/', MyView(template_name='other/template.html'), name='some-name'),
    # ⋮
]

A class-based function thus acts as a template method pattern

Decorators

Another way to do this is to work with decorators that implement small pieces of logic that run before/after a call to the function-based view, and can alter the request/response, or decide to raise an error instead of calling the view.

For example the @require_http_methods(…) decorator [Django-doc] [GitHub] is implemented to first check if the method is one of the listed ones. This is implemented as:

def require_http_methods(request_method_list):
    # ⋮
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            if request.method not in request_method_list:
                response = HttpResponseNotAllowed(request_method_list)
                log_response(
                    'Method Not Allowed (%s): %s', request.method, request.path,
                    response=response,
                    request=request,
                )
                return response
            return func(request, *args, **kwargs)
        return inner
    return decorator

here the decorator thus checks if the request.method is a member of the request_method_list. If that is not the case, it will return a HTTP 405 response, and specify that that method is not allowed.

While Django offers a lot of decorators, most decorators have a mixin counterpart for a class-based views, and some are implemented already in the View class. For example if the View does not contains a get method, then it will return a HTTP 405 response, so here the required_http_method is not needed as a mixin/decorator for a class-based view.

Skeleton functions

You can implement a view function and use parameters instead to pass values. Usually however this will not be as flexible as a class-based view: it is rather easy to pass some parameters, but it is less useful to specify behavior in a pattern: in that case you need to pass a reference to a function, but then the question arises what parameters should be passed to that.

For example if we want to make a function that renders the template, we can work with a view that looks like:

from django.shortcuts import render

def render_some_template(request, parameter, context_generator, template='our/template.html'):
    context = context_generator()
    return render(request, template, context)

but perhaps the context_generator function should be called together with the parameter? or perhaps know what template will be rendered. This is one of the reasons why altering code flow is usually done with a class-based view, and less with a function-based view.

While a function-based view can work with a skeleton function, it is usually less exendible than the class-based counterpart. Django's builtin apps (the ones defined in the django.contrib module) are moving mainly towards class-based views, since it is easier to extend these.

You can do a lot better if just give a name to any button wich lunch this function:

<button name='lukaku'>
<button name='ronaldo'>

then into the view check for the name

if form.get('lukaku'):
    do something
if form.get('ronaldo'):
    do other thing

and so on. this can be limitless

Related