Задача: отображать в админке статистические данные с Redis
На проекте понадобилось собирать статистику обращения к API одной библиотеки. Создали logger, который собирает статистику и сохраняет её в Redis. Как он работает - неважно в контексте данной статьи.
Идея состоит в том, чтобы зарегистрировать в админке фейковую модель и на созданных страницах отображать наши данные. Этот способ больше похож на "хак" админки. На самом деле аналогичный результат можно добиться другим способом. Но понадобилось отображать всё почти так же как и для обычных моделей, вот и пришла идея переопределить ModelAdmin. Да и кода понадобилось не так много.
class LogFakeModel(models.Model):
class Meta:
managed = False
verbose_name = u'API usage statistic'
verbose_name_plural = u'API usage statistic'
Это будет наша модель. "managed = False" - чтобы Django не пыталась создать таблицу в БД.
Теперь создадим ModelAdmin, которое и будет отображать наши данные.
from django.contrib.admin.options import ModelAdmin
class LogAdmin(ModelAdmin):
_registery = {} #здесь будут наши logger-ы
def has_change_permission(self, request, obj=None):
#obj==None при проверке доступа к списку объектов
#какой-то конкретный объект нам редактировать не нужно
return not bool(obj)
def has_add_permission(self, request):
#добавлять ничего нам не нужно
return False
def has_delete_permission(self, request, obj=None):
#и удалять...
return False
Подправили методы проверки прав доступа. Теоретически часть из них никогда не вызовется, но так меньше шансов получить 500. Наш гипотетический logger имеет метод list, который возвращает список элементов такой структуры:
{
'cls': 'class name',
'method': 'method name',
'value': 'number'
}
За создание страницы со списком объектов отвечает метод changelist_view класса django.contrib.admin.options.ModelAdmin. Вот его мы и переопределим.
class LogAdmin(ModelAdmin):
_registery = {}
def changelist_view(self, request, extra_context=None):
opts = self.model._meta
app_label = opts.app_label
title = opts.verbose_name_plural #берем название из модели, что бы не хардкодить в двух местах
selected_logger = request.GET.get('logger') #это для фильтрации
#какая-то логика выбора logger-ов для отображения
if selected_logger and selected_logger in self._registery:
registery = {}
registery[selected_logger] = self._registery[selected_logger]
else:
registery = self._registery
selected_logger = None
filter_choices = self._registery.keys()
context = {
'app_label': app_label,
'registery': registery, #список logger-ов для отображения
'selected_logger': selected_logger, #название выбранного logger-а
'filter_choices': filter_choices,
'title': title
}
return direct_to_template(request, 'statistic/log_methods/changelist_view.html', context)
def has_change_permission(self, request, obj=None):
return not bool(obj)
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
Осталось создать шаблон. Берем шаблон changelist_view.html из django.contrib.admin, убираем все что нам не нужно, поправляем то, что осталось. Чтобы подробно описать, что происходит в шаблоне, нужно описать структуру шаблонов админки. Должно быть и так понятно, кому интересно - можно посмотреть в исходном коде Django (не зря же он открыт). Можете создать свой шаблон с нуля, просто этот вариант использует стили и структуру админки.
{% extends "admin/base_site.html" %}
{% load adminmedia admin_list i18n %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/changelists.css" />
{% endblock %}
{% block extrahead %}
{{ block.super }}
{% endblock %}
{% block bodyclass %}change-list{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="../../">
{% trans "Home" %}
</a>
›
<a href="../">
{{ app_label|capfirst }}
</a>
›
{{ title }}
</div>
{% endblock %}
{% block coltype %}flex{% endblock %}
{% block content %}
<div id="content-main">
<div class="module filtered" id="changelist">
{% block filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
<h3>{% trans "By logger" %}</h3>
<ul>
<li {% if not selected_logger %}class="selected"{% endif %}>
<a href="?">{% trans "all" %}</a>
</li>
{% for name in filter_choices %}
<li {% if selected_logger == name %}class="selected"{% endif %}>
<a href="?logger={{ name|urlencode }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% for name, logger in registery.items %}
<h2>{{ name }}</h2>
<table cellspacing="0" id="result_list">
<thead>
<tr>
<th>{% trans "Class" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Value" %}</th>
</tr>
</thead>
<tbody>
{% for obj in logger.list %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>{{ obj.cls }}</td>
<td>{{ obj.method }}</td>
<td>{{ obj.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</div>
{% endblock %}
И не забываем добавить нашу модель в admin.py:
admin.site.register(LogFakeModel, LogAdmin)
Вот такое у нас получилось: