mod: mod: модели (8) методы для лайков/дислайков
This commit is contained in:
+66
-14
@@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
import hashlib
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from hashids import Hashids
|
from hashids import Hashids
|
||||||
from hypn0.settings import *
|
from hypn0.settings import *
|
||||||
@@ -76,6 +77,9 @@ class TbHypn0Item(models.Model):
|
|||||||
default=0,
|
default=0,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name="Число лайков",
|
verbose_name="Число лайков",
|
||||||
|
help_text="Число лайков гипнотической SVG-картины. Поле используется для отображения в интерфейсе. Для расчета"
|
||||||
|
" рейтинга популярности намеренно НЕ ИСПОЛЬЗУЕТСЯ. Рейтинг будет рассчитан на основе таблицы"
|
||||||
|
" <tt>Голоса/Лайка</tt> с учетом весов лайков и клаймов.",
|
||||||
)
|
)
|
||||||
i_views_count = models.PositiveIntegerField(
|
i_views_count = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
@@ -84,6 +88,9 @@ class TbHypn0Item(models.Model):
|
|||||||
i_claims_count = models.PositiveIntegerField(
|
i_claims_count = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
verbose_name="Число жалоб",
|
verbose_name="Число жалоб",
|
||||||
|
help_text="Число жалоб на гипнотическую SVG-картину. Поле используется для отображения в интерфейсе. Для расчета"
|
||||||
|
" рейтинга популярности намеренно НЕ ИСПОЛЬЗУЕТСЯ. Рейтинг будет рассчитан на основе таблицы"
|
||||||
|
" <tt>Голоса/Лайка</tt> с учетом весов лайков и клаймов.",
|
||||||
)
|
)
|
||||||
f_score = models.FloatField(
|
f_score = models.FloatField(
|
||||||
default=0.0,
|
default=0.0,
|
||||||
@@ -143,21 +150,66 @@ class TbHypn0Item(models.Model):
|
|||||||
|
|
||||||
def increment_views(self):
|
def increment_views(self):
|
||||||
"""Безопасный инкремент просмотров"""
|
"""Безопасный инкремент просмотров"""
|
||||||
TbHypn0Item.objects.get(id=self.id).update(i_views_count=F('i_views_count') + 1)
|
TbHypn0Item.objects.filter(id=self.id).update(i_views_count=F('i_views_count') + 1)
|
||||||
|
|
||||||
def increment_likes(self):
|
def vote(self, visitor_uuid_or_fp: str, direction: int = VOTE_LIKE) -> bool:
|
||||||
"""Безопасный инкремент лайков"""
|
"""
|
||||||
TbHypn0Item.objects.get(id=self.id).update(i_likes_count=F('i_likes_count') + 1)
|
Универсальный метод учета голоса (лайк, жалоба или авторство).
|
||||||
# Здесь будет код для записи в TbVote
|
|
||||||
|
|
||||||
def increment_claims(self):
|
Принимает:
|
||||||
"""Безопасный инкремент жалоб"""
|
- visitor_uuid_or_fp: UUIDv4 из куки hypn0_vid или готовый 64-символьный SHA256-хэш отпечатка
|
||||||
TbHypn0Item.objects.get(id=self.id).update(i_claims_count=F('i_claims_count') + 1)
|
- direction: направление голоса (Direction.LIKE, Direction.CLAIM, Direction.AUTHOR)
|
||||||
# Здесь будет код для записи в TbVote
|
|
||||||
|
Возвращает:
|
||||||
|
- True: голос успешно зафиксирован и счетчик увеличен
|
||||||
|
- False: голос уже существовал (защита от повторного голосования) или передан пустой токен
|
||||||
|
"""
|
||||||
|
if not visitor_uuid_or_fp:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Если передан сырой UUID устройства, вычисляем SHA256 отпечаток
|
||||||
|
if len(visitor_uuid_or_fp) == 64:
|
||||||
|
fingerprint = visitor_uuid_or_fp
|
||||||
|
else:
|
||||||
|
fingerprint = hashlib.sha256(f"{visitor_uuid_or_fp}:{SECRET_KEY}".encode()).hexdigest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# В SQLite при возникновении IntegrityError текущая транзакция может перейти в состояние ошибки. Чтобы это
|
||||||
|
# не затрагивало последующие операции с базой, try-записи изолирована в `with transaction.atomic()`
|
||||||
|
with transaction.atomic():
|
||||||
|
TbVote.objects.create(
|
||||||
|
k_item=self,
|
||||||
|
i_direction=direction,
|
||||||
|
s_fingerprint=fingerprint,
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
# Нарушение UniqueConstraint (k_item, s_fingerprint) — голос с этого устройства уже учтен
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Атомарный инкремент нужного счетчика в зависимости от направления
|
||||||
|
# Используем `objects.filter()` вместо `objects.get()`, так как метод .update() вызывается только у QuerySet.
|
||||||
|
if direction in (TbVote.Direction.LIKE, TbVote.Direction.AUTHOR):
|
||||||
|
TbHypn0Item.objects.filter(id=self.id).update(i_likes_count=F('i_likes_count') + 1)
|
||||||
|
elif direction == TbVote.Direction.CLAIM:
|
||||||
|
TbHypn0Item.objects.filter(id=self.id).update(i_claims_count=F('i_claims_count') + 1)
|
||||||
|
|
||||||
|
# Рейтинг популярности f_score намеренно НЕ пересчитывается на лету при каждом лайке:
|
||||||
|
# 1. f_score зависит от времени (возраста картины), поэтому непрерывно меняется сам по себе.
|
||||||
|
# 2. Значение f_score необходимо только периодическому процессу умной очистки диска (Smart Retention).
|
||||||
|
# 3. Пересчет вынесен в пакетную cron-задачу перед ротацией хранилища, что исключает лишние I/O-операции в SQLite.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def increment_likes(self, visitor_uuid_or_fp: str) -> bool:
|
||||||
|
"""Безопасный инкремент лайков с фиксацией в TbVote"""
|
||||||
|
return self.vote(visitor_uuid_or_fp, direction=TbVote.Direction.LIKE)
|
||||||
|
|
||||||
|
def increment_claims(self, visitor_uuid_or_fp: str) -> bool:
|
||||||
|
"""Безопасный инкремент жалоб с фиксацией в TbVote"""
|
||||||
|
return self.vote(visitor_uuid_or_fp, direction=TbVote.Direction.CLAIM)
|
||||||
|
|
||||||
def increment_promo_clicks(self):
|
def increment_promo_clicks(self):
|
||||||
"""Безопасный инкремент кликов по промо"""
|
"""Безопасный инкремент кликов по промо"""
|
||||||
TbHypn0Item.objects.get(id=self.id).update(i_promo_clicks=F('i_promo_clicks') + 1)
|
TbHypn0Item.objects.filter(id=self.id).update(i_promo_clicks=F('i_promo_clicks') + 1)
|
||||||
|
|
||||||
def rescore(self):
|
def rescore(self):
|
||||||
r"""Пересчет рейтинга популярности картины
|
r"""Пересчет рейтинга популярности картины
|
||||||
@@ -167,7 +219,7 @@ class TbHypn0Item(models.Model):
|
|||||||
"""
|
"""
|
||||||
age_in_hours = (timezone.now() - self.d_created_at).total_seconds() / 3600 + 1
|
age_in_hours = (timezone.now() - self.d_created_at).total_seconds() / 3600 + 1
|
||||||
new_score = (self.i_likes_count + self.i_level) / ((age_in_hours + 2) ** GAMMA)
|
new_score = (self.i_likes_count + self.i_level) / ((age_in_hours + 2) ** GAMMA)
|
||||||
TbHypn0Item.objects.get(id=self.id).update(f_score=new_score)
|
TbHypn0Item.objects.filter(id=self.id).update(f_score=new_score)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -247,8 +299,8 @@ class TbVote(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Голос / Лайк"
|
verbose_name = "Голос/Лайк"
|
||||||
verbose_name_plural = "Голоса / Лайки"
|
verbose_name_plural = "Голоса/Лайки"
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["k_item", "s_fingerprint"],
|
fields=["k_item", "s_fingerprint"],
|
||||||
|
|||||||
Reference in New Issue
Block a user