Python

Как добавить пользовательские кнопки в Django Admin

Spread the love

В этой статье «шпаргалке» рассмотрено добавление пользовательских кнопок в интерфейс Django Admin. В первой части рассказано как добавить одно кнопку на страницу списка выбранной модели — list view. Например кнопку импорта чего либо. Во второй части рассказано как добавить пользовательские кнопки действий (actions) для каждой выбранной записи отдельно с дополнительными формами.


Предположим, что нам нужно добавить отдельную кнопку в списочный интерфейс какой нибудь модели. В данном случае предположим, что это будет кнопка импорта данных. По нажатия на которую будет запускать импорт данных.

Для этого нам нужно будет внести изменения в файл admin.py и добавить дополнительный шаблон админки для соответствующей модели.

Начнем с файла шаблона. Наш шаблон будет основан на стандартном шаблоне, мы просто внесем в него небольшое изменение. В папке соответствующего приложения добавим каталоги: xxx/templates/admin/model_change_list.html.

В этот шаблон добавим следующие изменения.

{% extends 'admin/change_list.html' %}

{% block object-tools %}
  <form action="import/" method="POST">
    {% csrf_token %}
    <input type="submit" value="Импорт" />
  </form>
    {{ block.super }}
{% endblock %}

В файле admin.py добавим указание на файл этот шаблон. Добавим функцию для регистрации нового URL get_urls. В нее добавим маршрут импорта с указанием на функцию импорта. И далее добавим саму функцию, импорта:

from django.contrib import admin
from django.http import HttpResponseRedirect
from django.conf.urls import url

from monitor.models import LoginMonitor
from monitor.import_custom import ImportCustom

@admin.register(LoginMonitor)<br>
class LoginMonitorAdmin(admin.ModelAdmin):<br>
    change_list_template = "admin/monitor_change_list.html"

    def get_urls(self):
        urls = super(LoginMonitorAdmin, self).get_urls()
 custom_urls = [
     url('^import/$', self.process_import, name='process_import'),]
 return custom_urls + urls
    def process_import_btmp(self, request):
 import_custom = ImportCustom()
 count = import_custom.import_data()
 self.message_user(request, f"создано {count} новых записей")
 return HttpResponseRedirect("../")

Все, для добавление новой кнопки этого достаточно.


Далее будет перевод старой, но все еще актуальной статьи: Haki Benita — How to Add Custom Action Buttons to Django Admin


В последнем посте мы представили шаблон, который мы часто используем в наших моделях Django. Мы использовали приложение по управлению банковского счета с моделями Action и Action, чтобы продемонстрировать, как мы решаем общие проблемы, такие как параллелизм и проверка. У банковского счета было две операции, которые мы хотели выставить в интерфейсе администратора — deposit (добавления на счет) и withdraw (снятие со счета).

Мы собираемся добавить кнопки в интерфейсе администратора Django для ввода и вывода денег со счета, и мы сделаем это менее чем за 100 строк кода!

Как это будет выглядеть в конце?

Django Admin interface with custom action buttons
Источник: https://hakibenita.com/images/01-how-to-add-custom-action-buttons-to-django-admin.png

Почему бы не использовать существующие action в интерфейсе администратора?

Встроенные action в интерфейса администратора работают с набором запросов queryset. Они скрыты в выпадающем меню на верхней панели инструментов и в основном полезны для выполнения массовых операций. Хорошим примером является действие по умолчанию. Вы отмечаете несколько строк и выбираете «удалить строки» из выпадающего меню. Это не очень быстро и не всегда подходит для некоторых случаев использования.

Django built in actions
Источник: https://hakibenita.com/images/02-how-to-add-custom-action-buttons-to-django-admin.png

Другим недостатком является то, что действия не доступны в detail view. Чтобы добавить кнопки в подробный вид, вам нужно переопределить шаблон.

Формы

Прежде всего, нам нужно получить дополнительные данные от пользователя для выполнения действия. Естественно для этого, нам нужна форма. Нам нужна одна форма для deposit и одна форма для withdraw.

