From 6e3b0d3eb41d4e14e614a611498ba6ccea665855 Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 11 Oct 2026 23:39:22 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B8=20=D1=80=D0=B0=D1=81=D1=88?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BD=D1=8B=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8-=D0=BA=D0=BE=D0=BD=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D1=82=D1=8B=20=D0=B2=20constants.py=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0006_alter_tbarticle_l_article_type.py | 18 + lpon_site/frontend/models.py | 144 +++----- lpon_site/lpon_site/constants.py | 313 ++++++++++++++++++ lpon_site/lpon_site/settings.py | 57 +--- 4 files changed, 371 insertions(+), 161 deletions(-) create mode 100644 lpon_site/frontend/migrations/0006_alter_tbarticle_l_article_type.py create mode 100644 lpon_site/lpon_site/constants.py diff --git a/lpon_site/frontend/migrations/0006_alter_tbarticle_l_article_type.py b/lpon_site/frontend/migrations/0006_alter_tbarticle_l_article_type.py new file mode 100644 index 0000000..02ec7ec --- /dev/null +++ b/lpon_site/frontend/migrations/0006_alter_tbarticle_l_article_type.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.5 on 2026-10-11 18:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('frontend', '0005_remove_tboffer_s_offer_skip32_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tbarticle', + name='l_article_type', + field=models.CharField(blank=True, choices=[('artist', 'Artist: артист, группа или бренд'), ('style', 'Style: музыкальный стиль'), ('item', 'Item: Альбом, релиз или товар (кассета, hifi, аксессуар)'), ('label', 'Label: Лейбл, издатель или компания'), ('offer', 'Offer: конкретное предложение от продавца'), ('seller', 'Seller: продавец или магазин'), ('blog', 'Blog: Новость или блог'), ('action', 'Action: Спецпредложение, акция, распродажа и т.п.'), ('adv', 'Adv: Реклама или баннер'), ('HUB', 'HUB: СТАТЬЯ-ХАБ (использует DSL для отображения контента)'), ('info', 'Info: Текстовый контент (для FAQ, условий конфиденциальности, правил/соглашений и т.д.)'), ('--', '?¿?: Другое')], db_index=True, default='--', max_length=7, verbose_name='Тип статьи'), + ), + ] diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index 469937e..78449f1 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -172,15 +172,34 @@ from filer.fields.image import FilerImageField from filer.fields.file import FilerFileField from frontend.utils import make_slug, update_synonyms_in_metadata, create_or_get_related_article from frontend.utils_validators import validate_and_raise_for_duplicates +from lpon_site.constants import ( + KEY_IMAGE_TYPE, + VALUE_IMAGE_ABSTRACT, + KEY_IMAGE_FROM, + VALUE_IMAGE_FROM_USER, + KEY_IMAGE_URL, + KEY_IMAGE_NOTE, + KEY_OFFER_NOTE, + KEY_OFFER_ALL_MEDIA, + get_image_metadata_default, + get_offer_metadata_default, + get_item_metadata_default, + ArticleType, + SellerType, + Currency, + Condition, + Format, + SourceType, + MediaType, + Market, + TapeType, + MediaLength, + ShellColor, + WriteProtectTabs, + InlayCondition, + Packaging, +) from lpon_site.settings import ( - KEY_IMAGE_TYPE, # Ключ, о том, какой тип изображения (реальное или абстрактное) - VALUE_IMAGE_ABSTRACT, # Реальная картинка (актуально, для фото товаров в TbOffer) - KEY_IMAGE_FROM, # Ключ, о том, как получено изображение - VALUE_IMAGE_FROM_USER, # Картинка загружена пользователем через админку - KEY_IMAGE_URL, # Ключ, о том, откуда получена картинка (URL источника) - KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.) - KEY_OFFER_NOTE, # Ключ, для заметок о коммерческом предложении (например, "замят угол конверта" и т.д.) - KEY_OFFER_ALL_MEDIA, # Ключ, для хранения информации о том, какие носители входят в оффер (например, CD+2LP) OFFER_HASHIDS_SALT, # Соль для криптографического кодирования ID оферов OFFER_HASHIDS_MIN_LENGTH, # Минимальная длина кода ) @@ -188,34 +207,6 @@ from lpon_site.settings import ( logger = logging.getLogger(__name__) -# ============================================================================ -# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ DEFAULTS В МОДЕЛЯХ -# ============================================================================ - -def get_image_metadata_default(): - """ - Возвращает значение по умолчанию для j_img_metadata. - Используется как callable default в Django моделях (Django не может сериализовать lambda). - """ - return { - KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT, - KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER, - KEY_IMAGE_URL: None, - KEY_IMAGE_NOTE: '???', - } - - -def get_offer_metadata_default(): - """ - Возвращает значение по умолчанию для j_offer_metadata. - Используется как callable default в Django моделях (Django не может сериализовать lambda). - """ - return { - KEY_OFFER_NOTE: '', - KEY_OFFER_ALL_MEDIA: [], - } - - # ============================================================================ # МЕТАДАННЫЕ ИЗОБРАЖЕНИЙ # ============================================================================ @@ -317,30 +308,8 @@ class TbArticle(models.Model): • Соединитель для Artist, Item, Label, Seller, Style (все 1:1→article) • Хранит текстовый контент, SEO, изображения справочников """ - class ArticleType(models.TextChoices): - """ - Типы статей и контентных разделов. - - ВАЖНО ДЛЯ РОУТИНГА И URL: - Значение каждого элемента (например, 'info', 'blog', 'artist', 'item' и т.д.) - используется напрямую как первый сегмент канонического URL-пути статьи: - /// (например: /info/privacy-policy/, /blog/new-release-2026-september/) - - Исключения: - - HUB ('HUB'): статьи-хабы публикуются напрямую в корне сайта (//). - """ - ARTIST = 'artist', 'Artist: артист, группа или бренд' - STYLE = 'style', 'Style: музыкальный стиль' - ITEM = 'item', 'Item: Альбом, релиз или товар (кассета, hifi, аксессуар)' - LABEL = 'label', 'Label: Лейбл, издатель или компания' - OFFER = 'offer', 'Offer: конкретное предложение от продавца' - SELLER = 'seller', 'Seller: продавец или магазин' - BLOG = 'blog', 'Blog: Новость или блог' - ACTION = 'action', 'Action: Спецпредложение, акция, распродажа и т.п.' - ADV = 'adv', 'Adv: Реклама или баннер' - HUB = 'HUB', 'HUB: СТАТЬЯ-ХАБ (использует DSL для отображения контента)' - INFO = 'info', 'Info: Текстовый контент (для FAQ, условий конфиденциальности, правил/соглашений и т.д.)' - OTHER = '--', '?¿?: Другое' + # Алиас на перечисление типов статей из constants.py (для обратной совместимости) + ArticleType = ArticleType s_article_title = models.CharField( max_length=255, @@ -1081,25 +1050,9 @@ class TbSeller(models.Model): • save(): создаёт связанную статью автоматически (синонимы не используются) - Если статья не привязана: создаёт новую через create_or_get_related_article() """ - class SellerType(models.TextChoices): - SELLER = 'seller', 'Продавец' - LABEL = 'label', 'Лейбл (издатель)' - DIY = 'diy', 'Самиздат группы' - CROWD = 'crowd', 'Краудфандинг' - OTHER = '???', 'Другое' - - class Currency(models.TextChoices): - RUB = 'rub', 'RUB: российский рубль' - USD = 'usd', 'USD: американский доллар' - EUR = 'eur', 'EUR: евро' - AMD = 'amd', 'AMD: армянских драм' - TRY = 'try', 'TRY: турецкая лира' - JPY = 'jpy', 'JPY: японская иена' - GBP = 'gbp', 'GBP: британский фунт' - CNY = 'cny', 'CNY: китайский юань' - BYN = 'byn', 'BYN: белорусский рубль' - TON = 'ton', 'TON: криптовалюта TON' - OTHER = '??', 'Other' + # Алиасы на перечисления из constants.py (для обратной совместимости) + SellerType = SellerType + Currency = Currency # Используем SmallAutoField для оптимизации (макс ~32k) # Продавцов обычно до 1000-10000, поэтому достаточно @@ -1215,29 +1168,9 @@ class TbOffer(models.Model): В админке M2M выглядит как список картинок с сортировкой и метаданными. """ - class Condition(models.TextChoices): - S = 's', 'Still Sealed (новое, запечатано)' - M = 'm', 'Mint (новое, распакованное)' - NM = 'nm', 'Near Mint (почти новое)' - VG = 'vg', 'Very Good (очень хорошее)' - G = 'g', 'Good (хорошее)' - F = 'f', 'Fair (удовлетворительное)' - P = 'p', 'Poor (плохое)' - OTHER = '??', 'Other' - - class Format(models.TextChoices): - LP = 'lp', 'Vinyl Long-Play (12")' - EP = 'ep', 'Vinyl Extended-Play (12", 10", 7")' - V45 = '45', 'Vinyl 7" (45 rpm)' - CD = 'cd', 'Compact Disc' - LD = 'ld', 'LaserDisc' - REC_MD = 'md', 'MiniDisc Record' - USE_MD = 'ms', 'Used MiniDisc (для записи)' - REC_CS = 'cs', 'Cassette Record' - USE_CS = 'uc', 'Used Cassette (для записи)' - REC_RR = 'tp', 'Tape Reel Record' - USE_RR = 'ur', 'Used Tape Reel (для записи)' - OTHER = '??', 'Other' + # Алиасы на перечисления из constants.py (для обратной совместимости) + Condition = Condition + Format = Format s_offer = models.CharField( max_length=128, @@ -1535,11 +1468,8 @@ class TbSource(models.Model): МЕТОДЫ: • Нет переопределённых методов save(). Используется стандартное поведение Django. """ - class SourceType(models.TextChoices): - EXCEL = 'excel', 'Excel-файл от продавца или издателя' - CSV = 'csv', 'CSV-файл от продавца или издателя' - URL = 'url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)' - OTHER = '??', 'Другое (включая ручной ввод)' + # Алиас на перечисление из constants.py (для обратной совместимости) + SourceType = SourceType # Используем SmallAutoField для оптимизации (макс ~32k) # Источников обычно до 1000, достаточно diff --git a/lpon_site/lpon_site/constants.py b/lpon_site/lpon_site/constants.py new file mode 100644 index 0000000..2612ae1 --- /dev/null +++ b/lpon_site/lpon_site/constants.py @@ -0,0 +1,313 @@ +""" +Константы, перечисления и ключи метаданных проекта LPON. + +Данный модуль является единым источником правды для: + • Ключей JSON-метаданных (TbItem.j_item_metadata, TbOffer.j_offer_metadata и др.) + • Контролируемых словарей и перечислений (типы носителей, типы статей, продавцов, валют) + • Функций генерации дефолтных JSON-структур для моделей + • Валидационных настроек и системных констант +""" + +from enum import IntEnum +from django.db import models + + +# ============================================================================ +# СИСТЕМНЫЕ НАСТРОЙКИ И ОГРАНИЧЕНИЯ +# ============================================================================ + +# Максимальная длина slug: Весь URL оптимально иметь до 70 символов (методичка Google). +# SLUG_MAX_LENGTH задает ограничение для слагов, создаваемых автоматически. +SLUG_MAX_LENGTH = 60 + +# Минимальная длина слова для поиска совпадений схожести в синонимах +MIN_SYNONYM_WORD_LENGTH = 4 + + +# ============================================================================ +# КЛЮЧИ МЕТАДАННЫХ ДЛЯ ОБЩИХ ТАБЛИЦ (TbLabel, TbSeller, TbArtist, TbMusicStyle) +# ============================================================================ + +KEY_SYNONYM_EN = 'SYN_EN' # Синонимы на английском языке (для поиска и парсеров) +KEY_SYNONYM_RU = 'SYN_RU' # Синонимы на русском языке (для отображения на фронтенде) + + +# ============================================================================ +# КЛЮЧИ И ЗНАЧЕНИЯ ДЛЯ МЕТАДАННЫХ ИЗОБРАЖЕНИЙ (TbImageMetadata) +# ============================================================================ + +KEY_IMAGE_TYPE = 'IMG_IS' # Ключ для хранения типа картинки +VALUE_IMAGE_ABSTRACT = 'abstract' # Абстрактная картинка (логотипы, промо, типовые обложки) +VALUE_IMAGE_REAL = 'real' # Реальная фотография физического лота + +KEY_IMAGE_FROM = 'IMG_FROM' # Источник происхождения изображения +VALUE_IMAGE_FROM_USER = '_adm' # Загружено пользователем через панель управления +VALUE_IMAGE_FROM_PARSE = 'parse' # Получено автоматически через парсер (Discogs, Bandcamp и т.д.) + +KEY_IMAGE_URL = 'IMG_URL' # URL оригинального источника изображения +KEY_IMAGE_NOTE = 'IMG_NOTE' # Текстовая заметка или подпись к изображению + + +# ============================================================================ +# КЛЮЧИ МЕТАДАННЫХ СТАТЕЙ (TbArticle) +# ============================================================================ + +KEY_ARTICLE_HUB = 'HUB' # Флаг того, что статья является хабом/сборником + + +# ============================================================================ +# КЛЮЧИ МЕТАДАННЫХ ДЛЯ ТОВАРОВ И МОДЕЛЕЙ НОСИТЕЛЕЙ (TbItem.j_item_metadata) +# ============================================================================ + +KEY_ITEM_MEDIA_TYPE = 'media_type' # Тип сущности: музыкальный альбом, модель носителя и т.д. +KEY_ITEM_MEDIA_FORMAT = 'media_format' # Формат физического носителя (кассета, MD, винил, CD) +KEY_ITEM_TAPE_TYPE = 'tape_type' # Тип магнитной ленты (для кассет: Type I, II, III, IV) +KEY_ITEM_LENGTH = 'length' # Хронометраж носителя (46, 60, 74, 90 мин) +KEY_ITEM_SHELL_COLOR = 'shell_color' # Цвет корпуса кассеты или минидиска +KEY_ITEM_RELEASE_YEAR = 'release_year' # Год или диапазон годов производства модели/релиза +KEY_ITEM_MARKET = 'market' # Регион дистрибуции (JP, US, EU, GLOBAL) +KEY_ITEM_GALLERY_MODEL = 'gallery_model' # Список ID эталонных изображений модели в django-filer +KEY_ITEM_CATALOG_NUM = 'catalog_num' # Оригинальный каталожный номер производителя +KEY_ITEM_BARCODE = 'barcode' # Штрихкод (EAN/UPC) + + +# ============================================================================ +# КЛЮЧИ МЕТАДАННЫХ ДЛЯ КОММЕРЧЕСКИХ ПРЕДЛОЖЕНИЙ (TbOffer.j_offer_metadata) +# ============================================================================ + +KEY_OFFER_NOTE = 'OFR_NT' # Текстовая заметка по лоту (дефекты, нюансы) +KEY_OFFER_ALL_MEDIA = 'OFR_MF' # Состав комплекта носителей (например, {"lp": 2, "cd": 1}) +KEY_OFFER_IS_RECORDED = 'is_recorded' # Флаг наличия уникальной аудиозаписи на носителе +KEY_OFFER_RECORDING_INFO = 'recording_info' # Описание записанного материала (эфир, альбом, бутлег) +KEY_OFFER_TABS = 'write_protect_tabs' # Состояние язычков защиты от записи (INTACT, BROKEN) +KEY_OFFER_INLAY = 'inlay_condition' # Состояние бумажного вкладыша J-Card (CLEAN, PENCIL, WRITTEN) +KEY_OFFER_SERIES_TAG = 'series_tag' # Слаг/метка серии или коллекции +KEY_OFFER_PACKAGING = 'packaging' # Сохранность упаковки (SEALED, OPENED) + + +# ============================================================================ +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ГЕНЕРАЦИИ ДЕФОЛТНЫХ ЗНАЧЕНИЙ JSON (CALLABLE DEFAULTS) +# ============================================================================ + +def get_image_metadata_default() -> dict: + """ + Возвращает значение по умолчанию для TbImageMetadata.j_img_metadata. + Используется как callable default в Django моделях и миграциях. + """ + return { + KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT, + KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER, + KEY_IMAGE_URL: None, + KEY_IMAGE_NOTE: '???', + } + + +def get_offer_metadata_default() -> dict: + """ + Возвращает значение по умолчанию для TbOffer.j_offer_metadata. + Используется как callable default в Django моделях и миграциях. + """ + return { + KEY_OFFER_NOTE: '', + KEY_OFFER_ALL_MEDIA: [], + } + + +def get_item_metadata_default() -> dict: + """ + Возвращает значение по умолчанию для TbItem.j_item_metadata. + """ + return { + KEY_ITEM_MEDIA_TYPE: 'blank_media', + KEY_ITEM_MEDIA_FORMAT: 'other', + } + + +# ============================================================================ +# КОНТРОЛИРУЕМЫЕ ПЕРЕЧИСЛЕНИЯ (ENUMS / CHOICES ДЛЯ МОДЕЛЕЙ И JSON) +# ============================================================================ + +class ArticleType(models.TextChoices): + """ + Типы статей и контентных разделов (TbArticle.l_article_type). + + ВАЖНО ДЛЯ РОУТИНГА И URL: + Значение каждого элемента (например, 'info', 'blog', 'artist', 'item' и т.д.) + используется напрямую как первый сегмент канонического URL-пути статьи: + /// (например: /info/privacy-policy/, /blog/new-release-2026-september/) + + Исключения: + - HUB ('HUB'): статьи-хабы публикуются напрямую в корне сайта (//). + """ + ARTIST = 'artist', 'Artist: артист, группа или бренд' + STYLE = 'style', 'Style: музыкальный стиль' + ITEM = 'item', 'Item: Альбом, релиз или товар (кассета, hifi, аксессуар)' + LABEL = 'label', 'Label: Лейбл, издатель или компания' + OFFER = 'offer', 'Offer: конкретное предложение от продавца' + SELLER = 'seller', 'Seller: продавец или магазин' + BLOG = 'blog', 'Blog: Новость или блог' + ACTION = 'action', 'Action: Спецпредложение, акция, распродажа и т.п.' + ADV = 'adv', 'Adv: Реклама или баннер' + HUB = 'HUB', 'HUB: СТАТЬЯ-ХАБ (использует DSL для отображения контента)' + INFO = 'info', 'Info: Текстовый контент (для FAQ, условий конфиденциальности, правил/соглашений и т.д.)' + OTHER = '--', '?¿?: Другое' + + +class SellerType(models.TextChoices): + """Тип продавца или магазина (TbSeller.l_seller_type).""" + SELLER = 'seller', 'Продавец' + LABEL = 'label', 'Лейбл (издатель)' + DIY = 'diy', 'Самиздат группы' + CROWD = 'crowd', 'Краудфандинг' + OTHER = '???', 'Другое' + + +class Currency(models.TextChoices): + """Валюта источника/продавца (TbSeller.l_seller_currency).""" + RUB = 'rub', 'RUB: российский рубль' + USD = 'usd', 'USD: американский доллар' + EUR = 'eur', 'EUR: евро' + AMD = 'amd', 'AMD: армянских драм' + TRY = 'try', 'TRY: турецкая лира' + JPY = 'jpy', 'JPY: японская иена' + GBP = 'gbp', 'GBP: британский фунт' + CNY = 'cny', 'CNY: китайский юань' + BYN = 'byn', 'BYN: белорусский рубль' + TON = 'ton', 'TON: криптовалюта TON' + OTHER = '??', 'Other' + + +class Condition(models.TextChoices): + """ + Международный грейдинг состояния носителя, обложки, вкладышей, плакатов и т.п. (Goldmine Standard). + Используется в TbOffer.l_offer_condition_media и TbOffer.l_offer_condition_sleeve. + """ + S = 's', 'Still Sealed (новое, запечатано)' + M = 'm', 'Mint (новое, распакованное)' + NM = 'nm', 'Near Mint (почти новое)' + VG = 'vg', 'Very Good (очень хорошее)' + G = 'g', 'Good (хорошее)' + F = 'f', 'Fair (удовлетворительное)' + P = 'p', 'Poor (плохое)' + OTHER = '??', 'Other' + + +class Format(models.TextChoices): + """Физический формат носителя оффера (TbOffer.l_offer_to_format).""" + LP = 'lp', 'Vinyl Long-Play (12")' + EP = 'ep', 'Vinyl Extended-Play (12", 10", 7")' + V45 = '45', 'Vinyl 7" (45 rpm)' + CD = 'cd', 'Compact Disc' + LD = 'ld', 'LaserDisc' + REC_MD = 'md', 'MiniDisc Record' + USE_MD = 'ms', 'Used MiniDisc (для записи)' + REC_CS = 'cs', 'Cassette Record' + USE_CS = 'uc', 'Used Cassette (для записи)' + REC_RR = 'tp', 'Tape Reel Record' + USE_RR = 'ur', 'Used Tape Reel (для записи)' + OTHER = '??', 'Other' + + +class SourceType(models.TextChoices): + """Тип источника данных о товарах (TbSource.l_source_type).""" + EXCEL = 'excel', 'Excel-файл от продавца или издателя' + CSV = 'csv', 'CSV-файл от продавца или издателя' + URL = 'url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)' + OTHER = '??', 'Другое (включая ручной ввод)' + + +class MediaType(models.TextChoices): + """Тип сущности товара в каталоге.""" + M_ALBUM = 'music_album', 'Музыкальный альбом / релиз (кассета, CD, винил, MD и т.д.)' + M_MEDIA = 'blank_media', 'Носитель под запись (чистый или б/у модель)' + M_RARE = 'rare_recording', 'Раритетная аудиозапись или бутлег' + M_EQUIPMENT = 'equipment', 'Аудиотехника и аксессуары' + + +class Market(models.TextChoices): + """Регион дистрибуции релиза или модели носителя.""" + JAPAN = 'jp', 'Япония (Japan / NTSC-J)' + USA = 'us', 'США / Северная Америка (USA / NTSC-U)' + EUROPE = 'eu', 'Европа (Europe / PAL)' + UK = 'uk', 'Великобритания (United Kingdom)' + USSR = 'su', 'СССР / Мелодия' + RU = 'ru', 'Россия' + GLOBAL = 'global', 'Международный / Global' + + +class TapeType(models.TextChoices): + """Тип магнитной ленты для аудиокассет.""" + TYPE_1_NORMAL = 'type_1', 'Type I: Normal Position (Fe / Fe2O3)' + TYPE_2_HIGH = 'type_2', 'Type II: High Position (CrO2 / Chrome / Cobalt)' + TYPE_3_FECR = 'type_3', 'Type III: Ferro-Chrome (FeCr)' + TYPE_4_METAL = 'type_4', 'Type IV: Metal Position (Metal Tape)' + + +class MediaLength(models.IntegerChoices): + """Стандартные хронометражи кассет и минидисков в минутах.""" + L10 = 10, '10 минут' + L20 = 20, '20 минут' + L30 = 30, '30 минут' + L40 = 40, '40 минут' + L46 = 46, '46 минут' + L50 = 50, '50 минут' + L54 = 54, '54 минуты' + L60 = 60, '60 минут' + L74 = 74, '74 минуты (стандарт MiniDisc)' + L80 = 80, '80 минут' + L90 = 90, '90 минут' + L100 = 100, '100 минут' + L110 = 110, '110 минут' + L120 = 120, '120 минут' + L150 = 150, '150 минут' + + +class ShellColor(models.TextChoices): + """Цвет корпуса компакт-кассеты или минидиска.""" + AMBER = 'amber', 'Янтарный (Amber / Прозрачный оранжевый)' + SMOKE = 'smoke', 'Дымчатый (Smoke / Полупрозрачный серый)' + CLEAR = 'clear', 'Прозрачный (Clear)' + BLACK = 'black', 'Черный (Solid Black)' + WHITE = 'white', 'Белый (Solid White)' + GOLD = 'gold', 'Золотой / Металлик (Gold Metallic)' + BLUE = 'blue', 'Синий / Голубой (Blue / Cyan)' + RED = 'red', 'Красный / Рубиновый (Red / Ruby)' + GREEN = 'green', 'Зеленый (Green)' + + +class WriteProtectTabs(models.TextChoices): + """Состояние язычков защиты от записи на компакт-кассетах.""" + INTACT = 'intact', 'Целы (Intact / Запись возможна)' + BROKEN = 'broken', 'Выломаны (Broken / Защита активна)' + + +class InlayCondition(models.TextChoices): + """Состояние бумажного вкладыша (J-Card).""" + CLEAN = 'clean', 'Чистый (Без надписей / как новый)' + PENCIL = 'pencil', 'Надписи простым карандашом (стираемые)' + WRITTEN = 'written', 'Исписан ручкой / штампы / наклейки' + MISSING = 'missing', 'Вкладыш отсутствует' + + +class Packaging(models.TextChoices): + """Тип сохранности упаковки физического экземпляра.""" + SEALED = 'sealed', 'Заводская запечатка (Still Sealed)' + OPENED = 'opened', 'Вскрыт / б/у (Opened)' + + +# ============================================================================ +# КОНСТАНТЫ ДЛЯ ПОИСКА ДУБЛИКАТОВ И ВАЛИДАЦИИ +# ============================================================================ + +VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE' +VALIDATE_KEY__MODEL = 'MODEL' +VALIDATE_KEY__VALUE = 'MATCH_VALUE' + + +class ValidateMatchType(IntEnum): + """Типы совпадений при поиске дубликатов.""" + IS_DUPLICATE = 1 # Точное совпадение основного поля (s_label, s_artist и т.д.) + FIND_IN_SYNONYM = 2 # Точное совпадение в синонимах + EXACT_SYNONYM_MATCH = 3 # Точное совпадение синонимов с синонимами остальных записей + PARTIAL_MATCH = 4 # Схожесть: частичное совпадение + PARTIAL_MATCH__RISK_SHORT_WORDS = 5 # Схожесть: совпадение по коротким словам (риск ложных срабатываний) diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py index f8bba04..6885186 100644 --- a/lpon_site/lpon_site/settings.py +++ b/lpon_site/lpon_site/settings.py @@ -316,58 +316,7 @@ LOGGING = { # =================================================== # СПЕЦИФИЧНЫЕ ДЛЯ ПРОЕКТА КОНСТАНТЫ И НАСТРОЙКИ - -# Максимальная длина slug: Весь URL оптимально иметь до 70 символов (методичка Google, так как такой URL виден целиком -# на страничке поисковой выдачи, вызывает больше доверия и кликов). С учетом того, что -# в URL входит имя домена и slug-и разделов, оптимально устанавливать slug даже короче -# (20-30 символов максимум). -# SLUG_MAX_LENGTH задает ограничение для slug-ов созданных автоматически. Вручную в admin можно задать до 255 символов. -SLUG_MAX_LENGTH = 60 - -# Ключи для типовых параметров в мета-полях (для TbLabel, TbSeller, TbArtist, TbMusicStyle и т.д.) -KEY_SYNONYM_EN = 'SYN_EN' # Ключ для хранения синонимов на английском языке (для поиска по синонимам) и ДЛЯ ПАРСЕРОВ -KEY_SYNONYM_RU = 'SYN_RU' # Ключ для хранения синонимов на русском языке (для представления на фронтенд) - -# Ключи и значения для метаданных картинок -KEY_IMAGE_TYPE = 'IMG_IS' # Ключ для хранения типа картинки -VALUE_IMAGE_ABSTRACT = 'abstract' # Абстрактная картинка (лого лейбла, фото исполнителя, типовая обложка альбома...) -VALUE_IMAGE_REAL = 'real' # Реальная картинка (актуально, для фото товаров в TbOffer) -KEY_IMAGE_FROM = 'IMG_FROM' # Ключ, о том, как получено изображение -VALUE_IMAGE_FROM_USER = '_adm' # Картинка загружена пользователем через админку -VALUE_IMAGE_FROM_PARSE = 'parse' # Картинка получена через парсер (например, с сайта Discogs, Bandcamp и т.д.) -KEY_IMAGE_URL = 'IMG_URL' # Ключ для хранения URL оригинальной картинки (обычно это должен прописать парсер) -KEY_IMAGE_NOTE = 'IMG_NOTE' # Ключ для хранения заметки о картинке - -# Ключи и значения для коммерческих предложений (офферов, см. TbOffer и OfferAdmin) -KEY_OFFER_NOTE = 'OFR_NT' # Ключ для заметок по офферу (например, "замят угол конверта" и т.д.) -KEY_OFFER_ALL_MEDIA = 'OFR_MF' # Ключ для хранения информации о том, какие носители входят в оффер (например, CD+2LP) - -# Ключи метанных статей (TbArticle). -KEY_ARTICLE_HUB = 'HUB' # Ключ для хранения информации о том, что статья является хабом (содержит ссылки на другие статьи) - - -# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.) -VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE' -VALIDATE_KEY__MODEL = 'MODEL' -VALIDATE_KEY__VALUE = 'MATCH_VALUE' - -# Типы совпадений как Enum (более информативно чем просто числа, работает в match-case) -class ValidateMatchType(IntEnum): - """Типы совпадений при поиске дубликатов.""" - IS_DUPLICATE = 1 # Точное совпадение основного поля (s_label, s_artist и т.д.) - FIND_IN_SYNONYM = 2 # Точное совпадение в синонимах - EXACT_SYNONYM_MATCH = 3 # Точное совпадение синонимов проверяемой записи с синонимами остальных записей - PARTIAL_MATCH = 4 # Схожесть: частичное совпадение (например, часть названия артиста или лейбла в других записях). - # Основного поля и синонимами проверяемой записи и всех остальных записей. - PARTIAL_MATCH__RISK_SHORT_WORDS = 5 # Схожесть: Частичное совпадение, но с короткими словами (например, - # Японская группа "B'Z" или лейбл "XL"), что может вызвать ложные срабатывания - # схожести. Нужны быть аккуратнее, проверять вручную (особенно если короткие - # слова среди синонимов внутри метаданных других записей). Довольно частое - # явление для распростарннеых "суффиксов" MC, DJ, артиклей, инициалов - # исполнителей и т.п. - - -# Минимальная длина слова для поиска совпадений схожести в синонимах (например, в "The Beatles": "The" не учитывается, -# а "Beatles" учитывается) -MIN_SYNONYM_WORD_LENGTH = 4 +# Вынесены в constants.py для изоляции бизнес-логики и чистоты настроек +# =================================================== +from .constants import *