/
Anubis55
/
Web-Polytech
Обзор
Документация
Войти
/
Anubis55
/
Web-Polytech
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
comics/models.py
418 строк
14 KB
Anubis55
оптимизированы страницы: главная, страница комикса с 21 и 17 запросов до 11 и 12 соответственно
25 июн 2026, 10:35
25 июн 2026, 10:35
10caa89
Код
Авторство
О чём код?
from typing import Optional, List, Union, Any from django.db import models from django.contrib.auth.models import User from django.utils import timezone from django.urls import reverse from django.db.models import Q, QuerySet from django.core.exceptions import ValidationError import re from simple_history.models import HistoricalRecords class Genre(models.Model): """ Модель жанра комикса. """ name = models.CharField('Название жанра', max_length=100, unique=True) slug = models.SlugField('Слаг', max_length=100, unique=True, blank=True) class Meta: verbose_name = 'Жанр' verbose_name_plural = 'Жанры' ordering = ['name'] def __str__(self) -> str: """Возвращает название жанра.""" return self.name def get_absolute_url(self) -> str: """Возвращает URL для просмотра жанра.""" return reverse('genre_detail', args=[self.slug]) class Author(models.Model): """ Модель автора комикса. """ name = models.CharField('Имя автора', max_length=100) slug = models.SlugField('Слаг', max_length=100, unique=True, blank=True) collaborators = models.ManyToManyField( 'self', through='Authorship', through_fields=('author_from', 'author_to'), blank=True ) class Meta: verbose_name = 'Автор' verbose_name_plural = 'Авторы' ordering = ['name'] def __str__(self) -> str: """Возвращает имя автора.""" return self.name def get_absolute_url(self) -> str: """Возвращает URL для просмотра автора.""" return reverse('author_detail', args=[self.slug]) def all_collaborators(self) -> QuerySet: """ Возвращает всех соавторов автора. Returns: QuerySet: Список авторов, с которыми есть связь через Authorship. """ return Author.objects.filter( Q(outgoing__author_to=self) | Q(incoming__author_from=self) ).distinct() class Authorship(models.Model): """ Модель связи соавторства между авторами. """ author_from = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='outgoing' ) author_to = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='incoming' ) strength = models.PositiveSmallIntegerField(default=1) class Meta: unique_together = ('author_from', 'author_to') def __str__(self) -> str: """Возвращает строковое представление связи соавторства.""" return f"{self.author_from} -> {self.author_to} (strength={self.strength})" class Comic(models.Model): """ Модель комикса (манги, манхвы). """ STATUS_CHOICES = ( ('ongoing', 'Выходит'), ('completed', 'Завершён'), ) title = models.CharField('Название', max_length=200, unique=True) slug = models.SlugField('Слаг', max_length=200, unique=True, blank=True) description = models.TextField('Описание', blank=True) cover = models.ImageField('Обложка', upload_to='covers/') authors = models.ManyToManyField( Author, verbose_name='Авторы', blank=True, related_name='comics' ) genres = models.ManyToManyField( Genre, verbose_name='Жанры', blank=True, related_name='comics' ) status = models.CharField( 'Статус', max_length=20, choices=STATUS_CHOICES, default='ongoing' ) views = models.PositiveIntegerField('Просмотры', default=0) official_url = models.URLField( 'Официальный сайт', max_length=200, blank=True, help_text='Ссылка на официальную страницу комикса' ) created_at = models.DateTimeField('Дата добавления', auto_now_add=True) updated_at = models.DateTimeField('Дата обновления', auto_now=True) history = HistoricalRecords() class Meta: verbose_name = 'Комикс' verbose_name_plural = 'Комиксы' ordering = ['-created_at'] def __str__(self) -> str: """Возвращает название комикса.""" return self.title def get_absolute_url(self) -> str: """Возвращает URL для просмотра комикса.""" return reverse('comics:manga_detail', args=[self.slug]) @property def average_rating(self) -> float: """ Вычисляет среднее арифметическое всех оценок комикса. """ ratings = self.ratings.all() if ratings: return sum(r.score for r in ratings) / len(ratings) return 0 def display_authors(self) -> str: """ Возвращает список авторов комикса в виде строки. Returns: str: Имена авторов через запятую. """ return ', '.join([a.name for a in self.authors.all()]) def clean(self) -> None: """ Проверяет, что название содержит минимум 2 символа. """ if len(self.title) < 2: raise ValidationError( {'title': 'Название комикса должно содержать минимум 2 символа.'} ) def save(self, *args, **kwargs) -> None: """ Сохраняет комикс с предварительной валидацией. Args: *args: Дополнительные позиционные аргументы. **kwargs: Дополнительные именованные аргументы. """ self.full_clean() super().save(*args, **kwargs) class Chapter(models.Model): """ Модель главы комикса. """ comic = models.ForeignKey( Comic, on_delete=models.CASCADE, verbose_name='Комикс', related_name='chapters' ) number = models.PositiveIntegerField('Номер главы') title = models.CharField('Название главы', max_length=200, blank=True) content = models.TextField('Содержание') file = models.FileField( 'Файл главы (PDF, ZIP и др.)', upload_to='chapters/', blank=True, null=True ) views = models.PositiveIntegerField('Просмотры', default=0) published_at = models.DateTimeField('Дата публикации', default=timezone.now) history = HistoricalRecords() class Meta: verbose_name = 'Глава' verbose_name_plural = 'Главы' ordering = ['comic', 'number'] unique_together = ('comic', 'number') def __str__(self) -> str: """Возвращает строковое представление главы.""" return f'{self.comic.title} – Глава {self.number}' def get_absolute_url(self) -> str: """Возвращает URL для просмотра главы.""" return reverse('comics:chapter_detail', args=[self.pk]) class Rating(models.Model): """ Модель оценки комикса пользователем. """ user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='ratings') comic = models.ForeignKey(Comic, on_delete=models.CASCADE, verbose_name='Комикс', related_name='ratings') score = models.PositiveSmallIntegerField('Оценка', choices=[(i, i) for i in range(1, 6)]) rated_at = models.DateTimeField('Дата оценки', auto_now_add=True) class Meta: verbose_name = 'Оценка' verbose_name_plural = 'Оценки' unique_together = ('user', 'comic') def __str__(self) -> str: """Возвращает строковое представление оценки.""" return f'{self.user.username} оценил {self.comic.title} на {self.score}' class Comment(models.Model): """ Модель комментария к комиксу. """ user = models.ForeignKey( User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='comments' ) comic = models.ForeignKey( Comic, on_delete=models.CASCADE, verbose_name='Комикс', related_name='comments' ) parent = models.ForeignKey( 'self', on_delete=models.CASCADE, verbose_name='Ответ на', blank=True, null=True, related_name='replies' ) text = models.TextField('Текст комментария') created_at = models.DateTimeField('Дата создания', auto_now_add=True) is_approved = models.BooleanField('Одобрен', default=True) history = HistoricalRecords() class Meta: verbose_name = 'Комментарий' verbose_name_plural = 'Комментарии' ordering = ['-created_at'] def __str__(self) -> str: """Возвращает строковое представление комментария.""" return f'{self.user.username}: {self.text[:30]}...' def save(self, *args, **kwargs) -> None: """ Сохраняет комментарий с применением цензуры. """ if not self.text or self.text.strip() == '': self.text = '(пустое сообщение)' else: replacements = { 'блин': 'б**н', 'чёрт': 'ч**т', 'дурак': 'д***к', 'идиот': 'и****т', 'дебил': 'д***л', 'урод': 'у**д', 'козел': 'к**л', 'лох': 'л**', 'олень': 'о***ь', 'сволочь': 'с****ь', 'гад': 'г**', 'тварь': 'т***ь', 'сука': 'с**а', 'блядь': 'б****ь', 'хуй': 'х**', 'пизда': 'п****а', 'залупа': 'з****а', 'мудак': 'м***к', 'редиска': 'р****а', } for bad, good in replacements.items(): self.text = re.sub(re.escape(bad), good, self.text, flags=re.IGNORECASE) super().save(*args, **kwargs) class Profile(models.Model): """ Модель профиля пользователя. """ ROLE_CHOICES = ( ('user', 'Читатель'), ('moderator', 'Модератор'), ('admin', 'Администратор'), ) user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='profile') avatar = models.ImageField('Аватар', upload_to='avatars/', blank=True, null=True) bio = models.TextField('О себе', blank=True) role = models.CharField('Роль', max_length=20, choices=ROLE_CHOICES, default='user') class Meta: verbose_name = 'Профиль' verbose_name_plural = 'Профили' def __str__(self) -> str: """Возвращает строковое представление профиля.""" return f'Профиль {self.user.username}' class Favorite(models.Model): """ Модель закладки (избранного) комикса для пользователя. """ STATUS_CHOICES = ( ('reading', 'Читаю'), ('completed', 'Прочитано'), ) user = models.ForeignKey( User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='favorites' ) comic = models.ForeignKey( Comic, on_delete=models.CASCADE, verbose_name='Комикс', related_name='favorites' ) status = models.CharField( 'Статус', max_length=20, choices=STATUS_CHOICES, default='reading' ) added_at = models.DateTimeField('Дата добавления', auto_now_add=True) class Meta: verbose_name = 'Избранное' verbose_name_plural = 'Избранное' unique_together = ('user', 'comic') def __str__(self) -> str: """Возвращает строковое представление закладки.""" return f'{self.user.username} – {self.comic.title} ({self.get_status_display()})' class Appeal(models.Model): """ Модель обращения пользователя в техподдержку. """ name = models.CharField('Ваше имя', max_length=100) email = models.EmailField('Email') message = models.TextField('Сообщение') created_at = models.DateTimeField('Дата создания', auto_now_add=True) class Meta: verbose_name = 'Обращение' verbose_name_plural = 'Обращения' ordering = ['-created_at'] def __str__(self) -> str: """Возвращает строковое представление обращения.""" return f'Обращение от {self.name} ({self.email})' class AppealFile(models.Model): """ Модель файла, прикреплённого к обращению. """ appeal = models.ForeignKey( Appeal, on_delete=models.CASCADE, related_name='files', verbose_name='Обращение' ) file = models.FileField('Файл', upload_to='appeals/') class Meta: verbose_name = 'Файл обращения' verbose_name_plural = 'Файлы обращений' def __str__(self) -> str: """Возвращает строковое представление файла обращения.""" return f'Файл для обращения #{self.appeal.id}: {self.file.name}'