В дополнение к выполнению действия мы также добавим опцию, возможность отправить уведомление по электронной почте владельцу учетной записи, сообщая ему о действии, выполненном в его учетной записи.

Все наши actions имеют общие аргументы, такие как comment и send_email. Actions также обрабатывают успех и неудачу аналогичным образом.

Давайте начнем с базовой формы для обработки общего action:

# forms.py

from django import forms
from common.utils import send_email

from . import errors

class AccountActionForm(forms.Form):
    comment = forms.CharField(
        required=False,
        widget=forms.Textarea,
    )
    send_email = forms.BooleanField(
        required=False,
    )

    @property
    def email_subject_template(self):
        return 'email/account/notification_subject.txt'

    @property
    def email_body_template(self):
        raise NotImplementedError()

    def form_action(self, account, user):
        raise NotImplementedError()

    def save(self, account, user):
        try:
            account, action = self.form_action(account, user)
        except errors.Error as e:
            error_message = str(e)
            self.add_error(None, error_message)
            raise

        if self.cleaned_data.get('send_email', False):
            send_email(
                to=[account.user.email],
                subject_template=self.email_subject_template,
                body_template=self.email_body_template,
                context={
                    "account": account,
                    "action": action,
                }
            )

    return account, action

  • Каждое action имеет комментарий comment и возможность отправить уведомление, если действие выполнено успешно.
  • Фактическая операция выполняется при сохранении формы. Это похоже на то, как работает ModelForm.
  • В целях ведения журнала и аудита вызывающая сторона должна предоставить пользователя, выполняющего action.
  • Обязательные свойства, которые не реализованы производными формами, вызовут NotImplementedError. Таким образом, мы гарантируем, что разработчик получит информативное сообщение об ошибке, если он забудет что-то реализовать.
  • Ошибки обрабатываются на уровне базового класса исключений. Наши модели определяют базовый класс ошибок, поэтому мы можем легко перехватить все (и только) исключения, связанные с аккаунтом, и обработать их соответствующим образом.

Теперь, когда у нас есть простой базовый класс, давайте добавим форму для withdraw. Для withdraw нам нужно добавить поле суммы:

# forms.py
from django.utils import timezone

from .models import Account, Action

class WithdrawForm(AccountActionForm):
    amount = forms.IntegerField(
        min_value=Account.MIN_WITHDRAW,
        max_value=Account.MAX_WITHDRAW,
        required=True,
        help_text='How much to withdraw?',
    )

    email_body_template = 'email/account/withdraw.txt'

    field_order = (
        'amount',
        'comment',
        'send_email',
    )

    def form_action(self, account, user):
        return Account.withdraw(
            id=account.pk,
            user=account.user,
            amount=self.cleaned_data['amount'],
            withdrawn_by=user,
            comment=self.cleaned_data['comment'],
            asof=timezone.now(),
        )

  • Мы расширили базу AccountActionForm и добавили поле amount с соответствующими проверками.
  • Мы заполнили необходимые атрибуты, email_body_template.
  • Мы реализовали action формы, используя classmethod из предыдущего поста. Модель заботится о блокировке записи, обновлении любых вычисляемых полей и добавлении соответствующих действий в журнал.

Следующим шагом является добавление deposit. Для deposit требуются поля amount, reference и reference_type:

# forms.py

class DepositForm(AccountActionForm):
    amount = forms.IntegerField(
        min_value=Account.MIN_DEPOSIT,
        max_value=Account.MAX_DEPOSIT,
        required=True,
        help_text='How much to deposit?',
    )
    reference_type = forms.ChoiceField(
        required=True,
        choices=Action.REFERENCE_TYPE_CHOICES,
    )
    reference = forms.CharField(
        required=False,
    )

    email_body_template = 'email/account/deposit.txt'

    field_order = (
        'amount',
        'reference_type',
        'reference',
        'comment',
        'send_email',
    )

    def form_action(self, account, user):
        return Account.deposit(
            id=account.pk,
            user=account.user,
            amount=self.cleaned_data['amount'],
            deposited_by=user,
            reference=self.cleaned_data['reference'],
            reference_type=self.cleaned_data['reference_type'],
            comment=self.cleaned_data['comment'],
            asof=timezone.now(),
        )


