Генерал Венделер обладал редким даром излагать свои решения в краткой, ясной и доходчивой форме. (С) х/ф "Приключения принца Флоризеля."
Коллега обратился с запросом.
"Хочу забрать в свой уютный екзель данные с корпоративного сайта прямо в том виде, как я их там отфильтровал и отсортировал. Кнопку такую хочу рядом с табличкой сайта."
Сайт сделан на админке 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
Суровый коллега доволен, говорит: "Круто". Это типа похвала :)
kesn
Всё, что не с underscore начинается, публичное же! Че сразу грязный хак, нормальное решение
vb64 Автор
Ну так то да. С другой стороны, этот класс в штатной доке Django не упоминается, соответственно никаких гарантий по обратной совместимости при выходе новых версий Django не дает.
kesn
Вот именно в таких случаях и пишут тесты ;)
vb64 Автор
Тесты ценны сами по себе, безотносительно любых случаев.
Я вообще в последнее время склоняюсь к мысли, что главной ценностью компании-разработчика являются не исходники приложения, а тестсьют для них.
И coverage 100% хоть и не является "серебряной пулей", но очень на нее похож.