
Генерал Венделер обладал редким даром излагать свои решения в краткой, ясной и доходчивой форме. (С) х/ф "Приключения принца Флоризеля."
Коллега обратился с запросом.
"Хочу забрать в свой уютный екзель данные с корпоративного сайта прямо в том виде, как я их там отфильтровал и отсортировал. Кнопку такую хочу рядом с табличкой сайта."
Сайт сделан на админке Django. Будем реализовывать это лапидарное ТЗ от коллеги.
Вьюшка в админке для выгрузки данных
Таблица, из которой нужно выгрузить данные, расположена на сайте по адресу http://example.com/admin/telemetry/data/.
Вьюшка должна быть не "отдельно стоящей", а вписанной в контекст админки. Т.е. иметь (например) адрес http://example.com/admin/telemetry/data/csv и подчиняться всем требованиям настроек админки по безопасности, разделению полномочий пользователей и т.п.
Для этой цели в ModelAdmin предусмотрена функция get_urls, переопределив которую, можно добавлять свои вьюшки в схему адресов админки.
from django.urls import path from django.http import HttpResponse from django.contrib import admin class Admin(admin.ModelAdmin): """Модель админки с опцией выгрузки данных.""" def get_urls(self): """Добавляем к стандартным вьюшкам админки свою для выгрузки данных. Обертываем ее в вызов admin_site.admin_view, чтобы она наследовала все правила работы с разделами админки по безопасности и полномочиям пользователей. """ urls = super().get_urls() urls.append(path('csv', self.admin_site.admin_view(self.csv_download))) return urls def csv_download(self, request): """Наша вьюшка для выгрузки данных.""" return HttpResponse( 'тут должны быть выгружамые данные', headers={ 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="data.csv"', } )
Ссылка на вьюшку рядом с таблицей в админке
Разместить на нужной странице админки ссылку на нашу вьюшку можно с помощью механизма переопределения шаблонов.
В каталоге шаблонов нашего сайта нужно создать шаблон, расширяющий стандартный шаблон админки Django. Он должен располагаться в каталоге templates/admin/telemetry/data/ и называться change_list.html.
В settigs.py нужно расположить ссылку на наш каталог templates перед ссылкой на каталог с шаблонами админки. Тогда Django найдет наш шаблон с добавленной ссылкой первым и будет использовать именно его.
Должен сказать, что данный механизм с расположением шаблона в определенном месте дерева каталогов я не проверял. В реальном проекте используется прямое указание имени шаблона, т.к. там присутствуют не относящиеся к рассматриваемой теме фичи. Но, по заверениям документации Djano, описанный выше механизм должен работать.
В коде шаблоне мы наследуем шаблон Django для таблицы объектов и добавляем html код со ссылкой на нашу вьюшку над содержимым блока content.
{% extends "admin/change_list.html" %} {% block content %} <div><a href="csv/?{{ request.GET.urlencode }}">Скачать данные</a></div> {{ block.super }} {% endblock %}
Получение данных с учетом наложенных пользователем фильтров и сортировок
В приведенном выше коде шаблона присутствует один важный момент. Вот этот:
{{ request.GET.urlencode }}
Когда пользователь применяет фильтры и сортирует содержимое таблицы в админке, Django сохраняет эти настройки в виде параметров в командной строке браузера. Чтобы получить настройки фильтров и сортировок пользователя, нам нужно скопировать их как строку параметров нашей вьюшки выгрузки данных.
И тут нас ожидает неприятный сюрприз. Штатная функция ModelAdmin.get_queryset возвращает набор данных без учета пользовательских фильтров и сортировок. Видимо они применяются после вызова этой функции к возвращенному результату.
У нас остается два варианта действий.
Обрабатывать переданные параметры GET-запроса и самостоятельно накладывать на queryset фильтры и сортировки, дублируя штатный механизм админки Django (ужас)
Потупив глаза, объяснять суровому коллеге, что эээ ввиду некоторых ээээ технических особенностей устройства корпоративного сайта эээ ... (ужас-ужас)
К счастью, третий вариант действий нашелся (как обычно) на stackoverflow. Класс django.contrib.admin.views.main.ChangeList располагается в иерархии выше ModelAdmin, учитывает все его варианты фильтров и сортировок и возвращает queryset с учетом их настроек.
Критически настроенный читатель скажет, что это "грязный хак" и мы лезем "под капот" Django. Что в будущем интерфейсы этого внутреннего класса могут поменяться и наш код перестанет работать.
Отчасти это так. С 2009 года там действительно кое-то поменялось. Немного изменились имена методов, добавились обязательные позиционные аргументы. Но в целом, если заглянуть в код Django, все достаточно понятно. В любом случае, для меня этот вариант был существенно лучше двух предыдущих.
Поэтому переписываем код нашей вьюшки выгрузки данных следующим образом.
from django.contrib.admin.views.main import ChangeList ... def csv_download(self, request): """Наша вьюшка для выгрузки данных с учетом пользовательских фильтров и сортировок. """ clist = ChangeList( request, self.model, self.list_display, self.list_display_links, self.list_filter, self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_max_show_all, self.list_editable, self, self.sortable_by ) return HttpResponse( queryset2csv(clist.get_queryset(request)), headers={ 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="data.csv"', } )
Остается написать функцию queryset2csv для трансляции queryset в содержание выгружаемого csv-файла.
import io import csv def queryset2csv(qset): """Преобразует Django queryset в содержание csv файла.""" out = io.StringIO() writer = csv.writer(out, delimiter=';', lineterminator='\n') for row in qset: writer.writerow([row.field1, row.field2, ...]) content = out.getvalue() out.close() return content
Суровый коллега доволен, говорит: "Круто". Это типа похвала :)
