From 8271f881e4739c5006bf9f58ae7bef9069069c49 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 1 Sep 2026 22:43:55 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D0=B3=D0=B0=D0=BB=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=8F=20(3)=20=D0=BF=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20=D1=8D?= =?UTF-8?q?=D1=82=D0=B0=D0=B6=20(=D0=BF=D0=BB=D0=B5=D1=81=D0=BA=20=D0=B1?= =?UTF-8?q?=D0=B5=D1=81=D1=81=D0=BE=D0=B7=D0=BD=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hypn0/hypn0_site/tests.py | 129 ++++++++++++++++++++++ hypn0/hypn0_site/urls.py | 1 + hypn0/hypn0_site/views.py | 57 +++++++++- hypn0/templates/block/gallery_card.html | 33 ++++++ hypn0/templates/block/publish_status.html | 5 + hypn0/templates/index.html | 27 ++--- 6 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 hypn0/templates/block/gallery_card.html diff --git a/hypn0/hypn0_site/tests.py b/hypn0/hypn0_site/tests.py index 9b2709b..18caf4b 100644 --- a/hypn0/hypn0_site/tests.py +++ b/hypn0/hypn0_site/tests.py @@ -568,3 +568,132 @@ class UnconsciousMatrixTests(TestCase): self.assertContains(response, f"#{item.s_hash_id}") self.assertContains(response, "btn-nav-prev") self.assertContains(response, "btn-nav-next") + + +class GalleryFreshFloorTests(TestCase): + """Тестирование 1-го этажа («Плеск бессознательного»), выборки и пагинации.""" + + def setUp(self): + self.client = Client() + self.vid = "123e4567-e89b-12d3-a456-426614174000" + + def test_get_floor_fresh_filtering_and_ordering(self): + from hypn0_site.views import get_floor_fresh + + # 1. Создаем картины разных уровней и с разным числом просмотров + svg_bytes = b'' + + # Свежий кандидат с 5 просмотрами + item_cand = TbHypn0Item( + s_title="Кандидат", + file_svg=ContentFile(svg_bytes, name="c.svg"), + i_views_count=5, + i_level=TbHypn0Item.Level.CANDIDATE, + is_public=True, + ) + item_cand.save(visitor_uuid_or_fp=self.vid) + + # Level 1 с 1 просмотром (должен быть первым, т.к. просмотров меньше) + item_lvl1 = TbHypn0Item( + s_title="Level 1", + file_svg=ContentFile(svg_bytes, name="l1.svg"), + i_views_count=1, + i_level=TbHypn0Item.Level.LEVEL_1, + is_public=True, + ) + item_lvl1.save(visitor_uuid_or_fp=self.vid) + + # Level 2 (2-й этаж, не должен попасть в 1-й) + item_lvl2 = TbHypn0Item( + s_title="Level 2 Curated", + file_svg=ContentFile(svg_bytes, name="l2.svg"), + i_views_count=0, + i_level=TbHypn0Item.Level.LEVEL_2, + is_public=True, + ) + item_lvl2.save(visitor_uuid_or_fp=self.vid) + + # Непубличная картина (не должна попасть) + item_private = TbHypn0Item( + s_title="Private", + file_svg=ContentFile(svg_bytes, name="p.svg"), + i_views_count=0, + i_level=TbHypn0Item.Level.CANDIDATE, + is_public=False, + ) + item_private.save(visitor_uuid_or_fp=self.vid) + + results = list(get_floor_fresh(limit=10)) + self.assertEqual(len(results), 2) + # Сначала с наименьшим i_views_count + self.assertEqual(results[0].pk, item_lvl1.pk) + self.assertEqual(results[1].pk, item_cand.pk) + + def test_index_view_renders_fresh_stream(self): + # Создаем 2 картины для 1 этажа + svg_bytes = b'' + item = TbHypn0Item( + s_title="Свежий шедевр", + file_svg=ContentFile(svg_bytes, name="f1.svg"), + i_views_count=2, + i_level=TbHypn0Item.Level.CANDIDATE, + is_public=True, + ) + item.save(visitor_uuid_or_fp=self.vid) + + response = self.client.get(reverse("hypn0_site:index")) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Плеск бессознательного") + self.assertContains(response, "FRESH STREAM") + self.assertContains(response, f"gallery-card-{item.s_hash_id}") + self.assertContains(response, f"¤{item.s_hash_id}") + + def test_publish_includes_oob_swap_for_fresh_grid(self): + self.client.cookies["hypn0_vid"] = self.vid + sample_svg = '' + + response = self.client.post( + reverse("hypn0_site:publish"), + data={ + "svg_content": sample_svg, + "shape": "circle", + "cols": "35", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'hx-swap-oob="afterbegin:#fresh-stream-grid"') + item = TbHypn0Item.objects.first() + self.assertContains(response, f"gallery-card-{item.s_hash_id}") + + def test_gallery_floor_fresh_pagination(self): + # Создаем 10 картин + svg_bytes = b'' + for i in range(10): + item = TbHypn0Item( + s_title=f"Кандидат #{i}", + file_svg=ContentFile(svg_bytes, name=f"cand_{i}.svg"), + i_views_count=i, + i_level=TbHypn0Item.Level.CANDIDATE, + is_public=True, + ) + item.save(visitor_uuid_or_fp=self.vid) + + # Страница 1 (должно быть 8 штук) + url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "fresh"}) + response_p1 = self.client.get(url) + self.assertEqual(response_p1.status_code, 200) + self.assertContains(response_p1, "Плеск бессознательного") + self.assertContains(response_p1, "FRESH STREAM") + self.assertEqual(len(response_p1.context["page_obj"]), 8) + self.assertContains(response_p1, "Фаза 1 из 2") + + # Страница 2 (должно быть 2 штуки) + response_p2 = self.client.get(url, data={"page": 2}) + self.assertEqual(response_p2.status_code, 200) + self.assertEqual(len(response_p2.context["page_obj"]), 2) + self.assertContains(response_p2, "Фаза 2 из 2") + + def test_gallery_floor_unknown_404(self): + url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "non_existent"}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/hypn0/hypn0_site/urls.py b/hypn0/hypn0_site/urls.py index df4e148..7c68151 100644 --- a/hypn0/hypn0_site/urls.py +++ b/hypn0/hypn0_site/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ path("publish", views.publish, name="publish"), path("gallery/random", views.gallery_random, name="gallery_random"), path("gallery/random-pool", views.gallery_random, name="gallery_random_pool"), + path("gallery/floor/", views.gallery_floor, name="gallery_floor"), path("gallery/", views.gallery_detail, name="gallery_detail"), path("gallery//download", views.gallery_download, name="gallery_download"), path("gallery//vote", views.gallery_vote, name="gallery_vote"), diff --git a/hypn0/hypn0_site/views.py b/hypn0/hypn0_site/views.py index 3ce9972..777833e 100644 --- a/hypn0/hypn0_site/views.py +++ b/hypn0/hypn0_site/views.py @@ -3,6 +3,7 @@ import random from django.conf import settings from django.core.files.base import ContentFile +from django.core.paginator import Paginator from django.http import Http404, HttpRequest, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.csrf import ensure_csrf_cookie @@ -19,9 +20,63 @@ from .services.halftone import ( from .services.naming import generate_hypno_title +def get_floor_fresh(limit: int | None = 6, offset: int = 0): + """ + 1-й ЭТАЖ: «Плеск бессознательного» (Инкубатор открытий). + Выборка: свежие кандидаты и первичный поток (Level.CANDIDATE, Level.LEVEL_1). + Сортировка: по наименьшему числу просмотров i_views_count (чтобы дать шанс всем) и свежести -d_created_at. + """ + qs = TbHypn0Item.objects.filter( + i_level__in=[TbHypn0Item.Level.CANDIDATE, TbHypn0Item.Level.LEVEL_1], + is_public=True, + ).order_by("i_views_count", "-d_created_at") + + if limit is not None: + return qs[offset : offset + limit] + return qs + + +def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse: + """ + Страница полного просмотра конкретного этажа галереи с пагинацией (по 8 карточек). + """ + floors_config = { + "fresh": { + "title": "Плеск бессознательного", + "badge": "FRESH STREAM", + "badge_color": "amber", + "subtitle": "Свежие галлюцинации из инкубатора • Первичная оценка сообщества", + "getter": get_floor_fresh, + }, + } + + if floor_slug not in floors_config: + raise Http404("Этаж транса не обнаружен в матрице") + + cfg = floors_config[floor_slug] + items_qs = cfg["getter"](limit=None) + + paginator = Paginator(items_qs, 8) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) + + context = { + "floor_slug": floor_slug, + "floor_title": cfg["title"], + "floor_badge": cfg["badge"], + "floor_badge_color": cfg["badge_color"], + "floor_subtitle": cfg["subtitle"], + "page_obj": page_obj, + } + return render(request, "gallery/floor.html", context) + + @ensure_csrf_cookie def index(request: HttpRequest | None) -> HttpResponse: - return render(request, "index.html", {}) + fresh_items = get_floor_fresh(limit=6) + return render(request, "index.html", { + "fresh_items": fresh_items, + }) def tmp(request: HttpRequest | None) -> HttpResponse: diff --git a/hypn0/templates/block/gallery_card.html b/hypn0/templates/block/gallery_card.html new file mode 100644 index 0000000..f732e26 --- /dev/null +++ b/hypn0/templates/block/gallery_card.html @@ -0,0 +1,33 @@ +{% load static %} + diff --git a/hypn0/templates/block/publish_status.html b/hypn0/templates/block/publish_status.html index d60fe20..8fb53cc 100644 --- a/hypn0/templates/block/publish_status.html +++ b/hypn0/templates/block/publish_status.html @@ -51,6 +51,11 @@ #{{ item.s_hash_id }} + +
+ {% include "block/gallery_card.html" %} +
+ {% elif error %} - +
-
+
@@ -513,22 +513,19 @@

Свежие галлюцинации из инкубатора • Первичная оценка сообщества

- Свежее • до 100 лайков + + Смотреть весь поток → +
-
- + {% endfor %}