Admin actions в качестве views с промежуточной формой

Полезность admin actions в django трудно переоценить. До версии 1.1 этого механизма в django не было и как только не приходилось извращаться даже для самых элементарных задач вроде удаления группы записей.

Обычно admin action просто молча выполняет операцию над группой записей и пользователь возвращается обратно к списку объектов. Однако есть мало известная, хотя и документированная, возможность не бросать пользователя в changelist, а вернуть какой-нибудь HttpResponse. К примеру перед обработкой переданных записей мы можем вывести форму с дополнительным вопросом о параметрах обработки.

Документация в качестве примера предлагает возвращать HttpResponseRedirect и передавать список обрабатываемых объектов в виде GET-параметра полноценной view для последующей самостоятельной работы. Однако, ничего не мешает использовать в качестве этой view-функции прамо наш action.

Допустим у нас есть магазин, в котором товары разбиты по категориям:

class Category(models.Model):
    name = models.CharField(u'Наименование', max_length=100)

    def __unicode__(self):
        return self.name

class Product(models.Model):
    category = models.ForeignKey(Category, verbose_name=u'Категория')
    name = models.CharField(u'Наименование', max_length=100)
    price = models.DecimalField(u'Цена', max_digits=15, decimal_places=2)

Время от времени пользователю хочется перенести пачку товаров из одной категории в другую (к примеру разбили "Картплоттеры" на "Плоттеры Garmin" и "Плоттеры Raymarine" и надо раскидать товары "согласно вновь утверждённому плану"). Чтобы человек не мучался, выставляя каждому такому товару новую категорию, мы и напишем нехитрый action.

class CategoryForm(forms.Form):
    category = forms.ModelChoiceField(queryset=Category.objects.all())

class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'category', 'price', )
    actions = ['set_category_action']

    def set_category_action(self, request, queryset):
        if 'do_action' in request.POST:
            form = CategoryForm(request.POST)
            if form.is_valid():
                category = form.cleaned_data['category']
                updated = queryset.update(category=category)
                messages.success(request,
                                 '{} товаров перемещено в категорию {}'.format(
                                                    updated, category))
                # Ничего не возвращаем, это вернет нас на список товаров
                return
        else:
            form = CategoryForm()
        return render(request,
            'admin/shop/set_category.html',
            {'title': u'Укажите категорию, в которую надо переместить товары',
             'objects': queryset,
             'form': form})
    set_category_action.short_description = u'Переместить в категорию'

Поддерживающий это дело set_category.html прост. Через hidden-поля action и _selected_action мы заставляем админку опять вызвать наш action, но на этот раз в POST попадают две дополнительные переменные: do_action (флажок, что мы пришли из формы) и category (та категория, которую надо назначить).

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

{% block content %}

    <form action="" method="post">

        {% csrf_token %}
        <input type="hidden" name="action" value="set_category_action">
        <input type="hidden" name="do_action" value="yes">

        <div>
            {{ form.category }}
            <input type="submit" class="default" style="float: none" value="Переместить">
            {{ form.category.errors }}
        </div>

        <h2>Товары для перемещения</h2>

        <ul>
            {% for object in objects %}
                <li>
                    <a href="{{ object.pk }}/">{{ object.name }}</a> - {{ object.category }}
                    <input type="hidden" name="_selected_action" value="{{ object.pk }}">
                </li>
            {% endfor %}
        </ul>

    </form>

{% endblock %}

В итоге всё это построение выглядит и работает примерно так:

Admin action с промежуточной формой

Вот таким простым образом мы избавились от необходимости:

  • писать где-то в приложении лишний view с проверкой прав;
  • самостоятельно выдергивать в нем список объектов;
  • придумывать url и делать редирект обратно на change list.


Copyright © 2015 by Sergey Tkachuk.