Django Admin: собственные формы в стандартном стиле

Иногда возникает необходимость в административной панели Django создать собственный раздел, используя стандартное оформление. При этом хочется минимизировать дублирование встроенных шаблонов и стилей. Что можно попробовать?

Рассмотрим вариант решения проблемы на примере страницы импорта гипотетического списка брендов из текстового файла.

1. Создайте класс формы

Например:

from django import forms


class BrandImportForm(forms.Form):
    zip_file = forms.FileField(
        label='ZIP-файл',
        help_text='Структура архива должна соответствовать требованиям.'
    )

2. Создайте файл шаблона

Например:

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

{% block breadcrumbs %}
    <div class="breadcrumbs">
        <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
        &rsaquo;
        <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
        &rsaquo;
        <a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a>
        &rsaquo;
        {{ title }}
    </div>
{% endblock %}

{% block content %}
    <form action="./" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div>
            {% for fieldset in adminform %}
                {% include "admin/includes/fieldset.html" %}
            {% endfor %}
        </div>
        <div class="submit-row">
            <input type="submit" value="Импортировать" class="default"/>
        </div>
    </form>
{% endblock content %}

3. Подключите шаблон к ModelAdmin

Определите свойство import_template в классе-потомке от ModelAdmin, в качестве значения укажите путь к HTML-шаблону.

4. Создайте представление для ModelAdmin

Определите собственный view-метод в классе-потомке от ModelAdmin. Например:

from django.contrib.admin import ModelAdmin
from django.contrib.admin.helpers import AdminForm
from django.template.response import TemplateResponse
from django.utils.text import force_text
from .forms import BrandImportForm


class BrandAdmin(ModelAdmin):
    def import_view(self, request):
        model = self.model
        opts = model._meta

        if request.method == 'POST':
            form = BrandImportForm(request.POST, request.FILES)
            if form.is_valid():
                # your import logic
        else:
            form = BrandImportForm()

        fieldsets = [(None, {'fields': form.base_fields})]
        adminform = AdminForm(form, fieldsets, self.get_prepopulated_fields(request))
        context = dict(
            self.admin_site.each_context(request),
            title='Импорт брендов',
            adminform=adminform,
            opts=opts,
            module_name=force_text(opts.verbose_name_plural.title())
        )
        request.current_app = self.admin_site.name

        return TemplateResponse(request, self.import_template, context)

Обратите внимание на использование вспомогательного класса AdminForm из django.contrib.admin.helpers. Именно он позволяет выводить группы элементов формы, используя цикл и стандартный шаблон admin/includes/fieldset.html (см. шаг 2 выше).

5. Зарегистрируйте маршрут для созданного представления

Переопределите метод get_urls() класса ModelAdmin, добавив свой url pattern. Можно так:

from django.contrib.admin import ModelAdmin
from functools import update_wrapper


class BrandAdmin(ModelAdmin):
    def get_urls(self):
        from django.conf.urls import url

        def wrap(view):
            def wrapper(*args, **kwargs):
                return self.admin_site.admin_view(view)(*args, **kwargs)

            wrapper.model_admin = self
            return update_wrapper(wrapper, view)

        info = self.model._meta.app_label, self.model._meta.model_name

        urls_existing = super().get_urls()
        urls_new = [
            url(r'^import/$', wrap(self.import_view), name='%s_%s_import' % info),
        ]

        return urls_new + urls_existing

Выполнив эти шаги, вы получите собственный раздел с формой, которая будет выглядеть аналогично форме типа change_form в Django Admin.

Лайфхак

Возможно, вы зададитесь вопросом, как добавить кнопку-ссылку на созданную страницу. Например, на странице редактирования модели, рядом с кнопкой «Добавить (сущность)».

Можно просто переопределить шаблон change_list.html для данного Model Admin (см. атрибут change_list_template) таким образом:

{% extends "admin/change_list.html" %}
{% load admin_list i18n admin_urls %}

{% block object-tools-items %}
    {{ block.super }}
    <li>
        <a href="{% url opts|admin_urlname:'import' %}">Импорт из архива</a>
    </li>
{% endblock %}