# Спецификация: LPON Store Hub DSL (Компактный конструктор интерфейсов на кортежах) драфт ## 1. Обзор и цели Мы реализуем гибкий, управляемый данными (Data-Driven) конструктор страниц внутри бэкенда Django для **LPON.RU**. Вместо написания отдельных вьюх (Views) и шаблонов под каждый формат или раздел (например, `/catalog/cassettes/`, `/catalog/vinyl/`), мы формируем динамические **Страницы-Хабы (Hub Pages)** с помощью компактного JSON Tuple DSL, который хранится прямо в базе данных. Система завязана на модель `TbArticle`. Конфигурация хранится в JSON-поле метаданных статьи (`j_article_metadata['hub']`). Механизм HUB может использоваться как для чистых хаб-страниц (`l_article_type = 'HUB'`), так и в качестве опционального блока **в любых других статьях/сущностях** для подключения динамических витрин и модулей. ### Генерализация списков страниц (Отказ от списочных View/Templates) Все списочные страницы (например, список текстовых материалов `/info/`, список новостей `/blog/`, каталоги форматов) рассматриваются как **частный случай хаба**: 1. Вместо написания отдельной вьюхи (например, `txt_articles_list`) в БД создаётся статья со слагом `info` и типом `HUB`. 2. В её DSL-конфигурации прописываются требуемые блоки (например, `[["info-articles", "list", 50]]`). 3. Рендеринг выполняют не отдельные функции-представления, а единый универсальный диспетчер хабов (`HubView`). 4. **Каноничность URL элементов:** Внутри шаблонов визуальных блоков ссылки на карточки и статьи выводится строго через `href="{{ item.get_absolute_url }}"`. Модель статьи/товара сама определяет свой канонический адрес по типу (например, `/info/privacy-policy` или `/item/album-slug`), исключая путаницу префиксов в ссылках независимо от того, в каком хабе опубликован блок. --- ## 2. Синтаксис и структура JSON DSL Массив `hub` представляет собой список позиционных кортежей (массивов). Порядок элементов в кортеже строго фиксирован. Все элементы, кроме первого (`slug` / `source`), являются опциональными и имеют безопасные значения по умолчанию. Позиционный порядок параметров: `[ "slug", "view_mode", limit, "sort", extra ]` ### Описание позиций кортежа: 1. **`slug` / `source` (str, 1-я позиция, обязательный)**: - Слаг точечной статьи/категории (например, `"cassettes-studio"`). - Динамическая выборка с двоеточием (например, `"offers:artist:the-beatles"`, `"articles:type:info"`). - Модуль/плагин с префиксом `!` (например, `"!promo-mandarin"`). 2. **`view_mode` (str, 2-я позиция, опциональный, default зависит от `ArticleType` или `"grid"`)**: - Название компонента рендеринга (шаблона): `carousel`, `grid`, `list`, `teaser`, `full`, `slider`. - Поддерживает составной синтаксис через `+` (например, `"teaser+list"`). 3. **`limit` (int | str, 3-я позиция, опциональный, default=`10`)**: - Количество выбираемых элементов (например, `10`, `50`). - Для составных шаблонов задаётся через `+` (например, `"2+15"`). 4. **`sort` (str, 4-я позиция, опциональный, default=`"new+"`)**: - Правило сортировки элементов выборки (см. правила ниже: `new+`, `price+`, `price-`, `random` и т.д.). 5. **`extra` (dict, 5-я позиция, опциональный, default=`{}`)**: - Произвольный словарь с дополнительными настройками блока (например, `{"title": "Кастомный заголовок"}`). ### Правила синтаксиса: ```json { "hub": [ ["source-slug", "view_mode"], ["source-slug", "view_mode", limit], ["source-slug", "view_mode", limit, "sort_rule"], ["source-slug", "view_mode", limit, "sort_rule", {"title": "Кастомный заголовок"}], ["articles:type:info", "teaser+list", "2+15", "new+"], ["!module-slug"] ] } ``` --- ## 3. Спецификация блоков и Мета-язык ### Специальные символы синтаксиса DSL | Символ | Назначение | Пример | Описание | |:--------|:-------------------------------------|:--------------------------------------------------|:-----------------------------------------------------------| | **`!`** | Модуль / Плагин | `"!promo-mandarin"`, `"!cart-bonus"` | Запуск зарегистрированного Python-модуля | | **`:`** | Динамическая выборка (Namespace) | `"articles:type:info"`, `"offers:artist:beatles"` | Выборка из БД (офферы или статьи) по критерию | | **`+`** | Составной режим отображения и лимита | `"teaser+list"`, `"2+15"` | Один SQL-запрос с разбиением результатов на разные шаблоны | --- ### Правила сортировки (`sort`) Для параметра `sort` (4-я позиция) используются короткие наглядные обозначения направления сортировки (`+` — по убыванию/свежие/популярные, `-` — по возрастанию/старые/дешевые): | Значение `sort` | Смысл сортировки | Эквивалент поля в БД | | :--- | :--- | :--- | | **`new+`** *(default)* | Сначала новые (новинки) | `-t_created` / `-t_article_created` | | **`new-`** | Сначала старые | `t_created` / `t_article_created` | | **`price+`** | Сначала дешевые | `f_offer_price` (по возрастанию) | | **`price-`** | Сначала дорогие | `-f_offer_price` (по убыванию) | | **`view+`** | Самые просматриваемые (хиты) | `-i_views` / `-i_article_views` | | **`view-`** | Наименее просматриваемые | `i_views` / `i_article_views` | | **`favorite+`** | Часто в избранном | `-i_favorites` / `-i_article_favorites` | | **`favorite-`** | Редко в избранном | `i_favorites` / `i_article_favorites` | | **`random`** | Случайная выборка | `?` (`order_by('?')`) | *Примечание:* Для обратной совместимости и отказоустойчивости парсер бэкенда также принимает устаревшие текстовые значения (`newest` $\rightarrow$ `new+`, `popular` $\rightarrow$ `view+`, `price_asc` $\rightarrow$ `price+`, `price_desc` $\rightarrow$ `price-`). --- ### Тип A: Конкретная точечная сущность (Обычный slug) Загружает конкретную статью, категорию или жанр по их собственному первичному слагу в БД. Формат: `[ "slug", "view_mode", limit, "sort", extra ]` Примеры: - `["cassettes-studio"]` — конкретная категория/статья (дефолтные view_mode по `ArticleType`, limit=10, sort="new+") - `["guide-how-to-clean-heads", "teaser"]` — тизер конкретной статьи --- ### Тип B: Динамическая выборка / Запрос к БД (Префикс с двоеточием `:`) Позволяет формировать динамические списки офферов (`offers:...`) или статей (`articles:...`) по заданным критериям без создания отдельных вьюх. Формат источника (`source`): - `offers:artist:` — офферы конкретного исполнителя (например, `offers:artist:the-beatles`) - `offers:format:` — офферы конкретного формата (например, `offers:format:vinyl`) - `offers:style:` — офферы музыкального стиля (например, `offers:style:jazz`) - `articles:type:` — статьи определенного типа `ArticleType` (например, `articles:type:info`, `articles:type:blog`) - `articles:artist:` — статьи, связанные с исполнителем (например, `articles:artist:the-beatles`) Примеры: - `["articles:type:info", "list", 50]` — до 50 статей типа `INFO` в виде списка - `["offers:artist:the-beatles", "carousel", 10, "view+"]` — 10 популярных релизов The Beatles в карусели --- ### Составной режим отображения (`view_mode` + `limit` через `+`) Позволяет вывести результаты одного SQL-запроса через комбинацию нескольких шаблонов (например, первые 2 элемента крупными тизерами, а остальные 15 элементов — компактным списком снизу) без дублирования блоков. Синтаксис: `"teaser+list"`, `"2+15"` Пример: - `["articles:type:info", "teaser+list", "2+15", "new+", {"title": "Инструкции и справка"}]` - Выполняется 1 SQL-запрос с `LIMIT = 2 + 15 = 17`. - Первые 2 статьи передаются в шаблон `teaser`. - Следующие 15 статей передаются в шаблон `list`. --- ### Тип C: Динамический модуль / Плагин (Префикс `!`) Запускает кастомный Python-модуль, зарегистрированный в системе (например, интерактивные виджеты, спецпредложения корзины, акции). Формат: `[ "!module-slug", extra ]` Отличительный признак: `slug` начинается с символа **`!`** (восклицательный знак). Так как знак `!` недопустим в обычных URL-слагах Django, парсер однозначно и безошибочно определяет, что данный блок является вызовом Python-плагина. Пример: - `["!promo-mandarin"]` - `["!cart-bonus", {"threshold": 5000}]` --- ## 4. Полный пример конфига (j_article_metadata) ```json { "hub": [ ["hero-cassettes-2026", "slider"], ["articles:type:info", "teaser+list", "2+15", "new+", {"title": "Инструкции и справочные материалы"}], ["offers:artist:the-beatles", "carousel", 10, "view+", {"title": "Релизы The Beatles в наличии"}], ["cassettes-studio", "carousel", 6, "view+"], ["!promo-mandarin"], ["offers:format:vinyl", "grid", 20, "price+"], ["!cart-bonus", {"threshold": 5000}] ] } ``` --- ## 5. Защита и логика бэкенда (Требования к Python / Django) При разработке парсера DSL: 1. Защита от зацикливания и бесконечной рекурсии: - Передавать множество `visited_slugs` при рендере страницы. - Если вложенный блок пытается загрузить `slug`, который уже есть в `visited_slugs`, пропускать блок или выводить отладочный HTML-комментарий (``). 2. Ограничение глубины вложенности: - Жестко ограничить максимальную глубину рекурсии: `MAX_RECURSION_DEPTH = 2` (устанавливается в `settings.py`). 3. Отказоустойчивость (Graceful Degradation): - Если `limit` передан не числом или `sort` содержит неизвестную строку, парсер должен тихо применить безопасные дефолты (автовыбор `view_mode` по `ArticleType` с фолбэком на `'grid'`, `limit=10`, `sort='new+'`). Ошибка синтаксиса в JSON никогда не должна приводить к `500 Server Error`. - Если `slug` (без `!`) не найден в базе, блок пропускается, а в HTML выводится комментарий: ``. - Если модуль `!slug` не зарегистрирован в PluginRegistry, выводится комментарий: ``. 4. Изоляция данных: - Внутри DSL запрещено использовать сырые имена полей БД (например, k_offer_to_item__s_item). Все выборки данных маппятся строго через `slug` и внутренние обработчики запросов (Query Handlers). 5. Валидация slug - Добавить в валидатор админки статей (TbArticle) проверку на несовпадение slug с существующими роутами сайта из `urls.py` (чтобы не было конфликта URL-ов). film0069t-2001.08.xx