Как стать автором
Обновить

Выгрузка отфильтрованных пользователем данных из таблицы в админке Django

Время на прочтение4 мин
Количество просмотров3.1K

Генерал Венделер обладал редким даром излагать свои решения в краткой, ясной и доходчивой форме. (С) х/ф "Приключения принца Флоризеля."

Коллега обратился с запросом.

"Хочу забрать в свой уютный екзель данные с корпоративного сайта прямо в том виде, как я их там отфильтровал и отсортировал. Кнопку такую хочу рядом с табличкой сайта."

Сайт сделан на админке 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

Суровый коллега доволен, говорит: "Круто". Это типа похвала :)

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 2: ↑2 и ↓0+2
Комментарии6

Публикации

Истории

Работа

Python разработчик
135 вакансий
Data Scientist
60 вакансий

Ближайшие события