Переделал модель Parameter и связь с ObjItem

This commit is contained in:
2025-11-10 22:32:26 +03:00
parent b24ef940ce
commit 1b345a3fd9
9 changed files with 535 additions and 447 deletions

View File

@@ -218,11 +218,11 @@ def export_objects_to_csv(modeladmin, request, queryset):
queryset = queryset.select_related( queryset = queryset.select_related(
'geo_obj', 'geo_obj',
'created_by__user', 'created_by__user',
'updated_by__user' 'updated_by__user',
).prefetch_related( 'parameter_obj',
'parameters_obj__id_satellite', 'parameter_obj__id_satellite',
'parameters_obj__polarization', 'parameter_obj__polarization',
'parameters_obj__modulation' 'parameter_obj__modulation'
) )
response = HttpResponse(content_type='text/csv; charset=utf-8') 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: for obj in queryset:
param = next(iter(obj.parameters_obj.all()), None) param = getattr(obj, 'parameter_obj', None)
geo = obj.geo_obj geo = obj.geo_obj
# Форматирование координат # Форматирование координат
@@ -284,12 +284,25 @@ def export_objects_to_csv(modeladmin, request, queryset):
# Inline Admin Classes # Inline Admin Classes
# ============================================================================ # ============================================================================
class ParameterObjItemInline(admin.StackedInline): class ParameterInline(admin.StackedInline):
model = ObjItem.parameters_obj.through """Inline для редактирования параметра объекта."""
extra = 0 model = Parameter
extra = 0
max_num = 1 max_num = 1
can_delete = True
verbose_name = "ВЧ загрузка" 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", "bod_velocity",
"snr", "snr",
"standard", "standard",
"related_objitem",
"sigma_parameter" "sigma_parameter"
) )
list_display_links = ("frequency", "id_satellite") 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 = ( list_filter = (
HasSigmaParameterFilter, HasSigmaParameterFilter,
("objitem", MultiSelectRelatedDropdownFilter),
("id_satellite", MultiSelectRelatedDropdownFilter), ("id_satellite", MultiSelectRelatedDropdownFilter),
("polarization__name", MultiSelectDropdownFilter), ("polarization__name", MultiSelectDropdownFilter),
("modulation", MultiSelectRelatedDropdownFilter), ("modulation", MultiSelectRelatedDropdownFilter),
@@ -395,12 +410,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"modulation__name", "modulation__name",
"polarization__name", "polarization__name",
"standard__name", "standard__name",
"objitem__name",
) )
ordering = ("-frequency",) ordering = ("-frequency",)
autocomplete_fields = ("objitems",) autocomplete_fields = ("objitem",)
inlines = [SigmaParameterInline] 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): def sigma_parameter(self, obj):
"""Отображает связанный параметр Sigma.""" """Отображает связанный параметр Sigma."""
sigma_obj = obj.sigma_parameter.all() sigma_obj = obj.sigma_parameter.all()
@@ -636,16 +660,25 @@ class ObjItemAdmin(BaseAdmin):
"updated_at", "updated_at",
) )
list_display_links = ("name",) 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 = ( list_filter = (
UniqueToggleFilter, UniqueToggleFilter,
("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter), ("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter),
("parameters_obj__frequency", NumericRangeFilterBuilder()), ("parameter_obj__frequency", NumericRangeFilterBuilder()),
("parameters_obj__freq_range", NumericRangeFilterBuilder()), ("parameter_obj__freq_range", NumericRangeFilterBuilder()),
("parameters_obj__snr", NumericRangeFilterBuilder()), ("parameter_obj__snr", NumericRangeFilterBuilder()),
("parameters_obj__modulation", MultiSelectRelatedDropdownFilter), ("parameter_obj__modulation", MultiSelectRelatedDropdownFilter),
("parameters_obj__polarization", MultiSelectRelatedDropdownFilter), ("parameter_obj__polarization", MultiSelectRelatedDropdownFilter),
GeoKupDistanceFilter, GeoKupDistanceFilter,
GeoValidDistanceFilter, GeoValidDistanceFilter,
("created_at", DateRangeQuickSelectListFilterBuilder()), ("created_at", DateRangeQuickSelectListFilterBuilder()),
@@ -655,12 +688,12 @@ class ObjItemAdmin(BaseAdmin):
search_fields = ( search_fields = (
"name", "name",
"geo_obj__location", "geo_obj__location",
"parameters_obj__frequency", "parameter_obj__frequency",
"parameters_obj__id_satellite__name", "parameter_obj__id_satellite__name",
) )
ordering = ("-updated_at",) ordering = ("-updated_at",)
inlines = [ParameterObjItemInline, GeoInline] inlines = [GeoInline, ParameterInline]
actions = [show_selected_on_map, export_objects_to_csv] actions = [show_selected_on_map, export_objects_to_csv]
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
@@ -676,7 +709,7 @@ class ObjItemAdmin(BaseAdmin):
def get_queryset(self, request): def get_queryset(self, request):
""" """
Оптимизированный queryset с использованием select_related и prefetch_related. Оптимизированный queryset с использованием select_related.
Загружает связанные объекты одним запросом для улучшения производительности. Загружает связанные объекты одним запросом для улучшения производительности.
""" """
@@ -684,31 +717,30 @@ class ObjItemAdmin(BaseAdmin):
return qs.select_related( return qs.select_related(
"geo_obj", "geo_obj",
"created_by__user", "created_by__user",
"updated_by__user" "updated_by__user",
).prefetch_related( "parameter_obj",
"parameters_obj__id_satellite", "parameter_obj__id_satellite",
"parameters_obj__polarization", "parameter_obj__polarization",
"parameters_obj__modulation", "parameter_obj__modulation",
"parameters_obj__standard" "parameter_obj__standard"
) )
def sat_name(self, obj): def sat_name(self, obj):
"""Отображает название спутника из связанного параметра.""" """Отображает название спутника из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param and param.id_satellite: if obj.parameter_obj.id_satellite:
return param.id_satellite.name return obj.parameter_obj.id_satellite.name
return "-" return "-"
sat_name.short_description = "Спутник" 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): def freq(self, obj):
"""Отображает частоту из связанного параметра.""" """Отображает частоту из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param: return obj.parameter_obj.frequency
return param.frequency
return "-" return "-"
freq.short_description = "Частота, МГц" freq.short_description = "Частота, МГц"
freq.admin_order_field = "parameters_obj__frequency" freq.admin_order_field = "parameter_obj__frequency"
def distance_geo_kup(self, obj): def distance_geo_kup(self, obj):
"""Отображает расстояние между геолокацией и Кубсатом.""" """Отображает расстояние между геолокацией и Кубсатом."""
@@ -736,42 +768,39 @@ class ObjItemAdmin(BaseAdmin):
def pol(self, obj): def pol(self, obj):
"""Отображает поляризацию из связанного параметра.""" """Отображает поляризацию из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param and param.polarization: if obj.parameter_obj.polarization:
return param.polarization.name return obj.parameter_obj.polarization.name
return "-" return "-"
pol.short_description = "Поляризация" pol.short_description = "Поляризация"
def freq_range(self, obj): def freq_range(self, obj):
"""Отображает полосу частот из связанного параметра.""" """Отображает полосу частот из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param: return obj.parameter_obj.freq_range
return param.freq_range
return "-" return "-"
freq_range.short_description = "Полоса, МГц" 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): def bod_velocity(self, obj):
"""Отображает символьную скорость из связанного параметра.""" """Отображает символьную скорость из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param: return obj.parameter_obj.bod_velocity
return param.bod_velocity
return "-" return "-"
bod_velocity.short_description = "Сим. v, БОД" bod_velocity.short_description = "Сим. v, БОД"
def modulation(self, obj): def modulation(self, obj):
"""Отображает модуляцию из связанного параметра.""" """Отображает модуляцию из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param and param.modulation: if obj.parameter_obj.modulation:
return param.modulation.name return obj.parameter_obj.modulation.name
return "-" return "-"
modulation.short_description = "Модуляция" modulation.short_description = "Модуляция"
def snr(self, obj): def snr(self, obj):
"""Отображает отношение сигнал/шум из связанного параметра.""" """Отображает отношение сигнал/шум из связанного параметра."""
param = next(iter(obj.parameters_obj.all()), None) if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
if param: return obj.parameter_obj.snr
return param.snr
return "-" return "-"
snr.short_description = "ОСШ" snr.short_description = "ОСШ"

View File

@@ -108,6 +108,12 @@ class NewEventForm(forms.Form):
}) })
) )
class ParameterForm(forms.ModelForm): class ParameterForm(forms.ModelForm):
"""
Форма для создания и редактирования параметров ВЧ загрузки.
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
"""
class Meta: class Meta:
model = Parameter model = Parameter
fields = [ fields = [
@@ -115,22 +121,92 @@ class ParameterForm(forms.ModelForm):
'bod_velocity', 'modulation', 'snr', 'standard' 'bod_velocity', 'modulation', 'snr', 'standard'
] ]
widgets = { widgets = {
'id_satellite': forms.Select(attrs={'class': 'form-select'}, choices=[]), 'id_satellite': forms.Select(attrs={
'frequency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'class': 'form-select',
'freq_range': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'required': True
'bod_velocity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), }),
'snr': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'frequency': forms.NumberInput(attrs={
'polarization': forms.Select(attrs={'class': 'form-select'}, choices=[]), 'class': 'form-control',
'modulation': forms.Select(attrs={'class': 'form-select'}, choices=[]), 'step': '0.000001',
'standard': forms.Select(attrs={'class': 'form-select'}, choices=[]), '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): def __init__(self, *args, **kwargs):
super().__init__(*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()] # Динамически загружаем choices для select полей
self.fields['modulation'].choices = [(m.id, m.name) for m in Modulation.objects.all()] self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
self.fields['standard'].choices = [(s.id, s.name) for s in Standard.objects.all()] 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 GeoForm(forms.ModelForm):
class Meta: class Meta:
@@ -143,9 +219,49 @@ class GeoForm(forms.ModelForm):
} }
class ObjItemForm(forms.ModelForm): class ObjItemForm(forms.ModelForm):
"""
Форма для создания и редактирования объектов (источников сигнала).
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
через ParameterForm с использованием OneToOne связи.
"""
class Meta: class Meta:
model = ObjItem model = ObjItem
fields = ['name'] fields = ['name']
widgets = { widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}), '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

View File

@@ -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='Объект'),
),
]

