diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index 1a80fa0..7b2abca 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -4,6 +4,7 @@ from __future__ import annotations import re +import json import pytils import random import logging @@ -16,7 +17,7 @@ from django.utils.html import mark_safe from django.http import HttpRequest from django.db.models import Model from lpon_site.settings import ( - SLUG_MAX_LENGTH, KEY_SYNONYM_EN, + SLUG_MAX_LENGTH, KEY_SYNONYM_EN, KEY_ARTICLE_HUB, ) @@ -417,3 +418,60 @@ def create_or_get_related_article( # })) return article + + +def parse_article_metadata(article: Model) -> tuple[Optional[Dict[str, Any]], Optional[str]]: + """ + Парсит и валидирует метаданные статьи (поле `j_article_metadata`). + + Служебный хелпер для безопасного извлечения JSON-объекта метаданных. + + Args: + article (TbArticle): Объект статьи. + + Returns: + tuple[dict | None, str | None]: + - (metadata_dict, None) — при успешном парсинге JSON-объекта; + - (None, error_message) — если поле пустое, содержит невалидный JSON + или данные не являются словарем. + """ + raw_metadata = getattr(article, "j_article_metadata", None) + if not raw_metadata: + return None, "отсутствуют мата-данные" + + try: + data = json.loads(raw_metadata) + if not isinstance(data, dict): + return None, "метаданные j_article_metadata не являются JSON-объектом (словарём)" + return data, None + except (json.JSONDecodeError, TypeError) as e: + return None, f"невалидный JSON в j_article_metadata ({e})" + + +def get_hub_context(article: Model, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Универсальный хелпер для формирования контекста шаблона статьи или хаба (DSL). + + Принимает статью (TbArticle) и опционально предварительно распарсенные метаданные. + Если у статьи в метаданных есть ключ KEY_ARTICLE_HUB ('HUB'), подмешивает в контекст + данные хаба (`hub_data`) для рендеринга визуальных блоков. + + Args: + article (TbArticle): Объект статьи. + metadata (dict | None): Предварительно распарсенный словарь j_article_metadata. + + Returns: + dict: Словарь контекста для передачи в render(). + """ + if metadata is None: + metadata, _ = parse_article_metadata(article) + + context: Dict[str, Any] = { + "article": article, + } + + if metadata and isinstance(metadata, dict) and KEY_ARTICLE_HUB in metadata: + # Заглушка под будущий рендеринг блоков HUB DSL через DSLQueryRegistry + context["hub_data"] = metadata.get(KEY_ARTICLE_HUB) + + return context diff --git a/lpon_site/frontend/views.py b/lpon_site/frontend/views.py index f04831b..89adada 100644 --- a/lpon_site/frontend/views.py +++ b/lpon_site/frontend/views.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from django.shortcuts import render, get_object_or_404, redirect from django.http import HttpRequest, HttpResponse, Http404 from frontend.models import TbArticle +from frontend.utils import get_hub_context, parse_article_metadata from lpon_site.settings import * import json @@ -190,11 +191,10 @@ def info_article_detail(request: HttpRequest | None, slug: str) -> HttpResponse: BreadcrumbItem(title=article_title), ] - # Передаем объект статьи и крошки в шаблон детального просмотра - context = { - "article": article, - "breadcrumbs": breadcrumbs, - } + # Получаем контекст с учетом возможных блоков HUB DSL + context = get_hub_context(article) + context["breadcrumbs"] = breadcrumbs + return render(request, "content/article_detail.html", context) @@ -205,10 +205,10 @@ def hub_detail(request: HttpRequest, slug: str) -> HttpResponse: Функциональность: ----------------- 1. Ищет опубликованную статью (b_article_published=True) по её слагу (slug). - 2. Если статья принадлежит к каноническому типу с отдельным роутом (например, ArticleType.INFO), - выполняет HTTP 301 Permanent Redirect на её канонический адрес (/info//). - 3. Если статья не найдена — отдаёт честный HTTP 404 status (с отображением страницы 404, - где для авторизованного администратора request.user.is_staff выводится блок-подсказка). + 2. Если статья принадлежит к каноническому типу с отдельным роутом (не HUB и не OTHER), + выполняет HTTP 301 Permanent Redirect на её канонический адрес (article.get_absolute_url()). + 3. Валидирует j_article_metadata и наличие ключа HUB. Если статья — хаб, но метаданные + невалидны или ключ HUB отсутствует — отдаёт HTTP 404 status с подсказкой для администратора. 4. Увеличивает счётчик просмотров статьи, формирует хлебные крошки и рендерит шаблон хаба. Аргументы: @@ -225,45 +225,41 @@ def hub_detail(request: HttpRequest, slug: str) -> HttpResponse: ).first() # Если статья не найдена — отдаём честный статус 404 - if not article: - amin_hint = (f"Статья или хаб со слагом «{ slug }» отсутствует в базе данных." - f" Создайте статью со слагом «{ slug }» (тип HUB)" - " в админис­тративной панели.") - return render(request, "404.html", {"amin_hint": amin_hint}, status=404) - # Если нашлась статья с таким слагом и она не HUB, то делаем канонический 301-редирект на собственную ветку роутинга - # Предполагается, что у всех статей с типом ArticleType.INFO, ArticleType.TXT и т.д. есть роутинг в urls.py - elif article and article.l_article_type != TbArticle.ArticleType.HUB: + if article is None: + admin_hint = (f"Статья или хаб со слагом «{ slug }» отсутствует в базе данных." + f" Создайте статью со слагом «{ slug }» (тип HUB)" + " в админис­тративной панели.") + return render(request, "404.html", {"admin_hint": admin_hint}, status=404) + + # Если нашлась статья с таким слагом и у неё каноническая ветка роутинга — 301-редирект + if article.l_article_type not in (TbArticle.ArticleType.HUB, TbArticle.ArticleType.OTHER): return redirect(article.get_absolute_url(), permanent=True) - # Проверяем, что в мета-данных j_article_metadata JSON-объект - try: - metadata = json.loads(article.j_article_metadata) - except json.JSONDecodeError: - amin_hint = (f"Статья со слагом «{ slug }» (id={ article.id }) объявлена" - f" как хаб, но в её мета-данных j_article_metadata не валидный JSON." - " Исправьте данные в административной панели.") - return render(request, "404.html", {"amin_hint": amin_hint}, status=404) + # Проверяем и валидируем метаданные j_article_metadata + metadata, error_msg = parse_article_metadata(article) + if error_msg or not metadata: + admin_hint = (f"Статья со слагом «{ slug }» (id={ article.id }) объявлена" + f" как хаб, но у неё { error_msg }. Добавьте описание хаба (ключ" + f" { KEY_ARTICLE_HUB }) в метаданные через адми­нистра­тивную панель.") + return render(request, "404.html", {"admin_hint": admin_hint}, status=404) - # Проверяем, что в мета-данных есть ключ HUB. + # Проверяем, что в метаданных есть ключ HUB if KEY_ARTICLE_HUB not in metadata: - amin_hint = (f"Статья со слагом «{ slug }» (id={ article.id }) объявлена" - f" как хаб, но в её мета-данных нет ключа { KEY_ARTICLE_HUB }" - " с описанием хаба. Внесите изменения в административной панели.") - return render(request, "404.html", {"amin_hint": amin_hint}, status=404) + admin_hint = (f"Статья со слагом «{ slug }» (id={ article.id }) объявлена" + f" как хаб, но в её мета-данных нет ключа { KEY_ARTICLE_HUB }" + " с описанием хаба. Внесите изменения в адми­нистра­тивную панели.") + return render(request, "404.html", {"admin_hint": admin_hint}, status=404) - # Это статья-ХАБ!! ТУТ будет сложный код, над которым я думаю. + # Формируем контекст с данными хаба и хлебными крошками + context = get_hub_context(article, metadata) + article_title = article.s_article_title_html or article.s_article_title + context["breadcrumbs"] = [ + BreadcrumbItem(title=article_title), + ] - context = {} - pass + # Безопасно увеличиваем счетчик просмотров + article.increment_views() - # Формирование хлебных крошек - # article_title = article.s_article_title_html or article.s_article_title - # breadcrumbs = [ - # BreadcrumbItem(title=article_title), - # ] - # - # context = { - # "article": article, - # "breadcrumbs": breadcrumbs, - # } return render(request, "content/hub.html", context) \ No newline at end of file diff --git a/lpon_site/templates/404.html b/lpon_site/templates/404.html index f65ae36..f42e017 100644 --- a/lpon_site/templates/404.html +++ b/lpon_site/templates/404.html @@ -50,10 +50,10 @@

Ошибка 404

Страница не найдена…

Запрошенная страница не существует или была перемещена.

- {% if user.is_staff and amin_hint %} + {% if user.is_staff and admin_hint %}
Администратор!
-

{{ amin_hint|safe }}

+

{{ admin_hint|safe }}

{% endif %} ← На главную страницу diff --git a/lpon_site/templates/content/article_detail.html b/lpon_site/templates/content/article_detail.html index 7500590..9aae8d4 100644 --- a/lpon_site/templates/content/article_detail.html +++ b/lpon_site/templates/content/article_detail.html @@ -33,6 +33,13 @@ {{ article.s_article_content_html|safe }} {% endif %} + {# Блоки HUB DSL, если статья содержит расширенную структуру #} + {% if hub_data %} +
+

Дополнительные блоки (HUB DSL):

+
{{ hub_data }}
+
+ {% endif %}
← Назад к списку материалов