Django admin: Inline в Fieldset'і або як впихнути невпихуєме
Вони хочуть впихнути невпихуєме
— Іван Плющ
Позиціонування TabularInline, здавалось доволі просте завдання з яким починають стикатись Django програмісти коли розмір ModelAdmin форми починає перевалювати за декілька "екранів" і порядок полів стає критично важливим для орієнтування, але на диво в Django немає стандарного механізму для вирішення цього завдання.
Проблема
Для прикладу створимо "сферичний додаток у вакуумі" sample_app
з двома моделями: ModelA
та Image
.
models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _
class ModelA(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
blank=True,
null=True,
)
description = models.TextField(
verbose_name=_('Description'),
blank=True,
null=True,
)
def __str__(self):
return self.title
class Image(models.Model):
model_a = models.ForeignKey(
'ModelA',
verbose_name='model A'
)
image = models.ImageField(
upload_to='images/'
)
А тепер давайте спробуємо створити простенький Tabular Inline і розмістити елементи на формі у наступному порядку:
title
ImageInline
description
admin.py
from django.contrib import admin
from .models import ModelA, Image
class ImageInline(admin.TabularInline):
model = Image
class ModelAAdmin(admin.ModelAdmin):
fields = (
'title',
ImageInline,
'description'
)
admin.site.register(ModelA, ModelAAdmin)
Як можна здогадатись у нас нічого не вийде і натомість ми отримаємо помилку:
TypeError at /admin/sample_app/modela/add/
sequence item 0: expected str instance, MediaDefiningClass found
....
Вирішення
У всіх варіаціях цієї проблеми на StackOverFlow фігурує абстрактна відповідь: "змінюйте change_form.html
", і чомусь ніде я не знайшов готового сніпету тому ось вам власний.
admin.py
Для початку додамо змінну класу (class variable) insert_after
до ImageInline
, і вкажемо, що цей inline ми хочемо бачити після поля title, сам ImageInline
помістимо у відповідний список.
from django.contrib import admin
from .models import ModelA, Image
class ImageInline(admin.TabularInline):
model = Image
insert_after = 'title'
class ModelAAdmin(admin.ModelAdmin):
fields = (
'title',
'description'
)
inlines = [
ImageInline,
]
change_form_template = 'admin/custom/change_form.html'
class Media:
css = {
'all': (
'css/admin.css',
)
}
admin.site.register(ModelA, ModelAAdmin)
Тепер створимо власний шаблон для форми редагування на основі базового шаблону. Нас цікавлять цикли які відповідають за fieldset'и і за вивід inline'ів:
templates/admin/custom/change_form.html
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static admin_modify %}
{% block field_sets %}
{% for fieldset in adminform %}
{# Власний шаблон для виводу fieldset'ів #}
{% include "admin/custom/fieldset.html" with inline_admin_formsets=inline_admin_formsets %}
{% endfor %}
{% endblock %}
{# Для того щоб уникнути повторного виведення inline'ів кінці форми фільтруємо їх в циклі #}
{% block inline_field_sets %}
{% for inline_admin_formset in inline_admin_formsets %}
{% if not inline_admin_formset.opts.insert_after %}
{% include inline_admin_formset.opts.template %}
{% endif %}
{% endfor %}
{% endblock %}
І оновлюємо шаблон для fieldset'у який і рендеритиме інлайни:
templates/admin/custom/fieldset.html
<fieldset class="module aligned {{ fieldset.classes }}">
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
{% if fieldset.description %}
<div class="description">{{ fieldset.description|safe }}</div>
{% endif %}
{% for line in fieldset %}
<div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
{% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %}
{% for field in line %}
<div{% if not line.fields|length_is:'1' %}
class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}"{% elif field.is_checkbox %}
class="checkbox-row"{% endif %}>
{% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
{% if field.is_checkbox %}
{{ field.field }}{{ field.label_tag }}
{% else %}
{{ field.label_tag }}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
{% endif %}
{% if field.field.help_text %}
<div class="help">{{ field.field.help_text|safe }}</div>
{% endif %}
</div>
{# Вставляємо inline'и після поля #}
{% for inline_admin_formset in inline_admin_formsets %}
{% if inline_admin_formset.opts.insert_after == field.field.name %}
{% include inline_admin_formset.opts.template %}
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% endfor %}
</fieldset>
Після цього ми можемо "насолоджуватись" наступною картиною:
Залишилось трішки підправити CSS, щоб при вигляді нашої адмінки у перфекціоністів не сіпалось око:
static/css/admin.css
.form-row .inline-group {
margin-top: 30px;
}
Результат
Разом з кінцевим результатом додаю посилання на демо-проект який можна "помацати руками".
Якщо я зекономив вам трохи часу, будь ласка, поділіться цим постом в Twitter/Facebook або будь-якій іншій соціальній мережі, дякую.