diff --git a/headphones/admin.py b/headphones/admin.py
index 3b5c98c..4e8a8dd 100644
--- a/headphones/admin.py
+++ b/headphones/admin.py
@@ -146,6 +146,34 @@ class HeadphonesDriverInline(admin.TabularInline):
autocomplete_fields = ['driver']
+# class HeadphonesColorInline(admin.TabularInline):
+# model = Headphones.colors.through # Используем автоматическую промежуточную модель
+# extra = 1
+# # autocomplete_fields = ['casecolors'] # Убедитесь, что это правильное имя related_name
+# verbose_name = "Доступный цвет"
+# verbose_name_plural = "Доступные цвета"
+
+
+class CaseColorsInline(admin.TabularInline):
+ model = CaseColors
+ extra = 1
+ fields = ('name_ru', 'name_en', 'image', 'image_preview', 'slug')
+ readonly_fields = ('image_preview', 'slug')
+
+ def image_preview(self, obj):
+ if obj.image:
+ return format_html('
', obj.image.url)
+ return "Нет изображения"
+ image_preview.short_description = "Превью"
+
+ def get_formset(self, request, obj=None, **kwargs):
+ formset = super().get_formset(request, obj, **kwargs)
+ # Устанавливаем начальное значение для slug, если объект новый
+ if obj and not obj.pk:
+ formset.form.base_fields['slug'].initial = f"{obj.model}_"
+ return formset
+
+
@admin.register(Headphones)
class HeadphonesAdmin(admin.ModelAdmin):
form = HeadphonesAdminForm
@@ -154,7 +182,7 @@ class HeadphonesAdmin(admin.ModelAdmin):
list_filter = ('brand', 'connection_type', 'published', 'case_type')
search_fields = ('model', 'brand__name', 'description')
prepopulated_fields = {'slug': ('brand', 'model')}
- filter_horizontal = ('tags', 'colors')
+ filter_horizontal = ('tags',)
list_editable = ('published',)
actions = ['make_published', 'make_unpublished']
readonly_fields = (
@@ -186,7 +214,7 @@ class HeadphonesAdmin(admin.ModelAdmin):
}),
('Внешний вид', {
'fields': [
- 'colors',
+ # 'colors',
('weight', 'release_year')
]
}),
@@ -198,7 +226,7 @@ class HeadphonesAdmin(admin.ModelAdmin):
}),
]
- inlines = [HeadphonesDriverInline, HeadphonesImagesInline]
+ inlines = [HeadphonesDriverInline, HeadphonesImagesInline, CaseColorsInline]
# Методы для отображения
def get_reviews_count(self, obj):
@@ -223,10 +251,11 @@ class HeadphonesAdmin(admin.ModelAdmin):
driver_configuration_display.short_description = 'Конфигурация драйверов'
def get_inline_instances(self, request, obj=None):
- inlines = [
- HeadphonesDriverInline(self.model, self.admin_site),
- HeadphonesImagesInline(self.model, self.admin_site)
- ]
+ # inlines = [
+ # HeadphonesDriverInline(self.model, self.admin_site),
+ # HeadphonesImagesInline(self.model, self.admin_site)
+ # ]
+ inlines = super().get_inline_instances(request, obj)
if obj:
if obj.connection_type == 'wired':
@@ -288,8 +317,28 @@ class BrandCountryAdmin(admin.ModelAdmin):
@admin.register(CaseColors)
class CaseColorsAdmin(admin.ModelAdmin):
- list_display = ('name', 'hex_code')
- search_fields = ('name',)
+ list_display = ('name_ru', 'name_en', 'headphones', 'image_preview', 'slug')
+ list_filter = ('headphones__brand',)
+ search_fields = ('name_ru', 'name_en', 'headphones__model')
+ readonly_fields = ('image_preview', 'slug')
+ list_select_related = ('headphones', 'headphones__brand')
+
+ fieldsets = [
+ ('Основная информация', {
+ 'fields': ['headphones', 'name_ru', 'name_en', 'slug']
+ }),
+ ('Изображение', {
+ 'fields': ['image', 'image_preview']
+ })
+ ]
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related('headphones__brand')
+
+ def save_model(self, request, obj, form, change):
+ if not obj.slug:
+ obj.slug = slugify(f"{obj.headphones.model}_{obj.name_en}")
+ super().save_model(request, obj, form, change)
@admin.register(CaseType)
diff --git a/headphones/models.py b/headphones/models.py
index c8d255f..719fb58 100644
--- a/headphones/models.py
+++ b/headphones/models.py
@@ -18,6 +18,11 @@ 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}"
+
# ======================
# Базовые справочники
# ======================
@@ -33,16 +38,45 @@ class BrandCountry(models.Model):
class CaseColors(models.Model):
- name = models.CharField(max_length=50, verbose_name="Название цвета")
- hex_code = models.CharField(max_length=50, verbose_name="HEX-код цвета")
+ 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)
- class Meta:
- verbose_name = "Цвет корпуса"
- verbose_name_plural = "Цвета корпусов"
+ 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} {self.hex_code}"
+ return f"{self.name_ru} ({self.headphones.model})"
+ class Meta:
+ verbose_name = 'Цвет корпуса'
+ verbose_name_plural = 'Цвета корпусов'
+ ordering = ['headphones__model', 'name_ru']
# ======================
# Основные модели брендов и категорий
@@ -212,7 +246,15 @@ class Driver(models.Model):
verbose_name_plural = "Драйверы"
def __str__(self):
- return f"{self.driver_type.name} {self.size}mm"
+ 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):
@@ -701,12 +743,13 @@ class Headphones(models.Model):
blank=True
)
- # Внешний вид и физические параметры
- colors = models.ManyToManyField(
- CaseColors,
- related_name='headphones',
- verbose_name="Цвета"
- )
+ # # Внешний вид и физические параметры
+ # colors = models.ManyToManyField(
+ # CaseColors,
+ # related_name='headphones',
+ # blank=True,
+ # verbose_name="Доступные цвета"
+ # )
weight = models.PositiveSmallIntegerField(
verbose_name="Вес (г)",
null=True,
@@ -742,6 +785,15 @@ class Headphones(models.Model):
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',