View File

@@ -243,11 +243,11 @@ class ObjItemQuerySet(models.QuerySet):
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"source_type_obj", "source_type_obj",
).prefetch_related( "parameter_obj",
"parameters_obj__id_satellite", "parameter_obj__id_satellite",
"parameters_obj__polarization", "parameter_obj__polarization",
"parameters_obj__modulation", "parameter_obj__modulation",
"parameters_obj__standard", "parameter_obj__standard",
) )
def recent(self, days=30): def recent(self, days=30):
@@ -449,8 +449,14 @@ class Parameter(models.Model):
verbose_name="Стандарт", verbose_name="Стандарт",
) )
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True) # 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 = models.OneToOneField(
ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True 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, 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) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)

View File

@@ -116,72 +116,70 @@
</div> </div>
</div> </div>
<!-- ВЧ загрузки --> <!-- ВЧ загрузка -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>ВЧ загрузка</h4> <h4>ВЧ загрузка</h4>
</div> </div>
{% for param in object.parameters_obj.all %} {% if object.parameter_obj %}
<div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> <div class="row">
<div class="row"> <div class="col-md-3">
<div class="col-md-3"> <div class="mb-3">
<div class="mb-3"> <label class="form-label">Спутник:</label>
<label class="form-label">Спутник:</label> <div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
<div class="readonly-field">{{ param.id_satellite.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Частота (МГц):</label>
<div class="readonly-field">{{ param.frequency|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Полоса (МГц):</label>
<div class="readonly-field">{{ param.freq_range|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Поляризация:</label>
<div class="readonly-field">{{ param.polarization.name|default:"-" }}</div>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="col-md-3">
<div class="col-md-3"> <div class="mb-3">
<div class="mb-3"> <label class="form-label">Частота (МГц):</label>
<label class="form-label">Символьная скорость:</label> <div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
<div class="readonly-field">{{ param.bod_velocity|default:"-" }}</div>
</div>
</div> </div>
<div class="col-md-3"> </div>
<div class="mb-3"> <div class="col-md-3">
<label class="form-label">Модуляция:</label> <div class="mb-3">
<div class="readonly-field">{{ param.modulation.name|default:"-" }}</div> <label class="form-label">Полоса (МГц):</label>
</div> <div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
</div> </div>
<div class="col-md-3"> </div>
<div class="mb-3"> <div class="col-md-3">
<label class="form-label">ОСШ:</label> <div class="mb-3">
<div class="readonly-field">{{ param.snr|default:"-" }}</div> <label class="form-label">Поляризация:</label>
</div> <div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ param.standard.name|default:"-" }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% empty %} <div class="row">
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Символьная скорость:</label>
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Модуляция:</label>
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">ОСШ:</label>
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
</div>
</div>
</div>
{% else %}
<div class="mb-3"> <div class="mb-3">
<p>Нет данных о ВЧ загрузке</p> <p>Нет данных о ВЧ загрузке</p>
</div> </div>
{% endfor %} {% endif %}
</div> </div>
<!-- Блок с картой --> <!-- Блок с картой -->

View File

@@ -63,7 +63,7 @@
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2> <h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
<div> <div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" class="btn btn-primary btn-action">Сохранить</button> <button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
{% if object %} {% if object %}
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a> <a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
{% endif %} {% endif %}
@@ -73,7 +73,7 @@
</div> </div>
</div> </div>
<form method="post"> <form method="post" id="objitem-form">
{% csrf_token %} {% csrf_token %}
<!-- Основная информация --> <!-- Основная информация -->
@@ -124,53 +124,41 @@
</div> </div>
</div> </div>
<!-- ВЧ загрузки --> <!-- ВЧ загрузка -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header d-flex justify-content-between align-items-center"> <div class="form-section-header">
<h4>ВЧ загрузка</h4> <h4>ВЧ загрузка</h4>
{% if not parameter_forms.forms.0.instance.pk %}
<button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button>
{% endif %}
</div> </div>
<div id="parameters-container"> <div id="parameters-container">
{% for param_form in parameter_forms %} <div class="row">
{% comment %} <div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% endcomment %} <div class="col-md-3">
<div class="dynamic-form-header"> {% include 'mainapp/components/_form_field.html' with field=parameter_form.id_satellite %}
{% if parameter_forms.forms|length > 1 %}
<button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button>
{% endif %}
</div> </div>
<div class="row"> <div class="col-md-3">
<div class="col-md-3"> {% include 'mainapp/components/_form_field.html' with field=parameter_form.frequency %}
{% include 'mainapp/components/_form_field.html' with field=param_form.id_satellite %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.frequency %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.freq_range %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.polarization %}
</div>
</div> </div>
<div class="row"> <div class="col-md-3">
<div class="col-md-3"> {% include 'mainapp/components/_form_field.html' with field=parameter_form.freq_range %}
{% include 'mainapp/components/_form_field.html' with field=param_form.bod_velocity %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.modulation %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.snr %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=param_form.standard %}
</div>
</div> </div>
{% comment %} </div> {% endcomment %} <div class="col-md-3">
{% endfor %} {% include 'mainapp/components/_form_field.html' with field=parameter_form.polarization %}
</div>
</div>
<div class="row">
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=parameter_form.bod_velocity %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=parameter_form.modulation %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=parameter_form.snr %}
</div>
<div class="col-md-3">
{% include 'mainapp/components/_form_field.html' with field=parameter_form.standard %}
</div>
</div>
</div> </div>
</div> </div>
@@ -348,42 +336,6 @@
<script> <script>
// Динамическое добавление ВЧ загрузок
let parameterIndex = {{ parameter_forms|length }};
document.getElementById('add-parameter')?.addEventListener('click', function() {
const container = document.getElementById('parameters-container');
const template = document.querySelector('.dynamic-form');
if (template) {
const clone = template.cloneNode(true);
clone.querySelectorAll('[id]').forEach(el => {
el.id = el.id.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelectorAll('[name]').forEach(el => {
el.name = el.name.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelectorAll('[for]').forEach(el => {
el.htmlFor = el.htmlFor.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelector('.dynamic-form-header h5').textContent = `ВЧ загрузка #${parameterIndex + 1}`;
clone.dataset.parameterIndex = parameterIndex;
container.appendChild(clone);
parameterIndex++;
}
});
// Удаление ВЧ загрузок
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-parameter')) {
if (document.querySelectorAll('.dynamic-form').length > 1) {
e.target.closest('.dynamic-form').remove();
} else {
alert('Должна быть хотя бы одна ВЧ загрузка');
}
}
});
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты // Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5); const map = L.map('map').setView([55.75, 37.62], 5);

View File

@@ -163,16 +163,6 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
source = stroka[1]["Объект наблюдения"] source = stroka[1]["Объект наблюдения"]
user_to_use = current_user if current_user else CustomUser.objects.get(id=1) user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
vch_load_obj, _ = Parameter.objects.get_or_create(
id_satellite=sat,
polarization=polarization_obj,
frequency=freq,
freq_range=freq_line,
bod_velocity=v,
modulation=mod_obj,
snr=snr,
)
geo, _ = Geo.objects.get_or_create( geo, _ = Geo.objects.get_or_create(
timestamp=timestamp, timestamp=timestamp,
coords=geo_point, coords=geo_point,
@@ -187,14 +177,40 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
geo.save() geo.save()
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors)) geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
existing_obj_items = ObjItem.objects.filter( # Check if ObjItem with same geo already exists
parameters_obj=vch_load_obj, geo_obj=geo existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
if existing_obj_item:
# Check if parameter with same values exists for this object
if (
hasattr(existing_obj_item, 'parameter_obj') and
existing_obj_item.parameter_obj and
existing_obj_item.parameter_obj.id_satellite == sat and
existing_obj_item.parameter_obj.polarization == polarization_obj and
existing_obj_item.parameter_obj.frequency == freq and
existing_obj_item.parameter_obj.freq_range == freq_line and
existing_obj_item.parameter_obj.bod_velocity == v and
existing_obj_item.parameter_obj.modulation == mod_obj and
existing_obj_item.parameter_obj.snr == snr
):
# Skip creating duplicate
continue
# Create new ObjItem and Parameter
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
vch_load_obj = Parameter.objects.create(
id_satellite=sat,
polarization=polarization_obj,
frequency=freq,
freq_range=freq_line,
bod_velocity=v,
modulation=mod_obj,
snr=snr,
objitem=obj_item
) )
if not existing_obj_items.exists():
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use) geo.objitem = obj_item
obj_item.parameters_obj.set([vch_load_obj]) geo.save()
geo.objitem = obj_item
geo.save()
def add_satellite_list(): def add_satellite_list():
@@ -316,14 +332,6 @@ def get_points_from_csv(file_content, current_user=None):
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"]) mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
user_to_use = current_user if current_user else CustomUser.objects.get(id=1) user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
vch_load_obj, _ = Parameter.objects.get_or_create(
id_satellite=sat_obj,
polarization=pol_obj,
frequency=row["freq"],
freq_range=row["f_range"],
# defaults={'id_user_add': user_to_use}
)
geo_obj, _ = Geo.objects.get_or_create( geo_obj, _ = Geo.objects.get_or_create(
timestamp=row["time"], timestamp=row["time"],
coords=Point(row["lon"], row["lat"], srid=4326), coords=Point(row["lon"], row["lat"], srid=4326),
@@ -334,14 +342,34 @@ def get_points_from_csv(file_content, current_user=None):
) )
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst)) geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
existing_obj_items = ObjItem.objects.filter( # Check if ObjItem with same geo already exists
parameters_obj=vch_load_obj, geo_obj=geo_obj existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
if existing_obj_item:
# Check if parameter with same values exists for this object
if (
hasattr(existing_obj_item, 'parameter_obj') and
existing_obj_item.parameter_obj and
existing_obj_item.parameter_obj.id_satellite == sat_obj and
existing_obj_item.parameter_obj.polarization == pol_obj and
existing_obj_item.parameter_obj.frequency == row["freq"] and
existing_obj_item.parameter_obj.freq_range == row["f_range"]
):
# Skip creating duplicate
continue
# Create new ObjItem and Parameter
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
vch_load_obj = Parameter.objects.create(
id_satellite=sat_obj,
polarization=pol_obj,
frequency=row["freq"],
freq_range=row["f_range"],
objitem=obj_item
) )
if not existing_obj_items.exists():
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use) geo_obj.objitem = obj_item
obj_item.parameters_obj.set([vch_load_obj]) geo_obj.save()
geo_obj.objitem = obj_item
geo_obj.save()
def get_vch_load_from_html(file, sat: Satellite) -> None: def get_vch_load_from_html(file, sat: Satellite) -> None:
@@ -598,29 +626,22 @@ def parse_pagination_params(
def get_first_param_subquery(field_name: str): def get_first_param_subquery(field_name: str):
""" """
Создает подзапрос для получения первого параметра объекта. Возвращает F() выражение для доступа к полю параметра через OneToOne связь.
Используется для аннотации queryset с полями из связанной модели Parameter. После рефакторинга связи Parameter-ObjItem с ManyToMany на OneToOne,
Возвращает значение указанного поля из первого параметра объекта. эта функция упрощена для возврата прямого F() выражения вместо подзапроса.
Args: Args:
field_name (str): Имя поля модели Parameter для извлечения. field_name (str): Имя поля модели Parameter для извлечения.
Может включать связанные поля через __ (например, 'id_satellite__name'). Может включать связанные поля через __ (например, 'id_satellite__name').
Returns: Returns:
Subquery: Django Subquery объект для использования в annotate(). F: Django F() объект для использования в annotate().
Example: Example:
>>> from django.db.models import Subquery, OuterRef >>> freq_expr = get_first_param_subquery('frequency')
>>> freq_subq = get_first_param_subquery('frequency') >>> objects = ObjItem.objects.annotate(first_freq=freq_expr)
>>> objects = ObjItem.objects.annotate(first_freq=Subquery(freq_subq))
>>> for obj in objects: >>> for obj in objects:
... print(obj.first_freq) ... print(obj.first_freq)
""" """
from django.db.models import OuterRef return F(f"parameter_obj__{field_name}")
return (
Parameter.objects.filter(objitems=OuterRef("pk"))
.order_by("id")
.values(field_name)[:1]
)

View File

@@ -1,30 +1,24 @@
# Standard library imports # Standard library imports
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from io import BytesIO from io import BytesIO
# Django imports # Django imports
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.gis.geos import Point
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import models from django.db import models
from django.db.models import OuterRef, Prefetch, Subquery from django.db.models import F
from django.forms import inlineformset_factory, modelformset_factory
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_GET
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
FormView, FormView,
TemplateView,
UpdateView, UpdateView,
) )
@@ -45,18 +39,17 @@ from .forms import (
VchLinkForm, VchLinkForm,
) )
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite from .models import Geo, Modulation, ObjItem, Polarization, Satellite
from .utils import ( from .utils import (
add_satellite_list, add_satellite_list,
compare_and_link_vch_load, compare_and_link_vch_load,
fill_data_from_df, fill_data_from_df,
get_first_param_subquery,
get_points_from_csv, get_points_from_csv,
get_vch_load_from_html, get_vch_load_from_html,
kub_report, kub_report,
parse_pagination_params, parse_pagination_params,
) )
from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml from mapsapp.utils import parse_transponders_from_xml
class AddSatellitesView(LoginRequiredMixin, View): class AddSatellitesView(LoginRequiredMixin, View):
@@ -150,9 +143,12 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
class GetLocationsView(LoginRequiredMixin, View): class GetLocationsView(LoginRequiredMixin, View):
def get(self, request, sat_id): def get(self, request, sat_id):
locations = ( locations = (
ObjItem.objects.filter(parameters_obj__id_satellite=sat_id) ObjItem.objects.filter(parameter_obj__id_satellite=sat_id)
.select_related("geo_obj") .select_related(
.prefetch_related("parameters_obj__polarization") "geo_obj",
"parameter_obj",
"parameter_obj__polarization",
)
) )
if not locations.exists(): if not locations.exists():
@@ -163,11 +159,10 @@ class GetLocationsView(LoginRequiredMixin, View):
if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords: if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
continue continue
params = list(loc.parameters_obj.all()) param = getattr(loc, 'parameter_obj', None)
if not params: if not param:
continue continue
param = params[0]
features.append( features.append(
{ {
"type": "Feature", "type": "Feature",
@@ -220,11 +215,12 @@ class ShowMapView(RoleRequiredMixin, View):
points = [] points = []
if ids: if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()] id_list = [int(x) for x in ids.split(",") if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related( locations = ObjItem.objects.filter(id__in=id_list).select_related(
"parameters_obj__id_satellite", "parameter_obj",
"parameters_obj__polarization", "parameter_obj__id_satellite",
"parameters_obj__modulation", "parameter_obj__polarization",
"parameters_obj__standard", "parameter_obj__modulation",
"parameter_obj__standard",
"geo_obj", "geo_obj",
) )
for obj in locations: for obj in locations:
@@ -234,7 +230,9 @@ class ShowMapView(RoleRequiredMixin, View):
or not obj.geo_obj.coords or not obj.geo_obj.coords
): ):
continue continue
param = obj.parameters_obj.get() param = getattr(obj, 'parameter_obj', None)
if not param:
continue
points.append( points.append(
{ {
"name": f"{obj.name}", "name": f"{obj.name}",
@@ -265,11 +263,12 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
points = [] points = []
if ids: if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()] id_list = [int(x) for x in ids.split(",") if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related( locations = ObjItem.objects.filter(id__in=id_list).select_related(
"parameters_obj__id_satellite", "parameter_obj",
"parameters_obj__polarization", "parameter_obj__id_satellite",
"parameters_obj__modulation", "parameter_obj__polarization",
"parameters_obj__standard", "parameter_obj__modulation",
"parameter_obj__standard",
"geo_obj", "geo_obj",
) )
for obj in locations: for obj in locations:
@@ -279,7 +278,9 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
or not obj.geo_obj.coords or not obj.geo_obj.coords
): ):
continue continue
param = obj.parameters_obj.get() param = getattr(obj, 'parameter_obj', None)
if not param:
continue
points.append( points.append(
{ {
"name": f"{obj.name}", "name": f"{obj.name}",
@@ -429,7 +430,7 @@ class DeleteSelectedObjectsView(RoleRequiredMixin, View):
class ObjItemListView(LoginRequiredMixin, View): class ObjItemListView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
satellites = ( satellites = (
Satellite.objects.filter(parameters__objitems__isnull=False) Satellite.objects.filter(parameters__objitem__isnull=False)
.distinct() .distinct()
.only("id", "name") .only("id", "name")
.order_by("name") .order_by("name")
@@ -472,32 +473,31 @@ class ObjItemListView(LoginRequiredMixin, View):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"parameter_obj",
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
"parameter_obj__standard",
) )
.prefetch_related( .filter(parameter_obj__id_satellite_id__in=selected_satellites)
"parameters_obj__id_satellite",
"parameters_obj__polarization",
"parameters_obj__modulation",
"parameters_obj__standard",
)
.filter(parameters_obj__id_satellite_id__in=selected_satellites)
) )
else: else:
objects = ObjItem.objects.select_related( objects = ObjItem.objects.select_related(
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
).prefetch_related( "parameter_obj",
"parameters_obj__id_satellite", "parameter_obj__id_satellite",
"parameters_obj__polarization", "parameter_obj__polarization",
"parameters_obj__modulation", "parameter_obj__modulation",
"parameters_obj__standard", "parameter_obj__standard",
) )
if freq_min is not None and freq_min.strip() != "": if freq_min is not None and freq_min.strip() != "":
try: try:
freq_min_val = float(freq_min) freq_min_val = float(freq_min)
objects = objects.filter( objects = objects.filter(
parameters_obj__frequency__gte=freq_min_val parameter_obj__frequency__gte=freq_min_val
) )
except ValueError: except ValueError:
pass pass
@@ -505,7 +505,7 @@ class ObjItemListView(LoginRequiredMixin, View):
try: try:
freq_max_val = float(freq_max) freq_max_val = float(freq_max)
objects = objects.filter( objects = objects.filter(
parameters_obj__frequency__lte=freq_max_val parameter_obj__frequency__lte=freq_max_val
) )
except ValueError: except ValueError:
pass pass
@@ -514,7 +514,7 @@ class ObjItemListView(LoginRequiredMixin, View):
try: try:
range_min_val = float(range_min) range_min_val = float(range_min)
objects = objects.filter( objects = objects.filter(
parameters_obj__freq_range__gte=range_min_val parameter_obj__freq_range__gte=range_min_val
) )
except ValueError: except ValueError:
pass pass
@@ -522,7 +522,7 @@ class ObjItemListView(LoginRequiredMixin, View):
try: try:
range_max_val = float(range_max) range_max_val = float(range_max)
objects = objects.filter( objects = objects.filter(
parameters_obj__freq_range__lte=range_max_val parameter_obj__freq_range__lte=range_max_val
) )
except ValueError: except ValueError:
pass pass
@@ -530,13 +530,13 @@ class ObjItemListView(LoginRequiredMixin, View):
if snr_min is not None and snr_min.strip() != "": if snr_min is not None and snr_min.strip() != "":
try: try:
snr_min_val = float(snr_min) snr_min_val = float(snr_min)
objects = objects.filter(parameters_obj__snr__gte=snr_min_val) objects = objects.filter(parameter_obj__snr__gte=snr_min_val)
except ValueError: except ValueError:
pass pass
if snr_max is not None and snr_max.strip() != "": if snr_max is not None and snr_max.strip() != "":
try: try:
snr_max_val = float(snr_max) snr_max_val = float(snr_max)
objects = objects.filter(parameters_obj__snr__lte=snr_max_val) objects = objects.filter(parameter_obj__snr__lte=snr_max_val)
except ValueError: except ValueError:
pass pass
@@ -544,7 +544,7 @@ class ObjItemListView(LoginRequiredMixin, View):
try: try:
bod_min_val = float(bod_min) bod_min_val = float(bod_min)
objects = objects.filter( objects = objects.filter(
parameters_obj__bod_velocity__gte=bod_min_val parameter_obj__bod_velocity__gte=bod_min_val
) )
except ValueError: except ValueError:
pass pass
@@ -552,19 +552,19 @@ class ObjItemListView(LoginRequiredMixin, View):
try: try:
bod_max_val = float(bod_max) bod_max_val = float(bod_max)
objects = objects.filter( objects = objects.filter(
parameters_obj__bod_velocity__lte=bod_max_val parameter_obj__bod_velocity__lte=bod_max_val
) )
except ValueError: except ValueError:
pass pass
if selected_modulations: if selected_modulations:
objects = objects.filter( objects = objects.filter(
parameters_obj__modulation__id__in=selected_modulations parameter_obj__modulation__id__in=selected_modulations
) )
if selected_polarizations: if selected_polarizations:
objects = objects.filter( objects = objects.filter(
parameters_obj__polarization__id__in=selected_polarizations parameter_obj__polarization__id__in=selected_polarizations
) )
if has_kupsat == "1": if has_kupsat == "1":
@@ -609,22 +609,14 @@ class ObjItemListView(LoginRequiredMixin, View):
else: else:
selected_sat_id = None selected_sat_id = None
first_param_freq_subq = get_first_param_subquery("frequency")
first_param_range_subq = get_first_param_subquery("freq_range")
first_param_snr_subq = get_first_param_subquery("snr")
first_param_bod_subq = get_first_param_subquery("bod_velocity")
first_param_sat_name_subq = get_first_param_subquery("id_satellite__name")
first_param_pol_name_subq = get_first_param_subquery("polarization__name")
first_param_mod_name_subq = get_first_param_subquery("modulation__name")
objects = objects.annotate( objects = objects.annotate(
first_param_freq=Subquery(first_param_freq_subq), first_param_freq=F("parameter_obj__frequency"),
first_param_range=Subquery(first_param_range_subq), first_param_range=F("parameter_obj__freq_range"),
first_param_snr=Subquery(first_param_snr_subq), first_param_snr=F("parameter_obj__snr"),
first_param_bod=Subquery(first_param_bod_subq), first_param_bod=F("parameter_obj__bod_velocity"),
first_param_sat_name=Subquery(first_param_sat_name_subq), first_param_sat_name=F("parameter_obj__id_satellite__name"),
first_param_pol_name=Subquery(first_param_pol_name_subq), first_param_pol_name=F("parameter_obj__polarization__name"),
first_param_mod_name=Subquery(first_param_mod_name_subq), first_param_mod_name=F("parameter_obj__modulation__name"),
) )
valid_sort_fields = { valid_sort_fields = {
@@ -664,11 +656,7 @@ class ObjItemListView(LoginRequiredMixin, View):
processed_objects = [] processed_objects = []
for obj in page_obj: for obj in page_obj:
param = None param = getattr(obj, 'parameter_obj', None)
if hasattr(obj, "parameters_obj") and obj.parameters_obj.all():
param_list = list(obj.parameters_obj.all())
if param_list:
param = param_list[0]
geo_coords = "-" geo_coords = "-"
geo_timestamp = "-" geo_timestamp = "-"
@@ -874,40 +862,33 @@ class ObjItemFormView(
# Сохраняем параметры возврата для кнопки "Назад" # Сохраняем параметры возврата для кнопки "Назад"
context["return_params"] = self.request.GET.get('return_params', '') context["return_params"] = self.request.GET.get('return_params', '')
ParameterFormSet = modelformset_factory( # Работаем с одной формой параметра вместо formset
Parameter, if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
form=ParameterForm, context["parameter_form"] = ParameterForm(
extra=self.get_parameter_formset_extra(), instance=self.object.parameter_obj, prefix="parameter"
can_delete=True,
)
if self.object:
parameter_queryset = self.object.parameters_obj.all()
context["parameter_forms"] = ParameterFormSet(
queryset=parameter_queryset, prefix="parameters"
) )
if hasattr(self.object, "geo_obj"):
context["geo_form"] = GeoForm(
instance=self.object.geo_obj, prefix="geo"
)
else:
context["geo_form"] = GeoForm(prefix="geo")
else: else:
context["parameter_forms"] = ParameterFormSet( context["parameter_form"] = ParameterForm(prefix="parameter")
queryset=Parameter.objects.none(), prefix="parameters"
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
context["geo_form"] = GeoForm(
instance=self.object.geo_obj, prefix="geo"
) )
else:
context["geo_form"] = GeoForm(prefix="geo") context["geo_form"] = GeoForm(prefix="geo")
return context return context
def get_parameter_formset_extra(self):
"""Возвращает количество дополнительных форм для параметров."""
return 0 if self.object else 1
def form_valid(self, form): def form_valid(self, form):
context = self.get_context_data() # Получаем форму параметра
parameter_forms = context["parameter_forms"] if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
parameter_form = ParameterForm(
self.request.POST,
instance=self.object.parameter_obj,
prefix="parameter"
)
else:
parameter_form = ParameterForm(self.request.POST, prefix="parameter")
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj: if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo") geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
@@ -919,17 +900,26 @@ class ObjItemFormView(
self.set_user_fields() self.set_user_fields()
self.object.save() self.object.save()
# Сохраняем связанные параметры # Сохраняем связанный параметр
if parameter_forms.is_valid(): if parameter_form.is_valid():
self.save_parameters(parameter_forms) self.save_parameter(parameter_form)
else:
context = self.get_context_data()
context.update({
'form': form,
'parameter_form': parameter_form,
'geo_form': geo_form,
})
return self.render_to_response(context)
# Сохраняем геоданные # Сохраняем геоданные
if geo_form.is_valid(): if geo_form.is_valid():
self.save_geo_data(geo_form) self.save_geo_data(geo_form)
else: else:
context = self.get_context_data()
context.update({ context.update({
'form': form, 'form': form,
'parameter_forms': parameter_forms, 'parameter_form': parameter_form,
'geo_form': geo_form, 'geo_form': geo_form,
}) })
return self.render_to_response(context) return self.render_to_response(context)
@@ -940,51 +930,12 @@ class ObjItemFormView(
"""Устанавливает поля пользователя для объекта.""" """Устанавливает поля пользователя для объекта."""
raise NotImplementedError("Subclasses must implement set_user_fields()") raise NotImplementedError("Subclasses must implement set_user_fields()")
def save_parameters(self, parameter_forms): def save_parameter(self, parameter_form):
"""Сохраняет параметры объекта с проверкой дубликатов.""" """Сохраняет параметр объекта через OneToOne связь."""
instances = parameter_forms.save(commit=False) if parameter_form.is_valid():
instance = parameter_form.save(commit=False)
# Обрабатываем удаленные параметры instance.objitem = self.object
for deleted_obj in parameter_forms.deleted_objects: instance.save()
# Отвязываем параметр от объекта
deleted_obj.objitems.remove(self.object)
# Если параметр больше не связан ни с одним объектом, удаляем его
if not deleted_obj.objitems.exists():
deleted_obj.delete()
for instance in instances:
# Проверяем, существует ли уже такая ВЧ загрузка
existing_param = Parameter.objects.filter(
id_satellite=instance.id_satellite,
polarization=instance.polarization,
frequency=instance.frequency,
freq_range=instance.freq_range,
bod_velocity=instance.bod_velocity,
modulation=instance.modulation,
snr=instance.snr,
standard=instance.standard,
).exclude(pk=instance.pk if instance.pk else None).first()
if existing_param:
# Если найден дубликат, удаляем старую запись из объекта
if instance.pk:
# Отвязываем старый параметр от объекта
instance.objitems.remove(self.object)
# Если старый параметр больше не связан ни с одним объектом, удаляем его
if not instance.objitems.exists():
instance.delete()
# Используем существующий параметр
self.link_parameter_to_object(existing_param)
else:
# Сохраняем новый параметр
instance.save()
self.link_parameter_to_object(instance)
def link_parameter_to_object(self, parameter):
"""Связывает параметр с объектом."""
raise NotImplementedError(
"Subclasses must implement link_parameter_to_object()"
)
def save_geo_data(self, geo_form): def save_geo_data(self, geo_form):
"""Сохраняет геоданные объекта.""" """Сохраняет геоданные объекта."""
@@ -1019,11 +970,6 @@ class ObjItemUpdateView(ObjItemFormView):
def set_user_fields(self): def set_user_fields(self):
self.object.updated_by = self.request.user.customuser self.object.updated_by = self.request.user.customuser
def link_parameter_to_object(self, parameter):
# Добавляем объект к параметру, если его там еще нет
if self.object not in parameter.objitems.all():
parameter.objitems.add(self.object)
class ObjItemCreateView(ObjItemFormView, CreateView): class ObjItemCreateView(ObjItemFormView, CreateView):
"""Представление для создания ObjItem.""" """Представление для создания ObjItem."""
@@ -1034,9 +980,6 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
self.object.created_by = self.request.user.customuser self.object.created_by = self.request.user.customuser
self.object.updated_by = self.request.user.customuser self.object.updated_by = self.request.user.customuser
def link_parameter_to_object(self, parameter):
parameter.objitems.add(self.object)
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView): class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
model = ObjItem model = ObjItem
@@ -1066,11 +1009,11 @@ class ObjItemDetailView(LoginRequiredMixin, View):
'geo_obj', 'geo_obj',
'updated_by__user', 'updated_by__user',
'created_by__user', 'created_by__user',
).prefetch_related( 'parameter_obj',
'parameters_obj__id_satellite', 'parameter_obj__id_satellite',
'parameters_obj__polarization', 'parameter_obj__polarization',
'parameters_obj__modulation', 'parameter_obj__modulation',
'parameters_obj__standard', 'parameter_obj__standard',
).first() ).first()
if not obj: if not obj:

View File

@@ -19,42 +19,42 @@ services:
networks: networks:
- app-network - app-network
web: # web:
build: # build:
context: ./dbapp # context: ./dbapp
dockerfile: Dockerfile # dockerfile: Dockerfile
container_name: django-app-dev # container_name: django-app-dev
restart: unless-stopped # restart: unless-stopped
environment: # environment:
- DEBUG=True # - DEBUG=True
- ENVIRONMENT=development # - ENVIRONMENT=development
- DJANGO_SETTINGS_MODULE=dbapp.settings.development # - DJANGO_SETTINGS_MODULE=dbapp.settings.development
- SECRET_KEY=django-insecure-dev-key-change-in-production # - SECRET_KEY=django-insecure-dev-key-change-in-production
- DB_ENGINE=django.contrib.gis.db.backends.postgis # - DB_ENGINE=django.contrib.gis.db.backends.postgis
- DB_NAME=geodb # - DB_NAME=geodb
- DB_USER=geralt # - DB_USER=geralt
- DB_PASSWORD=123456 # - DB_PASSWORD=123456
- DB_HOST=db # - DB_HOST=db
- DB_PORT=5432 # - DB_PORT=5432
- ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 # - ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
ports: # ports:
- "8000:8000" # - "8000:8000"
volumes: # volumes:
# Монтируем только код приложения, не весь проект # # Монтируем только код приложения, не весь проект
- ./dbapp/dbapp:/app/dbapp # - ./dbapp/dbapp:/app/dbapp
- ./dbapp/mainapp:/app/mainapp # - ./dbapp/mainapp:/app/mainapp
- ./dbapp/mapsapp:/app/mapsapp # - ./dbapp/mapsapp:/app/mapsapp
- ./dbapp/lyngsatapp:/app/lyngsatapp # - ./dbapp/lyngsatapp:/app/lyngsatapp
- ./dbapp/static:/app/static # - ./dbapp/static:/app/static
- ./dbapp/manage.py:/app/manage.py # - ./dbapp/manage.py:/app/manage.py
- static_volume_dev:/app/staticfiles # - static_volume_dev:/app/staticfiles
- media_volume_dev:/app/media # - media_volume_dev:/app/media
- logs_volume_dev:/app/logs # - logs_volume_dev:/app/logs
depends_on: # depends_on:
db: # db:
condition: service_healthy # condition: service_healthy
networks: # networks:
- app-network # - app-network
# tileserver: # tileserver:
# image: maptiler/tileserver-gl:latest # image: maptiler/tileserver-gl:latest
@@ -72,9 +72,9 @@ services:
volumes: volumes:
postgres_data_dev: postgres_data_dev:
static_volume_dev: # static_volume_dev:
media_volume_dev: # media_volume_dev:
logs_volume_dev: # logs_volume_dev:
# tileserver_config_dev: # tileserver_config_dev:
networks: networks: