193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
from django.db import models
|
|
|
|
|
|
class HalftoneItem(models.Model):
|
|
"""Единая модель для генераций, публичных шеров и витрины галереи."""
|
|
|
|
hash_id = models.CharField(
|
|
max_length=16,
|
|
unique=True,
|
|
db_index=True,
|
|
verbose_name="Хэш-идентификатор",
|
|
)
|
|
title = models.CharField(
|
|
max_length=150,
|
|
verbose_name="Название картины",
|
|
)
|
|
svg_file = models.FileField(
|
|
upload_to="svg/%Y/%m/",
|
|
verbose_name="SVG-файл",
|
|
)
|
|
|
|
# Снимок параметров для возможного клонирования настроек
|
|
params_json = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
verbose_name="Параметры генерации",
|
|
)
|
|
file_size = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text="Размер файла в байтах",
|
|
verbose_name="Размер файла",
|
|
)
|
|
|
|
# Популярность и модерация
|
|
likes_count = models.PositiveIntegerField(
|
|
default=0,
|
|
db_index=True,
|
|
verbose_name="Количество лайков",
|
|
)
|
|
views_count = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name="Количество просмотров",
|
|
)
|
|
reports_count = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name="Количество жалоб",
|
|
)
|
|
score = models.FloatField(
|
|
default=0.0,
|
|
db_index=True,
|
|
help_text="Рейтинг для Smart Retention",
|
|
verbose_name="Рейтинг популярности",
|
|
)
|
|
|
|
is_curated = models.BooleanField(
|
|
default=False,
|
|
help_text="Выбор модератора / галерея (иммунитет к удалению)",
|
|
verbose_name="Выбор редакции",
|
|
)
|
|
is_public = models.BooleanField(
|
|
default=True,
|
|
help_text="Доступность по прямой ссылке",
|
|
verbose_name="Публичный доступ",
|
|
)
|
|
|
|
# Промо-блок (на перспективу)
|
|
promo_url = models.URLField(
|
|
blank=True,
|
|
default="",
|
|
verbose_name="Промо-ссылка",
|
|
)
|
|
promo_title = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default="",
|
|
verbose_name="Бейдж/автор промо",
|
|
)
|
|
promo_clicks = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name="Клики по промо",
|
|
)
|
|
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
db_index=True,
|
|
verbose_name="Дата создания",
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Гипно-картина"
|
|
verbose_name_plural = "Гипно-картины"
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.title} ({self.hash_id})"
|
|
|
|
|
|
class HalftoneVote(models.Model):
|
|
"""Анонимный учет голосов (лайков) без сохранения ПДн."""
|
|
|
|
item = models.ForeignKey(
|
|
HalftoneItem,
|
|
on_delete=models.CASCADE,
|
|
related_name="votes",
|
|
verbose_name="Картина",
|
|
)
|
|
fingerprint = models.CharField(
|
|
max_length=64,
|
|
db_index=True,
|
|
help_text="SHA256(visitor_uuid + SECRET_KEY) для защиты от накрутки и разделения устройств",
|
|
verbose_name="Хэш устройства/отпечаток",
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name="Дата голосования",
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Голос / Лайк"
|
|
verbose_name_plural = "Голоса / Лайки"
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["item", "fingerprint"],
|
|
name="unique_item_fingerprint_vote",
|
|
)
|
|
]
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"Голос за {self.item.hash_id} [{self.fingerprint[:8]}...]"
|
|
|
|
|
|
class BlogPost(models.Model):
|
|
"""
|
|
Статьи блога, документация и инфо-страницы (Privacy Policy и др.).
|
|
|
|
+ виртуальные поля для управления типографом etpgrf
|
|
"""
|
|
|
|
title = models.CharField(
|
|
max_length=200,
|
|
help_text="HTML-заголовок, очищенный и типографированный etpgrf",
|
|
verbose_name="Заголовок",
|
|
)
|
|
slug = models.SlugField(
|
|
max_length=200,
|
|
unique=True,
|
|
verbose_name="URL-слаг",
|
|
)
|
|
summary = models.TextField(
|
|
max_length=500,
|
|
blank=True,
|
|
help_text="HTML-контент тизера, очищенный и типографированный etpgrf",
|
|
verbose_name="Краткое описание / Лид",
|
|
)
|
|
content = models.TextField(
|
|
help_text="HTML-контент статьи, очищенный и типографированный etpgrf",
|
|
verbose_name="Основной HTML-контент",
|
|
)
|
|
cover_image = models.ImageField(
|
|
upload_to="blog/covers/%Y/",
|
|
blank=True,
|
|
null=True,
|
|
verbose_name="Обложка",
|
|
)
|
|
|
|
is_published = models.BooleanField(
|
|
default=False,
|
|
verbose_name="Опубликовано",
|
|
)
|
|
published_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
db_index=True,
|
|
verbose_name="Дата публикации",
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name="Дата создания",
|
|
)
|
|
updated_at = models.DateTimeField(
|
|
auto_now=True,
|
|
verbose_name="Дата обновления",
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Статья блога / Страница"
|
|
verbose_name_plural = "Статьи блога / Страницы"
|
|
ordering = ["-published_at", "-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.title
|