mod: проверка статей-хабов (2) и заглушки для хабов.
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
import pytils
|
import pytils
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
@@ -16,7 +17,7 @@ from django.utils.html import mark_safe
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from lpon_site.settings import (
|
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
|
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
|
||||||
|
|||||||
+39
-43
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.http import HttpRequest, HttpResponse, Http404
|
from django.http import HttpRequest, HttpResponse, Http404
|
||||||
from frontend.models import TbArticle
|
from frontend.models import TbArticle
|
||||||
|
from frontend.utils import get_hub_context, parse_article_metadata
|
||||||
from lpon_site.settings import *
|
from lpon_site.settings import *
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -190,11 +191,10 @@ def info_article_detail(request: HttpRequest | None, slug: str) -> HttpResponse:
|
|||||||
BreadcrumbItem(title=article_title),
|
BreadcrumbItem(title=article_title),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Передаем объект статьи и крошки в шаблон детального просмотра
|
# Получаем контекст с учетом возможных блоков HUB DSL
|
||||||
context = {
|
context = get_hub_context(article)
|
||||||
"article": article,
|
context["breadcrumbs"] = breadcrumbs
|
||||||
"breadcrumbs": breadcrumbs,
|
|
||||||
}
|
|
||||||
return render(request, "content/article_detail.html", context)
|
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).
|
1. Ищет опубликованную статью (b_article_published=True) по её слагу (slug).
|
||||||
2. Если статья принадлежит к каноническому типу с отдельным роутом (например, ArticleType.INFO),
|
2. Если статья принадлежит к каноническому типу с отдельным роутом (не HUB и не OTHER),
|
||||||
выполняет HTTP 301 Permanent Redirect на её канонический адрес (/info/<slug>/).
|
выполняет HTTP 301 Permanent Redirect на её канонический адрес (article.get_absolute_url()).
|
||||||
3. Если статья не найдена — отдаёт честный HTTP 404 status (с отображением страницы 404,
|
3. Валидирует j_article_metadata и наличие ключа HUB. Если статья — хаб, но метаданные
|
||||||
где для авторизованного администратора request.user.is_staff выводится блок-подсказка).
|
невалидны или ключ HUB отсутствует — отдаёт HTTP 404 status с подсказкой для администратора.
|
||||||
4. Увеличивает счётчик просмотров статьи, формирует хлебные крошки и рендерит шаблон хаба.
|
4. Увеличивает счётчик просмотров статьи, формирует хлебные крошки и рендерит шаблон хаба.
|
||||||
|
|
||||||
Аргументы:
|
Аргументы:
|
||||||
@@ -225,45 +225,41 @@ def hub_detail(request: HttpRequest, slug: str) -> HttpResponse:
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Если статья не найдена — отдаём честный статус 404
|
# Если статья не найдена — отдаём честный статус 404
|
||||||
if not article:
|
if article is None:
|
||||||
amin_hint = (f"Статья или хаб со слагом «<strong>{ slug }</strong>» отсутствует в базе данных."
|
admin_hint = (f"Статья или хаб со слагом «<strong>{ slug }</strong>» отсутствует в базе данных."
|
||||||
f" Создайте статью со слагом «<strong>{ slug }</strong>» (тип <code>HUB</code>)"
|
f" Создайте статью со слагом «<strong>{ slug }</strong>» (тип <code>HUB</code>)"
|
||||||
" в админис­тративной панели.")
|
" в админис­тративной панели.")
|
||||||
return render(request, "404.html", {"amin_hint": amin_hint}, status=404)
|
return render(request, "404.html", {"admin_hint": admin_hint}, status=404)
|
||||||
# Если нашлась статья с таким слагом и она не HUB, то делаем канонический 301-редирект на собственную ветку роутинга
|
|
||||||
# Предполагается, что у всех статей с типом ArticleType.INFO, ArticleType.TXT и т.д. есть роутинг в urls.py
|
# Если нашлась статья с таким слагом и у неё каноническая ветка роутинга — 301-редирект
|
||||||
elif article and article.l_article_type != TbArticle.ArticleType.HUB:
|
if article.l_article_type not in (TbArticle.ArticleType.HUB, TbArticle.ArticleType.OTHER):
|
||||||
return redirect(article.get_absolute_url(), permanent=True)
|
return redirect(article.get_absolute_url(), permanent=True)
|
||||||
|
|
||||||
# Проверяем, что в мета-данных j_article_metadata JSON-объект
|
# Проверяем и валидируем метаданные j_article_metadata
|
||||||
try:
|
metadata, error_msg = parse_article_metadata(article)
|
||||||
metadata = json.loads(article.j_article_metadata)
|
if error_msg or not metadata:
|
||||||
except json.JSONDecodeError:
|
admin_hint = (f"Статья со слагом «<strong>{ slug }</strong>» (<code>id={ article.id }</code>) объявлена"
|
||||||
amin_hint = (f"Статья со слагом «<strong>{ slug }</strong>» (<code>id={ article.id }</code>) объявлена"
|
f" как хаб, но у неё <u>{ error_msg }</u>. Добавьте описание хаба (ключ"
|
||||||
f" как хаб, но в её мета-данных j_article_metadata не валидный JSON."
|
f" <code>{ KEY_ARTICLE_HUB }</code>) в метаданные через <a target='_blank'"
|
||||||
" Исправьте данные в административной панели.")
|
f" href='/{ADMIN_URL}frontend/tbarticle/{ article.id }/change/'>адми­нистра­тивную панель</a>.")
|
||||||
return render(request, "404.html", {"amin_hint": amin_hint}, status=404)
|
return render(request, "404.html", {"admin_hint": admin_hint}, status=404)
|
||||||
|
|
||||||
# Проверяем, что в мета-данных есть ключ HUB.
|
# Проверяем, что в метаданных есть ключ HUB
|
||||||
if KEY_ARTICLE_HUB not in metadata:
|
if KEY_ARTICLE_HUB not in metadata:
|
||||||
amin_hint = (f"Статья со слагом «<strong>{ slug }</strong>» (<code>id={ article.id }</code>) объявлена"
|
admin_hint = (f"Статья со слагом «<strong>{ slug }</strong>» (<code>id={ article.id }</code>) объявлена"
|
||||||
f" как хаб, но в её мета-данных нет ключа <code>{ KEY_ARTICLE_HUB }</code>"
|
f" как хаб, но в её мета-данных нет ключа <code>{ KEY_ARTICLE_HUB }</code>"
|
||||||
" с описанием хаба. Внесите изменения в административной панели.")
|
" с описанием хаба. Внесите изменения в <a target='_blank'"
|
||||||
return render(request, "404.html", {"amin_hint": amin_hint}, status=404)
|
f" href='/{ADMIN_URL}frontend/tbarticle/{ article.id }/change/'>адми­нистра­тивную панели</a>.")
|
||||||
|
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)
|
return render(request, "content/hub.html", context)
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
<h1 class="text-5xl font-bold mb-4 drop-shadow-lg font-serif">Ошибка 404</h1>
|
<h1 class="text-5xl font-bold mb-4 drop-shadow-lg font-serif">Ошибка 404</h1>
|
||||||
<p class="text-2xl drop-shadow font-serif">Страница не найдена…</p>
|
<p class="text-2xl drop-shadow font-serif">Страница не найдена…</p>
|
||||||
<p class="my-10 text-slate-500 dark:text-slate-500">Запрошенная страница не существует или была перемещена.</p>
|
<p class="my-10 text-slate-500 dark:text-slate-500">Запрошенная страница не существует или была перемещена.</p>
|
||||||
{% if user.is_staff and amin_hint %}
|
{% if user.is_staff and admin_hint %}
|
||||||
<div class="mb-18 rounded-xl border border-amber-300 bg-amber-50 p-6 text-left text-amber-900 dark:border-amber-700 dark:bg-amber-950/80 dark:text-amber-200">
|
<div class="mb-18 rounded-xl border border-amber-300 bg-amber-50 p-6 text-left text-amber-900 dark:border-amber-700 dark:bg-amber-950/80 dark:text-amber-200">
|
||||||
<div class="font-bold text-amber-800 dark:text-amber-300">Администратор!</div>
|
<div class="font-bold text-amber-800 dark:text-amber-300">Администратор!</div>
|
||||||
<p class="mt-2 leading-relaxed">{{ amin_hint|safe }}</p>
|
<p class="mt-2 leading-relaxed">{{ admin_hint|safe }}</p>
|
||||||
</div>{% endif %}
|
</div>{% endif %}
|
||||||
<a href="/" class="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold drop-shadow-lg">
|
<a href="/" class="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold drop-shadow-lg">
|
||||||
← На главную страницу
|
← На главную страницу
|
||||||
|
|||||||
@@ -33,6 +33,13 @@
|
|||||||
{{ article.s_article_content_html|safe }}
|
{{ article.s_article_content_html|safe }}
|
||||||
</div>
|
</div>
|
||||||
<!-- /Основное содержимое -->{% endif %}
|
<!-- /Основное содержимое -->{% endif %}
|
||||||
|
{# Блоки HUB DSL, если статья содержит расширенную структуру #}
|
||||||
|
{% if hub_data %}
|
||||||
|
<div class="my-8 rounded-xl border border-dashed border-slate-300 p-6 dark:border-slate-700">
|
||||||
|
<h2 class="mb-2 text-lg font-semibold text-slate-800 dark:text-slate-200">Дополнительные блоки (HUB DSL):</h2>
|
||||||
|
<pre class="overflow-x-auto text-xs text-slate-600 dark:text-slate-400">{{ hub_data }}</pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<!-- Кнопка возврата к списку -->
|
<!-- Кнопка возврата к списку -->
|
||||||
<div class="mt-10 border-t border-slate-200 pt-6 dark:border-slate-800">
|
<div class="mt-10 border-t border-slate-200 pt-6 dark:border-slate-800">
|
||||||
<a href="{% url 'info_articles_list' %}" class="inline-flex items-center gap-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100">← Назад к списку материалов</a>
|
<a href="{% url 'info_articles_list' %}" class="inline-flex items-center gap-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100">← Назад к списку материалов</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user