mod: в функциях корректно установлены значения по умолчанию и добавлены внутренние валидаторы наличия полей в моделях.

This commit is contained in:
2026-06-30 19:35:38 +03:00
parent be673483ae
commit 855017a0b6
3 changed files with 91 additions and 44 deletions
+1
View File
@@ -583,6 +583,7 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
is_new=not change, # Django: change=False для новых, True для существующих is_new=not change, # Django: change=False для новых, True для существующих
related_article=obj.k_label_to_article, related_article=obj.k_label_to_article,
obj_field_name='s_label', obj_field_name='s_label',
article_title_field='s_article_title',
) )
fieldsets = ( fieldsets = (
+4 -3
View File
@@ -161,7 +161,10 @@ def safe_html_special_symbols(s: str) -> str:
return normalize_string(result) return normalize_string(result)
def make_slug(slug_it: str, max_length: int | None = None, slug_default: str = "content") -> str: def make_slug(
slug_it: str,
max_length: int = SLUG_MAX_LENGTH,
slug_default: str = "content") -> str:
"""Готовит чистый slug из HTML/Unicode текста (включая русский текст). """Готовит чистый slug из HTML/Unicode текста (включая русский текст).
Преобразует текст в URL-friendly slug: Преобразует текст в URL-friendly slug:
@@ -188,8 +191,6 @@ def make_slug(slug_it: str, max_length: int | None = None, slug_default: str = "
if not slug_it: if not slug_it:
return f"{slug_default}-{random.randint(1, 4095):03x}" return f"{slug_default}-{random.randint(1, 4095):03x}"
max_length = max_length or SLUG_MAX_LENGTH
# Вычисляем минимальную длину fallback'а: "slug_default-xyz" # Вычисляем минимальную длину fallback'а: "slug_default-xyz"
min_fallback_length = len(slug_default) + 1 + 3 # "-" и 3 hex-символа min_fallback_length = len(slug_default) + 1 + 3 # "-" и 3 hex-символа
+86 -41
View File
@@ -503,8 +503,8 @@ def build_search_report(
def validate_entity_for_admin_form( def validate_entity_for_admin_form(
form_instance: ModelForm, form_instance: ModelForm,
cleaned_data: dict, cleaned_data: dict,
main_field_name: str = 's_label', main_field_name: str,
metadata_field_name: str = 'j_label_metadata', metadata_field_name: str,
request: HttpRequest | None = None, request: HttpRequest | None = None,
) -> None: ) -> None:
""" """
@@ -516,13 +516,18 @@ def validate_entity_for_admin_form(
Используется во всех админских forms: LabelAdminForm, ArtistAdminForm, MusicStyleAdminForm и т.д. Используется во всех админских forms: LabelAdminForm, ArtistAdminForm, MusicStyleAdminForm и т.д.
Args: Args:
form_instance: Экземпляр формы (self из clean методе) form_instance: Экземпляр формы (self из clean методе). Обязателен!
cleaned_data: Очищенные данные формы cleaned_data: Очищенные данные формы. Обязателен!
main_field_name: Имя основного поля ('s_label', 's_artist', 's_style_name') main_field_name: Имя основного поля модели. ОБЯЗАТЕЛЕН!
metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata') Примеры: 's_label' (для TbLabel), 's_artist' (для TbArtist), 's_style_name' (для TbMusicStyle)
Если поле не существует в модели, будет выброшено AttributeError.
metadata_field_name: Имя поля метаданных модели. ОБЯЗАТЕЛЕН!
Примеры: 'j_label_metadata', 'j_artist_metadata', 'j_style_synonyms'
Если поле не существует в модели, будет выброшено AttributeError.
request: HTTP request объект (опционально, используется для проверки GET параметра ignore_validate) request: HTTP request объект (опционально, используется для проверки GET параметра ignore_validate)
Raises: Raises:
AttributeError: Если main_field_name или metadata_field_name не существуют в модели
ValidationError: Если найдены совпадения (дубликаты) и GET параметр не установлен ValidationError: Если найдены совпадения (дубликаты) и GET параметр не установлен
Пример использования в LabelAdminForm: Пример использования в LabelAdminForm:
@@ -531,8 +536,8 @@ def validate_entity_for_admin_form(
validate_entity_for_admin_form( validate_entity_for_admin_form(
self, self,
cleaned_data, cleaned_data,
main_field_name='s_label', main_field_name='s_label', # ОБЯЗАТЕЛЕН!
metadata_field_name='j_label_metadata', metadata_field_name='j_label_metadata', # ОБЯЗАТЕЛЕН!
request=self.request if hasattr(self, 'request') else None, request=self.request if hasattr(self, 'request') else None,
) )
return cleaned_data return cleaned_data
@@ -541,6 +546,16 @@ def validate_entity_for_admin_form(
# Получаем класс модели из метаинформации формы # Получаем класс модели из метаинформации формы
model_class = form_instance.Meta.model # type: ignore model_class = form_instance.Meta.model # type: ignore
# === ВАЛИДАЦИЯ ПАРАМЕТРОВ ===
# Проверяем, что поля существуют в модели
for field_name in [main_field_name, metadata_field_name]:
if not hasattr(model_class, field_name):
raise AttributeError(
f"Model '{model_class.__name__}' has no field '{field_name}'. "
f"Please provide valid field names from the model."
)
# Получаем значения из формы # Получаем значения из формы
main_field_value = cleaned_data.get(main_field_name) main_field_value = cleaned_data.get(main_field_name)
metadata_dict = cleaned_data.get(metadata_field_name) or {} metadata_dict = cleaned_data.get(metadata_field_name) or {}
@@ -956,60 +971,74 @@ def generate_admin_save_message(
obj: Model, obj: Model,
is_new: bool, is_new: bool,
related_article: Model | None, related_article: Model | None,
obj_field_name: str = 's_label', obj_field_name: str,
article_title_field: str = 's_article_title', article_title_field: str,
) -> None: ) -> None:
""" """
Генерирует и отправляет информативное сообщение об сохранении объекта в админке. Генерирует и отправляет информативное сообщение об сохранении объекта в админке.
Функция анализирует тип операции (создание/редактирование/переименование) и Функция анализирует тип операции (создание/редактирование) и статус связанной статьи,
отправляет соответствующее сообщение с ссылкой на связанную статью. отправляя соответствующее сообщение (success или warning) с ссылкой на статью.
Использует Django messages framework для отправки сообщений:
- GREEN SUCCESS: при успешном создании/обновлении
- YELLOW WARNING: при создании без связанной статьи (ошибка!)
Использует Django messages framework для отправки (success/warning в зависимости от ситуации).
Ссылка на статью открывается в новой вкладке для удобства админа. Ссылка на статью открывается в новой вкладке для удобства админа.
Параметры: Параметры:
----------- -----------
request : HttpRequest request : HttpRequest
Объект HTTP-запроса для отправки сообщений через Django messages framework Объект HTTP-запроса для отправки сообщений через Django messages framework.
Обязателен!
obj : django.db.models.Model obj : django.db.models.Model
Сохраненный объект модели (уже сохранен в БД). Сохраненный объект модели (уже сохранен в БД). Обязателен!
Из этого объекта автоматически извлекается verbose_name модели через obj._meta.verbose_name Из этого объекта автоматически извлекается verbose_name модели через obj._meta.verbose_name
Также используется для получения старого значения при редактировании. Также используется для получения старого значения при редактировании.
is_new : bool is_new : bool
True если создается новая запись, False если редактируется существующая. True если создается новая запись, False если редактируется существующая. Обязателен!
Определяет тип сообщения (green success для нового или warning для переименования). При создании (is_new=True): отправляет success или warning в зависимости от наличия статьи.
При обновлении (is_new=False): отправляет success с информацией о текущем состоянии.
related_article : django.db.models.Model или None related_article : django.db.models.Model или None
Связанная статья (если есть). Если None, ссылка не добавляется в сообщение. Связанная статья для нового объекта или при редактировании.
При создании (is_new=True):
- Если present: отправляет GREEN SUCCESS и ссылку на статью
- Если None: отправляет YELLOW WARNING (это ошибка!)
При редактировании (is_new=False):
- Если present: отправляет SUCCESS со ссылкой на статью
- Если None: просто SUCCESS без ссылки
Обычно это поле вида k_model_to_article из модели. Обычно это поле вида k_model_to_article из модели.
При редактировании может содержать информацию о типе и содержимом статьи.
obj_field_name : str, опционально obj_field_name : str
Имя основного поля объекта для получения текущего значения. Имя основного поля объекта для получения текущего значения. ОБЯЗАТЕЛЕН!
По умолчанию 's_label' (для TbLabel). Примеры: 's_label' (для TbLabel), 's_artist' (для TbArtist), 's_style_name' (для TbMusicStyle)
Примеры: 's_artist' (для TbArtist), 's_style_name' (для TbMusicStyle) Если поле не существует в модели, будет выброшено AttributeError.
article_title_field : str, опционально article_title_field : str
Имя поля статьи для получения названия статьи. Имя поля статьи для получения названия статьи. ОБЯЗАТЕЛЕН!
По умолчанию 's_article_title' (для TbArticle). Обычно это 's_article_title' для всех моделей статей.
Обычно это поле одинаково у всех моделей статей. Если поле не существует в связанной статье, будет выброшено AttributeError.
Raises:
AttributeError: Если obj_field_name не существует в модели obj
AttributeError: Если article_title_field не существует в related_article (если он не None)
Отправляемые сообщения: Отправляемые сообщения:
----------------------- -----------------------
Новая запись (is_new=True): Новая запись с статьей (is_new=True, related_article существует):
- GREEN SUCCESS: "Лейбл «NAME» создан успешно. Статья создана автоматически." - GREEN SUCCESS: "Лейбл «NAME» создан успешно. Связанная статья «TITLE» создана автоматически."
- С ссылкой: "Проверить/Отредактировать статью →" - С ссылкой: "Проверить/Отредактировать статью →"
Переименование (is_new=False, имя изменилось): Новая запись БЕЗ статьи (is_new=True, related_article=None):
- YELLOW WARNING: "Лейбл «OLD» переименован на «NEW». ПРОВЕРЬТЕ СВЯЗАННУЮ СТАТЬЮ." - YELLOW WARNING: "Лейбл «NAME» создан успешно. [ОЙ-ОЙ-ОЙ] СТАТЬЯ НЕ БЫЛА СОЗДАНА. ЭТО НЕ ДОЛЖНО БЫЛО СЛУЧИТЬСЯ!"
- С ссылкой: "Проверить/Отредактировать статью →" - Сигнализирует об ошибке в процессе создания связанной статьи
Обновление (is_new=False, имя не изменилось): Обновление (is_new=False):
- GREEN SUCCESS: "Лейбл «NAME» обновлен. Связанная статья: «ARTICLE_TITLE»." - GREEN SUCCESS: "Лейбл «NAME» обновлен. Связанная статья: «ARTICLE_TITLE»."
- С ссылкой: "Проверить/Отредактировать статью →" - С ссылкой: "Проверить/Отредактировать статью →" (если статья существует)
Пример использования в admin.py (максимально чистый и поджарый код): Пример использования в admin.py (максимально чистый и поджарый код):
----------------------------------------------------------------------- -----------------------------------------------------------------------
@@ -1018,19 +1047,34 @@ def generate_admin_save_message(
# Основной механизм сохранения Django (создает статью если нужно) # Основной механизм сохранения Django (создает статью если нужно)
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
# Отправляем информативное сообщение админу # Отправляем информативное сообщение админу с явным указанием полей
# verbose_name берется автоматически из obj._meta.verbose_name
# старое значение получается из БД при необходимости
generate_admin_save_message( generate_admin_save_message(
request=request, request=request,
obj=obj, obj=obj,
is_new=not change, # Django: change=False для новых, True для существующих is_new=not change, # Django: change=False для новых, True для существующих
related_article=obj.k_label_to_article, related_article=obj.k_label_to_article,
obj_field_name='s_label', obj_field_name='s_label', # ОБЯЗАТЕЛЕН! Имя поля для основного названия
article_title_field='s_article_title', # ОБЯЗАТЕЛЕН! Имя поля для названия статьи
) )
""" """
# === ВАЛИДАЦИЯ ПАРАМЕТРОВ ===
# Проверяем, что obj_field_name существует в объекте модели
if not hasattr(obj, obj_field_name):
raise AttributeError(
f"Model '{obj.__class__.__name__}' has no field '{obj_field_name}'. "
f"Please provide a valid field name from the model."
)
# Если есть связанная статья, проверяем что article_title_field в ней существует
if related_article and not hasattr(related_article, article_title_field):
raise AttributeError(
f"Related article model '{related_article.__class__.__name__}' has no field '{article_title_field}'. "
f"Please provide a valid field name for the article title."
)
# Получаем текущее значение основного поля # Получаем текущее значение основного поля
current_field_value = getattr(obj, obj_field_name, '') current_field_value = getattr(obj, obj_field_name)
# Инициализируем для использования в сообщениях (если related_article есть, перезапишем) # Инициализируем для использования в сообщениях (если related_article есть, перезапишем)
article_title = '' article_title = ''
@@ -1039,7 +1083,7 @@ def generate_admin_save_message(
# Формируем ссылку на редактирование статьи и собираем информацию о ней # Формируем ссылку на редактирование статьи и собираем информацию о ней
if related_article: if related_article:
article_title = getattr(related_article, article_title_field, 'Статья') article_title = getattr(related_article, article_title_field)
article_link = (f' <a href=\'../../../tbarticle/{related_article.pk}/change/\'' article_link = (f' <a href=\'../../../tbarticle/{related_article.pk}/change/\''
f' target=\'_blank\'>Проверить/Отредактировать статью →</a>') f' target=\'_blank\'>Проверить/Отредактировать статью →</a>')
@@ -1054,9 +1098,10 @@ def generate_admin_save_message(
if related_article: if related_article:
msg += (f' Связанная статья «{article_title}» создана автоматически. >>' msg += (f' Связанная статья «{article_title}» создана автоматически. >>'
f' {article_info} {article_link}') f' {article_info} {article_link}')
messages.success(request, mark_safe(msg))
else: else:
msg += ' [ОЙ-ОЙ-ОЙ] СТАТЬЯ НЕ БЫЛА СОЗДАНА. ЭТО НЕ ДОЛЖНО БЫЛО СЛУЧИТЬСЯ!' msg += ' [ОЙ-ОЙ-ОЙ] СТАТЬЯ НЕ БЫЛА СОЗДАНА. ЭТО НЕ ДОЛЖНО БЫЛО СЛУЧИТЬСЯ!'
messages.success(request, mark_safe(msg)) messages.warning(request, mark_safe(msg))
else: else:
# РЕДАКТИРОВАНИЕ СУЩЕСТВУЮЩЕЙ ЗАПИСИ # РЕДАКТИРОВАНИЕ СУЩЕСТВУЮЩЕЙ ЗАПИСИ