На данный момент мы получили необходимые формы для принятия, проверки и оформления deposit и withdraw. Следующим шагом является его интеграция в представление списка администратора Django.


Admin

Прежде чем мы сможем добавить кнопки для actions, нам нужно настроить базовую страницу администратора для нашей модели Account:

# admin.py
from django.contrib import admin

from .models import Account

@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
    date_heirarchy = (
        'modified',
    )
    list_display = (
        'id',
        'user',
        'modified',
        'balance',
        'account_actions',
    )
    readonly_fields = (
        'id',
        'user',
        'modified',
        'balance',
        'account_actions',
    )
    list_select_related = (
        'user',
    )

    def account_actions(self, obj):
        # TODO: Render action buttons


Примечание: мы можем сделать просмотр списка намного лучше, добавив ссылку на пользователя и actions учетной записи. Мы можем добавить несколько полей поиска и многое другое. Ранее я писал о проблемах производительности в интерфейсе администратора при масштабировании приложения Django для сотен тысяч пользователей. Там есть несколько приятных трюков, которые могут сделать даже этот простой вид намного приятнее.

Добавление кнопок Action

Мы хотим добавить кнопки action для каждой учетной записи и сделать так, чтобы они ссылались на страницу с формой. В Django есть функция для регистрации URL get_urls в виде списка. Давайте использовать эту функцию, чтобы добавить маршруты для наших пользовательских действий:

# admin.py

from django.utils.html import format_html
from django.core.urlresolvers import reverse

class AccountAdmin(admin.ModelAdmin):

    # ...

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            url(
                r'^(?P<account_id>.+)/deposit/$',
                self.admin_site.admin_view(self.process_deposit),
                name='account-deposit',
            ),
            url(
                r'^(?P<account_id>.+)/withdraw/$',
                self.admin_site.admin_view(self.process_withdraw),
                name='account-withdraw',
            ),
        ]
        return custom_urls + urls

    def account_actions(self, obj):
        return format_html(
            '<a class="button" href="{}">Deposit</a> '
            '<a class="button" href="{}">Withdraw</a>',
            reverse('admin:account-deposit', args=[obj.pk]),
            reverse('admin:account-withdraw', args=[obj.pk]),
        )
    account_actions.short_description = 'Account Actions'
    account_actions.allow_tags = True

    def process_deposit(self):
        # TODO

    def process_withdraw(self):
        # TODO

  1. Мы зарегистрировали два URL, один для deposit и один для withdraw.
  2. Мы сослались на каждый маршрут на соответствующие функции process_deposit и process_withdraw. Эти функции отобразят промежуточную страницу с соответствующей формой и выполнят операцию.
  3. Мы добавили настраиваемое поле account_actions, чтобы отображать кнопки для каждого действия. Преимущество использования «обычного» поля администратора, такого как account_actions, заключается в том, что оно доступно как в подробностях (detail view), так и в виде списка (list view).

Давайте перейдем к реализации функций для обработки actions:

# admin.py

from django.http import HttpResponseRedirect
from django.template.response import TemplateResponse

from .forms import DepositForm, WithdrawForm

