add: роутинг, вьюха и шаблон для отображения статьи... +слой стилей TileWind для контента их базы.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
from frontend.models import TbArticle
|
from frontend.models import TbArticle
|
||||||
@@ -110,7 +110,7 @@ def catalog(request: HttpRequest | None) -> HttpResponse:
|
|||||||
return render(request, 'catalog.html', {"breadcrumbs": breadcrumbs})
|
return render(request, 'catalog.html', {"breadcrumbs": breadcrumbs})
|
||||||
|
|
||||||
|
|
||||||
def txt_articles_list(request: HttpRequest | None) -> HttpResponse:
|
def info_articles_list(request: HttpRequest | None) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
Представление (View) для отображения списка текстовых статей (тип ArticleType.TXT).
|
Представление (View) для отображения списка текстовых статей (тип ArticleType.TXT).
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ def txt_articles_list(request: HttpRequest | None) -> HttpResponse:
|
|||||||
3. Оптимизирует выборку связанного файла обложки (k_article_to_image) через select_related,
|
3. Оптимизирует выборку связанного файла обложки (k_article_to_image) через select_related,
|
||||||
чтобы избежать N+1 запросов при рендеринге изображений в списке.
|
чтобы избежать N+1 запросов при рендеринге изображений в списке.
|
||||||
4. Формирует хлебные крошки (BreadcrumbItem) с элементом "Инструкции".
|
4. Формирует хлебные крошки (BreadcrumbItem) с элементом "Инструкции".
|
||||||
5. Рендерит шаблон `txt_list.html`, передавая в контекст список статей и хлебные крошки.
|
5. Рендерит шаблон `info_list.html`, передавая в контекст список статей и хлебные крошки.
|
||||||
|
|
||||||
Аргументы:
|
Аргументы:
|
||||||
request (HttpRequest | None): Объект HTTP-запроса Django.
|
request (HttpRequest | None): Объект HTTP-запроса Django.
|
||||||
@@ -133,7 +133,7 @@ def txt_articles_list(request: HttpRequest | None) -> HttpResponse:
|
|||||||
# Выбираем опубликованные текстовые статьи из базы данных с правильной сортировкой
|
# Выбираем опубликованные текстовые статьи из базы данных с правильной сортировкой
|
||||||
articles = (
|
articles = (
|
||||||
TbArticle.objects.filter(
|
TbArticle.objects.filter(
|
||||||
l_article_type=TbArticle.ArticleType.TXT,
|
l_article_type=TbArticle.ArticleType.INFO,
|
||||||
b_article_published=True,
|
b_article_published=True,
|
||||||
)
|
)
|
||||||
.select_related('k_article_to_image')
|
.select_related('k_article_to_image')
|
||||||
@@ -150,4 +150,51 @@ def txt_articles_list(request: HttpRequest | None) -> HttpResponse:
|
|||||||
"articles": articles,
|
"articles": articles,
|
||||||
"breadcrumbs": breadcrumbs,
|
"breadcrumbs": breadcrumbs,
|
||||||
}
|
}
|
||||||
return render(request, "roll/txt_list.html", context)
|
return render(request, "roll/info_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def info_article_detail(request: HttpRequest | None, slug: str) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Представление (View) для детального отображения отдельной текстовой статьи (тип ArticleType.INFO).
|
||||||
|
|
||||||
|
Функциональность:
|
||||||
|
-----------------
|
||||||
|
1. Ищет опубликованную статью (b_article_published=True) по её слагу (slug).
|
||||||
|
Если статья не найдена или не опубликована — возвращает HTTP 404 (Page Not Found).
|
||||||
|
2. Вызывает метод `increment_views()` у объекта статьи для атомарного увеличения
|
||||||
|
счётчика просмотров `i_article_views`.
|
||||||
|
3. Формирует цепочку хлебных крошек (BreadcrumbItem):
|
||||||
|
- "Инструкции" (со ссылкой на /info/);
|
||||||
|
- Заголовок текущей статьи (без ссылки, текущий пункт).
|
||||||
|
4. Рендерит шаблон `content/article_detail.html`, передавая объект статьи и крошки.
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
request (HttpRequest | None): Объект HTTP-запроса Django.
|
||||||
|
slug (str): Уникальный URL-слаг статьи.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
HttpResponse: Сформированная HTML-страница детального просмотра статьи.
|
||||||
|
"""
|
||||||
|
# Получаем опубликованную статью по слагу или отдаем 404 Not Found
|
||||||
|
article = get_object_or_404(
|
||||||
|
TbArticle,
|
||||||
|
slug=slug,
|
||||||
|
b_article_published=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Безопасно инкрементируем счетчик просмотров статьи
|
||||||
|
article.increment_views()
|
||||||
|
|
||||||
|
# Формируем цепочку хлебных крошек (пункт "Главная" добавляется автоматически в шаблоне)
|
||||||
|
article_title = article.s_article_title_html or article.s_article_title
|
||||||
|
breadcrumbs = [
|
||||||
|
BreadcrumbItem(title="Инструкции", url="/info/"),
|
||||||
|
BreadcrumbItem(title=article_title),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Передаем объект статьи и крошки в шаблон детального просмотра
|
||||||
|
context = {
|
||||||
|
"article": article,
|
||||||
|
"breadcrumbs": breadcrumbs,
|
||||||
|
}
|
||||||
|
return render(request, "content/article_detail.html", context)
|
||||||
@@ -27,7 +27,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
re_path(r'^$', views.index),
|
re_path(r'^$', views.index),
|
||||||
re_path(r'^catalog/$', views.catalog),
|
re_path(r'^catalog/$', views.catalog),
|
||||||
re_path(r'^info/?$', views.txt_articles_list, name='txt_articles_list'),
|
re_path(r'^info/?$', views.info_articles_list, name='info_articles_list'),
|
||||||
|
re_path(r'^info/(?P<slug>[-a-zA-Z0-9_]+)/?$', views.info_article_detail, name='info_article_detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Сервировать медиа-файлы в разработке (в production использовать nginx/Apache)
|
# Сервировать медиа-файлы в разработке (в production использовать nginx/Apache)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{% extends "_base.html" %} {% load static %}
|
||||||
|
{# SEO Мета-данные статьи #}
|
||||||
|
{% block PAGE_TITLE %}{{ article.seo_title|default:article.s_article_title_html|striptags|default:article.s_article_title }} — LPON.RU{% endblock %}
|
||||||
|
|
||||||
|
{% block DESCRIPTION %}{{ article.seo_description|default:article.s_article_teaser_html|striptags|default:"" }}{% endblock %}
|
||||||
|
|
||||||
|
{% block CONTENT %}{# Подключение блока хлебных крошек #}{% include "block/breadcrumbs.html" %}{% comment %}
|
||||||
|
КОНТЕНТ НАЧАЛО
|
||||||
|
|
||||||
|
{% endcomment %} <article class="mx-auto grid grid-cols-3 gap-8 py-6">
|
||||||
|
{# ПРАВАЯ КОЛОНКА / Обложка / Иллюстрация статьи, если есть #}{% if article.k_article_to_image %}<div class="mb-8 max-h-[480px] overflow-hidden">
|
||||||
|
<img src="{{ article.k_article_to_image.url }}" alt="{{ article.k_article_to_image.default_alt_text|default:article.s_article_title }}" class="h-auto w-full rounded-xl border border-slate-200 object-cover dark:border-slate-800" />
|
||||||
|
</div>{% endif %}
|
||||||
|
{# ЛЕВАЯ КОЛОНКА / Основной текст статьи #}<div class="col-span-2">
|
||||||
|
<!-- Заголовл -->
|
||||||
|
<h1 class="mb-4 text-3xl leading-tight font-bold text-slate-900 dark:text-slate-100">{% if article.s_article_title_html %}{{ article.s_article_title_html|safe }}{% else %}{{ article.s_article_title }}{% endif %}</h1>
|
||||||
|
<!-- /Заголовок -->
|
||||||
|
<!-- Мета-данные -->
|
||||||
|
<div class="mb-6 items-center gap-4 border-b border-slate-200 pb-4 text-sm text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
||||||
|
<time datetime="{{ article.t_article_created|date:'Y-m-d' }}">{{ article.t_article_created|date:"d.m.Y" }}</time>
|
||||||
|
</div>
|
||||||
|
<!-- /Мета-данные -->
|
||||||
|
<!-- Тизер -->{% if article.s_article_teaser_html %}
|
||||||
|
<div class="mb-6 rounded-r-lg border-l-4 border-slate-400 bg-slate-50 p-4 text-lg leading-relaxed text-slate-700 italic md:text-xl dark:border-slate-600 dark:bg-slate-900/60 dark:text-slate-300">
|
||||||
|
{{ article.s_article_teaser_html|safe }}
|
||||||
|
</div>{% endif %}
|
||||||
|
<!-- /Тизер -->
|
||||||
|
<!-- Основное содержимое -->{% if article.s_article_content_html %}
|
||||||
|
<div class="article-content max-w-none">
|
||||||
|
{{ article.s_article_content_html|safe }}
|
||||||
|
</div>
|
||||||
|
<!-- /Основное содержимое -->{% endif %}
|
||||||
|
<!-- Кнопка возврата к списку -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<!-- /Кнопка возврата к списку -->
|
||||||
|
</div>
|
||||||
|
</article>{% endblock CONTENT %}
|
||||||
@@ -79,3 +79,71 @@
|
|||||||
hover:border-solid transition-colors;
|
hover:border-solid transition-colors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components { /* -- СЛОЙ COMPONENTS для компонентов и HTML-контента из БД -- {% comment %}
|
||||||
|
Стили для форматированного HTML-контента статей (из БД).
|
||||||
|
|
||||||
|
Поскольку HTML-текст статьи (s_article_content_html) приходит из CodeMirror-редактора
|
||||||
|
и с высокой вероятностью не содержит явных Tailwind-классов на каждом p, h2, ul, blockquote,
|
||||||
|
мы оборачиваем контент в шаблонный контейнер <div class="article-content">...</div>,
|
||||||
|
а здесь через @apply и каскадные селекторы задаем правила оформления элементов.
|
||||||
|
{% endcomment %} */
|
||||||
|
|
||||||
|
/* Контейнер форматированного текста статьи */
|
||||||
|
.article-content {
|
||||||
|
@apply text-slate-800 dark:text-slate-200 leading-relaxed text-base md:text-lg;
|
||||||
|
|
||||||
|
/* Заголовки внутри контента статьи */
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
@apply font-bold text-slate-900 dark:text-slate-100 mt-8 mb-4 leading-snug;
|
||||||
|
}
|
||||||
|
h1 { @apply text-3xl md:text-3xl border-b border-slate-200 dark:border-slate-800 pb-2; }
|
||||||
|
h2 { @apply text-xl md:text-2xl; }
|
||||||
|
h3 { @apply text-lg md:text-xl; }
|
||||||
|
h4 { @apply text-base md:text-lg text-slate-700 dark:text-slate-300; }
|
||||||
|
|
||||||
|
/* Параграфы и текстовые блоки */
|
||||||
|
p {
|
||||||
|
@apply my-4 leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Списки (маркированные и нумерованные) */
|
||||||
|
ul {
|
||||||
|
@apply list-disc list-outside space-y-2 pl-4;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
@apply list-decimal list-outside space-y-2 pl-4;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
@apply leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цитаты и выделенные блоки */
|
||||||
|
blockquote {
|
||||||
|
@apply border-l-4 border-indigo-500 bg-slate-100 dark:bg-slate-900/80
|
||||||
|
px-4 py-3 my-6 italic text-slate-700 dark:text-slate-300 rounded-r-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Изображения внутри контента */
|
||||||
|
img {
|
||||||
|
@apply my-6 rounded-xl border border-slate-200 dark:border-slate-800 h-auto max-w-full mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Инлайновый код и блоки кода */
|
||||||
|
code {
|
||||||
|
@apply bg-slate-200 dark:bg-slate-800 text-indigo-700 dark:text-indigo-300
|
||||||
|
px-1.5 py-0.5 rounded text-sm font-mono;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
@apply bg-slate-900 text-slate-100 p-4 rounded-xl my-6 overflow-x-auto text-sm font-mono;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
@apply bg-transparent text-inherit p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Горизонтальный разделитель */
|
||||||
|
hr {
|
||||||
|
@apply my-8 border-t border-slate-200 dark:border-slate-800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user