diff --git a/headphones/admin.py b/headphones/admin.py
index 8ada4fb..07721ef 100644
--- a/headphones/admin.py
+++ b/headphones/admin.py
@@ -176,6 +176,23 @@ class CaseColorsInline(admin.TabularInline):
return formset
+class FrequencyResponseInline(admin.StackedInline):
+ model = FrequencyResponse
+ extra = 1
+ fields = ('file', 'author', 'source_link', 'is_primary', 'notes', 'file_preview')
+ readonly_fields = ('file_preview',)
+
+ def file_preview(self, obj):
+ if obj.file:
+ return format_html(
+ '{}',
+ obj.file.url,
+ os.path.basename(obj.file.name)
+ )
+ return "-"
+ file_preview.short_description = "Файл"
+
+
@admin.register(Headphones)
class HeadphonesAdmin(admin.ModelAdmin):
form = HeadphonesAdminForm
@@ -209,7 +226,7 @@ class HeadphonesAdmin(admin.ModelAdmin):
'fields': [
('impedance', 'sensitivity'),
'frequency_range',
- ('frequency_response_chart', 'frequency_response_chart_author', 'frequency_response_chart_link'),
+ # ('frequency_response_chart', 'frequency_response_chart_author', 'frequency_response_chart_link'),
('microphone', 'noise_cancellation'),
'ip_rating',
]
@@ -228,7 +245,7 @@ class HeadphonesAdmin(admin.ModelAdmin):
}),
]
- inlines = [HeadphonesDriverInline, HeadphonesImagesInline, CaseColorsInline]
+ inlines = [HeadphonesDriverInline, HeadphonesImagesInline, CaseColorsInline, FrequencyResponseInline]
# Методы для отображения
def get_reviews_count(self, obj):
@@ -421,7 +438,6 @@ class AudioCodecAdmin(admin.ModelAdmin):
# ======================
# Регистрация разъемов и подключений
# ======================
-
@admin.register(CableConnectionType)
class CableConnectionTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'is_detachable')
@@ -464,6 +480,7 @@ class ChargingInterfaceAdmin(admin.ModelAdmin):
# Регистрация отзывов и медиа
# ======================
+
@admin.register(HeadphonesReviewType)
class HeadphonesReviewTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
diff --git a/headphones/models.py b/headphones/models.py
index eae290f..7ecd6bc 100644
--- a/headphones/models.py
+++ b/headphones/models.py
@@ -24,7 +24,10 @@ def upload_to_case_image(instance, filename):
return f"headphones/{instance.headphones.id}/case_images/{instance.slug}.{ext}"
def upload_to_frequency_response(instance, filename):
- return f"headphones/{instance.headphones.id}/frequency_response/{filename}"
+ """Путь для сохранения: headphones//frequency_response/"""
+ ext = filename.split('.')[-1]
+ unique_filename = f"{uuid.uuid4().hex}.{ext}"
+ return f"headphones/{instance.headphones.id}/frequency_response/{unique_filename}"
# ======================
# Базовые справочники
@@ -639,6 +642,67 @@ class HeadphonesCaseIP(models.Model):
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)
+
+
# ======================
# Основная модель наушников
# ======================
@@ -711,13 +775,13 @@ class Headphones(models.Model):
)
# Технические характеристики
- 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 = 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="Автор АЧХ",