Привет, Хабр! Меня зовут Вячеслав Разводов, я работаю backend-разработчиком.
Мир покера – увлекательный и непредсказуемый. Волнение перед каждой раздачей, расчет силы своей руки, анализ оппонентов – все это создает уникальную атмосферу напряжения и азарта. Было время, я страстно увлекался покером и уделял этому увлечению много времени, стараясь постоянно улучшать свои навыки в этой игре. Читал книги, учился считать ауты. Много играл с друзьями или онлайн площадках PokerStarts, PokerDom. Время шло, моя страсть к покеру подостыла. Однажды я получил предложение поучаствовать в проекте связанным с покерной тематикой. Конечно я согласился не раздумывая.
В первой версии проект состоял из 3 частей:
телеграмм бот с тестами, представляющими игровую ситуацию с двумя картами, указанием позиции за столом и текущим стеком игры, требуя выбрать правильный вариант действия.
админка, где заказчик набивал вопросы и варианты ответов.
api, по которому для телеграмм бот мог забирать данные о тестах, пользователях и так далее.
Я был ответственен за разработку административной панели и API. Тогда я активно изучал фреймворк Django. Django имеет встроенную административную панель (Django Admin), которая допускает ее гибкую настройку. С помощью дополнения Django REST framework (DRF), есть возможность создания REST API с минимальным затратами времени. Поэтому для реализации проекта выбрал Django.
Мы смогли быстро реализовать первую версию проекта, что вызвало большую радость у заказчика. В связи с этим, он просил нас доработать проект, добавив в него таблицу Hand Chart. Задача состояла в создании инструмента для работы с Hand Chart в Django Admin.
Используя данные добавленные в административной панели, нужно было выводить эту таблицу и для обычных пользователей. Создания инструмента для работы с таблицей Hand Chart в Django Admin стало для меня вызовом.
Что такое таблица Hand Chart?
Техасский холдем, является наиболее популярной разновидностью покера, которая привлекает игроков своей динамикой и стратегической глубиной. При игре в техасский холдем каждый игрок получает две стартовые карты, и затем на столе размещаются пять общих карт, с которыми игроки составляют свои комбинации. Две стартовые карты которые игрок получает в начале игры, называют “рукой” игрока.
Количество доступных комбинаций пар карт для успешной игры - зависит от нескольких факторов:
стек игры, в покере обозначает количество фишек, которыми обладает игрок в данный момент. Но говоря об игре, подразумевается количество доступных блайндов, которые может поставить игрок.
блайнды, это обязательные ставки, которые делают два игрока перед раздачей карт. Эти ставки создают начальный банк в игре и стимулируют действие на каждом раунде.
позиция за столом, это могут как быть позиции блайндов, когда игрок делает обязательную ставку, так и после них.
Стек является важным фактором для выбора стратегии игры. При большом количестве фишек (или размере стека), игроку доступен широкий диапазон действия: он может больше рисковать, пытаться запугивать оппонентов большими ставками и так далее. При малом стеке, игроку придется быть осторожным в своих действиях, чтобы не потерять все фишки слишком быстро. Соответственно с большим стеком, можете позволить играть более широкий диапазон рук, а с маленьким стеком диапазон рук значительно сокращается.
Позиция игрока за столом, не менее важный аспект стратегии игры в покер. Позиции за столом делятся на три категории:
Ранняя позиция: Это игроки, которые делают ставки первыми после раздачи. Это обычно самые трудные позиции, поскольку игроки не имеют информации о действиях других игроков. (utg1, utg2)
Средняя позиция: Игроки, которые делают ставки после тех, кто находится в ранней позиции. Они имеют немного больше информации, но все равно ограничены в своих возможностях. (mp1, mp2, mp3)
Поздняя позиция: Игроки, которые делают ставки последними. Поздняя позиция, включая место дилера, обычно считается наиболее выгодной, поскольку эти игроки могут принимать решения, имея максимально возможную информацию о текущем раунде. (co, bu)
Позиция меняется по кругу, после каждой раздачи карт.
Таблица Hand Chart (рис. 2) предоставляет игроку удобный и наглядный способ оценить силу своих стартовых карт и принять решение о дальнейшей стратегии игры. По диагонали расположены “пары” карт, одинакового достоинства. Выше диагонали, располагают комбинации стартовых рук, разных мастей, ниже когда комбинации карт одной масти. Такой подход позволяет быстро ориентироваться, среди 169 комбинаций.
Формируются таблицы Hand Chart на основе данных о проведенных ранее играх. Количество проведенных игры, имеет прямую корреляцию с ценностью рекомендаций таблицы. Но важно учитывать, таблица составляется по конкретный стека игры. При прочих, равных условиях рекомендации могут сильно разниться в зависимости от стека игры. Получается, для создания хорошей таблицы Hand Chart нужно затратить немало усилий и времени.
Заказчик ставил целью проекта сделать таблицы Hand Chart под разные стеки - информационным продуктом. Таблица должны была отвечать требованиям:
иметь “дружелюбным” для пользователя интерфейс, по сравнению с примером на рис. 2
более информативной, и несколько вариантов действий при каждой комбинации карт
Поэтому предложил заменить обычные квадратики (из примера рис. 2) на "фишки" (Рис. 3).
Структура "фишки" разделена на восемь секторов. На внешнем сером крае каждого сектора представлены сокращения, отражающие позиции за столом. Средняя часть сектора, которая на рис. 3 окрашена в зеленый цвет, заполняется разными цветами. Каждый цвет соответствует действию игрока. В одном секторе не выводится больше 2 вариантов. В центре расположено обозначение комбинации карт. В зависимости от цветового сочетания возможны следующие варианты:
Черные буквы на сером фоне обозначают пары карт одного достоинства.
Черные буквы на белом фоне представляют собой пары карт разных мастей.
Белые буквы на черном фоне обозначают пары карт одной масти.
Если взять таблицу с рис. 2 заменим, квадраты на описанный вариант “фишки” получается следующий результат рис. 4.
Такая методика превращает таблицу Hand Chart в удобный и наглядный инструмент. Игрок получает советы, основанные на позиции за столом и комбинации карт, которые у него на руках. Использование разных цветов облегчает восприятие и интерпритецию информации. Такой Hand Chart объединяет стек, позицию за столом и комбинации карт в руке, что приводит к более точным рекомендациям. В результате игроки получают лучшее понимание текущего положения и стратегии, что увеличивает шансы на победу.
Постановка задачи
Django Admin - мощный инструмент, который обеспечивает администраторам полный контроль над управлением и изменением данных. Стандартный функционал Django Admin оказывается недостаточным, для отображения таблицы Hand Chart. Традиционное представление записей в таблице админ-панели не будет удобным и понятным для пользователя.
Задачу создания инструмента для Hand Chart можно разбить на части:
реализовать CRUD в административной панели для стеков игры (
GameStacks
) и модель для хранения данныхреализовать CRUD в административной панели для вариантов действий (
OptionsAction
) и модель для хранения данныхреализовать справочник “фишек” в административной панели, подразумевается все комбинации карт для таблицы Hand Chart (
PokerChips
) и модель для хранения данныхдобавить свою страницу в Django Admin, чтобы она была доступа в меню как список стеков игры, список действий и т.д. НО вместо обычно таблицы, вывод соответствовал рис. 4. По клику на “фишку” - открывалась форма редактирования записи.
Разработка решения
С начала создаем приложение Django и назовем его hand_chart
. Приложение в Django - это модуль, состоящий из набора файлов, который можно использовать многократно в проекте. Это некий контейнер, содержащий функциональность, которая логически связана.
Для создания приложения, в консоли находясь в каталоге проекта выполните команду:
# Команда для создания шаблона приложения
> python3 manage.py startapp hand_chart
После выполнения командны, в проекте появится новая папка с название hand_chart
.
Создание моделей
Определим структуру базы данных нашего приложения, для этого внесем изменения в файл models.py. Файл содержит классы, наследующие класс Model. Каждый класс описывает структуру одной таблицы в базе данных.
Рассмотрим класс GameStacks
, который описывает хранение данных о стеках игры в БД.
from django.db import models
class GameStacks(models.Model):
"""Справочник стеков игры."""
class Meta:
verbose_name = 'Стек игры'
verbose_name_plural = '1. Стеки игры'
ordering = ('sort', )
name = models.CharField("Название", max_length=200)
sort = models.PositiveIntegerField("Сортировка", default=500)
def __str__(self):
return self.name
Таблица GameStacks
будет иметь два поля:
name, название стека, максимальная длина строки 200
sort, целочисленное значение по нему записи сортируются при выводе, по умолчанию равно 500.
class Meta
в модели Django используется для настройки метаданных модели. Метаданные - это всего лишь "данные о данных", они не являются полем модели. Некоторые из настроек, которые вы можете добавить в класс Meta
, включают verbose_name
, verbose_name_plural
, и ordering
.
verbose_name
- в Django используется для определения человекочитаемого имени модели или поля. Если не указано, автоматически заполняется как имя класса модели, в админ панели вместо надписи Стек игры, будет GameStacks.verbose_name_plural
-это человекочитаемого имя для объектов в множественном числе. Если это не указано, Django автоматически добавит 's' в конецverbose_name
.ordering
- это параметр, указывающий порядок, в котором должны быть возвращены объекты модели. Это может быть полем или несколькими полями. Здесь указана сортировка по полю sort ASC - то есть от меньшего к большему. Если написать ordering = ('-sort', ) - то сортировка будет от большего к меньшему (DESC)
Метод __str__
в Python является "магическим" методом, который возвращает строковое представление объекта. В контексте Django моделей, переопределение метода __str__
обычно используется для того, чтобы когда вы печатаете или отображаете экземпляр модели, возвращается информативное строковое представление.
Для создания модели справочника OptionsAction
нам потребуется использовать библиотеку colorfield. С её помощью мы определим поле color
. Хотя это поле сохраняет цвет в формате шестнадцатеричного кода в базе данных, оно предоставляет удобный инструмент для выбора цвета в административной панели (рис. 5).
В модели OptionsAction
определим следующие поля:
name, краткое обозначение действия;
color, цвет которым будем закрашивать сектор;
description, не обязательно поле описания действия - по умолчанию, будет пустым.
from colorfield.fields import ColorField
class OptionsAction(models.Model):
"""Справочник вариантов действий."""
class Meta:
verbose_name = 'Вариант действия'
verbose_name_plural = '2. Варианты действий'
name = models.CharField("Название", max_length=200)
color = ColorField("Цвет", default="#FFFFFF")
description = models.TextField("Описание", max_length=200, default='', blank=True)
def __str__(self):
return f'{self.name} ({self.description})'
Справочник комбинаций карт назовем PokerChips
, поскольку они будут представлены в форме покерных фишек. В этой модели, поле suit
не является просто текстовым полем - оно содержит один из вариантов из списка SUIT_CHOICES
. Первый элемент кортежа представляет собой кодовое обозначение, второй - это человекочитаемый текст, который будет отображаться в выпадающем списке (см. рис. 6). Такая реализация поля полезна, когда мы хотим предоставить пользователю в административной панели возможность выбора из предопределенного списка вариантов.
Поскольку буквы имеют разную ширину, поля delta_x
и delta_y
используются для хранения числовых значений, которые корректируют положение названия комбинации. Это необходимо, чтобы гарантировать центрирование названия во внутреннем круге фишки. Позиция фишки в таблице определяется порядковым номером, хранимом в поле position
.
class PokerChips(models.Model):
"""Справочник покерных фишек для таблицы."""
NOT_SUIT = 'NS'
ONE_SUIT = 'OS'
TWO_SUIT = 'TS'
SUIT_CHOICES = [
(NOT_SUIT, 'Масть не важна'),
(ONE_SUIT, 'Одномастные'),
(TWO_SUIT, 'Разная масть'),
]
class Meta:
verbose_name = 'Комбинация фишек'
verbose_name_plural = 'Комбинации фишек'
ordering = ('position', )
name = models.CharField("Комбинация", max_length=2, default='')
suit = models.CharField("Масть", max_length=2,
choices=SUIT_CHOICES, default=NOT_SUIT)
position = models.PositiveIntegerField("Позиция в таблице")
delta_x = models.IntegerField("Смещение текста по X", default=0)
delta_y = models.IntegerField("Смещение текста по Y", default=0)
def __str__(self):
return f'{self.name} {self.suit}'
Предполагаем, что для одного стека будет несколько таблиц Hand Chart, так как стратегия может существенно отличаться при одинаковом размере стека. Поэтому описываем отдельную таблицу с привязкой к игровому стеку (GameStacks
). Этот подход в будущем позволит расширить модель и настраивать каждую запись индивидуально, включая, например, установку цены за доступ. Модель назовем TableHandChart
.
class TableHandChart(models.Model):
"""Таблица Нand Chart."""
class Meta:
verbose_name = 'Таблица Нand Chart'
verbose_name_plural = '3. Таблицы Нand Chart'
ordering = ('id', )
name = models.CharField("Название таблицы", max_length=200, default='')
stack = models.ForeignKey(GameStacks, on_delete=models.SET_NULL, verbose_name='Стек игры', null=True)
description = models.TextField("Описание", max_length=2000, default='')
is_active = models.BooleanField('Активна')
def __str__(self):
stack = self.stack.name if self.stack is not None else '__'
return stack
def save(self, *args, **kwargs):
# вызов метода save родительского класса
super().save(*args, **kwargs)
rows = ContentHandChart.objects.filter(table_id=self.id).all()
if len(rows) == 0:
chips = PokerChips.objects.all()
for chip in chips:
ContentHandChart.objects.create(table_id=self.id, chip=chip)
Добавим логику при сохранении модели. Для это расширим метод save - он вызывается при сохранении модели. Сначала вызываем метод save родителя, чтобы отработала запись в базу данных. После проверяем, существует ли записи в ContentHandChart
связанные к текущей таблицей TableHandChart
. Если данных нет, то заполняем ContentHandChart
с помощью справочника PokerChips
. Таким образом, автоматически создает первичный контент для таблицы Hand Chart.
Рассмотри нашу основную модель ContentHandChart
. Эта таблица содержит следующее:
table, ссылка на описание таблицы (модель
TableHandChart
)chip, ссылка на конкретную комбинацию карт (модель
PokerChips
)utg, ссылки на выбранные действия (модель
OptionsAction
) для позиции UTG. UTG — это аббревиатура, образованная от английского выражения Under The Gun, переводится как «под прицелом». Относится к ранней позиций за покерным столом. Располагается сразу за блайндами — местами, на которых игроки ставят обязательные ставки вслепую.utg1, ссылки на выбранные действия (модель
OptionsAction
) для позиции UTG1.mp, ссылки на выбранные действия (модель
OptionsAction
) для позиции MP. В покере «MP» означает «Middle Position» или «среднюю позицию».mp1, ссылки на выбранные действия (модель
OptionsAction
) для позиции MP1.hj, ссылки на выбранные действия (модель
OptionsAction
) для позиции HJ. В покере «HJ» означает «Hijack». Это позиция за покерным столом, которая находится справа от «Cut‑off» и слева от «Button» в играх, где участвуют 6 или более игроков.co, ссылки на выбранные действия (модель
OptionsAction
) для позиции CO. В покере «CO» означает «Cut‑off». Позиция «Cut‑off» считается одной из самых выгодных в Техасском Холдеме и других видах покера. Игрок в позиции «Cut‑off» действует предпоследним в большинстве раундов торговли (кроме первого раунда предфлопа), что позволяет ему собирать информацию о действиях большинства других игроков перед тем, как принять решение.btn, ссылки на выбранные действия (модель
OptionsAction
) для позиции BTN. «BTN» или «Button» в покере обозначает позицию дилера. Это последняя и самая выгодная позиция в раунде ставок в Техасском холдеме. Преимущество этой позиции состоит в том, что игрок, сидящий на кнопке, действует последним после того, как все остальные игроки сделали свои ставки. Это дает ему возможность собрать максимум информации о действиях других игроков, прежде чем принять решение.sb, ссылки на выбранные действия (модель
OptionsAction
) для позиции SB. В покере «SB» означает «Small Blind». Это одна из двух «слепых» ставок, которые игроки делают до начала раздачи в покере, таком как Техасский Холдем. Позиция Small Blind находится непосредственно слева от дилера (Button) и обязана сделать ставку, которая обычно составляет половину минимальной ставки или половину большого блайнда (Big Blind).
В код модели ContentHandChart
выглядит следующим образом.
from django.core.cache import cache
class ContentHandChart(models.Model):
"""Содержимое таблицы Hand Chart."""
class Meta:
verbose_name = 'Запись в таблице Hand Chart'
verbose_name_plural = '4. Записи в таблице Hand Chart'
ordering = ('id', )
table = models.ForeignKey(TableHandChart, on_delete=models.CASCADE, verbose_name="Таблица Hand Chart",
null=True, related_name="table_content")
chip = models.ForeignKey(PokerChips, on_delete=models.SET_NULL, verbose_name="Комбинация", null=True)
utg = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция UTG", related_name='utg_color')
utg1 = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция UTG1", related_name='utg1_color')
mp = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция MP", related_name='mp_color')
mp1 = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция MP1", related_name='mp1_color')
hj = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция HJ", related_name='hj_color')
co = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция CO", related_name='co_color')
btn = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция BTN", related_name='btn_color')
sb = models.ManyToManyField(OptionsAction, blank=True, verbose_name="Позиция SB", related_name='sb_color')
update_date = models.DateTimeField(auto_now=True)
def __str__(self):
return self.chip.name
def save(self, *args, **kwargs):
# Выполянем метод save родиетльского класса (models.Model)
super().save(*args, **kwargs)
# Перебираем все связанные поля и удаляем кэш
for field_name in ['utg', 'utg1', 'mp', 'mp1', 'hj', 'co', 'btn', 'sb']:
options_actions = getattr(self, field_name).all()
for options_action in options_actions:
cache_key = f'stacks_cache_{options_action.id}'
if cache.get(cache_key):
cache.delete(cache_key)
В модели ContentHandChart
переоделяем метод save()
. Метод вызывается при сохранении модели. При сохранении записи мы извлекаем идентификаторы действий (OptionsAction.id
) из полей, которые содержат ссылки на каждую позицию. Затем мы проверяем, существует ли в кэше запись с ключом, содержащим этот идентификатор действия. Если кэш найден, то мы удаляем его. На текущий момент, может быть не понятно, зачем это нужно делать. Ниже будем создавать stacks_cache_ - там подобно разберемся зачем это нужно.
После того как описали все таблицы, в виде моделей Django. Нужно создать миграции - это скрипты, которые внесут изменения в базу данных. В нашем случае создадут таблицы по описанным моделям. Миграции создаются с помощью консольной команды:
> python manage.py makemigrations
После выполнения в папке hand_chart
, появится папка migrations
в ней хранятся скрипты миграций. Применять миграции, нужно с помощью консольной команды:
> python manage.py migrate
В конечном итоге базу можно будет описать следующей схемой (рис. 7).
В репозитории, содержащем материалы для этой статьи, найдете два набора тестовых данных (фикстур), для моделей GameStacks и PokerChips. Чтобы импортировать эти данные в базу данных, вам нужно выполнить две команды по очереди.
# загружает примеры стеков игр
> python manage.py loaddata gamestacks
# загружает данные справочника комбинаций карт
> python manage.py loaddata pokerchips
Создание административной панели
Переходим к описанию классов для административной панели Django. Создадим интерфейс для выполнения операций создания, чтения, обновления и удаления (CRUD) над объектами моделей GameStacks
, OptionsAction
, PokerChips
, TableHandChart
. Пользователи смогут взаимодействовать с записями в базе данных, создавая, читая, обновляя и удаляя их через созданный интерфейс в административной панели.
Начнем с интерфейса для GameStacks
. Это самый базовый интерфейс, который будем реализовывать. Описываем класс для административной панели Django, который будет наследовать admin.ModelAdmin
. С помощью декоратора admin.register
в Django Admin зарегистрируем модель, которую хотим отобразить на административной панели Django. В представленном коде регистрируем модель GameStacks
и определяем набор и последовательность полей, которые будут отображаться на специальной странице.
from django.contrib import admin
@admin.register(models.GameStacks)
class GameStacksAdmin(admin.ModelAdmin):
"""Справочник стеков игры"""
list_display = ('name', 'sort') # перечисляем поля которые будет выводиться на странице списка
После того как мы описали интерфейс для модели GameStacks
. Он появится в административной панели (рис. 8.)
В интерфейсе для комбинаций карт (PokerChipsAdmin
), страницу списка сделаем интерактивной. Добавив следующие функции:
в первую очередь выведем превью “фишки”, чтобы наглядно видеть как влияют значения
delta_x
,delta_y
на положение текста в фишке.поля
delta_x
,delta_y
- редактируемыми сразу из таблицы, такой подход упростит ситуацию когда нужно быстро сделать массовое редактирование.
Для отображения превью каждой записи в таблице создадим функцию (preview_chips
), которая будет внедрять текст комбинации и корректировки позиции в шаблон фишки. В качестве аргумента, функция получает obj
- это объект класса модели данных, в данном случае запись 1 комбинации карт из модели PokerChips
. Такие функции в рамках класса ModelAdmin
называются вычисляемыми полями. Можно использовать вычисляемые поля для отображения дополнительной информации об объектах модели или для отображения информации, которая производится путем обработки нескольких полей объекта модели.
В качестве шаблона фишки, используем svg-файл в нарисованной разметкой. В настройках проекта установлены две константы: SVG_X_TEXT
и SVG_Y_TEXT
, которые хранят координаты точки отсчета для позиционирования текста.
По умолчанию, Django автоматически экранирует все переменные, которые выводятся в шаблоне, чтобы предотвратить атаки с использованием межсайтового скриптинга (XSS). Это означает, что любые HTML-теги, содержащиеся в строке, будут отображаться как обычный текст, а не как HTML-код. Результат выполнения функции передаем в mark_safe
. Функция mark_safe
в Django позволяет обозначить конкретную строку как "безопасную" для отображения в HTML-шаблоне без экранирования.
import os
from django.conf import settings
from django.contrib import admin
from django.core.cache import cache
from django.db.models import Q
from django.utils.safestring import mark_safe
@admin.register(models.PokerChips)
class PokerChipsAdmin(admin.ModelAdmin):
"""Справочник комбинаций для таблицы"""
list_display = ('name', 'suit', 'position', 'delta_x', 'delta_y', 'preview_chips') # список столбцов
list_editable = ('delta_x', 'delta_y') # преодоствляет возможность массового редактирования в таблице
readonly_fields = ('preview_chips',) # Поле доступно только для чтения
fields = ('name', 'suit', 'position', 'delta_x', 'delta_y', 'preview_chips') # список полей на форме редактирования
list_per_page = 20 # количество элементов на 1 странице
def preview_chips(self, obj):
cache_key = f'preview_chips_cache_{obj.id}'
cached_preview = cache.get(cache_key)
if cached_preview is not None:
return mark_safe(cached_preview)
init_x = settings.SVG_X_TEXT
init_y = settings.SVG_Y_TEXT
init_str = f'<text id="AT_1_" transform="matrix(1 0 0 1 {init_x} {init_y})"'
new_x = init_x + obj.delta_x
nex_y = init_y + obj.delta_y
new_str = f'<text id="AT_1_" transform="matrix(1 0 0 1 {new_x} {nex_y})"'
svg = open(os.path.join(settings.BASE_DIR, 'static/hand_chart/img/chip.svg'))
data_svg = svg.read()
# Замена координат
data_svg = data_svg.replace(init_str, new_str, 1)
# Замена текста комбинации
data_svg = data_svg.replace('>AA<', f'>{obj.name}<', 1)
cache.set(cache_key, data_svg, timeout=3600) # Кэш без ограничения времени жизни
return mark_safe(data_svg)
preview_chips.short_description = "Превью фишки" # Таким образом мы задаем название столбца
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
cache_key = f'preview_chips_cache_{obj.id}'
cache.delete(cache_key)
Функция preview_chips
выполняется для каждой строчки в выводимой таблице. Выводится 25 записей, и при каждом открытии страницы происходит обработка 25 превью. Предлагаю оптимизировать работу функции preview_chips
за счет кэширования результатов работы.
Кэшированные данные будем хранить в Redis, в виде пары ключ и значение. Система для хранения кэша определятся в файлеDjangoHandChart/settings.py
. В начале функции в переменной cache_key
мы задаем ключ. Ключ состоит из произвольного обозначения “preview_chips_cache_
” и id
записи. Во-первых, такой ключ будет уникальным, потому что id
записи уникален в рамках одной таблицы. Во-вторых, в любой момент можем вычислить ключ, для конкретной записи PokerChips
.
С помощью модуля cache
из Django, проверяем существует ли кэш по нашему ключу. Если данные в кэше присутствуют, то возвращаем в виде в виде результата работы функции. В противном случае, генерируем как превью и строке cache.set(cache_key, data_svg, timeout=3600)
- сохряняем данные в кэш, под заданным ключом с временем жизни 60 мин (3600 секунд). По истечении заданного времени жизни, Redis сам удалит этот ключ.
Могут возникнуть ситуация, что пользователь изменил данные полей 'delta_x
', 'delta_y
', а у нас кешированная старая версия превью. Во избежание подобных случаев, переопеределяем метод save_model
и когда, происходит сохранение модели, удаляем кэш для сохраненной записи.
Django Admin предлагает гибкие возможности для настройки интерфейсов, модифицируя значения свойств класса ModelAdmin
. Например, для отображения превью фишки, добавляем имя функции в свойство list_display
. В этом свойстве перечислены поля, которые будут представлены в виде столбцов на странице со списком. В этот список можно включить как поля модели, для которой создается интерфейс, так и вычисляемые поля - preview_chips
. Указав кортеж в свойстве list_editable
, определяем, какие поля можно будет редактировать в массовом порядке на странице со списком.
В интерфейсе для модели OptionsActionAdmin
мы добавим два вычисляемых поля. Первое, preview_color
, представляет собой небольшой прямоугольник, заполненный выбранным цветом варианта, с шестнадцатеричным кодом цвета, отображаемым внутри этого прямоугольника (рис. 9).
Второе поле, stacks
, отображает список стеков, где используется данный вариант действия. Для отображения этого списка нужно пройти три этапа. На первом этапе мы делаем запрос через Django ORM, в котором выбираем все записи из ContentHandChart
, где наш вариант действий встречается в позициях. По умолчанию в filter
все условия объединятся оператором AND (И). В данном случаем в условии необходимо использовать OR (ИЛИ) потому что проверяемый вариант может встречаться в любом из 8 полей, но не обязательно будет в каждом. Для построения сложных запросов используют операнд Q
.
Q
используется для создания условий в запросах, которые могут объединяться с помощью операторов &
(и), |
(или) и ~
(не). Это позволяет строить более сложные и гибкие запросы.
values_list
- это метод в Django ORM, который позволяет извлечь определенные значения полей из queryset
в виде кортежей или списков. Задавая положительное значение флага flat
, определяем результат в виде списка.
distinct
- это метод в Django ORM, который применяется к queryset
и используется для удаления повторяющихся записей из результата запроса. Так мы выберем только уникальные значения, чтобы сократить выборку.
from django.db.models import Q
# Первый этап обработки данных
table_ids = models.ContentHandChart.objects.filter(
Q(utg__in=[obj.id]) |
Q(utg1__in=[obj.id]) |
Q(mp__in=[obj.id]) |
Q(mp1__in=[obj.id]) |
Q(hj__in=[obj.id]) |
Q(co__in=[obj.id]) |
Q(btn__in=[obj.id]) |
Q(sb__in=[obj.id])
).values_list('table__id', flat=True).distinct()
На втором этапе, делаем запрос с средствами ORM Django к модели TableHandChart
. Выбираем все записи, с id полученные на первом этапе. В фильтр запрос передаем условие id__in=table_ids
. Оператор __in
в Django ORM является фильтрационным оператором. Он используется для фильтрации queryset
по значениям, которые присутствуют в заданном списке. В контексте предоставленной кода, __in
используется для фильтрации объектов TableHandChart
, у которых id
находится в списке table_ids
.
# Второй этап обработки данных
# Запрос ORM Django к модели TableHandChart с использование select_related
tables = models.TableHandChart.objects.select_related('stack').filter(id__in=table_ids)
Метод select_related
в Django ORM используется для оптимизации запросов к базе данных через предварительную выборку (или "жадную" выборку) связанных записей одного-к-одному и многие-к-одному. В данном примере, select_related('stack')
заранее получает связанные объекты stack
для каждого объекта TableHandChart
, что позволяет избежать дополнительных запросов при обращении к связанному объекту stack
каждого объекта TableHandChart
.
В завершающем этапе, мы пройдемся через полученный список записей модели TableHandChart
в цикле. Мы поместим название стека в теге <p>
и соберем уникальный набор стеков игры с помощью функции set()
, которая автоматически удаляет все дубликаты.
# Третий этап обработки данных
# Собираем список уникальных стеков
stacks = set()
for table in tables:
stacks.add(f'<p>{table.stack.name}</p>')
Данный процесс, затратный по ресурсам. Выполняем 2 сложных запроса, и еще собираем html. Желательно, не выполнять эти операции при каждом открытии страницы, а использовать кэш данных. Принцип тот же, что описан в выше в модели PokerChipsAdmin
.
def stacks(self, obj):
cache_key = f'stacks_cache_{obj.id}'
cached_preview = cache.get(cache_key)
if cached_preview is not None:
return mark_safe(cached_preview)
...
cache.set(cache_key, html, timeout=600) # Кэш ограничения времени жизни 10 минут
return mark_safe(html)
Кроме уже описанных функциональных возможностей настройки интерфейса Django Admin - вроде вычисляемых полей, массового редактирования полей на странице со списком, управления списка полей в форме редактирования - существуют опции для определения собственных фильтров. Для этого, нужно просто передать в свойство list_filter
кортеж с полями модели, для которых требуется фильтрация.
В данном контексте, нам нужно добавить фильтрацию записей по стеку игры, к которому привязаны таблицы с используемыми вариантами действий. Однако, модель OptionsActionAdmin
не имеет прямой связи со стеком игры. В таком случае, Django Admin позволяет создать пользовательский фильтр.
Чтобы создать настраиваемый фильтр, мы разработаем класс под названием StackFilter
, который наследует от базового класса SimpleListFilter
. Этот фильтр потребует определения двух методов:
Метод
lookups
, который вернёт все возможные варианты стеков игры.Метод
queryset
, который подготавливает запрос к базе данных, применяя соответствующий фильтр, если он задан.
from django.contrib.admin import SimpleListFilter
import hand_chart.models as models
# hand_chart/admin/filter.py
class StackFilter(SimpleListFilter):
title = 'Стеки игры' # названия фильтра
parameter_name = 'stack2' # название GET-параметра
def lookups(self, request, model_admin):
"""Возвращает все возможные варианты."""
stacks = models.GameStacks.objects.all()
return [(s.id, s.name) for s in stacks]
def queryset(self, request, queryset):
if self.value() == 'ALL':
return queryset
if self.value():
result = models.ContentHandChart.objects.filter(table__stack_id=self.value()).prefetch_related(
'utg',
'utg1',
'mp',
'mp1',
'hj',
'co',
'btn',
'sb'
).values_list(
'utg__id',
'utg1__id',
'mp__id',
'mp1__id',
'hj__id',
'co__id',
'btn__id',
'sb__id'
)
actions_ids = []
for row in result:
for action_id in row:
if action_id:
actions_ids.append(action_id)
return queryset.filter(Q(id__in=set(actions_ids)) | Q(id__isnull=True))
В методе queryset мы осуществляем проверку: установлено значение фильтра ALL
(кнопка "Все" в фильтре) - значит не накладываем условий фильтрации по стеку. В противном случаем, фильтруем варианты действий которые используются в таблицах стеком из self.value()
.
Решать задачу построения запроса начнем с модели ContentHandChart
, потому что со связью Many-to-Many
("многие ко многим") получаем доступ к записям OptionsAction
. Нужно отфильтровать записи ContentHandChart
относящиеся к TableHandChart
, которых указан искомый стеком игры. Для наглядности, рассуждения об алгоритме работы запроса изображен на схеме на рис. 10.
В коде filter(table__stack_id=self.value())
задается фильтр к полю table модели ContentHandChart
. С помощью __
указываем Django ORM, что хотим обратиться к полю stack_id
в модели TableHandChart
.
После фильтра добавляем prefetch_related
- в Django позволяет загружать связанные объекты из базы данных вместе с основным объектом в единственном запросе, что снижает количество обращений к базе данных и улучшает производительность.
В этом примере, когда вы используете prefetch_related('utg')
, Django выполнит единственный запрос к базе данных для получения объекта ContentHandChart
с id=1 и все связанные с ним объекты OptionsAction
для отношения utg
. Под капотом ORM, таким образом указываем использовать JOIN в запросах к базе данных.
prefetch_related
особенно полезен, когда у вас есть множество объектов с множеством связей, и вы хотите избежать N+1 проблемы (множественные запросы к базе данных из-за связей).
Чтобы извлечь определенных полей из возвращенных объектов используем values_list
. В данном случае, выбираем следующие поля:
utg__id
utg1__id
mp__id
Запрос возвращает списки идентификаторов OptionsAction
для каждой из позиций (utg
, utg1
, mp
, mp1
, hj
, co
, btn
, sb
) в ContentHandChart
. Данные в переменой, будут иметь следующий вид:
result = [
(2, 2, 2, None, None, None, None, None),
....
]
Проходя в цикле по столбцам, соответствующим конкретной позиции, преобразуем матрицу nx8 в список ID. В завершающей строке к набору запросов добавляем фильтр, используя уникальные ID с помощью оператора __in
.
for row in result:
for action_id in row:
if action_id:
actions_ids.append(action_id)
queryset.filter(Q(id__in=set(actions_ids)) | Q(id__isnull=True))
С учетом всех наработок, код для фильтра вынесен в отдельный модуль (файлы .py). Код интерфейса для OptionsActionAdmin
примет следующий вид.
import os
from django.conf import settings
from django.contrib import admin
from django.utils.safestring import mark_safe
import hand_chart.models as models
from hand_chart.admin.filter import StackFilter
from hand_chart.sql import SQL_GET_STACKS_BY_ACTION
import os
from django.conf import settings
from django.contrib import admin
from django.core.cache import cache
from django.db.models import Q
from django.utils.safestring import mark_safe
import hand_chart.models as models
from hand_chart.admin.filter import StackFilter
@admin.register(models.OptionsAction)
class OptionsActionAdmin(admin.ModelAdmin):
"""Справочник вариантов ответов с цветами"""
list_display = ('name', 'preview_color', 'stacks', 'description')
list_filter = (StackFilter,)
def preview_color(self, obj):
html = f'<div style="background-color: {obj.color};width: 50%;height: 20px;text-align: center;' \
f'padding-top: 4px;">{obj.color}</div>'
return mark_safe(html)
def stacks(self, obj):
cache_key = f'stacks_cache_{obj.id}'
cached_preview = cache.get(cache_key)
if cached_preview is not None:
return mark_safe(cached_preview)
table_ids = models.ContentHandChart.objects.filter(
Q(utg__in=[obj.id]) |
Q(utg1__in=[obj.id]) |
Q(mp__in=[obj.id]) |
Q(mp1__in=[obj.id]) |
Q(hj__in=[obj.id]) |
Q(co__in=[obj.id]) |
Q(btn__in=[obj.id]) |
Q(sb__in=[obj.id])
).values_list('table__id', flat=True).distinct()
tables = models.TableHandChart.objects.select_related('stack').filter(id__in=table_ids)
stacks = [f'<p>{table.stack.name}</p>' for table in tables]
html = ''.join(stacks)
cache.set(cache_key, html, timeout=600) # Кэш ограничения времени жизни 10 минут
return mark_safe(html)
preview_color.short_description = "Превью цвета"
stacks.short_description = "Стеки игры"
Создание своей страницы
На примере выше описанных интерфейсов для моделей PokerChipsAdmin
, OptionsActionAdmin
проиллюстрировал возможности гибкой настройки Django Admin. Но, для того чтобы сделать полностью свою страницу в административной панели - используем proxy модели.
Proxy модель – это специальный тип модели в Django, который позволяет создавать "обертки" над существующими моделями без изменения оригинальной структуры базы данных. Это отличный способ добавить дополнительные поля, методы или параметры фильтрации к существующим моделям.
Нашу proxy модель будем строить на основе ContentHandChart
- так как, она содержит все необходимые данные для построения Hand Chart из примера (рис. 4.)
Для создания proxy модели в Django, необходимо создать новый класс, унаследованный от оригинальной модели, и добавить все необходимые изменения. В нашем примере, класс PreviewHandChart
будет унаследован от модели ContentHandChart
.
class PreviewHandChart(ContentHandChart):
"""Отображение таблицы Hand Chart."""
class Meta:
proxy = True # признак proxy модели
verbose_name = "Hand Chart"
verbose_name_plural = "Hand Chart"
ordering = ('id', )
Опишем интерфейс для proxy модели. В данном случаем, в силу того что хотим вывести свою страницу, нужно задать ее шаблон. Шаблон задаем в свойстве change_list_template
- это шаблон списочной страницы.
Django автоматически просматривает каталог "templates" в каждом из приложений проекта, в поисках файлов шаблонов. При этом, для удобства организации шаблонов, можно создавать подкаталоги внутри каталога "templates". В этом контексте, файл шаблона располагается по адресу hand_chart/templates/admin/preview_hand_chart_change_list.html. Разместим шаблон в подкаталоге "admin" с целью отделить от остальных шаблонов.
from django.contrib import admin
import hand_chart.models as models
from hand_chart.admin.form import ContentHandChartForm
@admin.register(models.PreviewHandChart)
class PreviewRyeRangeAdmin(admin.ModelAdmin):
form = ContentHandChartForm
change_list_template = 'admin/preview_hand_chart_change_list.html'
Детальное рассмотрение возможностей шаблонизации в Django требует целой серии статей, поэтому здесь мы остановимся только на некоторых аспектах.
Поскольку цель - не просто отобразить страницу, а именно интегрировать её в административную панель Django, так чтобы левое боковое меню и шапка сайта оставались на месте, будем использовать наследование шаблона административной панели. Вместо стандартной таблицы на странице списка будет отображаться наша собственная HTML-страница.
Наследовать будем шаблон admin/change_list.html - это шаблон списочной страницы для интерфейса в Django Admin. Таким образом можно наследоваться от любого шаблона админ панели, и расширять его.
# прописываем в самом начале admin/preview_hand_chart_change_list.html
{% extends "admin/change_list.html" %}
Для структурирования и повторного использования частей кода в Django используются блоки в системе шаблонов. Этот механизм позволяет определить в основном шаблоне области или "блоки", которые могут быть переопределены в дочерних шаблонах, предоставляя гибкость в создании различных макетов страниц.
Основная идея в том, чтобы определить общие элементы дизайна в базовом шаблоне, а затем переопределять только те части в дочерних шаблонах, которые отличаются. Например, вы можете определить шаблон, в котором есть заголовок сайта, навигационное меню, подвал сайта и основная область контента. Эта область контента может быть определена как блок, который будет разным для каждой страницы вашего сайта.
В файле change_list.html
блок с именем result_list
содержит таблицу, которую мы хотим заменить собственным HTML-кодом. Таким образом, для кастомизации нашего шаблона, нам нужно просто в блоке разместить код. Таким образом меняется логика вывода в шаблоне.
{% extends "admin/change_list.html" %}
<!-- Определяем блок с заголовком на странице --!>
{% block content_title %}
<h1> Таблица Hand Chart </h1>
{% endblock %}
{% block result_list %}
<!-- Здесь будет наш код формирующий HTML страницу --!>
{% endblock %}
В силу того, что таблиц Hand Chart у нас несколько, мы не будем делать делать страницу под каждую таблицу. Сделаем 1 страницу, но с возможностью выбора таблицы Hand Chart в списке.
<select name="table_id" >
{% if tables|length == 0 %}
<option>---------</option>
{% endif %}
{% for item in tables %}
{% if request.POST.table_id != None %}
<option value="{{ item.id }}" {% if item.id|stringformat:"i" == request.POST.table_id %}selected{% endif %}>{{ item.name }} (Стэк: {{ item.stack }})</option>
{% elif request.GET.table_id != None %}
<option value="{{ item.id }}" {% if item.id|stringformat:"i" == request.GET.table_id %}selected{% endif %}>{{ item.name }} (Стэк: {{ item.stack }})</option>
{% else %}
<option value="{{ item.id }}" >{{ item.name }} (Стэк: {{ item.stack }})</option>
{% endif %}
{% endfor %}
</select>
У элемента select устанавливаем атрибут name как table_id. При отправке формы методом GET, в параметрах запроса будет добавлен параметр "table_id" со значением выбранной опции.
Переменная tables
, переданная в шаблон, содержит список таблиц, доступных для отображения. Функция |length
в шаблонизаторе аналогична функции len()
в Python. Если список пуст, элемент select заполняется одним дефолтным option.
Проходим циклом по списку таблиц и проверяем следующие условия:
Пытаемся извлечь параметр table_id из POST-запроса. Если такой параметр отсутствует, его значение будет None.
Также пытаемся извлечь параметр table_id из GET-запроса. Если такой параметр отсутствует, его значение будет None.
Если первые два условия не сработали, просто выводим option. В качестве значения используем id записи, а текст формируется из названия и стека игры.
Если параметр table_id
задан, значит у option
должен быть указан атрибут selected. Это означает, что данный вариант выбран. Это делается для того, чтобы после перезагрузки страницы в select был выбран нужный вариант. В request.POST.table_id
значение хранится в виде строки, поэтому используем функцию |stringformat:"i"
. Функция |stringformat:"i"
в шаблонизаторе Django применяется для преобразования числового значения в строку, представляя его как целое число.
Переменная result
хранит список записей модели ContentHandChart
. В цикле проходимся по списку, и выводим фишки в виде svg. В зависимости от значения suit
устанавливаем css-класс в теге ellipse, по аналогии с указанием атрибута selected у тега option.
В предыдущих разделах упомянули, что разные буквы обладают разной шириной, что требует коррекции позиционирования текста внутри внутреннего круга фишки. Для осуществления этой коррекции мы используем функцию |add:
в строке {{ 142.9828|add:value.chip.delta_x }}
. В контексте шаблонизатора Django, функция |add
применяется для сложения двух значений. Например, {{ value|add:"2" }}
увеличит значение переменной value
на 2. Это не ограничено только числовыми значениями: если оба аргумента являются строками, функция |add
выполнит их конкатенацию.
При выше описанных возможностях шаблонизатора, они могут быть ограниченными для сложной бизнес логики, иначе код будет громоздким и не удобочитаемым. В таких случаях можем создавать собственный теги для шаблонизации. По сути своей это отдельные функции вызываемые в шаблоне. Перед выводом, их нужно импортировать в шаблон, это делается следующем образом.
{% extends "admin/change_list.html" %}
{% load chips_tag %} <!-- так мы импортируем набор наших тегов ---!>
{% load mathfilters %} <!-- так мы импортируем функции |add ---!>
{% block content_title %}
<h1> Таблица Hand Chart </h1>
{% endblock %}
{% block result_list %}
В корне нашего приложения hand_chart, мы создаем папку под названием templatetags. Внутри этой папки, мы создаем модуль с именем chips_tag.py. Имя этого файла соответствует имени, которое мы будем использовать при импорте. В этом файле мы опишем функцию, которая будет использоваться в шаблоне. Эта функция будет контролировать вывод секторов для каждой позиции, а также будет устанавливать css-класс, который окрашивает сектор в цвет выбранного действия. В рамках данной статьи мы не будем подробно разбирать код этой функции, так как он достаточно прост. Вы можете ознакомиться с его полной версией в репозитории, прилагаемом к этой статье.
# файл hand_chart/templatetags/chips_tag.py
from django import template
register = template.Library()
@register.simple_tag
def selector_color_v2(qs, is_cache=False):
...
# здесь будет код, исходник можно посмотреть в репозитории
# В файле admin/preview_hand_chart_change_list.html вызов нашего тега выглядит так
<g id="colors">
{% selector_color_v2 value %}
<ellipse id="center_1_" class="{% if value.chip.suit == 'NS' %}inner_circle_NS{% elif value.chip.suit == 'OS' %}inner_circle_OS{% else %}inner_circle_TS{% endif %}" cx="200" cy="200" rx="88" ry="87.9"/>
<text id="AT_1_" x="58" y="0" text-anchor="middle" transform="matrix(1 0 0 1 {{ 142.9828|add:value.chip.delta_x }} {{ 236.9528|add:value.chip.delta_y }})" class="{% if value.chip.suit == 'NS' %}text_NS{% elif value.chip.suit == 'OS' %}text_OS{% else %}text_TS{% endif %} st4 st5 st6">{{ value.chip.name }}</text>
</g>
В функции get_queryset
готовим исходные данные. Как было ранее объяснено, queryset
представляет собой подготовленный запрос. В данном случае мы пытаемся извлечь параметр table_id
из данных запроса и используем его для фильтрации значений модели ContentHandChart
.
@admin.register(models.PreviewHandChart)
class PreviewRyeRangeAdmin(admin.ModelAdmin):
form = ContentHandChartForm
change_list_template = 'admin/preview_hand_chart_change_list.html'
def get_queryset(self, request):
queryset = models.ContentHandChart.objects
if request.method == 'GET':
table_id = request.GET.get('table_id', None)
else:
table_id = request.POST.get('table_id', None)
if table_id is None:
table_id = request.GET.get('table_id', None)
if not table_id:
table = models.TableHandChart.objects.filter(is_active=True).first()
if table:
table_id = table.id
if table_id:
queryset = models.ContentHandChart.objects.filter(table_id=table_id)
return queryset
Когда входите в страницу списка в Django Admin, Django использует функцию changelist_view
для построения страницы. Эта функция получает HTTP-запрос, а затем возвращает HTTP-ответ, который обычно является HTML-страницей, отображающей список объектов.
Переопределение changelist_view
позволяет вам изменить этот процесс. В этой функции мы будем добавлять переменные в контекст, используемый в шаблоне для вывода данных:
tables, список записей модели
TableHandChart
result, список записей модели
ContentHandChart
, это наши “фишки”colors_class, список записей модели
OptionsAction
, все возможные варианты, на основе них делаем css-классы
@admin.register(models.PreviewHandChart)
class PreviewRyeRangeAdmin(admin.ModelAdmin):
...
def changelist_view(self, request, extra_context=None):
# Вызываем метод changelist_view, для родительского класса, чтобы получить исходный ответ.
response = super().changelist_view(
request,
extra_context=extra_context,
)
# Из переменой response пробуем получить, queryset.
try:
qs = response.context_data['cl'].queryset
except (AttributeError, KeyError):
return response
# Если записей нет в базе, возвращаем пустые значения
tables = models.TableHandChart.objects.filter(is_active=True).all()
if len(tables) == 0:
response.context_data['tables'] = []
response.context_data['result'] = []
response.context_data['colors_class'] = []
return response
# если записи есть, сохраняем значение в контексте
response.context_data['tables'] = tables
В Django Admin, cl
является сокращением от "ChangeList", что обозначает объект списка изменений. Объект создаётся при каждом отображении страницы администрирования и содержит всю информацию, необходимую для отображения этой страницы.
response.context_data['cl'].queryset
— это способ получить доступ к QuerySet, который был использован для генерации этой страницы. Он предоставляет доступ к тому же набору объектов, что и отображение на самой странице.
Используя queryset
, извлекаем список всех фишек из ContentHandChart
, которые связаны с определённой таблицей. Почему именно с определённой таблицей? Потому что, в методе get_queryset
мы ранее установили логику добавления фильтра по id таблицы. Этот полученный список нам нужно преобразовать в матрицу размером 13x13. Полученную матрицу затем сохраняем в контексте для последующего использования в шаблоне.
rows = qs.all()
i = 0
result = []
temp = []
for row in rows:
if i < 12:
temp.append(row)
i += 1
else:
temp.append(row)
result.append(temp)
i = 0
temp = []
response.context_data['result'] = result
Заканчивая разработку бизнес-логики для метода changelist_view
, получаем все возможные действия, которые сохранены в модели OptionsAction
. Используя эти данные, генерируем набор css-классов, которые используются для задания цветовой заливки секторов фишек.
rows = models.OptionsAction.objects.all()
colors = []
for row in rows:
colors.append('.color_'+str(row.id)+'_sector{fill:'+row.color+';}')
response.context_data['colors_class'] = colors
Зайдем в административную часть, и выбираем пункт меню “Hand Chart”. В результате выполнения метода changelist_view
, отобразится не стандартная списочная страница Hand Chart (рис. 12).
При клике на любую фишку открывается форма редактирования записи. В обычной форме нет валидатора, который проверяет условие, что в конкретной позиции может быть выбрано максимум 2 варианта действий. Чтобы ввести такую валидацию, заменим стандартную форму. В свойстве form
, класса PreviewRyeRangeAdmin
указывает класс описывающий новую форму редактирования записи ContentHandChartForm
.
from django import forms
import hand_chart.models as models
class ContentHandChartForm(forms.ModelForm):
class Meta:
model = models.ContentHandChart
fields = '__all__'
# В вариант с валидацией конкретного поля
def clean_utg(self):
utg = self.cleaned_data['utg']
if len(utg) > 2:
raise forms.ValidationError(
"Максимум можно выбрать только 2 варианта.")
return utg
# Варинат с валидацией всех полей
def clean(self):
cleaned_data = super().clean()
fields_to_check = ['utg', 'utg1', 'co', 'hl', 'mp', 'mp1', 'btn', 'sb']
for field_name in fields_to_check:
field_value = cleaned_data.get(field_name)
if field_value and len(field_value) > 2:
raise forms.ValidationError(f"Максимум можно выбрать только 2 варианта для поля {field_name}.")
return cleaned_data
Класс формы наследуем от ModelForm
и задаем два свойства:
model, модель которая используется как источник данных
fields, список полей, в данном случае ‘
__all__
’ - означает все поля из модели.
ModelForm
в Django представляет собой мощный инструмент для создания форм на основе моделей. Одна из его ключевых функций - валидация данных, которая позволяет гарантировать, что данные, вводимые пользователем, соответствуют ожидаемым требованиям и ограничениям модели.
Валидация определенного поля осуществляется в методе класса, имя которому задаем следуя паттерну “clean_<имя поля>
”. Метод выполняется после основного этапа валидации полей (метод clean
). Проверяем, что количество выбранных вариантов действий не превышает два. В случае превышения, возбуждаем исключение ValidationError
. Исключение будет обработано ModelForm
, и при попытке сохранить форму с некорректными данными, поле подсветится красным, и будет выведена ошибка (рис. 13).
Но описывать метод проверки для каждого поля, будет не корректным - потому что дублируется код. Хорошим решением будет, переопределить метод clean
. Сначала вызываем clean
у родительского класса ModelForm
. После того как пройдут, системные валидации, в цикле проверяем каждое поле, и если нарушено условие возбуждаем исключение ValidationError
.
Помимо валидации, на странице с формой желательно реализовать следующие возможности:
скрыть поля
table
- привязка к конкретной таблице иchip
- привязка к фишке из справочника. Поля в данном контексте, являются служебными и не должны показываться пользователю.отключить возможности добавления, просмотра, удаления. На форме эти возможности реализуются в виде дополнительных кнопок (рис. 14). Необходимо отставить только кнопку “Сохранить”.
изменить заголовок таблицы, сделав его более информативным.
Реализовать выше указанные требования, можно переопределив метод changeform_view
класса PreviewRyeRangeAdmin
. Метод changeform_view
в Django предназначен для обработки страницы редактирования объекта в административном интерфейсе Django. Он отвечает за обработку запросов на этих страницах и включает в себя функциональность для обработки формы редактирования, а также валидации данных формы.
@admin.register(models.PreviewHandChart)
class PreviewRyeRangeAdmin(admin.ModelAdmin):
....
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
# Вызываем метод changelist_view, для родительского класса, чтобы получить исходный ответ.
response = super().changeform_view(request, object_id, form_url, extra_context)
# Проверяем response это не HttpResponseRedirect - редирект, c формы авторизации
# Такая ситуация может вознинуть если нет прав на редактрование
if response.__class__.__name__ != 'HttpResponseRedirect':
# Здесь меняем заголовок на форме
response.context_data['title'] = 'Изменение комбинации '+response.context_data['original'].chip.name + \
' ('+response.context_data['original'].chip.get_suit_display()+')'
# Делаем запрет, на удаление, добавление, и просмотр
response.context_data['has_delete_permission'] = False
response.context_data['has_add_permission'] = False
response.context_data['has_view_permission'] = False
# У полей table и chip, меняет виджет для отображения на скрытое поле ввода
response.context_data['adminform'].form.fields['table'].widget = forms.HiddenInput(
)
response.context_data['adminform'].form.fields['chip'].widget = forms.HiddenInput(
)
else:
url = urlparse(request.META.get('HTTP_REFERER'))
url = response.url+'?'+url.query
return redirect(url)
return response
Выводы
Подводя итог, хочу отметить, что статья получилась довольно объемной, но я надеюсь, что она поможет прояснить некоторые аспекты работы с Django и Django Admin, включая:
вычисляемые поля;
настройку и создание собственных фильтров;
добавление дополнительной логики валидации в формы;
основы работы с шаблонами в Django;
создание собственной страницы в админ-панели.
В заключение хочется добавить, что Django Admin - это мощный и гибкий инструмент, возможности которого ограничены лишь вашей фантазией. Исходный код проекта можно найти здесь.
UPD: В первый раз статья, выходил в блоге компании. Но ее публикацию там отменили в связи с редакторской политикой компании. Поэтому публикую статью снова, от своего имени, и под новой обложкой. Так же некоторые моменты, были переделаны. В первом комментарии, были замечания про использование чистого SQL и отсутствие кэширования. Я подумал, что будет признаком хорошего тона, исправить эти замечания перед повторной публикацией.