headphones_backend/headphones/models.py

1242 lines
40 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/<product_id>/case_images/<slug>.<ext>"""
ext = filename.split('.')[-1]
return f"headphones/{instance.headphones.id}/case_images/{instance.slug}.{ext}"
def upload_to_frequency_response(instance, filename):
"""Путь для сохранения: headphones/<headphones_id>/frequency_response/<filename>"""
ext = filename.split('.')[-1]
unique_filename = f"{uuid.uuid4().hex}.{ext}"
return f"headphones/{instance.headphones.id}/frequency_response/{unique_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):
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('<img src="{}" width="50" height="50" />', 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="Тип драйвера")
encoding = models.CharField(blank=True, max_length=10, 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(
"{}<br>{}<br>{}",
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 HeadphonesMicrophoneType(models.Model):
type = models.CharField(max_length=50, verbose_name="Тип микрофона")
description = models.CharField(blank=True, verbose_name="Описание")
class Meta:
verbose_name = "Ресурс обзора"
def __str__(self):
return self.type
class HeadphonesCaseIP(models.Model):
code = models.CharField(max_length=50, verbose_name="Код степени защиты")
description = models.CharField(blank=True, verbose_name="Описание степени защиты")
class Meta:
verbose_name = "Степень защиты"
def __str__(self):
return self.code
class FrequencyResponse(models.Model):
headphones = models.ForeignKey(
'Headphones',
on_delete=models.CASCADE,
related_name='frequency_responses',
verbose_name="Наушники"
)
file = models.FileField(
upload_to=upload_to_frequency_response,
verbose_name="Файл АЧХ"
)
author = models.CharField(
max_length=100,
verbose_name="Автор измерения",
blank=True,
null=True
)
source_link = models.URLField(
verbose_name="Ссылка на источник",
blank=True,
null=True
)
notes = models.TextField(
verbose_name="Примечания",
blank=True,
null=True
)
is_primary = models.BooleanField(
default=False,
verbose_name="Основная АЧХ"
)
class Meta:
verbose_name = "АЧХ наушников"
verbose_name_plural = "АЧХ наушников"
ordering = ['-is_primary', 'headphones__model']
def __str__(self):
return f"АЧХ {self.headphones.model} ({self.author or 'без автора'})"
def save(self, *args, **kwargs):
if self.is_primary:
# Снимаем флаг is_primary у других АЧХ этих наушников
FrequencyResponse.objects.filter(
headphones=self.headphones
).exclude(
pk=self.pk
).update(is_primary=False)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Удаление файла при удалении записи"""
if self.file:
try:
if default_storage.exists(self.file.name):
default_storage.delete(self.file.name)
except Exception as e:
print(f"Ошибка при удалении файла АЧХ: {e}")
super().delete(*args, **kwargs)
# ======================
# Основная модель наушников
# ======================
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.FileField(
# verbose_name="АЧХ (CSV)",
# upload_to = upload_to_frequency_response,
# null=True,
# blank=True,
# help_text="Данные в формате [{'frequency': 20, 'amplitude': -2.5}, ...]"
# )
frequency_response_chart_author = models.CharField(
verbose_name="Автор АЧХ",
null=True,
blank=True,
)
frequency_response_chart_link = models.CharField(
verbose_name="Ссылка на АЧХ",
null=True,
blank=True,
)
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.ForeignKey(
HeadphonesMicrophoneType,
on_delete=models.PROTECT,
null=True,
blank=True,
verbose_name="Тип микрофона",
)
noise_cancellation = models.ForeignKey(
NoiseCancellationType,
on_delete=models.PROTECT,
null=True,
blank=True,
verbose_name="Шумоподавление",
)
ip_rating = models.ForeignKey(
HeadphonesCaseIP,
on_delete=models.PROTECT,
null=True,
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.FloatField(
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 WirelessHeadphonesChip(models.Model):
name = models.CharField(max_length=50, verbose_name="Название")
description = models.TextField(blank=True, verbose_name="Описание")
class Meta:
verbose_name = "Используемый чип"
def __str__(self):
return self.name
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(
blank=True,
null=True,
verbose_name="Время работы (ч)",
help_text="Время работы от батареи в часах"
)
charging_time = models.IntegerField(
blank=True,
null=True,
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_case_time = models.IntegerField(
blank=True,
null=True,
verbose_name="Время зарядки кейса (мин)",
help_text="Время зарядки кейса в минутах"
)
charging_interface = models.ForeignKey(
ChargingInterface,
on_delete=models.PROTECT,
verbose_name="Интерфейс зарядки"
)
chip = models.ForeignKey(
WirelessHeadphonesChip,
blank=True,
null=True,
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}'