From 855017a0b6c3ae89c67e882c75b2d8e6ec9084e8 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 30 Jun 2026 19:35:38 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=B2=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=D1=85=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D0=BD=D1=83=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=B2=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8F=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/admin.py | 1 + lpon_site/frontend/utils.py | 7 +- lpon_site/frontend/utils_validators.py | 127 +++++++++++++++++-------- 3 files changed, 91 insertions(+), 44 deletions(-) diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index ff56cf4..ff94eaa 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -583,6 +583,7 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin): is_new=not change, # Django: change=False для новых, True для существующих related_article=obj.k_label_to_article, obj_field_name='s_label', + article_title_field='s_article_title', ) fieldsets = ( diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index 7ace537..7f425d1 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -161,7 +161,10 @@ def safe_html_special_symbols(s: str) -> str: 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 текста (включая русский текст). Преобразует текст в 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: return f"{slug_default}-{random.randint(1, 4095):03x}" - max_length = max_length or SLUG_MAX_LENGTH - # Вычисляем минимальную длину fallback'а: "slug_default-xyz" min_fallback_length = len(slug_default) + 1 + 3 # "-" и 3 hex-символа diff --git a/lpon_site/frontend/utils_validators.py b/lpon_site/frontend/utils_validators.py index c177de9..ad5a8c9 100644 --- a/lpon_site/frontend/utils_validators.py +++ b/lpon_site/frontend/utils_validators.py @@ -503,8 +503,8 @@ def build_search_report( def validate_entity_for_admin_form( form_instance: ModelForm, cleaned_data: dict, - main_field_name: str = 's_label', - metadata_field_name: str = 'j_label_metadata', + main_field_name: str, + metadata_field_name: str, request: HttpRequest | None = None, ) -> None: """ @@ -516,13 +516,18 @@ def validate_entity_for_admin_form( Используется во всех админских forms: LabelAdminForm, ArtistAdminForm, MusicStyleAdminForm и т.д. Args: - form_instance: Экземпляр формы (self из clean методе) - cleaned_data: Очищенные данные формы - main_field_name: Имя основного поля ('s_label', 's_artist', 's_style_name') - metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata') + form_instance: Экземпляр формы (self из clean методе). Обязателен! + cleaned_data: Очищенные данные формы. Обязателен! + main_field_name: Имя основного поля модели. ОБЯЗАТЕЛЕН! + Примеры: '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) Raises: + AttributeError: Если main_field_name или metadata_field_name не существуют в модели ValidationError: Если найдены совпадения (дубликаты) и GET параметр не установлен Пример использования в LabelAdminForm: @@ -531,8 +536,8 @@ def validate_entity_for_admin_form( validate_entity_for_admin_form( self, cleaned_data, - main_field_name='s_label', - metadata_field_name='j_label_metadata', + main_field_name='s_label', # ОБЯЗАТЕЛЕН! + metadata_field_name='j_label_metadata', # ОБЯЗАТЕЛЕН! request=self.request if hasattr(self, 'request') else None, ) return cleaned_data @@ -541,6 +546,16 @@ def validate_entity_for_admin_form( # Получаем класс модели из метаинформации формы 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) metadata_dict = cleaned_data.get(metadata_field_name) or {} @@ -956,60 +971,74 @@ def generate_admin_save_message( obj: Model, is_new: bool, related_article: Model | None, - obj_field_name: str = 's_label', - article_title_field: str = 's_article_title', + obj_field_name: str, + article_title_field: str, ) -> None: """ Генерирует и отправляет информативное сообщение об сохранении объекта в админке. - Функция анализирует тип операции (создание/редактирование/переименование) и - отправляет соответствующее сообщение с ссылкой на связанную статью. + Функция анализирует тип операции (создание/редактирование) и статус связанной статьи, + отправляя соответствующее сообщение (success или warning) с ссылкой на статью. + + Использует Django messages framework для отправки сообщений: + - GREEN SUCCESS: при успешном создании/обновлении + - YELLOW WARNING: при создании без связанной статьи (ошибка!) - Использует Django messages framework для отправки (success/warning в зависимости от ситуации). Ссылка на статью открывается в новой вкладке для удобства админа. Параметры: ----------- request : HttpRequest - Объект HTTP-запроса для отправки сообщений через Django messages framework + Объект HTTP-запроса для отправки сообщений через Django messages framework. + Обязателен! obj : django.db.models.Model - Сохраненный объект модели (уже сохранен в БД). + Сохраненный объект модели (уже сохранен в БД). Обязателен! Из этого объекта автоматически извлекается verbose_name модели через obj._meta.verbose_name Также используется для получения старого значения при редактировании. is_new : bool - True если создается новая запись, False если редактируется существующая. - Определяет тип сообщения (green success для нового или warning для переименования). + True если создается новая запись, False если редактируется существующая. Обязателен! + При создании (is_new=True): отправляет success или warning в зависимости от наличия статьи. + При обновлении (is_new=False): отправляет success с информацией о текущем состоянии. 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 из модели. - При редактировании может содержать информацию о типе и содержимом статьи. - obj_field_name : str, опционально - Имя основного поля объекта для получения текущего значения. - По умолчанию 's_label' (для TbLabel). - Примеры: 's_artist' (для TbArtist), 's_style_name' (для TbMusicStyle) + obj_field_name : str + Имя основного поля объекта для получения текущего значения. ОБЯЗАТЕЛЕН! + Примеры: 's_label' (для TbLabel), 's_artist' (для TbArtist), 's_style_name' (для TbMusicStyle) + Если поле не существует в модели, будет выброшено AttributeError. - article_title_field : str, опционально - Имя поля статьи для получения названия статьи. - По умолчанию 's_article_title' (для TbArticle). - Обычно это поле одинаково у всех моделей статей. + article_title_field : str + Имя поля статьи для получения названия статьи. ОБЯЗАТЕЛЕН! + Обычно это 's_article_title' для всех моделей статей. + Если поле не существует в связанной статье, будет выброшено AttributeError. + + Raises: + AttributeError: Если obj_field_name не существует в модели obj + AttributeError: Если article_title_field не существует в related_article (если он не None) Отправляемые сообщения: ----------------------- - Новая запись (is_new=True): - - GREEN SUCCESS: "Лейбл «NAME» создан успешно. Статья создана автоматически." + Новая запись с статьей (is_new=True, related_article существует): + - GREEN SUCCESS: "Лейбл «NAME» создан успешно. Связанная статья «TITLE» создана автоматически." - С ссылкой: "Проверить/Отредактировать статью →" - Переименование (is_new=False, имя изменилось): - - YELLOW WARNING: "Лейбл «OLD» переименован на «NEW». ПРОВЕРЬТЕ СВЯЗАННУЮ СТАТЬЮ." - - С ссылкой: "Проверить/Отредактировать статью →" + Новая запись БЕЗ статьи (is_new=True, related_article=None): + - YELLOW WARNING: "Лейбл «NAME» создан успешно. [ОЙ-ОЙ-ОЙ] СТАТЬЯ НЕ БЫЛА СОЗДАНА. ЭТО НЕ ДОЛЖНО БЫЛО СЛУЧИТЬСЯ!" + - Сигнализирует об ошибке в процессе создания связанной статьи - Обновление (is_new=False, имя не изменилось): + Обновление (is_new=False): - GREEN SUCCESS: "Лейбл «NAME» обновлен. Связанная статья: «ARTICLE_TITLE»." - - С ссылкой: "Проверить/Отредактировать статью →" + - С ссылкой: "Проверить/Отредактировать статью →" (если статья существует) Пример использования в admin.py (максимально чистый и поджарый код): ----------------------------------------------------------------------- @@ -1018,19 +1047,34 @@ def generate_admin_save_message( # Основной механизм сохранения Django (создает статью если нужно) super().save_model(request, obj, form, change) - # Отправляем информативное сообщение админу - # verbose_name берется автоматически из obj._meta.verbose_name - # старое значение получается из БД при необходимости + # Отправляем информативное сообщение админу с явным указанием полей generate_admin_save_message( request=request, obj=obj, is_new=not change, # Django: change=False для новых, True для существующих 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 есть, перезапишем) article_title = '' @@ -1039,7 +1083,7 @@ def generate_admin_save_message( # Формируем ссылку на редактирование статьи и собираем информацию о ней if related_article: - article_title = getattr(related_article, article_title_field, 'Статья') + article_title = getattr(related_article, article_title_field) article_link = (f' Проверить/Отредактировать статью →') @@ -1054,9 +1098,10 @@ def generate_admin_save_message( if related_article: msg += (f' Связанная статья «{article_title}» создана автоматически. >>' f' {article_info} {article_link}') + messages.success(request, mark_safe(msg)) else: msg += ' [ОЙ-ОЙ-ОЙ] СТАТЬЯ НЕ БЫЛА СОЗДАНА. ЭТО НЕ ДОЛЖНО БЫЛО СЛУЧИТЬСЯ!' - messages.success(request, mark_safe(msg)) + messages.warning(request, mark_safe(msg)) else: # РЕДАКТИРОВАНИЕ СУЩЕСТВУЮЩЕЙ ЗАПИСИ