abidibo.net

How to customize django change form redirect url

django-admin django

Sometimes may happen you need to customize a bit more than usual your django admin interface. Maybe because you need a different dashboard approach, or you just need to aggregate functionalities in a way which differs from a pure CRUD interface. Anyway, if possible, you try to keep the change and delete form code of the django admin, because you really are too lazy to rewrite all the add/change/delete stuff.

In such a situation I found myself searching for a way to customize the redirect url after submitting an add/change/delete form, something like the next param you deal with when implementing authentication logic.

Unfortunately such next parameter is not handled in the core code, so you need a bit of work in order to support it, let's see what you need:

  1. you'll pass the next parameter as a query string parameter
  2. the add/change/delete views should read this parameter and set it in the view context
  3. the change/delete templates should add an input field handling the next value
  4. the add/change/delete views should redirect to the next url if set

The first step it's up to you, just call the change view adding the next param, i.e.

http://localhost:8000/admin/mymodel/5/change/?next=/my-redirect-url/

Let's see the other steps in detail.

Add the next param in the view context

Who manages the admin stuff is clearly the ModelAdmin class, that you can find in django here:

django/contrib/admin/options.py

So we need to subclass this class and provide the new created class to our models' admin classes. We need to overwrite 2 methods in particular in order to add the next param to the context: render_change_form (add and change) and render_delete_form (delete):

from django.contrib.admin import ModelAdmin

class ModelAdminWithNext(ModelAdmin):
    def render_change_form(self,
                           request,
                           context,
                           add=False,
                           change=False,
                           form_url='',
                           obj=None):
        context.update({'next': request.GET.get('next', None)})
        return super(ModelAdminWithNext, self).render_change_form(
            request, context, add, change, form_url, obj)

    def render_delete_form(self, request, context):
        context.update({'next': request.GET.get('next', None)})

        return super(ModelAdminWithNext, self).render_delete_form(
            request, context)

That's it! Now we can handle the next param in the templates.

Add the next parameter in the change/delete templates

It's quite easy indeed, easier for the change_form template because it contains a block which we can use, while the delete template should be completely overwritten. In order to overwrite django admin templates you just need to create a template with the following name in an application listed before contrib.admin in the INSTALLED_APPS setting.

The change form template (templates/admin/change_form.html):

{% extends "admin/change_form.html" %}

{% block form_top %}
    {% if next %}
        <input type="hidden" name="next" value="{{ next }}" />
    {% endif %}
{% endblock form_top %}

The delete form template (templates/admin/delete_confirmation.html):


{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}

{% block extrahead %}
    {{ block.super }}
    {{ media }}
    <script type="text/javascript" src="{% static 'admin/js/cancel.js' %}"></script>
{% endblock %}

{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst|escape }}</a>
› <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
› {% trans 'Delete' %}
</div>
{% endblock %}

{% block content %}
<div class="delete-confirmation-content">
{% if perms_lacking %}
    <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p>
    <ul>
    {% for obj in perms_lacking %}
        <li>{{ obj }}</li>
    {% endfor %}
    </ul>
{% elif protected %}
    <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would require deleting the following protected related objects:{% endblocktrans %}</p>
    <ul>
    {% for obj in protected %}
        <li>{{ obj }}</li>
    {% endfor %}
    </ul>
{% else %}
    <p>{% blocktrans with escaped_object=object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}</p>
    {% include "admin/includes/object_delete_summary.html" %}
    <h2>{% trans "Objects" %}</h2>
    <ul>{{ deleted_objects|unordered_list }}</ul>
    <form method="post">{% csrf_token %}
    {% if next %}
        <input type="hidden" name="next" value="{{ next }}" />
    {% endif %}
    <div>
    <input type="hidden" name="post" value="yes" />
    {% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
    {% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
    <input type="submit" value="{% trans "Yes, I'm sure" %}" />
    <a href="#" class="button cancel-link">{% trans "No, take me back" %}</a>
    </div>
    </form>
{% endif %}
</div>
{% endblock %}

This is the template rendered when you try to delete one item. I'll not cover here the steps you need to do the same thing with the changelist delete action.

Handle the redirect to the next url if present

We need to overwrite other 2 methods of the ModelAdmin class, so the entire ModelAdminWithNext class now looks like

from django.contrib.admin import ModelAdmin
from django.http import HttpResponseRedirect
from django.contrib.admin.options import IS_POPUP_VAR


class ModelAdminWithNext(ModelAdmin):
    def render_change_form(self,
                           request,
                           context,
                           add=False,
                           change=False,
                           form_url='',
                           obj=None):
        context.update({'next': request.GET.get('next', None)})
        return super(ModelAdminWithNext, self).render_change_form(
            request, context, add, change, form_url, obj)

    def render_delete_form(self, request, context):
        context.update({'next': request.GET.get('next', None)})

        return super(ModelAdminWithNext, self).render_delete_form(
            request, context)

    def response_post_save_change(self, request, obj):
        """
        Figure out where to redirect after the 'Save' button has been pressed
        when editing an existing object.
        """
        next = request.POST.get('next', None)

        if next:
            return HttpResponseRedirect(next)

        return super(ModelAdminWithNext, self).response_post_save_change(
            request, obj)

    def response_post_save_add(self, request, obj):
        """
        Figure out where to redirect after the 'Save' button has been pressed
        when adding a new object.
        """
        next = request.POST.get('next', None)

        if next:
            return HttpResponseRedirect(next)

        return super(ModelAdminWithNext, self).response_post_save_add(
            request, obj)

    def response_delete(self, request, obj_display, obj_id):
        next = request.POST.get('next', None)
        if IS_POPUP_VAR not in request.POST and next:
            return HttpResponseRedirect(next)

        return super(ModelAdminWithNext, self).response_delete(
            request, obj_display, obj_id)

Done!

Now just use this class in your models' admin classes:

from django.contrib import admin
from myapp.admin import ModelAdminWithNext
from .models import Stuff

class StuffAdmin(ModelAdminWithNex):
    list_display = ('name', )


admin.site.register(Stuff, StuffAdmin)

Hasta la proxima!

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured