add: модели и админка (1) драфт

This commit is contained in:
2026-08-17 23:53:54 +03:00
parent d430ac5f0a
commit 5ec89c323d
2 changed files with 262 additions and 2 deletions
+72 -1
View File
@@ -1,3 +1,74 @@
from django.contrib import admin
# Register your models here.
from .models import BlogPost, HalftoneItem, HalftoneVote
@admin.register(HalftoneItem)
class HalftoneItemAdmin(admin.ModelAdmin):
list_display = (
"hash_id",
"title",
"likes_count",
"views_count",
"score",
"is_curated",
"is_public",
"created_at",
)
list_filter = (
"is_curated",
"is_public",
"created_at",
)
search_fields = (
"hash_id",
"title",
"promo_title",
)
readonly_fields = (
"created_at",
"score",
"file_size",
)
@admin.register(HalftoneVote)
class HalftoneVoteAdmin(admin.ModelAdmin):
list_display = (
"item",
"fingerprint",
"created_at",
)
list_filter = ("created_at",)
search_fields = (
"item__hash_id",
"item__title",
"fingerprint",
)
readonly_fields = ("created_at",)
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
list_display = (
"title",
"slug",
"is_published",
"published_at",
"created_at",
)
list_filter = (
"is_published",
"published_at",
)
search_fields = (
"title",
"slug",
"summary",
"content",
)
prepopulated_fields = {"slug": ("title",)}
readonly_fields = (
"created_at",
"updated_at",
)
+190 -1
View File
@@ -1,3 +1,192 @@
from django.db import models
# Create your models here.
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