import os import uuid from django.core.files.storage import default_storage from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from django.db.models.signals import pre_delete from django.dispatch import receiver from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import slugify from django.conf import settings from imagekit.models import ProcessedImageField from imagekit.processors import ResizeToFit from PIL import Image def upload_to(instance, filename): return f"headphones/{instance.product.id}/{filename}" def upload_to_case_image(instance, filename): """Путь для сохранения: headphones//case_images/.""" ext = filename.split('.')[-1] return f"headphones/{instance.headphones.id}/case_images/{instance.slug}.{ext}" # ====================== # Базовые справочники # ====================== class BrandCountry(models.Model): name = models.CharField(max_length=50, verbose_name="Название страны") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Страна производитель" def __str__(self): return self.name class CaseColors(models.Model): headphones = models.ForeignKey( 'Headphones', on_delete=models.CASCADE, related_name='case_colors', verbose_name='Наушники' ) name_ru = models.CharField('Название цвета (рус)', max_length=100) name_en = models.CharField('Название цвета (англ)', max_length=100) slug = models.SlugField(max_length=150, unique=True, blank=True, verbose_name="URL-идентификатор") image = models.ImageField('Изображение', upload_to=upload_to_case_image) def save(self, *args, **kwargs): if not self.slug: # Генерируем slug в формате "модельнаушников_цветангл" base_slug = slugify(f"{self.headphones.model}_{self.name_en}") self.slug = base_slug # Проверяем уникальность slug counter = 1 while CaseColors.objects.filter(slug=self.slug).exists(): self.slug = f"{base_slug}-{counter}" counter += 1 super().save(*args, **kwargs) def image_preview(self): if self.image: return format_html('', self.image.url) return "Нет изображения" image_preview.short_description = 'Превью' image_preview.allow_tags = True def __str__(self): return f"{self.name_ru} ({self.headphones.model})" class Meta: verbose_name = 'Цвет корпуса' verbose_name_plural = 'Цвета корпусов' ordering = ['headphones__model', 'name_ru'] # ====================== # Основные модели брендов и категорий # ====================== class Brand(models.Model): name = models.CharField(max_length=50, verbose_name="Название бренда") country = models.ForeignKey( BrandCountry, on_delete=models.CASCADE, verbose_name="Страна производитель" ) year_founded = models.PositiveSmallIntegerField(verbose_name="Год основания") is_year_approx = models.BooleanField( verbose_name="Год указан приблизительно", default=False ) site = models.URLField(verbose_name="Официальный сайт") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") # Рекурсивная связь (может быть null, если это родительская компания) parent_company = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='subsidiaries', verbose_name="Родительская компания" ) class Meta: verbose_name = "Бренд" verbose_name_plural = "Бренды" def __str__(self): return self.name class CaseType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип корпуса") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Тип корпуса" verbose_name_plural = "Типы корпусов" def __str__(self): return self.name class CaseMaterial(models.Model): name = models.CharField(max_length=50, verbose_name="Материал корпуса") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Материал корпуса" verbose_name_plural = "Материалы корпусов" def __str__(self): return self.name class HeadphonePurpose(models.Model): name = models.CharField(max_length=50, verbose_name="Назначение") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Назначение наушников" verbose_name_plural = "Назначения наушников" def __str__(self): return self.name # ====================== # Теги и категории # ====================== class TagCategory(models.Model): name = models.CharField(max_length=50, verbose_name="Название категории") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Категория тегов" verbose_name_plural = "Категории тегов" def __str__(self): return self.name class Tags(models.Model): name = models.CharField(max_length=50, verbose_name="Название тега") category = models.ForeignKey( TagCategory, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Категория" ) slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Тег" verbose_name_plural = "Теги" ordering = ['category__name', 'name'] def __str__(self): return f"{self.category.name}: {self.name}" if self.category else self.name # ====================== # Аудио компоненты # ====================== class DriverModel(models.Model): name = models.CharField(max_length=50, verbose_name="Модель драйвера") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Модель драйвера" verbose_name_plural = "Модели драйверов" def __str__(self): return self.name class DriverType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип драйвера") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Тип драйвера" verbose_name_plural = "Типы драйверов" def __str__(self): return self.name class Driver(models.Model): driver_type = models.ForeignKey( DriverType, on_delete=models.CASCADE, verbose_name="Тип драйвера" ) driver_model = models.ForeignKey( DriverModel, on_delete=models.CASCADE, verbose_name="Модель драйвера", blank=True, null=True ) size = models.DecimalField( verbose_name="Размер (мм)", max_digits=4, decimal_places=1, null=True, blank=True ) frequency_range = models.CharField( verbose_name="Частотный диапазон (Гц)", max_length=50, null=True, blank=True ) class Meta: verbose_name = "Драйвер" verbose_name_plural = "Драйверы" def __str__(self): parts = [self.driver_type.name] if self.driver_model: parts.append(self.driver_model.name) if self.size: parts.append(f"{self.size}mm") return " ".join(parts) class NoiseCancellationType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип шумоподавления") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Тип шумоподавления" def __str__(self): return self.name class CodecType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип кодека") description = models.TextField(blank=True, verbose_name="Описание") class Meta: verbose_name = "Тип аудиокодека" verbose_name_plural = "Типы аудиокодеков" def __str__(self): return self.name class AudioCodec(models.Model): name = models.CharField(max_length=50, verbose_name="Название кодека") codec_type = models.ForeignKey( CodecType, on_delete=models.PROTECT, verbose_name="Тип кодека" ) max_bitrate = models.PositiveIntegerField( verbose_name="Макс. битрейт (кбит/с)", null=True, blank=True ) max_sample_rate = models.PositiveIntegerField( verbose_name="Макс. частота (кГц)", null=True, blank=True ) bit_depth = models.PositiveSmallIntegerField( verbose_name="Битовая глубина", null=True, blank=True ) latency = models.PositiveSmallIntegerField( verbose_name="Задержка (мс)", null=True, blank=True ) description = models.TextField(blank=True, verbose_name="Описание") class Meta: verbose_name = "Аудиокодек" verbose_name_plural = "Аудиокодеки" def __str__(self): return f"{self.name} ({self.codec_type})" # ====================== # Разъемы и подключения # ====================== class CableConnectionType(models.Model): name = models.CharField(max_length=50, verbose_name="Название разъема") description = models.CharField( max_length=100, blank=True, verbose_name="Описание" ) image = models.ImageField( upload_to='connectors/', blank=True, null=True, verbose_name="Изображение" ) is_detachable = models.BooleanField( default=True, verbose_name="Съемный разъем" ) class Meta: verbose_name = "Тип разъема кабеля" verbose_name_plural = "Типы разъемов кабелей" def __str__(self): return self.name class CableMaterial(models.Model): name = models.CharField(max_length=50, verbose_name="Материал кабеля") description = models.CharField( max_length=100, blank=True, verbose_name="Описание" ) class Meta: verbose_name = "Материал кабеля" verbose_name_plural = "Материал кабелей" def __str__(self): return self.name class DeviceConnectorType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип разъема") description = models.TextField(blank=True, verbose_name="Описание") class Meta: verbose_name = "Тип разъема устройства" verbose_name_plural = "Типы разъемов устройств" def __str__(self): return self.name class WirelessTechnology(models.Model): name = models.CharField(max_length=50, verbose_name="Технология") description = models.TextField(blank=True, verbose_name="Описание") class Meta: verbose_name = "Беспроводная технология" verbose_name_plural = "Беспроводные технологии" def __str__(self): return self.name class ChargingInterface(models.Model): name = models.CharField(max_length=50, verbose_name="Интерфейс зарядки") description = models.TextField(blank=True, verbose_name="Описание") class Meta: verbose_name = "Интерфейс зарядки" verbose_name_plural = "Интерфейсы зарядки" def __str__(self): return self.name # ====================== # Отзывы и медиа # ====================== class HeadphonesReviewType(models.Model): name = models.CharField(max_length=50, verbose_name="Тип обзора") slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") # Добавлено slug поле class Meta: verbose_name = "Тип обзора" verbose_name_plural = "Типы обзоров" def __str__(self): return self.name class HeadphonesReviewAuthorResource(models.Model): name = models.CharField(max_length=50, verbose_name="Название ресурса") url = models.URLField(verbose_name="URL ресурса", blank=True) class Meta: verbose_name = "Ресурс автора" verbose_name_plural = "Ресурсы авторов" def __str__(self): return self.name class HeadphonesReviewAuthor(models.Model): name = models.CharField(max_length=50, verbose_name="Имя автора") nickname = models.CharField(max_length=50, verbose_name="Никнейм автора") resources = models.ManyToManyField(HeadphonesReviewAuthorResource, related_name='author_resources', blank=True, verbose_name="Ресурсы автора") class Meta: verbose_name = "Автор обзора" verbose_name_plural = "Авторы обзоров" ordering = ['name'] def __str__(self): return self.nickname if self.nickname else self.name class HeadphonesReviewResource(models.Model): name = models.CharField(max_length=50, verbose_name="Ресурс обзора") type = models.ForeignKey( HeadphonesReviewType, on_delete=models.CASCADE, verbose_name="Тип обзора" ) url = models.URLField(verbose_name="URL ресурса", blank=True) # Добавлено поле для URL ресурса slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") class Meta: verbose_name = "Ресурс обзора" verbose_name_plural = "Ресурсы обзоров" ordering = ['name'] # Добавлена сортировка def __str__(self): return f"{self.name} ({self.type.name if self.type else 'без типа'})" class HeadphonesReviewLanguage(models.Model): language = models.CharField(max_length=50, verbose_name="Язык обзора") class Meta: verbose_name = "Язык обзора" def __str__(self): return self.language class HeadphonesReview(models.Model): headphones = models.ForeignKey( 'Headphones', on_delete=models.CASCADE, related_name='reviews', verbose_name="Наушники", null=True, blank=True ) # Изменено на связь с HeadphonesReviewResource (была ошибка в логике) resource = models.ForeignKey( HeadphonesReviewResource, on_delete=models.CASCADE, verbose_name="Ресурс обзора" ) author = models.ForeignKey( HeadphonesReviewAuthor, on_delete=models.CASCADE, verbose_name="Автор обзора", null = True, blank = True, related_name='reviews' ) language = models.ForeignKey( HeadphonesReviewLanguage, on_delete=models.CASCADE, null=True, blank=True, ) title = models.CharField(max_length=100, verbose_name="Заголовок обзора") # Изменено name на title url = models.URLField(verbose_name="Ссылка на обзор", unique=True) # Добавлен unique date = models.DateField( verbose_name="Дата публикации", null=True, blank=True ) class Meta: verbose_name = "Обзор наушников" verbose_name_plural = "Обзоры наушников" ordering = ['-date'] # Сортировка по дате indexes = [ models.Index(fields=['headphones', 'date']), # Добавлен индекс ] def __str__(self): # Более информативное представление return f"{self.title} ({self.headphones.model if self.headphones else 'No headphones'})" class HeadphonesImages(models.Model): product = models.ForeignKey( 'Headphones', on_delete=models.CASCADE, related_name='images', verbose_name="Наушники" ) photo = ProcessedImageField( upload_to=upload_to, processors=[ResizeToFit(1000, 1000)], # Макс. ширина/высота = 1000px format='WEBP', options={'quality': 85}, ) caption = models.CharField( max_length=200, blank=True, verbose_name="Подпись" ) is_main = models.BooleanField( default=False, verbose_name="Основное изображение", help_text="Используется как превью товара" ) def save(self, *args, **kwargs): # Если это основное изображение, снимаем флаг is_main у других изображений if self.is_main: HeadphonesImages.objects.filter(product=self.product).exclude(id=self.id).update(is_main=False) super().save(*args, **kwargs) def delete(self, *args, **kwargs): """Удаление файла при удалении записи""" if self.photo: try: if default_storage.exists(self.photo.name): default_storage.delete(self.photo.name) except Exception as e: # Логируем ошибку, но не прерываем удаление print(f"Ошибка при удалении файла: {e}") super().delete(*args, **kwargs) @property def image_info(self): """Вычисляемое поле с информацией о размере изображения""" if not self.photo: return mark_safe("—") try: img_path = self.photo.path if not os.path.exists(img_path): return mark_safe("Файл не найден") with Image.open(img_path) as img: width, height = img.size file_size = os.path.getsize(img_path) size_kb = file_size / 1024 file_ext = os.path.splitext(self.photo.name)[1][1:].upper() # Создаем отдельные безопасные строки dimensions = mark_safe(f"{width}×{height}") size = mark_safe(f"{size_kb:.1f} kb") ext = mark_safe(file_ext) # Комбинируем с помощью format_html return format_html( "{}
{}
{}", dimensions, size, ext ) except Exception as e: return mark_safe(f"Ошибка: {str(e)}") class Meta: verbose_name = "Изображение наушников" verbose_name_plural = "Изображения наушников" @receiver(pre_delete, sender=HeadphonesImages) def delete_image_file(sender, instance, **kwargs): """Обработчик сигнала для удаления файла при каскадном удалении""" if instance.photo: try: if os.path.isfile(instance.photo.path): os.remove(instance.photo.path) except Exception as e: print(f"Ошибка при удалении файла (сигнал): {e}") # ====================== # Основная модель наушников # ====================== class Headphones(models.Model): # Идентификация и базовые данные id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, verbose_name="Идентификатор" ) slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") model = models.CharField(max_length=50, verbose_name="Модель") description = models.TextField(verbose_name="Описание") CONNECTION_TYPE_CHOICES = [ ('wired', 'Проводные'), ('wireless', 'Беспроводные'), ('hybrid', 'Гибридные (проводные + беспроводные)'), ] connection_type = models.CharField( max_length=10, choices=CONNECTION_TYPE_CHOICES, default='wired', verbose_name="Тип подключения" ) case_type = models.ForeignKey( CaseType, on_delete=models.PROTECT, verbose_name="Тип корпуса" ) case_material = models.ForeignKey( CaseMaterial, on_delete=models.PROTECT, verbose_name="Материал корпуса", null=True, blank=True ) brand = models.ForeignKey( Brand, on_delete=models.PROTECT, verbose_name="Производитель" ) headphone_purpose = models.ForeignKey( HeadphonePurpose, on_delete=models.PROTECT, verbose_name="Назначение" ) # Медиа и ссылки product_url = models.URLField( blank=True, verbose_name="Ссылка на товар" ) # Теги и компоненты tags = models.ManyToManyField( Tags, related_name='headphones', blank=True, verbose_name="Теги" ) drivers = models.ManyToManyField( Driver, through='HeadphonesDriver', related_name='headphones', verbose_name="Драйверы", blank=True, null=True ) # Технические характеристики frequency_response_chart = models.JSONField( verbose_name="АЧХ (JSON)", null=True, blank=True, help_text="Данные в формате [{'frequency': 20, 'amplitude': -2.5}, ...]" ) impedance = models.DecimalField( verbose_name="Импеданс (Ом)", max_digits=5, decimal_places=2, null=True, blank=True, validators=[ MinValueValidator(4), # Минимум 4 Ом MaxValueValidator(600) # Максимум 600 Ом ] ) sensitivity = models.DecimalField( verbose_name="Чувствительность (дБ/мВт)", max_digits=5, decimal_places=2, null=True, blank=True, help_text="Уровень звукового давления" ) frequency_range = models.CharField( verbose_name="Частотный диапазон (Гц)", max_length=50, null=True, blank=True, help_text="Пример: 20-20000 Гц" ) # Дополнительные функции microphone = models.BooleanField( verbose_name="Встроенный микрофон", default=False, ) noise_cancellation = models.ForeignKey( NoiseCancellationType, on_delete=models.PROTECT, null=True, blank=True, verbose_name="Шумоподавление", ) ip_rating = models.CharField( max_length=10, blank=True, help_text="Например: IPX4", verbose_name="Степень защиты (IP)", ) official_price = models.DecimalField( verbose_name="Официальная цена на сайте", max_digits=6, decimal_places=2, null=True, blank=True ) # # Внешний вид и физические параметры # colors = models.ManyToManyField( # CaseColors, # related_name='headphones', # blank=True, # verbose_name="Доступные цвета" # ) weight = models.PositiveSmallIntegerField( verbose_name="Вес (г)", null=True, blank=True ) # Метаданные release_year = models.PositiveSmallIntegerField( verbose_name="Год выхода", null=True, blank=True, help_text="Год выпуска модели" ) date_added = models.DateTimeField( auto_now_add=True, verbose_name="Дата добавления" ) published = models.BooleanField( default=False, verbose_name="Опубликовано" ) class Meta: verbose_name = "Наушники" verbose_name_plural = "Наушники" ordering = ['brand', 'model'] def __str__(self): return f"{self.brand.name} {self.model}" def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(f"{self.brand.name} {self.model}") super().save(*args, **kwargs) # Обновляем slug для связанных цветов, если изменилась модель if self.pk and hasattr(self, 'case_colors'): for color in self.case_colors.all(): new_slug = slugify(f"{self.model}_{color.name_en}") if color.slug != new_slug: color.slug = new_slug color.save() class HeadphonesConnector(models.Model): headphones = models.ForeignKey( 'Headphones', on_delete=models.CASCADE, verbose_name="Наушники" ) wired_headphones = models.ForeignKey( 'WiredHeadphones', on_delete=models.CASCADE, null=True, blank=True, verbose_name="Проводные наушники" ) connector = models.ForeignKey( DeviceConnectorType, on_delete=models.PROTECT, verbose_name="Разъем" ) is_primary = models.BooleanField( default=False, verbose_name="Основной разъем" ) notes = models.CharField( max_length=100, blank=True, verbose_name="Примечания" ) class Meta: verbose_name = "Разъем наушников" verbose_name_plural = "Разъемы наушников" ordering = ['-is_primary', 'connector__name'] def __str__(self): return f"{self.connector.name} ({'основной' if self.is_primary else 'доп.'})" # ====================== # Специфичные модели для типов наушников # ====================== class WiredHeadphones(models.Model): headphones = models.OneToOneField( Headphones, on_delete=models.CASCADE, primary_key=True, related_name='wired_details', verbose_name="Наушники" ) cable_connection_type = models.ForeignKey( CableConnectionType, on_delete=models.PROTECT, verbose_name="Тип разъема кабеля" ) cable_length = models.DecimalField( max_digits=5, decimal_places=2, verbose_name="Длина кабеля (м)", help_text="Длина кабеля в метрах" ) # Удаляем старое поле connector_to_device custom_connector_name = models.CharField( max_length=50, blank=True, verbose_name="Проприетарный разъем" ) # Добавляем свойство для удобного доступа к разъемам @property def connectors(self): return self.headphones.headphonesconnector_set.all() def save(self, *args, **kwargs): super().save(*args, **kwargs) # Обновляем связь в разъемах HeadphonesConnector.objects.filter(headphones=self.headphones).update(wired_headphones=self) class Meta: verbose_name = "Характеристики проводных наушников" verbose_name_plural = "Характеристики проводных наушников" def __str__(self): return f"Проводные характеристики для {self.headphones.model}" class WirelessHeadphones(models.Model): headphones = models.OneToOneField( Headphones, on_delete=models.CASCADE, primary_key=True, related_name='wireless_details', verbose_name="Наушники" ) wireless_technology = models.ForeignKey( WirelessTechnology, on_delete=models.PROTECT, verbose_name="Беспроводная технология" ) bluetooth_version = models.CharField( max_length=20, blank=True, verbose_name="Версия Bluetooth" ) battery_life = models.IntegerField( verbose_name="Время работы (ч)", help_text="Время работы от батареи в часах" ) charging_time = models.IntegerField( verbose_name="Время зарядки (мин)", help_text="Время зарядки в минутах" ) has_charging_case = models.BooleanField( default=False, verbose_name="Зарядный кейс" ) charging_case_battery_life = models.IntegerField( blank=True, null=True, verbose_name="Время работы с кейсом (ч)", help_text="Общее время работы с кейсом в часах" ) charging_interface = models.ForeignKey( ChargingInterface, on_delete=models.PROTECT, verbose_name="Интерфейс зарядки" ) supported_codecs = models.ManyToManyField( AudioCodec, blank=True, verbose_name="Поддерживаемые кодеки" ) class Meta: verbose_name = "Характеристики беспроводных наушников" verbose_name_plural = "Характеристики беспроводных наушников" def __str__(self): return f"Беспроводные характеристики для {self.headphones.model}" class HybridHeadphones(models.Model): headphones = models.OneToOneField( Headphones, on_delete=models.CASCADE, primary_key=True, related_name='hybrid_details', verbose_name="Наушники" ) # Беспроводная часть wireless_technology = models.ForeignKey( WirelessTechnology, on_delete=models.PROTECT, verbose_name="Беспроводная технология" ) bluetooth_version = models.CharField( max_length=20, blank=True, verbose_name="Версия Bluetooth" ) battery_life = models.IntegerField( verbose_name="Время работы (ч)", help_text="В беспроводном режиме" ) charging_time = models.IntegerField( verbose_name="Время зарядки (мин)" ) supported_codecs = models.ManyToManyField( AudioCodec, blank=True, verbose_name="Поддерживаемые кодеки" ) # Проводная часть cable_connection_type = models.ForeignKey( CableConnectionType, on_delete=models.PROTECT, verbose_name="Тип разъема кабеля" ) cable_length = models.DecimalField( max_digits=5, decimal_places=2, verbose_name="Длина кабеля (м)", null = True, blank = True ) cable_material = models.ForeignKey( CableMaterial, on_delete=models.PROTECT, verbose_name="Материал кабеля", null=True, blank=True ) connector_to_device = models.ForeignKey( DeviceConnectorType, on_delete=models.PROTECT, verbose_name="Разъем для устройства" ) # Общие характеристики auto_switch_mode = models.BooleanField( default=False, verbose_name="Автопереключение режимов" ) simultaneous_connection = models.BooleanField( default=False, verbose_name="Одновременное подключение" ) # Зарядка charging_interface = models.ForeignKey( ChargingInterface, on_delete=models.PROTECT, verbose_name="Интерфейс зарядки" ) has_charging_case = models.BooleanField( default=False, verbose_name="Зарядный кейс" ) charging_case_battery_life = models.IntegerField( blank=True, null=True, verbose_name="Время работы с кейсом (ч)" ) class Meta: verbose_name = "Характеристики гибридных наушников" verbose_name_plural = "Характеристики гибридных наушников" def __str__(self): return f"Гибридные характеристики для {self.headphones.model}" # ====================== # Вспомогательные модели # ====================== class HeadphonesDriver(models.Model): headphones = models.ForeignKey( Headphones, on_delete=models.CASCADE, verbose_name="Наушники" ) driver = models.ForeignKey( Driver, on_delete=models.CASCADE, verbose_name="Драйвер" ) count = models.PositiveSmallIntegerField( default=1, verbose_name="Количество" ) class Meta: verbose_name = "Драйвер в наушниках" verbose_name_plural = "Драйверы в наушниках" unique_together = [('headphones', 'driver')] def __str__(self): return f"{self.driver.driver_type.name} x{self.count}" class Comment(models.Model): id = models.CharField( max_length=100, blank=True, unique=True, default=uuid.uuid4, primary_key=True, verbose_name="Идентификатор" ) date = models.DateTimeField( auto_now_add=True, verbose_name="Дата создания" ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Пользователь" ) body = models.TextField( blank=True, null=True, verbose_name="Текст комментария" ) post = models.ForeignKey( Headphones, on_delete=models.CASCADE, blank=True, related_name='comments', related_query_name='comment', verbose_name="Наушники" ) parent = models.ForeignKey( 'self', related_name='reply', null=True, blank=True, on_delete=models.CASCADE, verbose_name="Родительский комментарий" ) class Meta: verbose_name = "Комментарий" verbose_name_plural = "Комментарии" ordering = ['-date'] def __str__(self): return f'Комментарий к "{self.post.model}" от {self.user}'