mod: модели и админка (2) драфт

This commit is contained in:
2026-08-18 17:03:35 +03:00
parent 5ec89c323d
commit 6850df10e2
3 changed files with 142 additions and 118 deletions
+7
View File
@@ -199,6 +199,13 @@ STORAGES = {
}, },
} }
# Специфичные для проекта настройки
LVL_CANDIDATE = 10
LVL_PRE_MODERATED = 20
LVL_MODERATED = 30
LVL_LOCK_FOR_DELETION = 40
if DEBUG: if DEBUG:
# В деве оставляем стандартную отдачу статики Django без WhiteNoise. # В деве оставляем стандартную отдачу статики Django без WhiteNoise.
STORAGES['staticfiles'] = { STORAGES['staticfiles'] = {
+74 -74
View File
@@ -1,74 +1,74 @@
from django.contrib import admin # from django.contrib import admin
#
from .models import BlogPost, HalftoneItem, HalftoneVote # from .models import BlogPost, HalftoneItem, HalftoneVote
#
#
@admin.register(HalftoneItem) # @admin.register(HalftoneItem)
class HalftoneItemAdmin(admin.ModelAdmin): # class HalftoneItemAdmin(admin.ModelAdmin):
list_display = ( # list_display = (
"hash_id", # "hash_id",
"title", # "title",
"likes_count", # "likes_count",
"views_count", # "views_count",
"score", # "score",
"is_curated", # "is_curated",
"is_public", # "is_public",
"created_at", # "created_at",
) # )
list_filter = ( # list_filter = (
"is_curated", # "is_curated",
"is_public", # "is_public",
"created_at", # "created_at",
) # )
search_fields = ( # search_fields = (
"hash_id", # "hash_id",
"title", # "title",
"promo_title", # "promo_title",
) # )
readonly_fields = ( # readonly_fields = (
"created_at", # "created_at",
"score", # "score",
"file_size", # "file_size",
) # )
#
#
@admin.register(HalftoneVote) # @admin.register(HalftoneVote)
class HalftoneVoteAdmin(admin.ModelAdmin): # class HalftoneVoteAdmin(admin.ModelAdmin):
list_display = ( # list_display = (
"item", # "item",
"fingerprint", # "fingerprint",
"created_at", # "created_at",
) # )
list_filter = ("created_at",) # list_filter = ("created_at",)
search_fields = ( # search_fields = (
"item__hash_id", # "item__hash_id",
"item__title", # "item__title",
"fingerprint", # "fingerprint",
) # )
readonly_fields = ("created_at",) # readonly_fields = ("created_at",)
#
#
@admin.register(BlogPost) # @admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin): # class BlogPostAdmin(admin.ModelAdmin):
list_display = ( # list_display = (
"title", # "title",
"slug", # "slug",
"is_published", # "is_published",
"published_at", # "published_at",
"created_at", # "created_at",
) # )
list_filter = ( # list_filter = (
"is_published", # "is_published",
"published_at", # "published_at",
) # )
search_fields = ( # search_fields = (
"title", # "title",
"slug", # "slug",
"summary", # "summary",
"content", # "content",
) # )
prepopulated_fields = {"slug": ("title",)} # prepopulated_fields = {"slug": ("title",)}
readonly_fields = ( # readonly_fields = (
"created_at", # "created_at",
"updated_at", # "updated_at",
) # )
+61 -44
View File
@@ -1,61 +1,73 @@
from django.db import models from django.db import models
from hypn0.settings import *
class Item(models.Model):
class HalftoneItem(models.Model):
"""Единая модель для генераций, публичных шеров и витрины галереи.""" """Единая модель для генераций, публичных шеров и витрины галереи."""
class Level(models.IntegerChoices):
CANDIDATE = LVL_CANDIDATE, 'Шум сознания' # Свежая генерация. Запрет на удаление до Х дней.
LEVEL_1 = LVL_PRE_MODERATED, 'Плеск бессознательного' # Есть "лайки", не проверено, можно удалять при низком score
LEVEL_2 = LVL_MODERATED, 'Одобрено Мозговым Слизнем' # Есть "лайки", проверено, можно удалять при низком score
IMMORTAL = LVL_LOCK_FOR_DELETION, 'Глубокий транс' # Нельзя удалять, даже при низком score
hash_id = models.CharField( s_hash_id = models.CharField(
max_length=16, max_length=16,
unique=True, unique=True,
db_index=True, db_index=True,
verbose_name="Хэш-идентификатор", verbose_name="Хэш-идентификатор",
) )
title = models.CharField( s_title = models.CharField(
max_length=150, max_length=255,
verbose_name="Название картины", verbose_name="Название картины",
) )
svg_file = models.FileField( file_svg = models.FileField(
upload_to="svg/%Y/%m/", upload_to="svg/%Y/%m/",
verbose_name="SVG-файл", verbose_name="SVG-файл",
) )
i_sha1 = models.CharField(
# Снимок параметров для возможного клонирования настроек max_length=40,
params_json = models.JSONField( verbose_name="SHA1",
default=dict, )
blank=True, i_file_size = models.PositiveIntegerField(
verbose_name="Параметры генерации",
)
file_size = models.PositiveIntegerField(
default=0, default=0,
help_text="Размер файла в байтах", help_text="Размер файла в байтах",
verbose_name="Размер файла", verbose_name="Размер файла",
) )
# Популярность и модерация # Снимок параметров для возможного клонирования настроек
likes_count = models.PositiveIntegerField( j_params = models.JSONField(
default=dict,
blank=True,
verbose_name="Параметры генерации",
help_text="Метаданные и параметры для возможного клонирования настроек",
)
# Популярность
i_likes_count = models.PositiveIntegerField(
default=0, default=0,
db_index=True, db_index=True,
verbose_name="Количество лайков", verbose_name="Количество лайков",
) )
views_count = models.PositiveIntegerField( i_views_count = models.PositiveIntegerField(
default=0, default=0,
verbose_name="Количество просмотров", verbose_name="Количество просмотров",
) )
reports_count = models.PositiveIntegerField( i_reports_count = models.PositiveIntegerField(
default=0, default=0,
verbose_name="Количество жалоб", verbose_name="Количество жалоб",
) )
score = models.FloatField( f_score = models.FloatField(
default=0.0, default=0.0,
db_index=True, db_index=True,
help_text="Рейтинг для Smart Retention", help_text="Рейтинг для Smart Retention",
verbose_name="Рейтинг популярности", verbose_name="Рейтинг популярности",
) )
is_curated = models.BooleanField( # Модерация
default=False, i_level = models.IntegerField(
help_text="Выбор модератора / галерея (иммунитет к удалению)", choices=Level.choices,
verbose_name="Выбор редакции", default=Level.CANDIDATE,
help_text="Уровень доступа",
verbose_name="Уровень доступа",
) )
is_public = models.BooleanField( is_public = models.BooleanField(
default=True, default=True,
@@ -64,32 +76,37 @@ class HalftoneItem(models.Model):
) )
# Промо-блок (на перспективу) # Промо-блок (на перспективу)
promo_url = models.URLField( s_promo_url = models.URLField(
blank=True, blank=True,
default="", default="",
verbose_name="Промо-ссылка", verbose_name="Промо-ссылка",
) )
promo_title = models.CharField( s_promo_title = models.CharField(
max_length=100, max_length=100,
blank=True, blank=True,
default="", default="",
verbose_name="Бейдж/автор промо", verbose_name="Бейдж/автор промо",
) )
promo_clicks = models.PositiveIntegerField( i_promo_clicks = models.PositiveIntegerField(
default=0, default=0,
verbose_name="Клики по промо", verbose_name="Клики по промо",
) )
created_at = models.DateTimeField( d_created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
db_index=True, db_index=True,
verbose_name="Дата создания", verbose_name="Дата создания",
) )
d_updated_at = models.DateTimeField(
auto_now=True,
db_index=True,
verbose_name="Дата обновления",
)
class Meta: class Meta:
verbose_name = "Гипно-картина" verbose_name = "Гипно-картина"
verbose_name_plural = "Гипно-картины" verbose_name_plural = "Гипно-картины"
ordering = ["-created_at"] ordering = ["-d_created_at"]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.title} ({self.hash_id})" return f"{self.title} ({self.hash_id})"
@@ -98,19 +115,19 @@ class HalftoneItem(models.Model):
class HalftoneVote(models.Model): class HalftoneVote(models.Model):
"""Анонимный учет голосов (лайков) без сохранения ПДн.""" """Анонимный учет голосов (лайков) без сохранения ПДн."""
item = models.ForeignKey( k_item = models.ForeignKey(
HalftoneItem, Item,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="votes", related_name="votes",
verbose_name="Картина", verbose_name="Картина",
) )
fingerprint = models.CharField( s_fingerprint = models.CharField(
max_length=64, max_length=64,
db_index=True, db_index=True,
help_text="SHA256(visitor_uuid + SECRET_KEY) для защиты от накрутки и разделения устройств", help_text="SHA256(visitor_uuid + SECRET_KEY) для защиты от накрутки и разделения устройств",
verbose_name="Хэш устройства/отпечаток", verbose_name="Хэш устройства/отпечаток",
) )
created_at = models.DateTimeField( d_created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name="Дата голосования", verbose_name="Дата голосования",
) )
@@ -120,11 +137,11 @@ class HalftoneVote(models.Model):
verbose_name_plural = "Голоса / Лайки" verbose_name_plural = "Голоса / Лайки"
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["item", "fingerprint"], fields=["k_item", "s_fingerprint"],
name="unique_item_fingerprint_vote", name="unique_item_fingerprint_vote",
) )
] ]
ordering = ["-created_at"] ordering = ["-d_created_at"]
def __str__(self) -> str: def __str__(self) -> str:
return f"Голос за {self.item.hash_id} [{self.fingerprint[:8]}...]" return f"Голос за {self.item.hash_id} [{self.fingerprint[:8]}...]"
@@ -137,7 +154,7 @@ class BlogPost(models.Model):
+ виртуальные поля для управления типографом etpgrf + виртуальные поля для управления типографом etpgrf
""" """
title = models.CharField( s_title = models.CharField(
max_length=200, max_length=200,
help_text="HTML-заголовок, очищенный и типографированный etpgrf", help_text="HTML-заголовок, очищенный и типографированный etpgrf",
verbose_name="Заголовок", verbose_name="Заголовок",
@@ -147,17 +164,17 @@ class BlogPost(models.Model):
unique=True, unique=True,
verbose_name="URL-слаг", verbose_name="URL-слаг",
) )
summary = models.TextField( s_summary = models.TextField(
max_length=500, max_length=1024,
blank=True, blank=True,
help_text="HTML-контент тизера, очищенный и типографированный etpgrf", help_text="HTML-контент тизера, очищенный и типографированный etpgrf",
verbose_name="Краткое описание / Лид", verbose_name="Краткое описание / Лид",
) )
content = models.TextField( s_content = models.TextField(
help_text="HTML-контент статьи, очищенный и типографированный etpgrf", help_text="HTML-контент статьи, очищенный и типографированный etpgrf",
verbose_name="Основной HTML-контент", verbose_name="Основной HTML-контент",
) )
cover_image = models.ImageField( f_cover_img = models.ImageField(
upload_to="blog/covers/%Y/", upload_to="blog/covers/%Y/",
blank=True, blank=True,
null=True, null=True,
@@ -168,17 +185,17 @@ class BlogPost(models.Model):
default=False, default=False,
verbose_name="Опубликовано", verbose_name="Опубликовано",
) )
published_at = models.DateTimeField( d_published_at = models.DateTimeField(
null=True, null=True,
blank=True, blank=True,
db_index=True, db_index=True,
verbose_name="Дата публикации", verbose_name="Дата публикации",
) )
created_at = models.DateTimeField( d_created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name="Дата создания", verbose_name="Дата создания",
) )
updated_at = models.DateTimeField( d_created_at = models.DateTimeField(
auto_now=True, auto_now=True,
verbose_name="Дата обновления", verbose_name="Дата обновления",
) )
@@ -186,7 +203,7 @@ class BlogPost(models.Model):
class Meta: class Meta:
verbose_name = "Статья блога / Страница" verbose_name = "Статья блога / Страница"
verbose_name_plural = "Статьи блога / Страницы" verbose_name_plural = "Статьи блога / Страницы"
ordering = ["-published_at", "-created_at"] ordering = ["-d_published_at", "-d_created_at"]
def __str__(self) -> str: def __str__(self) -> str:
return self.title return self.s_title