mod: галерея (6) добавлен раздел "Одобрено Мозговым Слизнем" и "Глубокий транс"
This commit is contained in:
@@ -719,9 +719,9 @@ class GalleryFreshFloorTests(BaseMediaTestCase):
|
|||||||
self.assertContains(response, f"gallery-card-{item.s_hash_id}")
|
self.assertContains(response, f"gallery-card-{item.s_hash_id}")
|
||||||
|
|
||||||
def test_gallery_floor_fresh_pagination(self):
|
def test_gallery_floor_fresh_pagination(self):
|
||||||
# Создаем 10 картин
|
# Создаем 20 картин
|
||||||
svg_bytes = b'<svg><circle/></svg>'
|
svg_bytes = b'<svg><circle/></svg>'
|
||||||
for i in range(10):
|
for i in range(20):
|
||||||
item = TbHypn0Item(
|
item = TbHypn0Item(
|
||||||
s_title=f"Кандидат #{i}",
|
s_title=f"Кандидат #{i}",
|
||||||
file_svg=ContentFile(svg_bytes, name=f"cand_{i}.svg"),
|
file_svg=ContentFile(svg_bytes, name=f"cand_{i}.svg"),
|
||||||
@@ -731,21 +731,72 @@ class GalleryFreshFloorTests(BaseMediaTestCase):
|
|||||||
)
|
)
|
||||||
item.save(visitor_uuid_or_fp=self.vid)
|
item.save(visitor_uuid_or_fp=self.vid)
|
||||||
|
|
||||||
# Страница 1 (должно быть 8 штук)
|
# Страница 1 (должно быть 16 штук)
|
||||||
url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "fresh"})
|
url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "fresh"})
|
||||||
response_p1 = self.client.get(url)
|
response_p1 = self.client.get(url)
|
||||||
self.assertEqual(response_p1.status_code, 200)
|
self.assertEqual(response_p1.status_code, 200)
|
||||||
self.assertContains(response_p1, "Плеск бессознательного")
|
self.assertContains(response_p1, "Плеск бессознательного")
|
||||||
self.assertContains(response_p1, "FRESH STREAM")
|
self.assertContains(response_p1, "FRESH STREAM")
|
||||||
self.assertEqual(len(response_p1.context["page_obj"]), 8)
|
self.assertEqual(len(response_p1.context["page_obj"]), 16)
|
||||||
self.assertContains(response_p1, "Фаза 1 из 2")
|
self.assertContains(response_p1, "Фаза 1 из 2")
|
||||||
|
|
||||||
# Страница 2 (должно быть 2 штуки)
|
# Страница 2 (должно быть 4 штуки)
|
||||||
response_p2 = self.client.get(url, data={"page": 2})
|
response_p2 = self.client.get(url, data={"page": 2})
|
||||||
self.assertEqual(response_p2.status_code, 200)
|
self.assertEqual(response_p2.status_code, 200)
|
||||||
self.assertEqual(len(response_p2.context["page_obj"]), 2)
|
self.assertEqual(len(response_p2.context["page_obj"]), 4)
|
||||||
self.assertContains(response_p2, "Фаза 2 из 2")
|
self.assertContains(response_p2, "Фаза 2 из 2")
|
||||||
|
|
||||||
|
def test_gallery_floors_curated_and_top(self):
|
||||||
|
from hypn0_site.views import get_floor_curated, get_floor_top
|
||||||
|
|
||||||
|
svg_bytes = b'<svg><circle/></svg>'
|
||||||
|
item_curated = TbHypn0Item(
|
||||||
|
s_title="Одобренная картина",
|
||||||
|
file_svg=ContentFile(svg_bytes, name="cur.svg"),
|
||||||
|
i_level=TbHypn0Item.Level.LEVEL_2,
|
||||||
|
f_score=42.5,
|
||||||
|
is_public=True,
|
||||||
|
)
|
||||||
|
item_curated.save(visitor_uuid_or_fp=self.vid)
|
||||||
|
|
||||||
|
item_top = TbHypn0Item(
|
||||||
|
s_title="Шедевр золотого фонда",
|
||||||
|
file_svg=ContentFile(svg_bytes, name="top.svg"),
|
||||||
|
i_level=TbHypn0Item.Level.IMMORTAL,
|
||||||
|
i_likes_count=150,
|
||||||
|
f_score=99.0,
|
||||||
|
is_public=True,
|
||||||
|
)
|
||||||
|
item_top.save(visitor_uuid_or_fp=self.vid)
|
||||||
|
|
||||||
|
# Проверяем выборки
|
||||||
|
curated_res = list(get_floor_curated())
|
||||||
|
self.assertEqual(len(curated_res), 1)
|
||||||
|
self.assertEqual(curated_res[0].pk, item_curated.pk)
|
||||||
|
|
||||||
|
top_res = list(get_floor_top())
|
||||||
|
self.assertEqual(len(top_res), 1)
|
||||||
|
self.assertEqual(top_res[0].pk, item_top.pk)
|
||||||
|
|
||||||
|
# Проверяем рендеринг страниц этажей
|
||||||
|
url_curated = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "curated"})
|
||||||
|
resp_c = self.client.get(url_curated)
|
||||||
|
self.assertEqual(resp_c.status_code, 200)
|
||||||
|
self.assertContains(resp_c, "Одобрено Мозговым Слизнем")
|
||||||
|
self.assertContains(resp_c, "CURATED")
|
||||||
|
|
||||||
|
url_top = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "top"})
|
||||||
|
resp_t = self.client.get(url_top)
|
||||||
|
self.assertEqual(resp_t.status_code, 200)
|
||||||
|
self.assertContains(resp_t, "Глубокий транс")
|
||||||
|
self.assertContains(resp_t, "TOP TIER")
|
||||||
|
|
||||||
|
# Проверяем отображение на главной странице
|
||||||
|
resp_idx = self.client.get(reverse("hypn0_site:index"))
|
||||||
|
self.assertEqual(resp_idx.status_code, 200)
|
||||||
|
self.assertContains(resp_idx, f"gallery-card-{item_curated.s_hash_id}")
|
||||||
|
self.assertContains(resp_idx, f"gallery-card-{item_top.s_hash_id}")
|
||||||
|
|
||||||
def test_gallery_floor_unknown_404(self):
|
def test_gallery_floor_unknown_404(self):
|
||||||
url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "non_existent"})
|
url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "non_existent"})
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
|||||||
@@ -36,9 +36,41 @@ def get_floor_fresh(limit: int | None = 8, offset: int = 0):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def get_floor_curated(limit: int | None = 8, offset: int = 0):
|
||||||
|
"""
|
||||||
|
2-й ЭТАЖ: «Одобрено Мозговым Слизнем» (Кураторский отбор / Тренды).
|
||||||
|
Выборка: прошедшие первичный отбор (Level.LEVEL_2).
|
||||||
|
Сортировка: по гравитационному рейтингу -f_score (актуальные тренды) и свежести -d_created_at.
|
||||||
|
"""
|
||||||
|
qs = TbHypn0Item.objects.filter(
|
||||||
|
i_level=TbHypn0Item.Level.LEVEL_2,
|
||||||
|
is_public=True,
|
||||||
|
).order_by("-f_score", "-d_created_at")
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
return qs[offset : offset + limit]
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def get_floor_top(limit: int | None = 8, offset: int = 0):
|
||||||
|
"""
|
||||||
|
3-й ЭТАЖ: «Глубокий транс» (Золотой фонд / Высшая лига).
|
||||||
|
Выборка: бессмертные шедевры сообщества (Level.IMMORTAL).
|
||||||
|
Сортировка: по числу признания -i_likes_count, -f_score и свежести -d_created_at.
|
||||||
|
"""
|
||||||
|
qs = TbHypn0Item.objects.filter(
|
||||||
|
i_level=TbHypn0Item.Level.IMMORTAL,
|
||||||
|
is_public=True,
|
||||||
|
).order_by("-i_likes_count", "-f_score", "-d_created_at")
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
return qs[offset : offset + limit]
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse:
|
def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
Страница полного просмотра конкретного этажа галереи с пагинацией (по 8 карточек).
|
Страница полного просмотра конкретного этажа галереи с пагинацией (по 16 карточек).
|
||||||
"""
|
"""
|
||||||
floors_config = {
|
floors_config = {
|
||||||
"fresh": {
|
"fresh": {
|
||||||
@@ -48,6 +80,20 @@ def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse:
|
|||||||
"subtitle": "Свежие галлюцинации из инкубатора • Первичная оценка сообщества",
|
"subtitle": "Свежие галлюцинации из инкубатора • Первичная оценка сообщества",
|
||||||
"getter": get_floor_fresh,
|
"getter": get_floor_fresh,
|
||||||
},
|
},
|
||||||
|
"curated": {
|
||||||
|
"title": "Одобрено Мозговым Слизнем",
|
||||||
|
"badge": "CURATED",
|
||||||
|
"badge_color": "cyan",
|
||||||
|
"subtitle": "Проверено контролем качества • Высокий психоделический резонанс",
|
||||||
|
"getter": get_floor_curated,
|
||||||
|
},
|
||||||
|
"top": {
|
||||||
|
"title": "Глубокий транс",
|
||||||
|
"badge": "TOP TIER",
|
||||||
|
"badge_color": "emerald",
|
||||||
|
"subtitle": "Золотой фонд гипноза • Абсолютное подчинение воли",
|
||||||
|
"getter": get_floor_top,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if floor_slug not in floors_config:
|
if floor_slug not in floors_config:
|
||||||
@@ -56,7 +102,7 @@ def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse:
|
|||||||
cfg = floors_config[floor_slug]
|
cfg = floors_config[floor_slug]
|
||||||
items_qs = cfg["getter"](limit=None)
|
items_qs = cfg["getter"](limit=None)
|
||||||
|
|
||||||
paginator = Paginator(items_qs, 8)
|
paginator = Paginator(items_qs, 16)
|
||||||
page_number = request.GET.get("page", 1)
|
page_number = request.GET.get("page", 1)
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
@@ -74,8 +120,12 @@ def gallery_floor(request: HttpRequest, floor_slug: str) -> HttpResponse:
|
|||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def index(request: HttpRequest | None) -> HttpResponse:
|
def index(request: HttpRequest | None) -> HttpResponse:
|
||||||
fresh_items = get_floor_fresh(limit=8)
|
fresh_items = get_floor_fresh(limit=8)
|
||||||
|
curated_items = get_floor_curated(limit=8)
|
||||||
|
top_items = get_floor_top(limit=8)
|
||||||
return render(request, "index.html", {
|
return render(request, "index.html", {
|
||||||
"fresh_items": fresh_items,
|
"fresh_items": fresh_items,
|
||||||
|
"curated_items": curated_items,
|
||||||
|
"top_items": top_items,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-29
@@ -509,7 +509,7 @@
|
|||||||
|
|
||||||
<!-- 2-й ЭТАЖ: ОДОБРЕНО МОЗГОВЫМ СЛИЗНЕМ (Кураторский отбор) -->
|
<!-- 2-й ЭТАЖ: ОДОБРЕНО МОЗГОВЫМ СЛИЗНЕМ (Кураторский отбор) -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-5 h-5 rounded-full bg-cyan-500"></div>
|
<div class="w-5 h-5 rounded-full bg-cyan-500"></div>
|
||||||
<div>
|
<div>
|
||||||
@@ -520,28 +520,25 @@
|
|||||||
<p class="text-xs text-stone-400 dark:text-zinc-500">Проверено контролем качества • Высокий психоделический резонанс</p>
|
<p class="text-xs text-stone-400 dark:text-zinc-500">Проверено контролем качества • Высокий психоделический резонанс</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs font-semibold text-stone-400">100–999 лайков</span>
|
<a href="{% url 'hypn0_site:gallery_floor' 'curated' %}" class="text-xs font-bold text-cyan-600 dark:text-cyan-400 hover:underline">
|
||||||
|
Смотреть все одобренные →
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
|
||||||
<template x-for="i in 8">
|
{% for item in curated_items %}
|
||||||
<div class="aspect-square bg-stone-200 dark:bg-zinc-900 rounded-xl border border-stone-300 dark:border-zinc-800 overflow-hidden relative group cursor-pointer hover:border-cyan-500/50 transition-all hover:scale-[1.02]">
|
{% include "block/gallery_card.html" %}
|
||||||
<div class="absolute inset-0 flex items-center justify-center text-stone-400 group-hover:scale-110 transition-transform">
|
{% empty %}
|
||||||
<svg class="w-8 h-8 opacity-20" fill="currentColor" viewBox="0 0 24 24">
|
<div class="col-span-full py-10 text-center text-stone-400 bg-stone-100/50 dark:bg-zinc-900/50 rounded-2xl border border-dashed border-stone-300 dark:border-zinc-800 text-xs">
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
|
Мозговой слизень пока не отобрал картины для второго уровня. Голосуйте за свежие работы в инкубаторе!
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-2 left-2 flex items-center gap-1 text-[11px] font-bold text-cyan-600 dark:text-cyan-400 bg-white/90 dark:bg-zinc-900/90 px-2 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
♥ <span x-text="100 + Math.floor(Math.random()*400)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3-й ЭТАЖ: ГЛУБОКИЙ ТРАНС (Высшая лига) -->
|
<!-- 3-й ЭТАЖ: ГЛУБОКИЙ ТРАНС (Высшая лига) -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-5 h-5"><img src="{% static 'img/favicon.svg' %}" alt="Favicon" class="w-full h-full object-cover"></div>
|
<div class="w-5 h-5"><img src="{% static 'img/favicon.svg' %}" alt="Favicon" class="w-full h-full object-cover"></div>
|
||||||
<div>
|
<div>
|
||||||
@@ -552,25 +549,19 @@
|
|||||||
<p class="text-xs text-stone-400 dark:text-zinc-500">Золотой фонд гипноза • Абсолютное подчинение воли</p>
|
<p class="text-xs text-stone-400 dark:text-zinc-500">Золотой фонд гипноза • Абсолютное подчинение воли</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs font-semibold text-stone-400">1000+ лайков</span>
|
<a href="{% url 'hypn0_site:gallery_floor' 'top' %}" class="text-xs font-bold text-emerald-600 dark:text-emerald-400 hover:underline">
|
||||||
|
Смотреть весь золотой фонд →
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
|
||||||
<template x-for="i in 8">
|
{% for item in top_items %}
|
||||||
<div class="aspect-square bg-stone-200 dark:bg-zinc-900 rounded-2xl border-2 border-emerald-500/30 dark:border-emerald-500/20 overflow-hidden relative group cursor-pointer shadow-lg shadow-emerald-950/5 hover:border-emerald-500 transition-all hover:scale-[1.02]">
|
{% include "block/gallery_card.html" %}
|
||||||
<div class="absolute inset-0 flex items-center justify-center text-emerald-600/40 group-hover:scale-110 transition-transform">
|
{% empty %}
|
||||||
<svg class="w-10 h-10 opacity-30" fill="currentColor" viewBox="0 0 24 24">
|
<div class="col-span-full py-10 text-center text-stone-400 bg-stone-100/50 dark:bg-zinc-900/50 rounded-2xl border border-dashed border-stone-300 dark:border-zinc-800 text-xs">
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
|
Зал славы глубокого транса формируется. Только истинные бессмертные шедевры попадают сюда.
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="absolute top-2 right-2 text-[10px] font-black tracking-widest text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 backdrop-blur-md px-1.5 py-0.5 rounded border border-emerald-500/20">
|
|
||||||
★ 99%
|
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-2.5 left-2.5 flex items-center gap-1.5 text-xs font-bold text-white bg-emerald-600/90 backdrop-blur-md px-2.5 py-1 rounded-full shadow">
|
|
||||||
<span>♥</span> <span x-text="500 + Math.floor(Math.random()*500)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user