diff --git a/hypn0/hypn0_site/tests.py b/hypn0/hypn0_site/tests.py index 560be47..030d00a 100644 --- a/hypn0/hypn0_site/tests.py +++ b/hypn0/hypn0_site/tests.py @@ -923,3 +923,138 @@ class CardBgStyleTests(BaseMediaTestCase): self.assertContains(response, 'template shadowrootmode="open"') self.assertContains(response, "hypn0-card-svg") self.assertContains(response, "--hypn0-play: running !important") + + +class GalleryArchiveTests(BaseMediaTestCase): + """Тестирование общего архива галереи /gallery (фильтры, сортировка, пагинация).""" + + def setUp(self): + super().setUp() + self.vid = "123e4567-e89b-12d3-a456-426614174000" + svg_bytes = b'' + + # Создаем картины разных уровней + self.item_fresh = TbHypn0Item( + s_title="Свежая картина", + file_svg=ContentFile(svg_bytes, name="fresh.svg"), + i_level=TbHypn0Item.Level.CANDIDATE, + i_likes_count=5, + i_views_count=10, + f_score=1.5, + is_public=True, + ) + self.item_fresh.save(visitor_uuid_or_fp=self.vid) + + self.item_curated = TbHypn0Item( + s_title="Кураторская картина", + file_svg=ContentFile(svg_bytes, name="curated.svg"), + i_level=TbHypn0Item.Level.LEVEL_2, + i_likes_count=50, + i_views_count=200, + f_score=80.0, + is_public=True, + ) + self.item_curated.save(visitor_uuid_or_fp=self.vid) + + self.item_top = TbHypn0Item( + s_title="Бессмертная картина", + file_svg=ContentFile(svg_bytes, name="top.svg"), + i_level=TbHypn0Item.Level.IMMORTAL, + i_likes_count=500, + i_views_count=2000, + f_score=150.0, + is_public=True, + ) + self.item_top.save(visitor_uuid_or_fp=self.vid) + + def test_gallery_archive_default_renders_all_and_gravity(self): + url = reverse("hypn0_site:gallery_archive") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Галерея транса") + self.assertContains(response, "ARCHIVE") + self.assertContains(response, "Все волны") + self.assertContains(response, "Инкубатор") + self.assertContains(response, "Одобрено") + self.assertContains(response, "Золотой фонд") + # Все 3 картины присутствуют в выдаче + self.assertContains(response, f"gallery-card-{self.item_fresh.s_hash_id}") + self.assertContains(response, f"gallery-card-{self.item_curated.s_hash_id}") + self.assertContains(response, f"gallery-card-{self.item_top.s_hash_id}") + + # Проверяем правильный порядок (по f_score DESC: top, curated, fresh) + page_items = list(response.context["page_obj"]) + self.assertEqual(page_items[0].pk, self.item_top.pk) + self.assertEqual(page_items[1].pk, self.item_curated.pk) + self.assertEqual(page_items[2].pk, self.item_fresh.pk) + + def test_gallery_archive_filter_by_floor(self): + url = reverse("hypn0_site:gallery_archive") + + # Фильтр: Инкубатор (fresh) + resp_fresh = self.client.get(url, data={"floor": "fresh"}) + self.assertEqual(resp_fresh.status_code, 200) + fresh_ids = [it.pk for it in resp_fresh.context["page_obj"]] + self.assertIn(self.item_fresh.pk, fresh_ids) + self.assertNotIn(self.item_curated.pk, fresh_ids) + self.assertNotIn(self.item_top.pk, fresh_ids) + + # Фильтр: Одобрено (curated) + resp_curated = self.client.get(url, data={"floor": "curated"}) + self.assertEqual(resp_curated.status_code, 200) + curated_ids = [it.pk for it in resp_curated.context["page_obj"]] + self.assertNotIn(self.item_fresh.pk, curated_ids) + self.assertIn(self.item_curated.pk, curated_ids) + self.assertNotIn(self.item_top.pk, curated_ids) + + # Фильтр: Золотой фонд (top) + resp_top = self.client.get(url, data={"floor": "top"}) + self.assertEqual(resp_top.status_code, 200) + top_ids = [it.pk for it in resp_top.context["page_obj"]] + self.assertNotIn(self.item_fresh.pk, top_ids) + self.assertNotIn(self.item_curated.pk, top_ids) + self.assertIn(self.item_top.pk, top_ids) + + def test_gallery_archive_sorting(self): + url = reverse("hypn0_site:gallery_archive") + + # Сортировка: по числу лайков + resp_likes = self.client.get(url, data={"sort": "likes"}) + self.assertEqual(resp_likes.status_code, 200) + likes_items = list(resp_likes.context["page_obj"]) + self.assertEqual(likes_items[0].pk, self.item_top.pk) + self.assertEqual(likes_items[-1].pk, self.item_fresh.pk) + + # Сортировка: по числу просмотров (наименее просмотренные первые) + resp_views = self.client.get(url, data={"sort": "views"}) + self.assertEqual(resp_views.status_code, 200) + views_items = list(resp_views.context["page_obj"]) + self.assertEqual(views_items[0].pk, self.item_fresh.pk) + self.assertEqual(views_items[-1].pk, self.item_top.pk) + + def test_gallery_archive_pagination_and_link_preservation(self): + svg_bytes = b'' + # Создаем еще 15 свежих картин (всего 18 в базе) + for i in range(15): + item = TbHypn0Item( + s_title=f"Доп #{i}", + file_svg=ContentFile(svg_bytes, name=f"extra_{i}.svg"), + i_level=TbHypn0Item.Level.CANDIDATE, + f_score=float(i), + is_public=True, + ) + item.save(visitor_uuid_or_fp=self.vid) + + url = reverse("hypn0_site:gallery_archive") + resp_p1 = self.client.get(url, data={"floor": "all", "sort": "likes"}) + self.assertEqual(resp_p1.status_code, 200) + self.assertEqual(len(resp_p1.context["page_obj"]), 16) + self.assertContains(resp_p1, "Фаза 1 из 2") + # Проверяем сохранение query-параметров в ссылках пагинатора + self.assertContains(resp_p1, "floor=all&sort=likes&page=2") + + resp_p2 = self.client.get(url, data={"floor": "all", "sort": "likes", "page": 2}) + self.assertEqual(resp_p2.status_code, 200) + self.assertEqual(len(resp_p2.context["page_obj"]), 2) + self.assertContains(resp_p2, "Фаза 2 из 2") + self.assertContains(resp_p2, "floor=all&sort=likes&page=1") diff --git a/hypn0/hypn0_site/urls.py b/hypn0/hypn0_site/urls.py index 7c68151..dd1ab26 100644 --- a/hypn0/hypn0_site/urls.py +++ b/hypn0/hypn0_site/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ path("", views.index, name="index"), path("generate", views.generate, name="generate"), path("publish", views.publish, name="publish"), + path("gallery", views.gallery_archive, name="gallery_archive"), 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"), diff --git a/hypn0/hypn0_site/views.py b/hypn0/hypn0_site/views.py index e3fd7b0..eeaecf6 100644 --- a/hypn0/hypn0_site/views.py +++ b/hypn0/hypn0_site/views.py @@ -68,6 +68,79 @@ def get_floor_top(limit: int | None = 8, offset: int = 0): return qs +def gallery_archive(request: HttpRequest) -> HttpResponse: + """ + Общий архив галереи транса с фильтрацией по этажам и сортировкой. + Пагинация по 16 карточек на страницу. + """ + floor = request.GET.get("floor", "all") + sort = request.GET.get("sort", "gravity") + + # Базовый QuerySet + qs = TbHypn0Item.objects.filter(is_public=True) + + # Фильтрация по этажам + if floor == "fresh": + qs = qs.filter(i_level__in=[TbHypn0Item.Level.CANDIDATE, TbHypn0Item.Level.LEVEL_1]) + elif floor == "curated": + qs = qs.filter(i_level=TbHypn0Item.Level.LEVEL_2) + elif floor == "top": + qs = qs.filter(i_level=TbHypn0Item.Level.IMMORTAL) + else: + floor = "all" + + # Применение сортировки + if sort == "new": + qs = qs.order_by("-d_created_at") + elif sort == "likes": + qs = qs.order_by("-i_likes_count", "-f_score", "-d_created_at") + elif sort == "views": + qs = qs.order_by("i_views_count", "-d_created_at") + elif sort == "popular": + qs = qs.order_by("-i_views_count", "-d_created_at") + else: + sort = "gravity" + qs = qs.order_by("-f_score", "-d_created_at") + + # Подсчет количества работ для бейджей на табах + counts = { + "all": TbHypn0Item.objects.filter(is_public=True).count(), + "fresh": TbHypn0Item.objects.filter( + is_public=True, + i_level__in=[TbHypn0Item.Level.CANDIDATE, TbHypn0Item.Level.LEVEL_1] + ).count(), + "curated": TbHypn0Item.objects.filter( + is_public=True, + i_level=TbHypn0Item.Level.LEVEL_2 + ).count(), + "top": TbHypn0Item.objects.filter( + is_public=True, + i_level=TbHypn0Item.Level.IMMORTAL + ).count(), + } + + paginator = Paginator(qs, 16) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) + + sort_options = [ + {"id": "gravity", "title": "По гравитации (f_score)", "icon": "🌀"}, + {"id": "new", "title": "Свежие (по дате)", "icon": "✨"}, + {"id": "likes", "title": "По числу лайков", "icon": "♥"}, + {"id": "views", "title": "Редкие (мало показов)", "icon": "👁"}, + {"id": "popular", "title": "Популярные по показам", "icon": "🔥"}, + ] + + context = { + "page_obj": page_obj, + "current_floor": floor, + "current_sort": sort, + "counts": counts, + "sort_options": sort_options, + } + return render(request, "gallery/archive.html", context) + + def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse: """ Страница полного просмотра конкретного этажа галереи с пагинацией (по 16 карточек). diff --git a/hypn0/templates/block/gallery_card.html b/hypn0/templates/block/gallery_card.html index 9942c10..4c3edfd 100644 --- a/hypn0/templates/block/gallery_card.html +++ b/hypn0/templates/block/gallery_card.html @@ -36,19 +36,39 @@ - -
- - ¤{{ item.s_hash_id }} - + +
+ {% if item.i_level >= 1000 %} + + ★ ¤{{ item.s_hash_id }} + + {% elif item.i_level >= 30 %} + + ◉ ¤{{ item.s_hash_id }} + + {% else %} + + ¤{{ item.s_hash_id }} + + {% endif %}
- - ♥ {{ item.i_likes_count }} - + {% if item.i_level >= 1000 %} + + ♥ {{ item.i_likes_count }} + + {% elif item.i_level >= 30 %} + + ♥ {{ item.i_likes_count }} + + {% else %} + + ♥ {{ item.i_likes_count }} + + {% endif %} diff --git a/hypn0/templates/block/header.html b/hypn0/templates/block/header.html index 3af9391..2c593ff 100644 --- a/hypn0/templates/block/header.html +++ b/hypn0/templates/block/header.html @@ -9,7 +9,7 @@ Логотип hypn0
{# /ЛОГОТИП #} {# ПРАВОЕ МЕНЮ #} - галерея + галерея {# /ПРАВОЕ МЕНЮ #} diff --git a/hypn0/templates/index.html b/hypn0/templates/index.html index 02e2c71..6a9b8a5 100644 --- a/hypn0/templates/index.html +++ b/hypn0/templates/index.html @@ -501,7 +501,7 @@

Галерея транса

Многоуровневое хранилище зрительного подчинения

- Смотреть весь архив → + Смотреть весь архив →