diff --git a/hypn0/hypn0_site/services/halftone.py b/hypn0/hypn0_site/services/halftone.py index 2690032..3862dc7 100644 --- a/hypn0/hypn0_site/services/halftone.py +++ b/hypn0/hypn0_site/services/halftone.py @@ -1,6 +1,7 @@ import io import math import random +import re from collections import defaultdict from typing import BinaryIO, Union @@ -281,3 +282,101 @@ def prepare_gallery_svg(svg_content: str) -> str: return svg_content.replace("", f"{hover_css}", 1) return svg_content + + +def prepare_active_svg(svg_content: str) -> str: + """ + Возвращает полностью активный SVG без паузы анимации вне ховера + для страницы детального просмотра и скачивания пользователем. + """ + if not svg_content: + return svg_content + + hover_css = ( + "svg:not(:hover) .shape,svg:not(:hover) circle,svg:not(:hover) rect," + "svg:not(:hover) polygon,svg:not(:hover) path{animation-play-state:paused!important}" + ) + return svg_content.replace(hover_css, "") + + +def analyze_svg_structure(svg_content: str) -> dict: + """ + Анализирует разметку SVG и возвращает наукообразную детальную статистику + по геометрическим примитивам, классам, анимациям и энтропии данных. + """ + if not svg_content: + return { + "total_bytes": 0, + "total_kb": 0.0, + "total_oscillators": 0, + "use_count": 0, + "circle_count": 0, + "rect_count": 0, + "polygon_count": 0, + "path_count": 0, + "defs_count": 0, + "groups_count": 0, + "keyframes_count": 0, + "classes_count": 0, + "viewbox": "0 0 1000 1000", + "width": 1000, + "height": 1000, + "aspect_ratio": "1:1", + "density": 0.0, + "bytes_per_quantum": 0.0, + } + + total_bytes = len(svg_content.encode("utf-8")) + + # Подсчет векторных сущностей + use_count = len(re.findall(r"= 3 else 1000 + height = int(float(vb_parts[3])) if len(vb_parts) >= 4 else 1000 + except (ValueError, IndexError): + width, height = 1000, 1000 + + gcd_val = math.gcd(width, height) + aspect_ratio = f"{width // gcd_val}:{height // gcd_val}" if gcd_val > 0 else "1:1" + + # Наукообразные показатели + total_oscillators = use_count if use_count > 0 else defs_count + bytes_per_quantum = round(total_bytes / max(total_oscillators, 1), 1) + density = round(total_oscillators / max((width * height) / 100000.0, 1.0), 2) + + return { + "total_bytes": total_bytes, + "total_kb": round(total_bytes / 1024, 2), + "total_oscillators": total_oscillators, + "use_count": use_count, + "circle_count": circle_count, + "rect_count": rect_count, + "polygon_count": polygon_count, + "path_count": path_count, + "defs_count": defs_count, + "groups_count": groups_count, + "keyframes_count": keyframes_count, + "classes_count": classes_count, + "viewbox": viewbox, + "width": width, + "height": height, + "aspect_ratio": aspect_ratio, + "density": density, + "bytes_per_quantum": bytes_per_quantum, + } diff --git a/hypn0/hypn0_site/tests.py b/hypn0/hypn0_site/tests.py index cec4dfe..9b2709b 100644 --- a/hypn0/hypn0_site/tests.py +++ b/hypn0/hypn0_site/tests.py @@ -1,4 +1,5 @@ import io +from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client, TestCase from django.urls import reverse @@ -6,7 +7,13 @@ from PIL import Image from .forms import HalftoneGenerateForm from .models import TbHypn0Item, TbVote -from .services.halftone import encode_to_base36, generate_halftone_svg, prepare_gallery_svg +from .services.halftone import ( + analyze_svg_structure, + encode_to_base36, + generate_halftone_svg, + prepare_active_svg, + prepare_gallery_svg, +) from .services.naming import generate_hypno_title, generate_title_openrouter @@ -304,3 +311,260 @@ class PublishViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "Нет данных SVG для публикации") self.assertEqual(TbHypn0Item.objects.count(), 0) + + +class SvgAnalysisAndActiveSvgTests(TestCase): + """Тестирование анализа структуры SVG и очистки от паузы.""" + + def test_prepare_active_svg(self): + gallery_svg = '' + active = prepare_active_svg(gallery_svg) + self.assertNotIn("animation-play-state:paused!important", active) + + def test_analyze_svg_structure(self): + svg = ( + '' + '' + '' + '' + '' + '' + '' + '' + ) + stats = analyze_svg_structure(svg) + self.assertEqual(stats["use_count"], 3) + self.assertEqual(stats["total_oscillators"], 3) + self.assertEqual(stats["defs_count"], 2) + self.assertEqual(stats["circle_count"], 1) + self.assertEqual(stats["rect_count"], 1) + self.assertEqual(stats["groups_count"], 3) + self.assertEqual(stats["keyframes_count"], 1) + self.assertEqual(stats["viewbox"], "0 0 800 600") + self.assertEqual(stats["width"], 800) + self.assertEqual(stats["height"], 600) + self.assertEqual(stats["aspect_ratio"], "4:3") + self.assertTrue(stats["total_bytes"] > 0) + + +class GalleryDetailAndDownloadTests(TestCase): + """Тестирование страниц детального просмотра картины, скачивания и голосования.""" + + def setUp(self): + self.client = Client() + self.vid = "123e4567-e89b-12d3-a456-426614174000" + svg_code = ( + '' + '' + '' + '' + '' + ) + self.item = TbHypn0Item( + s_title="Астральный Транс Сознания #42", + file_svg=ContentFile(svg_code.encode("utf-8"), name="test_item.svg"), + i_file_size=len(svg_code), + j_metadata={ + "shape": "circle", + "cols": 35, + "max_radius": 8, + "blink": 6, + "rotation": 0, + "scale": 980, + "angle": 0, + "color": "#a855ff", + }, + i_level=TbHypn0Item.Level.CANDIDATE, + is_public=True, + ) + self.item.save(visitor_uuid_or_fp=self.vid) + + def test_gallery_detail_view_success(self): + response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": self.item.s_hash_id})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Астральный Транс Сознания #42") + self.assertContains(response, f"#{self.item.s_hash_id}") + self.assertContains(response, "Векторная анатомия") + self.assertContains(response, "Синтез психо-сетки") + self.assertContains(response, "Астральный паспорт") + self.assertContains(response, "Копировать SVG-код") + self.assertContains(response, "Скачать .svg") + + # Проверка инкремента просмотров + self.item.refresh_from_db() + self.assertGreaterEqual(self.item.i_views_count, 2) + + def test_gallery_detail_404_on_invalid_hash(self): + response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": "nonexistent999"})) + self.assertEqual(response.status_code, 404) + + def test_gallery_download_view_success(self): + response = self.client.get(reverse("hypn0_site:gallery_download", kwargs={"hash_id": self.item.s_hash_id})) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "image/svg+xml") + self.assertIn(f'filename="hypn0-{self.item.s_hash_id}.svg"', response["Content-Disposition"]) + # Должен быть чистый активный SVG (без паузы анимации) + self.assertNotIn("animation-play-state:paused!important", response.content.decode("utf-8")) + + def test_gallery_vote_with_cookie(self): + # Новый посетитель + voter_vid = "987e6543-e21b-12d3-a456-426614174999" + self.client.cookies["hypn0_vid"] = voter_vid + response = self.client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id})) + self.assertEqual(response.status_code, 200) + self.item.refresh_from_db() + self.assertEqual(self.item.i_likes_count, 2) + + def test_gallery_vote_without_cookie_returns_notice(self): + client = Client() + response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Подчинитесь воле Гипножабы!") + + +class GalleryRandomAndNavigationTests(TestCase): + """Тестирование случайной навигации и пула хэшей (/gallery/random).""" + + def setUp(self): + self.client = Client() + self.vid = "123e4567-e89b-12d3-a456-426614174000" + + # Создаем 3 картины в галерее + self.items = [] + for i in range(3): + svg_code = f'' + item = TbHypn0Item( + s_title=f"Гипно Картина #{i}", + file_svg=ContentFile(svg_code.encode("utf-8"), name=f"test_item_{i}.svg"), + i_file_size=len(svg_code), + j_metadata={"cols": 30 + i}, + is_public=True, + ) + item.save(visitor_uuid_or_fp=self.vid) + self.items.append(item) + + def test_gallery_random_direct_redirect(self): + response = self.client.get(reverse("hypn0_site:gallery_random")) + self.assertEqual(response.status_code, 302) + all_hashes = [it.s_hash_id for it in self.items] + self.assertTrue(any(h in response.url for h in all_hashes)) + + def test_gallery_random_json_pool(self): + response = self.client.get(reverse("hypn0_site:gallery_random"), data={"format": "json", "limit": 2}) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("next_hash_id", data) + self.assertIn("pool", data) + self.assertEqual(len(data["pool"]), 2) + all_hashes = {it.s_hash_id for it in self.items} + self.assertIn(data["next_hash_id"], all_hashes) + + def test_gallery_random_exclude_filters_seen(self): + exclude_hashes = [self.items[0].s_hash_id, self.items[1].s_hash_id] + response = self.client.get( + reverse("hypn0_site:gallery_random"), + data={"format": "json", "exclude": ",".join(exclude_hashes)}, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["next_hash_id"], self.items[2].s_hash_id) + + def test_gallery_random_exclude_all_loops_circle(self): + # Если исключены все 3 картины, кольцо замыкается + exclude_hashes = [it.s_hash_id for it in self.items] + response = self.client.get( + reverse("hypn0_site:gallery_random"), + data={ + "format": "json", + "exclude": ",".join(exclude_hashes), + "current": self.items[0].s_hash_id, + }, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIsNotNone(data["next_hash_id"]) + # Текущий элемент исключен, выбран один из оставшихся + self.assertIn(data["next_hash_id"], [self.items[1].s_hash_id, self.items[2].s_hash_id]) + + def test_gallery_random_empty_database(self): + TbHypn0Item.objects.all().delete() + # Direct visit redirects to index + response = self.client.get(reverse("hypn0_site:gallery_random")) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("hypn0_site:index")) + + # JSON returns null next_hash_id + json_resp = self.client.get(reverse("hypn0_site:gallery_random"), data={"format": "json"}) + self.assertEqual(json_resp.status_code, 200) + self.assertIsNone(json_resp.json()["next_hash_id"]) + self.assertEqual(json_resp.json()["pool"], []) + + +class UnconsciousMatrixTests(TestCase): + """Тестирование Матрицы бессознательного (build_unconscious_matrix) и навигации.""" + + def setUp(self): + self.client = Client() + self.vid = "123e4567-e89b-12d3-a456-426614174000" + + # Создаем 5 картин + self.items = [] + for i in range(5): + svg_code = f'' + item = TbHypn0Item( + s_title=f"Тестовый транс #{i}", + file_svg=ContentFile(svg_code.encode("utf-8"), name=f"test_matrix_{i}.svg"), + i_file_size=len(svg_code), + j_metadata={"cols": 20 + i}, + is_public=True, + ) + item.save(visitor_uuid_or_fp=self.vid) + self.items.append(item) + + def test_build_unconscious_matrix_basic(self): + from hypn0_site.views import build_unconscious_matrix + + current = self.items[0].s_hash_id + matrix_items, prev_h, next_h, prev_url, next_url, seed = build_unconscious_matrix(current, None) + + self.assertEqual(len(matrix_items), 5) + # Проверяем, что current отмечен как is_current + current_node = [n for n in matrix_items if n["s_hash_id"] == current][0] + self.assertTrue(current_node["is_current"]) + + # Другие узлы не current + other_nodes = [n for n in matrix_items if n["s_hash_id"] != current] + self.assertTrue(all(not n["is_current"] for n in other_nodes)) + + # Ссылки содержат seed + self.assertIn(f"seed={seed}", prev_url) + self.assertIn(f"seed={seed}", next_url) + + def test_matrix_stability_with_fixed_seed(self): + from hypn0_site.views import build_unconscious_matrix + + root_hash = self.items[2].s_hash_id + fixed_seed = f"42109_{root_hash}" + + # Первый запрос + m1, prev1, next1, _, _, seed1 = build_unconscious_matrix(self.items[0].s_hash_id, fixed_seed) + # Второй запрос с тем же seed на другую картину + m2, prev2, next2, _, _, seed2 = build_unconscious_matrix(self.items[1].s_hash_id, fixed_seed) + + self.assertEqual(seed1, fixed_seed) + self.assertEqual(seed2, fixed_seed) + + # Состав и порядок хэшей в матрице должны быть идентичны + hashes1 = [n["s_hash_id"] for n in m1] + hashes2 = [n["s_hash_id"] for n in m2] + self.assertEqual(hashes1, hashes2) + + def test_gallery_detail_renders_matrix(self): + item = self.items[0] + response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id})) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Матрица бессознательного") + self.assertContains(response, "5 узлов") + self.assertContains(response, f"#{item.s_hash_id}") + self.assertContains(response, "btn-nav-prev") + self.assertContains(response, "btn-nav-next") diff --git a/hypn0/hypn0_site/urls.py b/hypn0/hypn0_site/urls.py index ea2f2ac..df4e148 100644 --- a/hypn0/hypn0_site/urls.py +++ b/hypn0/hypn0_site/urls.py @@ -9,6 +9,11 @@ urlpatterns = [ path("", views.index, name="index"), path("generate", views.generate, name="generate"), 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/", views.gallery_detail, name="gallery_detail"), + path("gallery//download", views.gallery_download, name="gallery_download"), + path("gallery//vote", views.gallery_vote, name="gallery_vote"), ] if settings.DEBUG: diff --git a/hypn0/hypn0_site/views.py b/hypn0/hypn0_site/views.py index 75040bb..3ce9972 100644 --- a/hypn0/hypn0_site/views.py +++ b/hypn0/hypn0_site/views.py @@ -1,12 +1,21 @@ +import hashlib +import random + +from django.conf import settings from django.core.files.base import ContentFile -from django.http import HttpRequest, HttpResponse -from django.shortcuts import render +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 from django.views.decorators.http import require_POST from .forms import HalftoneGenerateForm -from .models import TbHypn0Item -from .services.halftone import generate_halftone_svg, prepare_gallery_svg +from .models import TbHypn0Item, TbVote +from .services.halftone import ( + analyze_svg_structure, + generate_halftone_svg, + prepare_active_svg, + prepare_gallery_svg, +) from .services.naming import generate_hypno_title @@ -170,3 +179,269 @@ def publish(request: HttpRequest) -> HttpResponse: "item": item, }, ) + + +def gallery_random(request: HttpRequest) -> HttpResponse: + """ + Быстрый эндпоинт для случайной навигации по галерее ("в транс") и предзагрузки пула хэшей. + Принимает список уже просмотренных хэшей в параметре `exclude` (через запятую) и текущий хэш `current`. + + Архитектура выборки: + - Запрашивает только плоский список индексированных строк `s_hash_id` (Index Only Scan). + - Перемешивание и срез происходят в памяти Python за доли миллисекунды без тяжелого `ORDER BY RANDOM()`. + - Если все исключенные работы покрыли базу, фильтр сбрасывается для замыкания кольца. + + ======================================================================================== + МАСШТАБИРОВАНИЕ НА 1 000 000+ ЗАПИСЕЙ (FUTURE SCALING NOTE): + При достижении миллионов строк в базе операция exclude(s_hash_id__in=...) может нагружать СУБД. + Для ультра-высоких нагрузок переключить на алгоритм: + 1. count = TbHypn0Item.objects.filter(is_public=True).count() + 2. random_offset = random.randint(0, max(0, count - slice_size)) + 3. pool_slice = list(TbHypn0Item.objects.filter(is_public=True) + .values_list('s_hash_id', flat=True)[random_offset : random_offset + slice_size]) + 4. candidates = [h for h in pool_slice if h not in exclude_set and h != current] + 5. return random.sample(candidates, min(len(candidates), limit)) + ======================================================================================== + """ + exclude_raw = request.GET.get("exclude", "") + exclude_hashes = set(h.strip() for h in exclude_raw.split(",") if h.strip()) + current_hash = request.GET.get("current", "").strip() + if current_hash: + exclude_hashes.add(current_hash) + + try: + limit = min(max(int(request.GET.get("limit", 20)), 1), 50) + except (ValueError, TypeError): + limit = 20 + + base_qs = TbHypn0Item.objects.filter(is_public=True) + + # 1. Попытка выбрать кандидатов среди еще не просмотренных + candidate_hashes = list( + base_qs.exclude(s_hash_id__in=exclude_hashes).values_list("s_hash_id", flat=True)[:300] + ) + + # 2. Если все просмотрены (или база меньше истории) — замыкаем кольцо, исключая только текущий + if not candidate_hashes and current_hash: + candidate_hashes = list( + base_qs.exclude(s_hash_id=current_hash).values_list("s_hash_id", flat=True)[:300] + ) + + # 3. Крайний случай — берем любые доступные + if not candidate_hashes: + candidate_hashes = list(base_qs.values_list("s_hash_id", flat=True)[:300]) + + if candidate_hashes: + random.shuffle(candidate_hashes) + pool = candidate_hashes[:limit] + next_hash = pool[0] + else: + pool = [] + next_hash = None + + # Определение формата ответа + is_json = ( + request.GET.get("format") == "json" + or request.headers.get("Accept") == "application/json" + or request.headers.get("X-Requested-With") == "XMLHttpRequest" + ) + + if is_json: + return JsonResponse({"next_hash_id": next_hash, "pool": pool}) + + if next_hash: + return redirect("hypn0_site:gallery_detail", hash_id=next_hash) + + return redirect("hypn0_site:index") + + +def build_unconscious_matrix(current_hash: str, seed_param: str | None) -> tuple[list[dict], str, str, str, str, str]: + """ + Формирует "Матрицу бессознательного" (до 200 псевдослучайных узлов) вокруг корневой картины. + + Формат seed: "_", например "74291_a1b2c3". + Алгоритм: + - При первом заходе без seed генерируется seed_num и origin_hash = current_hash. + - Выбираются -99 элементов слева и +100 элементов справа от origin_hash (всего до 200). + - Для N <= 200 перемешиваются все доступные картины, а origin_hash помещается в центр. + - Для текущего элемента current_hash определяются prev_hash и next_hash по кольцу. + """ + seed_num = None + origin_hash = current_hash + + if seed_param: + parts = str(seed_param).split("_", 1) + try: + seed_num = int(parts[0]) + if len(parts) > 1 and parts[1].strip(): + origin_hash = parts[1].strip() + except (ValueError, TypeError): + seed_num = None + + if seed_num is None: + seed_num = random.randint(10000, 99999) + origin_hash = current_hash + + effective_seed = f"{seed_num}_{origin_hash}" + rng = random.Random(seed_num) + + # 1. Запрашиваем все публичные хэши + all_hashes = list(TbHypn0Item.objects.filter(is_public=True).values_list("s_hash_id", flat=True)) + + if not all_hashes: + all_hashes = [current_hash] + + if origin_hash not in all_hashes: + origin_hash = current_hash + + other_hashes = [h for h in all_hashes if h != origin_hash] + + if len(other_hashes) <= 199: + rng.shuffle(other_hashes) + mid_idx = len(other_hashes) // 2 + trail = other_hashes[:mid_idx] + [origin_hash] + other_hashes[mid_idx:] + else: + chosen_others = rng.sample(other_hashes, 199) + left_part = chosen_others[:99] + right_part = chosen_others[99:] + trail = left_part + [origin_hash] + right_part + + # Гарантия наличия current_hash в trail + if current_hash not in trail: + trail.append(current_hash) + + idx = trail.index(current_hash) + total = len(trail) + prev_hash = trail[(idx - 1) % total] + next_hash = trail[(idx + 1) % total] + + prev_url = f"/gallery/{prev_hash}?seed={effective_seed}" + next_url = f"/gallery/{next_hash}?seed={effective_seed}" + + matrix_items = [ + { + "s_hash_id": h, + "is_current": (h == current_hash), + "url": f"/gallery/{h}?seed={effective_seed}", + } + for h in trail + ] + + return matrix_items, prev_hash, next_hash, prev_url, next_url, effective_seed + + +def gallery_detail(request: HttpRequest, hash_id: str) -> HttpResponse: + """ + Страница детального просмотра и шеринга картины из галереи транса. + Выводит активный живой SVG, кнопки скачивания/копирования, наукообразную телеметрию + и интерактивную Матрицу бессознательного (персональный поток хэшей по seed). + """ + item = get_object_or_404(TbHypn0Item, s_hash_id=hash_id, is_public=True) + + # Инкремент просмотров + item.increment_views() + + # Считывание содержимого SVG + svg_content = "" + if item.file_svg: + try: + with item.file_svg.open("r") as f: + svg_content = f.read() + if isinstance(svg_content, bytes): + svg_content = svg_content.decode("utf-8") + except Exception: + svg_content = "" + + active_svg = prepare_active_svg(svg_content) + svg_stats = analyze_svg_structure(svg_content) + + # Авторский отпечаток + author_vote = item.votes.filter(i_direction=TbVote.Direction.AUTHOR).first() + author_fp = author_vote.s_fingerprint if author_vote else None + + # Проверка, голосовал ли текущий посетитель + user_voted = False + visitor_uuid = request.COOKIES.get("hypn0_vid") + if visitor_uuid and item.pk: + fp = hashlib.sha256(f"{visitor_uuid}:{settings.SECRET_KEY}".encode()).hexdigest() + user_voted = item.votes.filter(s_fingerprint=fp).exists() + + # Построение матрицы бессознательного + seed_param = request.GET.get("seed") + matrix_items, prev_hash, next_hash, prev_url, next_url, seed = build_unconscious_matrix( + current_hash=item.s_hash_id, seed_param=seed_param + ) + + context = { + "item": item, + "active_svg": active_svg, + "raw_svg": svg_content, + "svg_stats": svg_stats, + "author_fp": author_fp, + "user_voted": user_voted, + "metadata": item.j_metadata or {}, + "matrix_items": matrix_items, + "prev_hash": prev_hash, + "next_hash": next_hash, + "prev_url": prev_url, + "next_url": next_url, + "seed": seed, + } + return render(request, "gallery/detail.html", context) + + +def gallery_download(request: HttpRequest, hash_id: str) -> HttpResponse: + """ + Эндпоинт для скачивания чистого SVG-файла картины. + """ + item = get_object_or_404(TbHypn0Item, s_hash_id=hash_id, is_public=True) + + if not item.file_svg: + raise Http404("SVG файл отсутствует") + + try: + with item.file_svg.open("r") as f: + svg_content = f.read() + if isinstance(svg_content, bytes): + svg_content = svg_content.decode("utf-8") + except Exception: + raise Http404("SVG файл не может быть прочитан") + + clean_svg = prepare_active_svg(svg_content) + response = HttpResponse(clean_svg, content_type="image/svg+xml") + filename = f"hypn0-{item.s_hash_id}.svg" + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response + + +@require_POST +def gallery_vote(request: HttpRequest, hash_id: str) -> HttpResponse: + """ + HTMX-эндпоинт для голосования (лайк) за картину в галерее. + """ + item = get_object_or_404(TbHypn0Item, s_hash_id=hash_id, is_public=True) + visitor_uuid = request.COOKIES.get("hypn0_vid") + + if not visitor_uuid: + return render( + request, + "block/vote_button.html", + { + "item": item, + "user_voted": False, + "not_agreed": True, + }, + ) + + voted = item.increment_likes(visitor_uuid) + item.refresh_from_db(fields=["i_likes_count"]) + + return render( + request, + "block/vote_button.html", + { + "item": item, + "user_voted": True, + "voted_now": voted, + }, + ) diff --git a/hypn0/templates/block/publish_status.html b/hypn0/templates/block/publish_status.html index 1a4f9a8..d60fe20 100644 --- a/hypn0/templates/block/publish_status.html +++ b/hypn0/templates/block/publish_status.html @@ -2,13 +2,13 @@