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}"
# ======================
# Базовые справочники
# ======================
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):
name = models.CharField(max_length=50, verbose_name="Название цвета")
hex_code = models.CharField(max_length=50, verbose_name="HEX-код цвета")
class Meta:
verbose_name = "Цвет корпуса"
verbose_name_plural = "Цвета корпусов"
def __str__(self):
return f"{self.name} {self.hex_code}"
# ======================
# Основные модели брендов и категорий
# ======================
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):
return f"{self.driver_type.name} {self.size}mm"
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',
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)
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}'