mod: блоги (1) список связанных статей к карточке SVG-генерации

This commit is contained in:
2026-09-08 00:28:26 +03:00
parent 46a2e57b0c
commit 4a4d009b04
4 changed files with 133 additions and 3 deletions
+5 -2
View File
@@ -368,6 +368,8 @@ class BlogPostAdminForm(CodeMirrorFormMixin):
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('slug', language='text', self.setup_codemirror_field('slug', language='text',
css_class='codemirror-width-l codemirror-no-lines') css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('i_order', language='text',
css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('s_teaser', language='html', self.setup_codemirror_field('s_teaser', language='html',
css_class='codemirror-width-xl codemirror-min-height-5') css_class='codemirror-width-xl codemirror-min-height-5')
self.setup_codemirror_field('s_content', language='html', self.setup_codemirror_field('s_content', language='html',
@@ -419,8 +421,9 @@ class TbBlogPostAdmin(RequestInFormMixin, admin.ModelAdmin):
fieldsets = ( fieldsets = (
("Атрибуты публикации и связи", { ("Атрибуты публикации и связи", {
"fields": ( "fields": (
("is_published", "slug", "d_published_at"), ("slug",),
("k_item", "k_parent_post", "i_order"), ("d_published_at", "is_published", ),
("k_item", "k_parent_post", "i_order",),
), ),
}), }),
("Основные поля", { ("Основные поля", {
+9
View File
@@ -211,6 +211,11 @@ class TbHypn0Item(models.Model):
dark_bg = f"rgb({max(9, int(9 + r * 0.07))}, {max(9, int(9 + g * 0.07))}, {max(13, int(13 + b * 0.08))})" dark_bg = f"rgb({max(9, int(9 + r * 0.07))}, {max(9, int(9 + g * 0.07))}, {max(13, int(13 + b * 0.08))})"
return f"--card-bg-light: {light_bg}; --card-bg-dark: {dark_bg};" return f"--card-bg-light: {light_bg}; --card-bg-dark: {dark_bg};"
@property
def published_blog_posts(self):
"""Возвращает опубликованные статьи блога, привязанные к данной картине."""
return self.blog_posts.filter(is_published=True).order_by("-d_published_at")
@property @property
def card_svg(self) -> str: def card_svg(self) -> str:
""" """
@@ -558,3 +563,7 @@ class TbBlogPost(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return self.s_title return self.s_title
def get_absolute_url(self) -> str:
"""Возвращает канонический URL статьи блога."""
return f"/blog/{self.slug}" if self.slug else f"/blog/{self.pk}"
+43
View File
@@ -1274,3 +1274,46 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
self.assertTrue(admin_obj.has_svg(post_with_svg)) self.assertTrue(admin_obj.has_svg(post_with_svg))
self.assertFalse(admin_obj.has_svg(post_without_svg)) self.assertFalse(admin_obj.has_svg(post_without_svg))
def test_gallery_detail_renders_blog_teaser(self):
# До создания постов блок блога не отображается
initial_response = self.client.get(f"/gallery/{self.item.s_hash_id}")
self.assertEqual(initial_response.status_code, 200)
self.assertNotContains(initial_response, "Материалы хроник гипноза")
# Опубликованный пост с тизером
published_post = TbBlogPost.objects.create(
s_title="Хроники гипно-транса: Урок 1",
slug="khroniki-hypno-transa-urok-1",
s_teaser="Секретные формулы векторного воздействия на сознание.",
s_content="Полный текст урока...",
k_item=self.item,
is_published=True,
)
# Неопубликованный черновик — не должен отображаться
unpublished_post = TbBlogPost.objects.create(
s_title="Секретный черновик",
slug="sekretniy-chernovik",
s_teaser="Черновик не должен быть виден.",
s_content="Скрытый контент...",
k_item=self.item,
is_published=False,
)
response = self.client.get(f"/gallery/{self.item.s_hash_id}")
self.assertEqual(response.status_code, 200)
# Проверяем, что отображается один из динамических заголовков секции
content = response.content.decode("utf-8")
possible_headings = (
"Сигналы из параллельного континуума",
"Психо-артефакты ноосферы",
"Заметки астрального консилиума",
"Рефлекторные отклики мозговых слизней",
"Публикации из глубин подсознания",
"Материалы хроник гипноза",
)
self.assertTrue(any(heading in content for heading in possible_headings))
self.assertContains(response, "Хроники гипно-транса: Урок 1")
self.assertContains(response, "Секретные формулы векторного воздействия")
self.assertContains(response, "/blog/khroniki-hypno-transa-urok-1")
self.assertNotContains(response, "Секретный черновик")
+76 -1
View File
@@ -170,12 +170,87 @@
</div> </div>
</div> </div>
<!-- МАТЕРИАЛЫ ИЗ ХРОНИК ГИПНОЗА (БЛОГ) -->
{% if item.published_blog_posts %}
<div class="space-y-6">
<div class="flex items-center gap-3">
<div class="w-3 h-3 rounded-full bg-purple-500 animate-pulse"></div>
<h2 class="text-lg font-bold text-stone-900 dark:text-stone-100 uppercase tracking-wider">
{% if item.id|divisibleby:6 %}Сигналы из параллельного континуума
{% elif item.id|divisibleby:5 %}Психо-артефакты ноосферы
{% elif item.id|divisibleby:4 %}Заметки астрального консилиума
{% elif item.id|divisibleby:3 %}Рефлекторные отклики мозговых слизней
{% elif item.id|divisibleby:2 %}Публикации из глубин подсознания
{% else %}Материалы хроник гипноза{% endif %}
</h2>
</div>
<div class="space-y-4">
{% for blog_post in item.published_blog_posts %}
<div class="bg-gradient-to-br from-purple-500/5 via-white/80 to-emerald-500/5 dark:from-purple-950/20 dark:via-zinc-900/80 dark:to-emerald-950/20 rounded-3xl border border-purple-200/80 dark:border-purple-900/40 p-5 sm:p-7 shadow-lg backdrop-blur-md transition-all hover:border-purple-400/60 dark:hover:border-purple-700/60">
<div class="flex flex-col md:flex-row items-start gap-5 sm:gap-6">
{% if blog_post.f_cover_img %}
<a href="{{ blog_post.get_absolute_url }}" class="w-full md:w-56 h-40 shrink-0 rounded-2xl overflow-hidden border border-stone-200/80 dark:border-zinc-700/60 group block shadow-inner">
<img src="{{ blog_post.f_cover_img.url }}"
alt="{{ blog_post.s_title|striptags }}"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
</a>
{% endif %}
<div class="flex-1 min-w-0 space-y-3">
<div class="flex flex-wrap items-center gap-2">
{% if blog_post.k_parent_post %}
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-bold uppercase tracking-wider bg-purple-500/10 text-purple-700 dark:text-purple-300 border border-purple-500/20">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
Серия: {{ blog_post.k_parent_post.s_title|safe }}
</span>
{% endif %}
<time datetime="{{ blog_post.d_published_at|date:"c" }}" class="text-[11px] text-stone-400 dark:text-stone-500 font-mono {% if blog_post.k_parent_post %}ml-auto{% endif %}">
{{ blog_post.d_published_at|date:"d E Y" }}
</time>
</div>
<h3 class="text-lg sm:text-xl font-bold text-stone-900 dark:text-stone-100 tracking-tight">
<a href="{{ blog_post.get_absolute_url }}" class="hover:text-purple-600 dark:hover:text-purple-400 transition-colors border-0">
{{ blog_post.s_title|safe }}
</a>
</h3>
{% if blog_post.s_teaser %}
<div class="text-xs sm:text-sm text-stone-600 dark:text-stone-300 leading-relaxed line-clamp-3">
{{ blog_post.s_teaser|safe }}
</div>
{% endif %}
<div class="pt-1">
<a href="{{ blog_post.get_absolute_url }}"
class="group inline-flex items-center gap-1.5 text-xs font-bold transition-colors">
{% if blog_post.id|divisibleby:5 %}и&nbsp;так далее, в&nbsp;глубинах подсознания
{% elif blog_post.id|divisibleby:4 %}подробнее, в&nbsp;блоге Гипножабы
{% elif blog_post.id|divisibleby:3 %}подробнее, в&nbsp;записках Мозгового Слизня
{% elif blog_post.id|divisibleby:2 %}занятный бред, продолжу чтение
{% else %}безвольно продолжить чтение{% endif %}
<svg class="w-3.5 h-3.5 shrink-0 transition-transform duration-200 group-hover:translate-x-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- БОЛЬШОЙ КУЛЬ НАУКООБРАЗНЫХ ПАРАМЕТРОВ И ТЕЛЕМЕТРИИ ТРАНСА --> <!-- БОЛЬШОЙ КУЛЬ НАУКООБРАЗНЫХ ПАРАМЕТРОВ И ТЕЛЕМЕТРИИ ТРАНСА -->
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div> <div class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div>
<h2 class="text-lg font-bold text-stone-900 dark:text-stone-100 uppercase tracking-wider"> <h2 class="text-lg font-bold text-stone-900 dark:text-stone-100 uppercase tracking-wider">
Психо-телеметрия и структура волновой матрицы Метрики и структура волновой матрицы
</h2> </h2>
</div> </div>