# Django imports from django import forms # Local imports from .models import ( Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Source, Standard, ) from .widgets import CheckboxSelectMultipleWidget # Import from mapsapp to avoid circular import issues from mapsapp.models import Transponders class UploadFileForm(forms.Form): file = forms.FileField( label="Выберите файл", widget=forms.FileInput(attrs={"class": "form-file-input"}), ) class LoadExcelData(forms.Form): file = forms.FileField( label="Выберите Excel файл", widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}), ) sat_choice = forms.ModelChoiceField( queryset=Satellite.objects.all(), label="Выберите спутник", widget=forms.Select(attrs={"class": "form-select"}), ) number_input = forms.IntegerField( label="Введите число объектов", min_value=0, widget=forms.NumberInput(attrs={"class": "form-control"}), ) is_automatic = forms.BooleanField( label="Автоматическая загрузка", required=False, initial=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), help_text="Если отмечено, точки не будут добавляться к объектам", ) class LoadCsvData(forms.Form): file = forms.FileField( label="Выберите CSV файл", widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}), ) is_automatic = forms.BooleanField( label="Автоматическая загрузка", required=False, initial=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), help_text="Если отмечено, точки не будут добавляться к объектам", ) class UploadVchLoad(UploadFileForm): sat_choice = forms.ModelChoiceField( queryset=Satellite.objects.all(), label="Выберите спутник", widget=forms.Select(attrs={"class": "form-select"}), ) class VchLinkForm(forms.Form): sat_choice = forms.ModelChoiceField( queryset=Satellite.objects.all(), label="Выберите спутник", widget=forms.Select(attrs={"class": "form-select"}), ) # ku_range = forms.ChoiceField( # choices=[(9750.0, '9750'), (10750.0, '10750')], # # coerce=lambda x: x == 'True', # widget=forms.Select(attrs={'class': 'form-select'}), # label='Выбор диапазона' # ) value1 = forms.FloatField( label="Разброс по частоте (не используется)", required=False, initial=0.0, widget=forms.NumberInput( attrs={ "class": "form-control", "placeholder": "Не используется - погрешность определяется автоматически", } ), ) value2 = forms.FloatField( label="Разброс по полосе (в %)", widget=forms.NumberInput( attrs={ "class": "form-control", "placeholder": "Введите погрешность полосы в процентах", "step": "0.1", } ), ) class NewEventForm(forms.Form): # sat_choice = forms.ModelChoiceField( # queryset=Satellite.objects.all(), # label="Выберите спутник", # widget=forms.Select(attrs={ # 'class': 'form-select' # }) # ) # pol_choice = forms.ModelChoiceField( # queryset=Polarization.objects.all(), # label="Выберите поляризацию", # widget=forms.Select(attrs={ # 'class': 'form-select' # }) # ) file = forms.FileField( label="Выберите файл", widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}), ) class FillLyngsatDataForm(forms.Form): """Форма для заполнения данных из Lyngsat с поддержкой кеширования""" REGION_CHOICES = [ ("europe", "Европа"), ("asia", "Азия"), ("america", "Америка"), ("atlantic", "Атлантика"), ] satellites = forms.ModelMultipleChoiceField( queryset=Satellite.objects.all().order_by("name"), label="Выберите спутники", widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}), required=True, help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников", ) regions = forms.MultipleChoiceField( choices=REGION_CHOICES, label="Выберите регионы", widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "4"}), required=True, initial=["europe", "asia", "america", "atlantic"], help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов", ) use_cache = forms.BooleanField( label="Использовать кеширование", required=False, initial=True, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), help_text="Использовать кешированные данные (ускоряет повторные запросы)", ) force_refresh = forms.BooleanField( label="Принудительно обновить данные", required=False, initial=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), help_text="Игнорировать кеш и получить свежие данные с сайта", ) class LinkLyngsatForm(forms.Form): """Форма для привязки источников LyngSat к объектам""" satellites = forms.ModelMultipleChoiceField( queryset=Satellite.objects.all().order_by("name"), label="Выберите спутники", widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}), required=False, help_text="Оставьте пустым для обработки всех спутников", ) frequency_tolerance = forms.FloatField( label="Допуск по частоте (МГц)", initial=0.5, min_value=0, widget=forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}), help_text="Допустимое отклонение частоты при сравнении", ) class ParameterForm(forms.ModelForm): """ Форма для создания и редактирования параметров ВЧ загрузки. Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь. """ class Meta: model = Parameter fields = [ "id_satellite", "frequency", "freq_range", "polarization", "bod_velocity", "modulation", "snr", "standard", ] widgets = { "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) # Динамически загружаем 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: model = Geo fields = ["location", "comment", "is_average", "mirrors"] widgets = { "location": forms.TextInput(attrs={"class": "form-control"}), "comment": forms.TextInput(attrs={"class": "form-control"}), "is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}), "mirrors": CheckboxSelectMultipleWidget( attrs={ "id": "id_geo-mirrors", "placeholder": "Выберите спутники...", } ), } labels = { "location": "Местоположение", "comment": "Комментарий", "is_average": "Усреднённое", "mirrors": "Спутники-зеркала, использованные для приёма", } help_texts = { "mirrors": "Выберите спутники из списка", } class ObjItemForm(forms.ModelForm): """ Форма для создания и редактирования объектов (источников сигнала). Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно через ParameterForm с использованием OneToOne связи. """ class Meta: model = ObjItem fields = ["name"] widgets = { "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 class SourceForm(forms.ModelForm): """Form for editing Source model with 4 coordinate fields.""" # Координаты ГЛ (coords_average) average_latitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} ), label="Широта ГЛ", ) average_longitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={ "class": "form-control", "step": "0.000001", "placeholder": "Долгота", } ), label="Долгота ГЛ", ) # Координаты Кубсата (coords_kupsat) kupsat_latitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} ), label="Широта Кубсата", ) kupsat_longitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={ "class": "form-control", "step": "0.000001", "placeholder": "Долгота", } ), label="Долгота Кубсата", ) # Координаты оперативников (coords_valid) valid_latitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} ), label="Широта оперативников", ) valid_longitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={ "class": "form-control", "step": "0.000001", "placeholder": "Долгота", } ), label="Долгота оперативников", ) # Координаты справочные (coords_reference) reference_latitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} ), label="Широта справочные", ) reference_longitude = forms.FloatField( required=False, widget=forms.NumberInput( attrs={ "class": "form-control", "step": "0.000001", "placeholder": "Долгота", } ), label="Долгота справочные", ) class Meta: model = Source fields = ['info', 'ownership', 'note'] widgets = { 'info': forms.Select(attrs={ 'class': 'form-select', 'id': 'id_info', }), 'ownership': forms.Select(attrs={ 'class': 'form-select', 'id': 'id_ownership', }), 'note': forms.Textarea(attrs={ 'class': 'form-control', 'rows': "3", 'id': 'id_note', }) } labels = { 'info': 'Тип объекта', 'ownership': 'Принадлежность объекта', 'note': 'Примечание' } help_texts = { 'info': 'Стационарные: координата усредняется. Подвижные: координата = последняя точка. При изменении типа координата пересчитывается автоматически.', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Заполняем поля координат из instance if self.instance and self.instance.pk: if self.instance.coords_average: self.fields[ "average_longitude" ].initial = self.instance.coords_average.x self.fields["average_latitude"].initial = self.instance.coords_average.y if self.instance.coords_kupsat: self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y if self.instance.coords_valid: self.fields["valid_longitude"].initial = self.instance.coords_valid.x self.fields["valid_latitude"].initial = self.instance.coords_valid.y if self.instance.coords_reference: self.fields[ "reference_longitude" ].initial = self.instance.coords_reference.x self.fields[ "reference_latitude" ].initial = self.instance.coords_reference.y def save(self, commit=True): from django.contrib.gis.geos import Point instance = super().save(commit=False) # Обработка coords_average avg_lat = self.cleaned_data.get("average_latitude") avg_lng = self.cleaned_data.get("average_longitude") if avg_lat is not None and avg_lng is not None: instance.coords_average = Point(avg_lng, avg_lat, srid=4326) else: instance.coords_average = None # Обработка coords_kupsat kup_lat = self.cleaned_data.get("kupsat_latitude") kup_lng = self.cleaned_data.get("kupsat_longitude") if kup_lat is not None and kup_lng is not None: instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326) else: instance.coords_kupsat = None # Обработка coords_valid val_lat = self.cleaned_data.get("valid_latitude") val_lng = self.cleaned_data.get("valid_longitude") if val_lat is not None and val_lng is not None: instance.coords_valid = Point(val_lng, val_lat, srid=4326) else: instance.coords_valid = None # Обработка coords_reference ref_lat = self.cleaned_data.get("reference_latitude") ref_lng = self.cleaned_data.get("reference_longitude") if ref_lat is not None and ref_lng is not None: instance.coords_reference = Point(ref_lng, ref_lat, srid=4326) else: instance.coords_reference = None if commit: instance.save() return instance class KubsatFilterForm(forms.Form): """Форма фильтров для страницы Кубсат""" satellites = forms.ModelMultipleChoiceField( queryset=None, # Будет установлен в __init__ label='Спутники', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'}) ) band = forms.ModelMultipleChoiceField( queryset=None, label='Диапазоны работы спутника', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) polarization = forms.ModelMultipleChoiceField( queryset=Polarization.objects.all().order_by('name'), label='Поляризация', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) frequency_min = forms.FloatField( label='Центральная частота от (МГц)', required=False, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}) ) frequency_max = forms.FloatField( label='Центральная частота до (МГц)', required=False, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}) ) freq_range_min = forms.FloatField( label='Полоса от (МГц)', required=False, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}) ) freq_range_max = forms.FloatField( label='Полоса до (МГц)', required=False, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}) ) modulation = forms.ModelMultipleChoiceField( queryset=Modulation.objects.all().order_by('name'), label='Модуляция', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) object_type = forms.ModelMultipleChoiceField( queryset=None, label='Тип объекта', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) ) object_ownership = forms.ModelMultipleChoiceField( queryset=None, label='Принадлежность объекта', required=False, widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) ) objitem_count = forms.ChoiceField( choices=[('', 'Все'), ('1', '1'), ('2+', '2 и более')], label='Количество привязанных точек ГЛ', required=False, widget=forms.RadioSelect() ) # Фиктивные фильтры has_plans = forms.ChoiceField( choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], label='Планы на Кубсат', required=False, widget=forms.RadioSelect() ) success_1 = forms.ChoiceField( choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], label='ГСО успешно?', required=False, widget=forms.RadioSelect() ) success_2 = forms.ChoiceField( choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], label='Кубсат успешно?', required=False, widget=forms.RadioSelect() ) date_from = forms.DateField( label='Дата от', required=False, widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}) ) date_to = forms.DateField( label='Дата до', required=False, widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}) ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from mainapp.models import Band, ObjectInfo, ObjectOwnership, Satellite, ObjItem from django.db.models import Exists, OuterRef # Фильтруем спутники: только те, у которых есть источники с точками satellites_with_sources = Satellite.objects.filter( parameters__objitem__source__isnull=False ).distinct().order_by('name') self.fields['satellites'].queryset = satellites_with_sources self.fields['band'].queryset = Band.objects.all().order_by('name') self.fields['object_type'].queryset = ObjectInfo.objects.all().order_by('name') self.fields['object_ownership'].queryset = ObjectOwnership.objects.all().order_by('name') class TransponderForm(forms.ModelForm): """ Форма для создания и редактирования транспондеров. При редактировании только name, zone_name и snr доступны для изменения. Остальные поля только для чтения. """ class Meta: model = Transponders fields = [ 'name', 'sat_id', 'downlink', 'uplink', 'frequency_range', 'zone_name', 'polarization', 'snr', ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Введите название транспондера' }), 'sat_id': forms.Select(attrs={'class': 'form-select'}), 'downlink': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.001', 'placeholder': 'Введите частоту downlink в МГц' }), 'uplink': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.001', 'placeholder': 'Введите частоту uplink в МГц' }), 'frequency_range': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.001', 'placeholder': 'Введите полосу частот в МГц' }), 'zone_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Введите название зоны покрытия' }), 'polarization': forms.Select(attrs={'class': 'form-select'}), 'snr': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.1', 'placeholder': 'Введите ОСШ в дБ' }), } labels = { 'name': 'Название транспондера', 'sat_id': 'Спутник', 'downlink': 'Downlink (МГц)', 'uplink': 'Uplink (МГц)', 'frequency_range': 'Полоса частот (МГц)', 'zone_name': 'Название зоны покрытия', 'polarization': 'Поляризация', 'snr': 'ОСШ (дБ)', } help_texts = { 'downlink': 'Частота downlink в МГц', 'uplink': 'Частота uplink в МГц', 'frequency_range': 'Полоса частот в МГц', 'snr': 'Отношение сигнал/шум в децибелах', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Загружаем choices для select полей self.fields['sat_id'].queryset = Satellite.objects.all().order_by('name') self.fields['polarization'].queryset = Polarization.objects.all().order_by('name') # Если это форма редактирования (instance существует), делаем поля readonly if self.instance and self.instance.pk: # Поля только для чтения при редактировании readonly_fields = ['sat_id', 'downlink', 'uplink', 'frequency_range', 'polarization'] for field_name in readonly_fields: self.fields[field_name].widget.attrs['readonly'] = True self.fields[field_name].widget.attrs['disabled'] = True self.fields[field_name].required = False else: # При создании все поля обязательны кроме name, zone_name и snr self.fields['sat_id'].required = True self.fields['downlink'].required = True self.fields['name'].required = False self.fields['zone_name'].required = False self.fields['snr'].required = False def clean(self): """Дополнительная валидация формы.""" cleaned_data = super().clean() # При редактировании восстанавливаем значения readonly полей из instance if self.instance and self.instance.pk: cleaned_data['sat_id'] = self.instance.sat_id cleaned_data['downlink'] = self.instance.downlink cleaned_data['uplink'] = self.instance.uplink cleaned_data['frequency_range'] = self.instance.frequency_range cleaned_data['polarization'] = self.instance.polarization return cleaned_data class SatelliteForm(forms.ModelForm): """ Форма для создания и редактирования спутников. """ class Meta: model = Satellite fields = [ 'name', 'norad', 'band', 'undersat_point', 'url', 'comment', 'launch_date', ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Введите название спутника', 'required': True }), 'norad': forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Введите NORAD ID' }), 'band': forms.SelectMultiple(attrs={ 'class': 'form-select', 'size': '5' }), 'undersat_point': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'placeholder': 'Введите подспутниковую точку в градусах' }), 'url': forms.URLInput(attrs={ 'class': 'form-control', 'placeholder': 'https://example.com' }), 'comment': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Введите комментарий' }), 'launch_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), } labels = { 'name': 'Название спутника', 'norad': 'NORAD ID', 'band': 'Диапазоны работы', 'undersat_point': 'Подспутниковая точка (градусы)', 'url': 'Ссылка на источник', 'comment': 'Комментарий', 'launch_date': 'Дата запуска', } help_texts = { 'name': 'Уникальное название спутника', 'norad': 'Идентификатор NORAD для отслеживания спутника', 'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)', 'undersat_point': 'Восточное полушарие с +, западное с -', 'url': 'Ссылка на сайт, где можно проверить информацию', 'launch_date': 'Дата запуска спутника', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from mainapp.models import Band # Загружаем choices для select полей self.fields['band'].queryset = Band.objects.all().order_by('name') # Делаем name обязательным self.fields['name'].required = True def clean_name(self): """Валидация поля name.""" name = self.cleaned_data.get('name') if name: # Удаляем лишние пробелы name = name.strip() # Проверяем что после удаления пробелов что-то осталось if not name: raise forms.ValidationError('Название не может состоять только из пробелов') # Проверяем уникальность (исключая текущий объект при редактировании) qs = Satellite.objects.filter(name=name) if self.instance and self.instance.pk: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): raise forms.ValidationError('Спутник с таким названием уже существует') return name