mod: модели (10) метод save() модели TbHypn0Item
This commit is contained in:
@@ -200,20 +200,6 @@ STORAGES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# СПЕЦИФИЧНЫЕ ДЛЯ ПРОЕКТА НАСТРОЙКИ
|
# СПЕЦИФИЧНЫЕ ДЛЯ ПРОЕКТА НАСТРОЙКИ
|
||||||
# ====== Уровни для SVG-генераций (используется для чисти файлов при достижении лимитов) ======
|
|
||||||
LVL_CANDIDATE = 0
|
|
||||||
LVL_PRE_MODERATED = 10
|
|
||||||
LVL_MODERATED = 30
|
|
||||||
LVL_LOCK_FOR_DELETION = 1000
|
|
||||||
|
|
||||||
# ====== Направление и веса для голосования (ТОЛЛЬКО ЦЕЛЫЕ ЧИСЛА) ======
|
|
||||||
VOTE_LIKE = 1
|
|
||||||
VOTE_CLAIM = -2
|
|
||||||
VOTE_AUTHOR = 2
|
|
||||||
|
|
||||||
# ====== Гамма для вычисления рейтинга ======
|
|
||||||
GAMMA = 1.5
|
|
||||||
|
|
||||||
# ====== HASHIDS настройка для s_hash_id ======
|
# ====== HASHIDS настройка для s_hash_id ======
|
||||||
# Криптографическое кодирование ID svg-файлов генерации в компактные, необратимые коды.
|
# Криптографическое кодирование ID svg-файлов генерации в компактные, необратимые коды.
|
||||||
# Благодаря hashids невозможно восстановить исходный ID из кода,
|
# Благодаря hashids невозможно восстановить исходный ID из кода,
|
||||||
|
|||||||
+59
-34
@@ -33,10 +33,10 @@ class TbHypn0Item(models.Model):
|
|||||||
• rescore(): пересчёт рейтинга f_score
|
• rescore(): пересчёт рейтинга f_score
|
||||||
"""
|
"""
|
||||||
class Level(models.IntegerChoices):
|
class Level(models.IntegerChoices):
|
||||||
CANDIDATE = LVL_CANDIDATE, 'Candidate: Шум сознания' # Свежая генерация. Запрет на удаление до Х дней.
|
CANDIDATE = 0, 'Candidate: Шум сознания' # Свежая генерация. Запрет на удаление до Х дней.
|
||||||
LEVEL_1 = LVL_PRE_MODERATED, 'Voted: Плеск бессознательного' # Прогрето, есть "лайки", не проверено модератором, можно удалять при низком score
|
LEVEL_1 = 10, 'Voted: Плеск бессознательного' # Прогрето, есть "лайки", не проверено модератором, можно удалять при низком score
|
||||||
LEVEL_2 = LVL_MODERATED, 'Moderated: Одобрено Мозговым Слизнем' # Есть "лайки", проверено, можно удалять при низком score
|
LEVEL_2 = 30, 'Moderated: Одобрено Мозговым Слизнем' # Есть "лайки", проверено, можно удалять при низком score
|
||||||
IMMORTAL = LVL_LOCK_FOR_DELETION, 'Locked: Глубокий транс' # Нельзя удалять, даже при низком score
|
IMMORTAL = 1000, 'Locked: Глубокий транс' # Нельзя удалять, даже при низком score
|
||||||
# Отрицательные / проблемные уровни (аномалии)
|
# Отрицательные / проблемные уровни (аномалии)
|
||||||
SHAMED = -20, 'Шейминг (аномальный наплыв жалоб)'
|
SHAMED = -20, 'Шейминг (аномальный наплыв жалоб)'
|
||||||
SUSPICIOUS = -10, 'Подозрение на накрутку (спайк лайков)'
|
SUSPICIOUS = -10, 'Подозрение на накрутку (спайк лайков)'
|
||||||
@@ -49,6 +49,7 @@ class TbHypn0Item(models.Model):
|
|||||||
s_hash_id = models.CharField(
|
s_hash_id = models.CharField(
|
||||||
max_length=16,
|
max_length=16,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
editable=False,
|
||||||
verbose_name="ID-Хэш",
|
verbose_name="ID-Хэш",
|
||||||
)
|
)
|
||||||
s_title = models.CharField(
|
s_title = models.CharField(
|
||||||
@@ -77,7 +78,7 @@ class TbHypn0Item(models.Model):
|
|||||||
|
|
||||||
# Популярность
|
# Популярность
|
||||||
i_likes_count = models.PositiveIntegerField(
|
i_likes_count = models.PositiveIntegerField(
|
||||||
default=0,
|
default=1,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name="Число лайков",
|
verbose_name="Число лайков",
|
||||||
help_text="Число лайков гипнотической SVG-картины. Поле используется для отображения в интерфейсе. Для расчета"
|
help_text="Число лайков гипнотической SVG-картины. Поле используется для отображения в интерфейсе. Для расчета"
|
||||||
@@ -85,7 +86,7 @@ class TbHypn0Item(models.Model):
|
|||||||
" <tt>Голоса/Лайка</tt> с учетом весов лайков и клаймов.",
|
" <tt>Голоса/Лайка</tt> с учетом весов лайков и клаймов.",
|
||||||
)
|
)
|
||||||
i_views_count = models.PositiveIntegerField(
|
i_views_count = models.PositiveIntegerField(
|
||||||
default=0,
|
default=1,
|
||||||
verbose_name="Число просмотров",
|
verbose_name="Число просмотров",
|
||||||
)
|
)
|
||||||
i_claims_count = models.PositiveIntegerField(
|
i_claims_count = models.PositiveIntegerField(
|
||||||
@@ -155,7 +156,7 @@ class TbHypn0Item(models.Model):
|
|||||||
"""Безопасный инкремент просмотров"""
|
"""Безопасный инкремент просмотров"""
|
||||||
TbHypn0Item.objects.filter(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 vote(self, visitor_uuid_or_fp: str, direction: int = VOTE_LIKE) -> bool:
|
def vote(self, visitor_uuid_or_fp: str, direction: int | None = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Универсальный метод учета голоса (лайк, жалоба или авторство).
|
Универсальный метод учета голоса (лайк, жалоба или авторство).
|
||||||
|
|
||||||
@@ -170,6 +171,9 @@ class TbHypn0Item(models.Model):
|
|||||||
if not visitor_uuid_or_fp:
|
if not visitor_uuid_or_fp:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if direction is None:
|
||||||
|
direction = TbVote.Direction.LIKE
|
||||||
|
|
||||||
# Если передан сырой UUID устройства, вычисляем SHA256 отпечаток
|
# Если передан сырой UUID устройства, вычисляем SHA256 отпечаток
|
||||||
if len(visitor_uuid_or_fp) == 64:
|
if len(visitor_uuid_or_fp) == 64:
|
||||||
fingerprint = visitor_uuid_or_fp
|
fingerprint = visitor_uuid_or_fp
|
||||||
@@ -214,37 +218,58 @@ class TbHypn0Item(models.Model):
|
|||||||
"""Безопасный инкремент кликов по промо"""
|
"""Безопасный инкремент кликов по промо"""
|
||||||
TbHypn0Item.objects.filter(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 save(self, *args, **kwargs):
|
def save(self, *args, visitor_uuid_or_fp: str | None = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Переопределенный метод save для:
|
Переопределенный метод save для TbHypn0Item.
|
||||||
1. Автоматического формирования s_hash_id (криптографический код)
|
|
||||||
|
|
||||||
ЛОГИКА s_hash_id:
|
ПРАВИЛА И СЦЕНАРИИ СОХРАНЕНИЯ:
|
||||||
- Для новых генераций: создаем код после получения ID
|
------------------------------
|
||||||
- Для старых генераций БЕЗ кода: кодируем существующий ID (миграция)
|
1. Создание новой картины (is_new=True):
|
||||||
- Для старых генераций С кодом: не трогаем
|
- Создание возможно ТОЛЬКО с фронтенда при наличии согласия на отслеживание (visitor_uuid_or_fp).
|
||||||
|
- Если пользователь не "подчинился Гипножабе" (visitor_uuid_or_fp отсутствует), создание БЛОКИРУЕТСЯ
|
||||||
|
выбрасыванием PermissionError (защита от сохранения анонимных работ без автора).
|
||||||
|
- В рамках одной атомарной транзакции:
|
||||||
|
а) Сохраняется запись картины и генерируется self.pk.
|
||||||
|
б) Генерируется криптографический s_hash_id.
|
||||||
|
в) Создается обязательная авторская запись TbVote(Direction.AUTHOR).
|
||||||
|
г) Стартовый счетчик i_likes_count по умолчанию равен 1.
|
||||||
|
|
||||||
|
2. Редактирование существующей картины (is_new=False, например из Django Admin):
|
||||||
|
- Картина уже существует в БД (self.pk есть).
|
||||||
|
- Разрешено обычное обновление любых полей (название, уровень модерации, рейтинг и т.д.).
|
||||||
|
- Никакие авторские записи повторно не создаются.
|
||||||
"""
|
"""
|
||||||
# 1. Проверяем нужно ли генерировать s_hash_id:
|
is_new = self.pk is None
|
||||||
# - ИЛИ это новый объект (self.pk == None)
|
|
||||||
# - ИЛИ это старый объект без s_hash_id (миграция)
|
|
||||||
if not self.pk or not self.s_hash_id:
|
|
||||||
# Если это новая генерация, сначала сохраняем её, чтобы получить ID
|
|
||||||
if not self.pk:
|
|
||||||
# Сохраняем БЕЗ s_hash_id чтобы Django создал запись и присвоил pk
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
# После save() Django автоматически заполнит self.pk
|
|
||||||
|
|
||||||
# Кодируем pk в компактный, необратимый код
|
# 0. Блокировка создания новой картины без подчинения Гипножабе
|
||||||
# Пример: pk=42 → "QBErd8"
|
if is_new and not visitor_uuid_or_fp:
|
||||||
self.s_hash_id = Hashids(salt=HASHIDS_SALT, min_length=HASHIDS_MIN_LENGTH).encode(self.pk)
|
raise PermissionError("Создание картины запрещено: пользователь не подчинился Гипножабе (отсутствует visitor UUID)")
|
||||||
|
|
||||||
# Сохраняем только поле s_hash_id (не перезаписываем остальное)
|
# 1. Вычисляем хэш автора для новой картины
|
||||||
super().save(update_fields=['s_hash_id'])
|
author_fp = None
|
||||||
else:
|
if is_new and visitor_uuid_or_fp:
|
||||||
# Объект существует И уже имеет s_hash_id: сохраняем как обычно
|
if len(visitor_uuid_or_fp) == 64:
|
||||||
# Не трогаем s_hash_id, он был сформирован при создании
|
author_fp = visitor_uuid_or_fp
|
||||||
|
else:
|
||||||
|
author_fp = hashlib.sha256(f"{visitor_uuid_or_fp}:{SECRET_KEY}".encode()).hexdigest()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# 2. Сохраняем объект в базу (для новых записей СУБД присваивает self.pk)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# 3. Формируем s_hash_id, если его еще нет
|
||||||
|
if not self.s_hash_id:
|
||||||
|
self.s_hash_id = Hashids(salt=HASHIDS_SALT, min_length=HASHIDS_MIN_LENGTH).encode(self.pk)
|
||||||
|
super().save(update_fields=['s_hash_id'])
|
||||||
|
|
||||||
|
# 4. Регистрируем автора в TbVote (гарантированно после получения self.pk)
|
||||||
|
if is_new and author_fp:
|
||||||
|
TbVote.objects.create(
|
||||||
|
k_item=self,
|
||||||
|
i_direction=TbVote.Direction.AUTHOR,
|
||||||
|
s_fingerprint=author_fp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TbVote(models.Model):
|
class TbVote(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -259,9 +284,9 @@ class TbVote(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
class Direction(models.IntegerChoices):
|
class Direction(models.IntegerChoices):
|
||||||
LIKE = VOTE_LIKE, 'Like (+1)'
|
LIKE = 1, 'Like (+1)'
|
||||||
CLAIM = VOTE_CLAIM, 'Claim (-2)'
|
CLAIM = -2, 'Claim (-2)'
|
||||||
AUTHOR = VOTE_AUTHOR, 'Author Like (+2)'
|
AUTHOR = 2, 'Author Like (+2)'
|
||||||
|
|
||||||
id = models.BigAutoField(
|
id = models.BigAutoField(
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user