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 %} + + + + + + + + + + + ¤{{ item.s_hash_id }} + + + + + + + + ♥ {{ item.i_likes_count }} + + + + + 👁 {{ item.i_views_count }} + + + 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 %} diff --git a/hypn0/templates/index.html b/hypn0/templates/index.html index 268e18d..8b2ff76 100644 --- a/hypn0/templates/index.html +++ b/hypn0/templates/index.html @@ -500,9 +500,9 @@ Смотреть весь архив → - + - + @@ -513,22 +513,19 @@ Свежие галлюцинации из инкубатора • Первичная оценка сообщества - Свежее • до 100 лайков + + Смотреть весь поток → + - - - - - - - - - - ♥ - + + {% for item in fresh_items %} + {% include "block/gallery_card.html" %} + {% empty %} + + Инкубатор бессознательного пуст. Сгенерируйте и опубликуйте первую картину выше! - + {% endfor %}
Свежие галлюцинации из инкубатора • Первичная оценка сообщества