diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index d7b9010..0917b3d 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -218,11 +218,11 @@ def export_objects_to_csv(modeladmin, request, queryset): queryset = queryset.select_related( 'geo_obj', 'created_by__user', - 'updated_by__user' - ).prefetch_related( - 'parameters_obj__id_satellite', - 'parameters_obj__polarization', - 'parameters_obj__modulation' + 'updated_by__user', + 'parameter_obj', + 'parameter_obj__id_satellite', + 'parameter_obj__polarization', + 'parameter_obj__modulation' ) response = HttpResponse(content_type='text/csv; charset=utf-8') @@ -248,7 +248,7 @@ def export_objects_to_csv(modeladmin, request, queryset): ]) for obj in queryset: - param = next(iter(obj.parameters_obj.all()), None) + param = getattr(obj, 'parameter_obj', None) geo = obj.geo_obj # Форматирование координат @@ -284,12 +284,25 @@ def export_objects_to_csv(modeladmin, request, queryset): # Inline Admin Classes # ============================================================================ -class ParameterObjItemInline(admin.StackedInline): - model = ObjItem.parameters_obj.through - extra = 0 +class ParameterInline(admin.StackedInline): + """Inline для редактирования параметра объекта.""" + model = Parameter + extra = 0 max_num = 1 + can_delete = True verbose_name = "ВЧ загрузка" - verbose_name_plural = "ВЧ загрузки" + verbose_name_plural = "ВЧ загрузка" + fields = ( + 'id_satellite', + 'frequency', + 'freq_range', + 'polarization', + 'modulation', + 'bod_velocity', + 'snr', + 'standard' + ) + autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard') # ============================================================================ @@ -370,13 +383,15 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "bod_velocity", "snr", "standard", + "related_objitem", "sigma_parameter" ) list_display_links = ("frequency", "id_satellite") - list_select_related = ("polarization", "modulation", "standard", "id_satellite") + list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem") list_filter = ( HasSigmaParameterFilter, + ("objitem", MultiSelectRelatedDropdownFilter), ("id_satellite", MultiSelectRelatedDropdownFilter), ("polarization__name", MultiSelectDropdownFilter), ("modulation", MultiSelectRelatedDropdownFilter), @@ -395,12 +410,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "modulation__name", "polarization__name", "standard__name", + "objitem__name", ) ordering = ("-frequency",) - autocomplete_fields = ("objitems",) + autocomplete_fields = ("objitem",) inlines = [SigmaParameterInline] + def related_objitem(self, obj): + """Отображает связанный ObjItem.""" + if hasattr(obj, 'objitem') and obj.objitem: + return obj.objitem.name + return "-" + related_objitem.short_description = "Объект" + related_objitem.admin_order_field = "objitem__name" + def sigma_parameter(self, obj): """Отображает связанный параметр Sigma.""" sigma_obj = obj.sigma_parameter.all() @@ -636,16 +660,25 @@ class ObjItemAdmin(BaseAdmin): "updated_at", ) list_display_links = ("name",) - list_select_related = ("geo_obj", "created_by__user", "updated_by__user") + list_select_related = ( + "geo_obj", + "created_by__user", + "updated_by__user", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", + "parameter_obj__modulation", + "parameter_obj__standard" + ) list_filter = ( UniqueToggleFilter, - ("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter), - ("parameters_obj__frequency", NumericRangeFilterBuilder()), - ("parameters_obj__freq_range", NumericRangeFilterBuilder()), - ("parameters_obj__snr", NumericRangeFilterBuilder()), - ("parameters_obj__modulation", MultiSelectRelatedDropdownFilter), - ("parameters_obj__polarization", MultiSelectRelatedDropdownFilter), + ("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter), + ("parameter_obj__frequency", NumericRangeFilterBuilder()), + ("parameter_obj__freq_range", NumericRangeFilterBuilder()), + ("parameter_obj__snr", NumericRangeFilterBuilder()), + ("parameter_obj__modulation", MultiSelectRelatedDropdownFilter), + ("parameter_obj__polarization", MultiSelectRelatedDropdownFilter), GeoKupDistanceFilter, GeoValidDistanceFilter, ("created_at", DateRangeQuickSelectListFilterBuilder()), @@ -655,12 +688,12 @@ class ObjItemAdmin(BaseAdmin): search_fields = ( "name", "geo_obj__location", - "parameters_obj__frequency", - "parameters_obj__id_satellite__name", + "parameter_obj__frequency", + "parameter_obj__id_satellite__name", ) ordering = ("-updated_at",) - inlines = [ParameterObjItemInline, GeoInline] + inlines = [GeoInline, ParameterInline] actions = [show_selected_on_map, export_objects_to_csv] readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") @@ -676,7 +709,7 @@ class ObjItemAdmin(BaseAdmin): def get_queryset(self, request): """ - Оптимизированный queryset с использованием select_related и prefetch_related. + Оптимизированный queryset с использованием select_related. Загружает связанные объекты одним запросом для улучшения производительности. """ @@ -684,31 +717,30 @@ class ObjItemAdmin(BaseAdmin): return qs.select_related( "geo_obj", "created_by__user", - "updated_by__user" - ).prefetch_related( - "parameters_obj__id_satellite", - "parameters_obj__polarization", - "parameters_obj__modulation", - "parameters_obj__standard" + "updated_by__user", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", + "parameter_obj__modulation", + "parameter_obj__standard" ) def sat_name(self, obj): """Отображает название спутника из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param and param.id_satellite: - return param.id_satellite.name + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if obj.parameter_obj.id_satellite: + return obj.parameter_obj.id_satellite.name return "-" sat_name.short_description = "Спутник" - sat_name.admin_order_field = "parameters_obj__id_satellite__name" + sat_name.admin_order_field = "parameter_obj__id_satellite__name" def freq(self, obj): """Отображает частоту из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param: - return param.frequency + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.frequency return "-" freq.short_description = "Частота, МГц" - freq.admin_order_field = "parameters_obj__frequency" + freq.admin_order_field = "parameter_obj__frequency" def distance_geo_kup(self, obj): """Отображает расстояние между геолокацией и Кубсатом.""" @@ -736,42 +768,39 @@ class ObjItemAdmin(BaseAdmin): def pol(self, obj): """Отображает поляризацию из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param and param.polarization: - return param.polarization.name + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if obj.parameter_obj.polarization: + return obj.parameter_obj.polarization.name return "-" pol.short_description = "Поляризация" def freq_range(self, obj): """Отображает полосу частот из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param: - return param.freq_range + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.freq_range return "-" freq_range.short_description = "Полоса, МГц" - freq_range.admin_order_field = "parameters_obj__freq_range" + freq_range.admin_order_field = "parameter_obj__freq_range" def bod_velocity(self, obj): """Отображает символьную скорость из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param: - return param.bod_velocity + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.bod_velocity return "-" bod_velocity.short_description = "Сим. v, БОД" def modulation(self, obj): """Отображает модуляцию из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param and param.modulation: - return param.modulation.name + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if obj.parameter_obj.modulation: + return obj.parameter_obj.modulation.name return "-" modulation.short_description = "Модуляция" def snr(self, obj): """Отображает отношение сигнал/шум из связанного параметра.""" - param = next(iter(obj.parameters_obj.all()), None) - if param: - return param.snr + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.snr return "-" snr.short_description = "ОСШ" diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 8e32447..2658289 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -108,6 +108,12 @@ class NewEventForm(forms.Form): }) ) class ParameterForm(forms.ModelForm): + """ + Форма для создания и редактирования параметров ВЧ загрузки. + + Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь. + """ + class Meta: model = Parameter fields = [ @@ -115,22 +121,92 @@ class ParameterForm(forms.ModelForm): 'bod_velocity', 'modulation', 'snr', 'standard' ] widgets = { - 'id_satellite': forms.Select(attrs={'class': 'form-select'}, choices=[]), - 'frequency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), - 'freq_range': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), - 'bod_velocity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), - 'snr': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), - 'polarization': forms.Select(attrs={'class': 'form-select'}, choices=[]), - 'modulation': forms.Select(attrs={'class': 'form-select'}, choices=[]), - 'standard': forms.Select(attrs={'class': 'form-select'}, choices=[]), + 'id_satellite': forms.Select(attrs={ + 'class': 'form-select', + 'required': True + }), + 'frequency': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.000001', + 'min': '0', + 'max': '50000', + 'placeholder': 'Введите частоту в МГц' + }), + 'freq_range': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.000001', + 'min': '0', + 'max': '1000', + 'placeholder': 'Введите полосу частот в МГц' + }), + 'bod_velocity': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.001', + 'min': '0', + 'placeholder': 'Введите символьную скорость в БОД' + }), + 'snr': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.001', + 'min': '-50', + 'max': '100', + 'placeholder': 'Введите ОСШ в дБ' + }), + 'polarization': forms.Select(attrs={'class': 'form-select'}), + 'modulation': forms.Select(attrs={'class': 'form-select'}), + 'standard': forms.Select(attrs={'class': 'form-select'}), + } + labels = { + 'id_satellite': 'Спутник', + 'frequency': 'Частота (МГц)', + 'freq_range': 'Полоса частот (МГц)', + 'polarization': 'Поляризация', + 'bod_velocity': 'Символьная скорость (БОД)', + 'modulation': 'Модуляция', + 'snr': 'ОСШ (дБ)', + 'standard': 'Стандарт', + } + help_texts = { + 'frequency': 'Частота в диапазоне от 0 до 50000 МГц', + 'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц', + 'bod_velocity': 'Символьная скорость должна быть положительной', + 'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['id_satellite'].choices = [(s.id, s.name) for s in Satellite.objects.all()] - self.fields['polarization'].choices = [(p.id, p.name) for p in Polarization.objects.all()] - self.fields['modulation'].choices = [(m.id, m.name) for m in Modulation.objects.all()] - self.fields['standard'].choices = [(s.id, s.name) for s in Standard.objects.all()] + + # Динамически загружаем choices для select полей + self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name') + self.fields['polarization'].queryset = Polarization.objects.all().order_by('name') + self.fields['modulation'].queryset = Modulation.objects.all().order_by('name') + self.fields['standard'].queryset = Standard.objects.all().order_by('name') + + # Делаем спутник обязательным полем + self.fields['id_satellite'].required = True + + def clean(self): + """ + Дополнительная валидация формы. + + Проверяет соотношение между частотой, полосой частот и символьной скоростью. + """ + cleaned_data = super().clean() + frequency = cleaned_data.get('frequency') + freq_range = cleaned_data.get('freq_range') + bod_velocity = cleaned_data.get('bod_velocity') + + # Проверка что частота больше полосы частот + if frequency and freq_range: + if freq_range > frequency: + self.add_error('freq_range', 'Полоса частот не может быть больше частоты') + + # Проверка что символьная скорость соответствует полосе частот + if bod_velocity and freq_range: + if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц + self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот') + + return cleaned_data class GeoForm(forms.ModelForm): class Meta: @@ -143,9 +219,49 @@ class GeoForm(forms.ModelForm): } class ObjItemForm(forms.ModelForm): + """ + Форма для создания и редактирования объектов (источников сигнала). + + Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно + через ParameterForm с использованием OneToOne связи. + """ + class Meta: model = ObjItem fields = ['name'] widgets = { - 'name': forms.TextInput(attrs={'class': 'form-control'}), - } \ No newline at end of file + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Введите название объекта', + 'maxlength': '100' + }), + } + labels = { + 'name': 'Название объекта', + } + help_texts = { + 'name': 'Уникальное название объекта/источника сигнала', + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Делаем поле name необязательным, так как оно может быть пустым + self.fields['name'].required = False + + def clean_name(self): + """ + Валидация поля name. + + Проверяет что название не состоит только из пробелов. + """ + name = self.cleaned_data.get('name') + + if name: + # Удаляем лишние пробелы + name = name.strip() + + # Проверяем что после удаления пробелов что-то осталось + if not name: + raise forms.ValidationError('Название не может состоять только из пробелов') + + return name \ No newline at end of file diff --git a/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py b/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py new file mode 100644 index 0000000..39a2ced --- /dev/null +++ b/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-11-10 18:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='parameter', + name='objitems', + ), + migrations.AddField( + model_name='parameter', + name='objitem', + field=models.OneToOneField(blank=True, help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parameter_obj', to='mainapp.objitem', verbose_name='Объект'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index a439225..0072da3 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -243,11 +243,11 @@ class ObjItemQuerySet(models.QuerySet): "updated_by__user", "created_by__user", "source_type_obj", - ).prefetch_related( - "parameters_obj__id_satellite", - "parameters_obj__polarization", - "parameters_obj__modulation", - "parameters_obj__standard", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", + "parameter_obj__modulation", + "parameter_obj__standard", ) def recent(self, days=30): @@ -449,8 +449,14 @@ class Parameter(models.Model): verbose_name="Стандарт", ) # id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True) - objitems = models.ManyToManyField( - ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True + objitem = models.OneToOneField( + ObjItem, + on_delete=models.CASCADE, + related_name="parameter_obj", + verbose_name="Объект", + null=True, + blank=True, + help_text="Связанный объект" ) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True) diff --git a/dbapp/mainapp/templates/mainapp/objitem_detail.html b/dbapp/mainapp/templates/mainapp/objitem_detail.html index 9d08ffa..4effc6f 100644 --- a/dbapp/mainapp/templates/mainapp/objitem_detail.html +++ b/dbapp/mainapp/templates/mainapp/objitem_detail.html @@ -116,72 +116,70 @@ - +
Нет данных о ВЧ загрузке