class AccountAdmin(admin.ModelAdmin):

   # ...

   def process_deposit(self, request, account_id, *args, **kwargs):
        return self.process_action(
            request=request,
            account_id=account_id,
            action_form=DepositForm,
            action_title='Deposit',
        )

   def process_withdraw(self, request, account_id, *args, **kwargs):
        return self.process_action(
            request=request,
            account_id=account_id,
            action_form=WithdrawForm,
            action_title='Withdraw',
        )

   def process_action(
        self,
        request,
        account_id,
        action_form,
        action_title
   ):
        account = self.get_object(request, account_id)

        if request.method != 'POST':
            form = action_form()

        else:
            form = action_form(request.POST)
            if form.is_valid():
                try:
                    form.save(account, request.user)
                except errors.Error as e:
                    # If save() raised, the form will a have a non
                    # field error containing an informative message.
                    pass
                else:
                    self.message_user(request, 'Success')
                    url = reverse(
                        'admin:account_account_change',
                       args=[account.pk],
                        current_app=self.admin_site.name,
                    )
                    return HttpResponseRedirect(url)

        context = self.admin_site.each_context(request)
        context['opts'] = self.model._meta
        context['form'] = form
        context['account'] = account
        context['title'] = action_title

        return TemplateResponse(
            request,
            'admin/account/account_action.html',
            context,
        )


Deposit и withdraw осуществляются очень похожим образом. Чтобы выполнить оба действия, мы хотим отобразить форму на промежуточной странице и выполнить действие, когда пользователь отправит форму.

process_action обрабатывает отправку формы для обоих действий. Функция принимает форму, название действия и идентификатор учетной записи. process_withdraw и process_deposit, используются для установки соответствующего контекста для каждой операции.

Для завершения процесса нам нужен template для промежуточной страницы, который содержит форму действия. Мы собираемся основывать наш шаблон на существующем шаблоне, используемом Django Admin:

<!-- templates/admin/account/account_action.html -->

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

{% block content %}

<div id="content-main">

  <form action-xhr="#" method="POST">

    {% csrf_token %}
    {% if form.non_field_errors|length > 0 %}
      <p class="errornote">
          "Please correct the errors below."
      </p>
      {{ form.non_field_errors }}
    {% endif %}

    <fieldset class="module aligned">
      {% for field in form %}
        <div class="form-row">
          {{ field.errors }}
          {{ field.label_tag }}
          {{ field }}
          {% if field.field.help_text %}
          <p class="help">
            {{ field.field.help_text|safe }}
          </p>
          {% endif %}
        </div>
      {% endfor %}
    </fieldset>

    <div class="submit-row">
      <input type="submit" class="default" value="Submit">
    </div>

  </form>
</div>

{% endblock %}


И это все!

Теперь наши сотрудники могут легко вносить и снимать средства прямо из интерфейса администратора. Не нужно создавать дорогую панель инструментов.

Я обещал, что мы сделаем это в 100 строк, и мы сделали это за меньше количество!


Заключение

Большая часть реализации взята из превосходного (превосходного!) Пакета django-import-export. Это сэкономило нам часы «Вы можете просто отправить мне данные в Excel?» и мы любим это за это. Если вы не знакомы с этим, вы должны обязательно проверить это.

Была ли вам полезна эта статья?
[10 / 2.4]

Spread the love
DenSP

View Comments

  • def account_actions(self, obj):
    return '<a class="button" href="{}">Print</a>'.format('y')
    account_actions.short_description = 'Account Actions'
    account_actions.allow_tags = True
    этот код у меня в Django aadmin отображается так
    <a class="button" href="y">Print</a>
    а хотелось бы кнопку. Подскажите

  • не корректно скопировал, уточняю.
    def button(self, obj):
        return '<a class="button" href="{}">Print</a>'.format('y')
        
     button.short_description = 'Actions'
     button.allow_tags = True

    • Вы можете использовать тег button вместо тега a
      типа так:
      <button class=»button»>Print</button>

      Или задать нужный внешний вид тега a через класс button

      • return '<button class="button">Print</button>'.format('y')
        результат этой строки
        так и выходит в таблице, кнопки нет.
        <button class="button">Print</button>

      • кнопка появилась только после
        from django.utils.safestring import mark_safe
        ...
        def button(self, obj):
           return mark_safe('<button class="button" href="{}">example</button>'.format(obj.pk))

  • xxx/templates/admin/model_change_list.html
    change_list_template = "admin/monitor_change_list.html"

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago