add: галерея (3) первый этаж (плеск бессознательного)

This commit is contained in:
2026-09-01 22:43:55 +03:00
parent e26b6f6fac
commit 8271f881e4
6 changed files with 236 additions and 16 deletions
+129
View File
@@ -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'<svg><circle/></svg>'
# Свежий кандидат с 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'<svg><circle/></svg>'
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 = '<svg xmlns="http://www.w3.org/2000/svg"><g><circle/></g></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'<svg><circle/></svg>'
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)
+1
View File
@@ -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/<slug:floor_slug>", views.gallery_floor, name="gallery_floor"),
path("gallery/<str:hash_id>", views.gallery_detail, name="gallery_detail"),
path("gallery/<str:hash_id>/download", views.gallery_download, name="gallery_download"),
path("gallery/<str:hash_id>/vote", views.gallery_vote, name="gallery_vote"),
+56 -1
View File
@@ -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: