doc: как совместить альбомы и кассетные серии (draft 1)
This commit is contained in:
1 parent
0d18586c79
commit
6ef11d4f64
2 files changed
+286
No files matched your search
@@ -0,0 +1,145 @@
|
|||||||
|
# План реализации кассетного каталога, серий и DSL-витрины
|
||||||
|
|
||||||
|
**Дата и время создания/обновления:** 2026-10-11 17:30
|
||||||
|
**Проект:** LPON.RU (Django 6.0+, Python 3.12)
|
||||||
|
**Статус:** Согласованный рабочий план
|
||||||
|
**Архитектурная спецификация метаданных:** `metadata_architecture.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Архитектурные принципы и ключевые решения
|
||||||
|
|
||||||
|
#### 1. Серии, коллекции и раритетные записи
|
||||||
|
* **Концепция:** Для тематических архивов (например, радиопередачи Oobu Joobu, японские бутлеги, циклы фолк-концертов) создается обзорная статья `TbArticle` (тип `HUB` или `COLLECTION`).
|
||||||
|
* **Контент:** Предыстория архива, контекст записей, аппаратура оцифровки/записи, сканы оригинальных вкладышей.
|
||||||
|
* **DSL-витрина серии:** Внутрь статьи встраивается DSL-блок, динамически выбирающий кассеты этой серии по тегу или источнику (например, `["offers:series:oobu-joobu", "grid"]`).
|
||||||
|
* **Покупка:** Поддерживается покупка отдельных выпусков/кассет поштучно, а также покупка всего сета через сборный оффер-комплект.
|
||||||
|
|
||||||
|
#### 2. Двухуровневая медиатека фотографий
|
||||||
|
* **Уровень 1 — Эталонные фото модели (`TbItem` / связанная `TbArticle`):**
|
||||||
|
* Фотографируются один раз на модель: цвет и фактура ленты (Fe, CrO2, Metal), металлизированный ракорд, люминесцентное свечение (TDK BEAM), керамические блоки, сканы каталогов тех лет.
|
||||||
|
* Автоматически подтягиваются во все офферы данной модели кассеты.
|
||||||
|
* **Уровень 2 — Фото конкретного лота (`TbOffer` через `TbImageMetadata`):**
|
||||||
|
* 2-3 честных фото продаваемого экземпляра: реальные потертости корпуса, состояние надписей на вкладыше, целостность язычков защиты от записи.
|
||||||
|
* **Отображение в карточке:** В основном блоке показывается конкретный продаваемый экземпляр, ниже выводится блок «Детали и макросъемка модели».
|
||||||
|
|
||||||
|
#### 3. Музейный каталог (по типу c-90.org / ez647.sk) и архивные лоты
|
||||||
|
* **Сохранение проданных позиций:** Офферы со статусом `quantity = 0` не удаляются и не закрываются ошибкой 404. Они получают статус «Архивный лот / Продано», оставаясь доступными для SEO, просмотра истории и изучения записей.
|
||||||
|
* **Матричная структура справочника (без жесткого дерева папок):**
|
||||||
|
* Ось брендов: `/cassettes/brands/` (TDK, Maxell, Sony, BASF, AXIA, Denon);
|
||||||
|
* Хронологические линейки: разбивка внутри бренда по модельным годам (например, модельный ряд TDK 1985, 1988, 1990 годов с группировкой по типам ленты Normal / High / Metal);
|
||||||
|
* Ось хронологии: `/cassettes/timeline/` (разбивка по эпохам: 70-е, 80-е, 90-е, 2000-е).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ближний горизонт (Пошаговый план разработки)
|
||||||
|
|
||||||
|
#### Шаг 1. Стандартизация схемы метаданных для кассет и всех форматов
|
||||||
|
* Спецификация полностью зафиксирована в `_prj_blueprint/metadata_architecture.md`.
|
||||||
|
* Модуль констант `lpon_site/lpon_site/constants.py` создан и подключен:
|
||||||
|
* Единый реестр перечислений: `MediaType`, `Market`, `TapeType`, `MediaLength`, `ShellColor`, `WriteProtectTabs`, `InlayCondition`, `Packaging`, `OfferExtra`, `Condition`, `Format`.
|
||||||
|
* Режим дефолтных шаблонов с подсказками через `|` (Inline Choice Placeholder) для защиты от ленивого заполнения.
|
||||||
|
* Структура полей `j_item_metadata` (для `TbItem`):
|
||||||
|
* `media_type`: `blank_media`, `music_album`, `audio_recording`, `equipment`;
|
||||||
|
* `tape_type`: `type_1`, `type_2`, `type_3`, `type_4`;
|
||||||
|
* `length`: `46`, `50`, `54`, `60`, `74`, `80`, `90`, `100`, `110`, `120`;
|
||||||
|
* `release_year`: год или диапазон годов выпуска;
|
||||||
|
* `market`: `jp`, `us`, `eu`, `uk`, `su`, `global`;
|
||||||
|
* `gallery_model`: список ID эталонных фото модели в `django-filer`.
|
||||||
|
* Структура полей `j_offer_metadata` (для `TbOffer`):
|
||||||
|
* `packaging`: `sealed`, `opened`;
|
||||||
|
* `is_recorded`: флаг наличия уникальной аудиозаписи;
|
||||||
|
* `recording_info`: краткое резюме записи (или ссылка на отдельную статью `k_offer_to_article`);
|
||||||
|
* `write_protect_tabs`: `intact`, `broken`;
|
||||||
|
* `inlay_condition`: `clean`, `pencil`, `written`, `missing`;
|
||||||
|
* `series_tag`: слаг серии/коллекции (при наличии);
|
||||||
|
* `extras`: словарь опций комплектации (`obi`, `insert`, `poster`, `case_original`, `stickers_sheet` и др.).
|
||||||
|
|
||||||
|
#### Шаг 2. Ядро DSL и реестр обработчиков (`DSLQueryRegistry`)
|
||||||
|
* Создать модуль реестра обработчиков (Query Handlers), разбирающий кортежи формата:
|
||||||
|
`["source", "view_mode", limit, "sort", extra]`
|
||||||
|
* Реализовать базовые обработчики источников:
|
||||||
|
* Одиночные сущности по слагу (`single_slug`);
|
||||||
|
* Выборки статей (`articles:type:<type>`, `articles:collection:<slug>`);
|
||||||
|
* Выборки офферов (`offers:format:cassette`, `offers:label:<slug>`, `offers:tape_type:<type>`, `offers:series:<tag>`);
|
||||||
|
* Реализовать сортировки:
|
||||||
|
* `new+`, `new-`, `price+`, `price-`, `year+`, `year-`, `view+`, `random`;
|
||||||
|
* Поддержка составных режимов и лимитов (`view_mode = "teaser+list"`, `limit = "2+15"`).
|
||||||
|
* Защита от зацикливания (`visited_slugs`, `MAX_RECURSION_DEPTH = 2`) и безопасная деградация при ошибках в JSON.
|
||||||
|
|
||||||
|
#### Шаг 3. Шаблоны компонентов и кастомный Template Tag
|
||||||
|
* Реализовать template tag `{% render_hub_blocks article %}` (или фильтр рендеринга хабов).
|
||||||
|
* Разработать базовую карточку кассеты (`cassette_card.html`):
|
||||||
|
* Фото экземпляра, бейджи типа ленты, хронометража и года, плашка состояния («Под перезапись» или «Запись: Название»), цена/статус наличия.
|
||||||
|
* Разработать шаблоны раскладки для разных `view_mode`:
|
||||||
|
* `grid.html` (адаптивная сетка карточек);
|
||||||
|
* `carousel.html` (горизонтальная прокрутка);
|
||||||
|
* `list.html` (компактный табличный список);
|
||||||
|
* `timeline.html` (раскладка по годам и линейкам для музейного каталога);
|
||||||
|
* `teaser.html` (промо-блок статьи/серии).
|
||||||
|
|
||||||
|
#### Шаг 4. Шаблоны страниц и виртуальные хабы
|
||||||
|
* Шаблон детальной страницы товара/кассеты:
|
||||||
|
* Основная галерея фото конкретного лота + ТТХ и состояние;
|
||||||
|
* Блок «Детали и макросъемка модели» (эталонные фото из `TbItem`);
|
||||||
|
* Блок описания аудиозаписи с треклистом (если привязана статья к офферу);
|
||||||
|
* Автоматический виртуальный DSL-блок («Другие кассеты этого бренда», «Другие кассеты Type II»).
|
||||||
|
* Шаблон страницы серии/коллекции:
|
||||||
|
* Вводная статья + встроенная DSL-сетка кассет серии + блок заказа всей подборки.
|
||||||
|
* Подключение рендеринга блоков в `content/hub.html` и `content/article_detail.html`.
|
||||||
|
|
||||||
|
#### Шаг 5. Пилотная партия и тестирование витрины
|
||||||
|
* Внести в базу 10-15 реальных кассет разных брендов и категорий:
|
||||||
|
* 3 чистые кассеты Type I (TDK D, Sony HF);
|
||||||
|
* 3 кассеты Type II (Maxell XL-II, TDK SA);
|
||||||
|
* 1 топовая кассета Type IV (Metal) или спец-серия со светящимся корпусом (TDK BEAM);
|
||||||
|
* 1 мини-серия из 3-4 кассет с редкими записями (Oobu Joobu / британский фолк) с оформленной статьей-хабом.
|
||||||
|
* Собрать главный хаб кассетного раздела `/cassettes/`.
|
||||||
|
* Проверить визуальное отображение, фильтрацию и мобильную верстку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Среднесрочный горизонт
|
||||||
|
|
||||||
|
#### 6. Бесконтактная корзина и анонимные заказы (Stateless Orders)
|
||||||
|
* Клиентский модуль `!cart-app` (хранение кодов офферов `s_offer_code` в LocalStorage/Cookies).
|
||||||
|
* Интеграция виджета доставки (СДЭК) и генерация анонимных трек-номеров без сбора ПДн в БД сайта (152-ФЗ compliance).
|
||||||
|
|
||||||
|
#### 7. История слагов и SEO-редиректы
|
||||||
|
* Модель `TbArticleAlias` для автоматического сохранения старых слагов при переименовании статей и отдачи `301 Permanent Redirect`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Дальний горизонт
|
||||||
|
|
||||||
|
#### 8. Парсеры и фоновые очереди
|
||||||
|
* Интеграция Celery / Redis воркеров для разбора внешних источников данных.
|
||||||
|
* Реализация интерфейса `ValidatorQueue` в админке для разрешения коллизий синонимов и дубликатов перед записью в основную базу.
|
||||||
|
|
||||||
|
---
|
||||||
|
```text
|
||||||
|
[ TbArtist ] ──(M2M)──► [ TbItem ] ──────► [ TbOffer ]
|
||||||
|
(Донован) (Релиз / Модель) (Конкретный лот на складе)
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
[ TbArticle ] [ TbArticle ]
|
||||||
|
(Эталонная статья) (История записи / лота)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Сущность | Что представляет | Источник наполнения | Связь с артистом |
|
||||||
|
|:-------------------------------|:------------------------------------|:-----------------------------|:---------------------------------|
|
||||||
|
| Винил (LP) | Музыкальный альбом на виниле | Парсер Discogs / MusicBrainz | Да (TbItem.k_item_to_artist) |
|
||||||
|
| Компакт-диск (CD) | Музыкальный альбом на CD | Парсер Discogs / MusicBrainz | Да (TbItem.k_item_to_artist) |
|
||||||
|
| Кассета с записью / Бутлег | Редкий концерт / оцифровка / альбом | Ручной ввод / CSV | Да (TbItem.k_item_to_artist) |
|
||||||
|
| Чистая кассета / MD под запись | Модель носителя (TDK, Maxell, Sony) | Музейный каталог кассет | Нет (привязана к TbLabel бренда) |
|
||||||
|
| Серия / Архив (Oobu Joobu) | Коллекция связанных кассет | Статья-хаб TbArticle | Да (через тег серии и артиста) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Технический долг и текущие замечания по коду
|
||||||
|
1. **Импорты во `frontend/views.py`:** Заменить `from lpon_site.settings import *` на `from django.conf import settings` и удалить неиспользуемый `import json`.
|
||||||
|
2. **Временные вьюхи:** Заменить временные статические функции `catalog()` и `info_articles_list()` на единый диспетчер хабов по мере готовности DSL.
|
||||||
|
3. **Заглушка `get_hub_context`:** Реализовать полноценный вызов `DSLQueryRegistry` вместо отдачи сырого JSON.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Архитектура метаданных и схема каталога товаров (LPON Blueprint)
|
||||||
|
|
||||||
|
**Дата и время создания/обновления:** 2026-10-18 20:30
|
||||||
|
**Проект:** LPON.RU (Django 6.0+, Python 3.12)
|
||||||
|
**Статус:** Согласованная архитектурная концепция
|
||||||
|
**Связанный документ:** `_planstep_for_cassettes.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Фундаментальный принцип: Трехуровневая модель данных
|
||||||
|
|
||||||
|
Для обеспечения масштабируемости каталога на тысячи позиций разных форматов (Кассеты, Минидиски, Винил, CD, Аудиотехника) данные разделены на три уровня:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[ Уровень 1: Реляционная БД ] ──► Базовые ключи (ID, Цены, Связи, Количество, Статусы)
|
||||||
|
[ Уровень 2: Универсальные JSON ] ──► Формат, Рынок, Год, Состояние, Упаковка, Комплектация
|
||||||
|
[ Уровень 3: Специфика формата ] ──► Тип ленты, Длина (Кассеты/MD), Скорость/Вес (Винил), Шторка (MD)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Разделение сущностей: «Заводской паспорт» vs «Физический экземпляр»
|
||||||
|
|
||||||
|
#### Тест-вопрос: «Это с конвейера или от жизни?»
|
||||||
|
|
||||||
|
#### Упрощенная типизация `media_type`:
|
||||||
|
* **`album`** — Музыкальный альбом / мастер-релиз (The Doors, Pink Floyd, Кино).
|
||||||
|
* **`media`** — Физический носитель под запись (кассеты, минидиски, бобины — чистые или б/у с записями).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Симметрия сущностей: Музыкальный альбом vs Модель носителя
|
||||||
|
|
||||||
|
Чтобы не было путаницы между уровнями, в каталоге соблюдается идеальная симметрия:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[ МУЗЫКАЛЬНЫЙ МИР ] [ МИР НОСИТЕЛЕЙ (КАССЕТЫ / MD) ]
|
||||||
|
1. TbArtist: The Doors 1. TbLabel: Sony / TDK / Maxell
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
2. TbItem (Альбом): L.A. Woman (1971) 2. TbItem (Модель): Sony UX 90 (1986, JP)
|
||||||
|
• Название, автор, треклист • Тип ленты: Type II (High)
|
||||||
|
• Год первого релиза • Хронометраж: 90 мин
|
||||||
|
• Обзорная статья • Год ревизии модели: 1986
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
3. TbOffer (Издание в продаже): 3. TbOffer (Экземпляр в продаже):
|
||||||
|
• Винил LP US Elektra 1971 (VG+/VG+, OBI) • Экземпляр А: Запечатанный (Sealed), 2200 ₽
|
||||||
|
• CD Japan 1991 (M/NM) • Экземпляр Б: Б/У с записью Doors, 350 ₽
|
||||||
|
```
|
||||||
|
|
||||||
|
#### А. Модель товара (`TbItem` → `j_item_metadata`)
|
||||||
|
* **Что хранит:** Неизменяемые характеристики альбома или заводской модели носителя.
|
||||||
|
* **Для альбомов (`media_type = 'album'`):** музыкальная суть (треклист, авторы, год оригинального релиза).
|
||||||
|
* **Для носителей (`media_type = 'media'`):** заводские параметры конкретной модели (`tape_type`, `length`, `release_year`, `market`, `shell_color`, `shell_screws`, `window_type`).
|
||||||
|
|
||||||
|
#### Б. Коммерческий лот (`TbOffer` → `j_offer_metadata`)
|
||||||
|
* **Что хранит:** Индивидуальные свойства конкретного физического экземпляра у продавца.
|
||||||
|
* **Примеры:**
|
||||||
|
* `packaging`: сохранность упаковки (`sealed` — запечатан, `opened` — вскрыт/б-у);
|
||||||
|
* `is_recorded`: признак аудиозаписи (`true`/`false`);
|
||||||
|
* `recording_info`: аннотация к аудиозаписи (радиоэфир, альбом, бутлег);
|
||||||
|
* `write_protect_tabs`: статус язычков защиты от записи (`intact` — целы, `broken` — выломаны);
|
||||||
|
* `inlay_condition`: состояние вкладыша J-Card (`clean`, `pencil`, `written`, `missing`);
|
||||||
|
* `series_tag`: слаг серии или тематической коллекции (например, `oobu-joobu`);
|
||||||
|
* `extras`: словарь комплектации и опций издания (наличие и грейд компонентов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Комплектация и опции издания (`extras` / `OfferExtra`)
|
||||||
|
|
||||||
|
В `j_offer_metadata['extras']` хранится словарь кодовых ключей из `OfferExtra`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"extras": {
|
||||||
|
"obi": {"condition": "vg", "qty": 1, "note": "Японское оригинальное OBI"},
|
||||||
|
"insert": {"condition": "clean"},
|
||||||
|
"case_original": {"condition": "vg"},
|
||||||
|
"stickers_sheet": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Допустимые коды комплектации:
|
||||||
|
* `obi` — ОБИ-лента (OBI strip);
|
||||||
|
* `insert` — вкладыш / лирика / статья (Lyric sheet / Insert);
|
||||||
|
* `poster` — оригинальный постер;
|
||||||
|
* `stickers_sheet` — чистый лист наклеек;
|
||||||
|
* `stickers_applied` — наклейки нанесены на носитель;
|
||||||
|
* `postcards` — коллекционные открытки / карточки;
|
||||||
|
* `booklet` — буклет / книжка;
|
||||||
|
* `case_original` — оригинальный родной футляр / подкассетник;
|
||||||
|
* `case_generic` — стандартный / неоригинальный футляр;
|
||||||
|
* `no_case` — без футляра;
|
||||||
|
* `slipcase` — картонный слипкейс / O-Card;
|
||||||
|
* `box_set` — подарочный бокс / коробка;
|
||||||
|
* `inner_sleeve` — оригинальный иллюстрированный внутренний конверт (для винила);
|
||||||
|
* `outer_sleeve` — внешний защитный пластиковый пакет;
|
||||||
|
* `autograph` — автограф исполнителя / музыканта.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Концепция ввода данных: Шаблоны с подсказками через «трубу» (`|`) и защита от лени
|
||||||
|
|
||||||
|
Для удобства оператора и 100% защиты от невалидных данных используется механика **«Inline Choice Placeholder»**:
|
||||||
|
|
||||||
|
1. **Генерация заготовки:** При создании новой записи в поле JSON подставляется готовый шаблон, где для каждого ключа через символ `|` перечислены все допустимые варианты:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"media_type": "blank_media",
|
||||||
|
"tape_type": "type_1|type_2|type_3|type_4",
|
||||||
|
"length": "46|54|60|74|90|100|120",
|
||||||
|
"market": "jp|us|eu|global"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **Правка оператором:** Оператор стирает лишние варианты, оставляя строго один: `"type_2"`.
|
||||||
|
3. **Строгая валидация (`clean()`):** Если запись сохранена с невыбранными вариантами (содержит `|`), валидатор блокирует сохранение и выводит понятную ошибку.
|
||||||
|
4. **Хранение в БД:** В базе данных всегда сохраняется только чистый, валидный код без лишних символов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Каскадное наследование свойств (Fallback Cascade)
|
||||||
|
|
||||||
|
Оффер `TbOffer` автоматически наследует эталонные параметры от родительского `TbItem`. Если лот имеет индивидуальную особенность, значение в `j_offer_metadata` перекрывает заводское значение:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_tape_type(self):
|
||||||
|
"""Возвращает тип ленты лота или эталонный тип ленты из модели Item."""
|
||||||
|
return self.j_offer_metadata.get('tape_type') or (
|
||||||
|
self.k_offer_to_item.j_item_metadata.get('tape_type')
|
||||||
|
if self.k_offer_to_item else None
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Единый источник истины в коде
|
||||||
|
|
||||||
|
Все константы, перечисления (`models.TextChoices`, `models.IntegerChoices`), ключи метаданных и функции дефолтных шаблонов зафиксированы в модуле:
|
||||||
|
* **`lpon_site/lpon_site/constants.py`** (с реэкспортом в `settings.py` и `frontend/models.py`).
|
||||||
Reference in new issue
Block a user