diff --git a/dbapp/lyngsatapp/migrations/0001_initial.py b/dbapp/lyngsatapp/migrations/0001_initial.py index 7a17001..cb3d8ba 100644 --- a/dbapp/lyngsatapp/migrations/0001_initial.py +++ b/dbapp/lyngsatapp/migrations/0001_initial.py @@ -1,37 +1,30 @@ -# Generated by Django 5.2.7 on 2025-11-10 20:03 - -import django.db.models.deletion -import mainapp.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('mainapp', '0007_remove_parameter_objitems_parameter_objitem'), - ] - - operations = [ - migrations.CreateModel( - name='LyngSat', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')), - ('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), - ('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Время')), - ('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')), - ('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')), - ('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')), - ('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник')), - ('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция')), - ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация')), - ('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт')), - ], - options={ - 'verbose_name': 'Источник LyngSat', - 'verbose_name_plural': 'Источники LyngSat', - }, - ), - ] +# Generated by Django 5.2.7 on 2025-11-12 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='LyngSat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')), + ('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), + ('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления')), + ('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')), + ('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')), + ('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')), + ], + options={ + 'verbose_name': 'Источник LyngSat', + 'verbose_name_plural': 'Источники LyngSat', + }, + ), + ] diff --git a/dbapp/lyngsatapp/migrations/0002_alter_lyngsat_last_update.py b/dbapp/lyngsatapp/migrations/0002_alter_lyngsat_last_update.py deleted file mode 100644 index 422a7dd..0000000 --- a/dbapp/lyngsatapp/migrations/0002_alter_lyngsat_last_update.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-11 13:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('lyngsatapp', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='lyngsat', - name='last_update', - field=models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления'), - ), - ] diff --git a/dbapp/lyngsatapp/migrations/0002_initial.py b/dbapp/lyngsatapp/migrations/0002_initial.py new file mode 100644 index 0000000..15613c1 --- /dev/null +++ b/dbapp/lyngsatapp/migrations/0002_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2025-11-12 14:21 + +import django.db.models.deletion +import mainapp.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('lyngsatapp', '0001_initial'), + ('mainapp', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='lyngsat', + name='id_satellite', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник'), + ), + migrations.AddField( + model_name='lyngsat', + name='modulation', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция'), + ), + migrations.AddField( + model_name='lyngsat', + name='polarization', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация'), + ), + migrations.AddField( + model_name='lyngsat', + name='standard', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт'), + ), + ] diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 1dc399e..5730a07 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -1,833 +1,876 @@ -# Django imports -from django import forms -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.auth.models import Group, User -from django.shortcuts import redirect -from django.urls import reverse -from django.utils import timezone - -# Third-party imports -from import_export.admin import ImportExportActionModelAdmin -from leaflet.admin import LeafletGeoAdmin -from more_admin_filters import ( - MultiSelectDropdownFilter, - MultiSelectRelatedDropdownFilter, -) -from rangefilter.filters import ( - DateRangeQuickSelectListFilterBuilder, - NumericRangeFilterBuilder, -) - -from .models import ( - Polarization, - Modulation, - Standard, - SigmaParMark, - SigmaParameter, - Parameter, - Satellite, - Mirror, - Geo, - ObjItem, - CustomUser -) -from .filters import ( - GeoKupDistanceFilter, - GeoValidDistanceFilter, - UniqueToggleFilter, - HasSigmaParameterFilter -) - - -admin.site.site_title = "Геолокация" -admin.site.site_header = "Geolocation" -admin.site.index_title = "Geo" - -# Unregister default User and Group since we're customizing them -admin.site.unregister(User) -admin.site.unregister(Group) - - -# ============================================================================ -# Base Admin Classes -# ============================================================================ - -class BaseAdmin(admin.ModelAdmin): - """ - Базовый класс для всех admin моделей. - - Предоставляет общую функциональность: - - Кнопки сохранения сверху и снизу - - Настройка количества элементов на странице - - Автоматическое заполнение полей created_by и updated_by - """ - save_on_top = True - list_per_page = 50 - - def save_model(self, request, obj, form, change): - """ - Автоматически заполняет поля created_by и updated_by при сохранении. - - Args: - request: HTTP запрос - obj: Сохраняемый объект модели - form: Форма с данными - change: True если это редактирование, False если создание - """ - if not change: - # При создании нового объекта устанавливаем created_by - if hasattr(obj, 'created_by') and not obj.created_by_id: - obj.created_by = getattr(request.user, 'customuser', None) - - # При любом сохранении обновляем updated_by - if hasattr(obj, 'updated_by'): - obj.updated_by = getattr(request.user, 'customuser', None) - - super().save_model(request, obj, form, change) - - -class CustomUserInline(admin.StackedInline): - model = CustomUser - can_delete = False - verbose_name_plural = 'Дополнительная информация пользователя' - - -class LocationForm(forms.ModelForm): - latitude_geo = forms.FloatField(required=False, label="Широта") - longitude_geo = forms.FloatField(required=False, label="Долгота") - latitude_kupsat = forms.FloatField(required=False, label="Широта") - longitude_kupsat = forms.FloatField(required=False, label="Долгота") - latitude_valid = forms.FloatField(required=False, label="Широта") - longitude_valid = forms.FloatField(required=False, label="Долгота") - - class Meta: - model = Geo - fields = '__all__' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance and self.instance.coords: - self.fields['latitude_geo'].initial = self.instance.coords[1] - self.fields['longitude_geo'].initial = self.instance.coords[0] - if self.instance and self.instance.coords_kupsat: - self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1] - self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0] - if self.instance and self.instance.coords_valid: - self.fields['latitude_valid'].initial = self.instance.coords_valid[1] - self.fields['longitude_valid'].initial = self.instance.coords_valid[0] - - def save(self, commit=True): - instance = super().save(commit=False) - from django.contrib.gis.geos import Point - lat = self.cleaned_data.get('latitude_geo') - lon = self.cleaned_data.get('longitude_geo') - if lat is not None and lon is not None: - instance.coords = Point(lon, lat, srid=4326) - - lat = self.cleaned_data.get('latitude_kupsat') - lon = self.cleaned_data.get('longitude_kupsat') - if lat is not None and lon is not None: - instance.coords_kupsat = Point(lon, lat, srid=4326) - - lat = self.cleaned_data.get('latitude_valid') - lon = self.cleaned_data.get('longitude_valid') - if lat is not None and lon is not None: - instance.coords_valid = Point(lon, lat, srid=4326) - - if commit: - instance.save() - return instance - - -class GeoInline(admin.StackedInline): - model = Geo - extra = 0 - verbose_name = "Гео" - verbose_name_plural = "Гео" - form = LocationForm - readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") - prefetch_related = ("mirrors",) - autocomplete_fields = ('mirrors',) - fieldsets = ( - ("Основная информация", { - "fields": ("mirrors", "location", "distance_coords_kup", - "distance_coords_valid", "distance_kup_valid", "timestamp", "comment",) - }), - ("Координаты: геолокация", { - "fields": ("longitude_geo", "latitude_geo", "coords"), - }), - ("Координаты: Кубсат", { - "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"), - }), - ("Координаты: Оперативный отдел", { - "fields": ("longitude_valid", "latitude_valid", "coords_valid"), - }), - ) - - -class UserAdmin(BaseUserAdmin): - inlines = [CustomUserInline] - -admin.site.register(User, UserAdmin) - - -# ============================================================================ -# Custom Admin Actions -# ============================================================================ - -@admin.action(description="Показать выбранные на карте") -def show_on_map(modeladmin, request, queryset): - """ - Action для отображения выбранных Geo объектов на карте. - - Оптимизирован для работы с большим количеством объектов: - использует values_list для получения только ID. - """ - selected_ids = queryset.values_list('id', flat=True) - ids_str = ','.join(str(pk) for pk in selected_ids) - return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}') - - -@admin.action(description="Показать выбранные объекты на карте") -def show_selected_on_map(modeladmin, request, queryset): - """ - Action для отображения выбранных ObjItem объектов на карте. - - Оптимизирован для работы с большим количеством объектов: - использует values_list для получения только ID. - """ - selected_ids = queryset.values_list('id', flat=True) - ids_str = ','.join(str(pk) for pk in selected_ids) - return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}') - - -@admin.action(description="Экспортировать выбранные объекты в CSV") -def export_objects_to_csv(modeladmin, request, queryset): - """ - Action для экспорта выбранных ObjItem объектов в CSV формат. - - Оптимизирован с использованием select_related и prefetch_related - для минимизации количества запросов к БД. - """ - import csv - from django.http import HttpResponse - - # Оптимизируем queryset - queryset = queryset.select_related( - 'geo_obj', - 'created_by__user', - 'updated_by__user', - 'parameter_obj', - 'parameter_obj__id_satellite', - 'parameter_obj__polarization', - 'parameter_obj__modulation' - ) - - response = HttpResponse(content_type='text/csv; charset=utf-8') - response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"' - response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel - - writer = csv.writer(response) - writer.writerow([ - 'Название', - 'Спутник', - 'Частота (МГц)', - 'Полоса (МГц)', - 'Поляризация', - 'Модуляция', - 'ОСШ', - 'Координаты геолокации', - 'Координаты Кубсата', - 'Координаты оперативного отдела', - 'Расстояние Гео-Куб (км)', - 'Расстояние Гео-Опер (км)', - 'Дата создания', - 'Дата обновления' - ]) - - for obj in queryset: - param = getattr(obj, 'parameter_obj', None) - geo = obj.geo_obj - - # Форматирование координат - def format_coords(coords): - if not coords: - return "-" - lon, lat = coords.coords[0], coords.coords[1] - lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W" - lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S" - return f"{lat_str} {lon_str}" - - writer.writerow([ - obj.name, - param.id_satellite.name if param and param.id_satellite else "-", - param.frequency if param else "-", - param.freq_range if param else "-", - param.polarization.name if param and param.polarization else "-", - param.modulation.name if param and param.modulation else "-", - param.snr if param else "-", - format_coords(geo) if geo and geo.coords else "-", - format_coords(geo) if geo and geo.coords_kupsat else "-", - format_coords(geo) if geo and geo.coords_valid else "-", - round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-", - round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-", - obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-", - obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-" - ]) - - return response - - -# ============================================================================ -# Inline Admin Classes -# ============================================================================ - -class ParameterInline(admin.StackedInline): - """Inline для редактирования параметра объекта.""" - model = Parameter - extra = 0 - max_num = 1 - can_delete = True - verbose_name = "ВЧ загрузка" - verbose_name_plural = "ВЧ загрузка" - fields = ( - 'id_satellite', - 'frequency', - 'freq_range', - 'polarization', - 'modulation', - 'bod_velocity', - 'snr', - 'standard' - ) - autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard') - - -# ============================================================================ -# Admin Classes -# ============================================================================ - -@admin.register(SigmaParMark) -class SigmaParMarkAdmin(BaseAdmin): - """Админ-панель для модели SigmaParMark.""" - list_display = ("mark", "timestamp") - search_fields = ("mark",) - ordering = ("-timestamp",) - list_filter = ( - ("timestamp", DateRangeQuickSelectListFilterBuilder()), - ) - - -@admin.register(Polarization) -class PolarizationAdmin(BaseAdmin): - """Админ-панель для модели Polarization.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) - - -@admin.register(Modulation) -class ModulationAdmin(BaseAdmin): - """Админ-панель для модели Modulation.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) - - -@admin.register(Standard) -class StandardAdmin(BaseAdmin): - """Админ-панель для модели Standard.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) - - -class SigmaParameterInline(admin.StackedInline): - model = SigmaParameter - extra = 0 - autocomplete_fields = ['mark'] - readonly_fields = ( - "datetime_begin", - "datetime_end", - ) - def has_add_permission(self, request, obj=None): - return False - - -@admin.register(Parameter) -class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): - """ - Админ-панель для модели Parameter. - - Оптимизирована для работы с большим количеством параметров: - - Использует select_related для оптимизации запросов - - Предоставляет фильтры по основным характеристикам - - Поддерживает импорт/экспорт данных - """ - list_display = ( - "id_satellite", - "frequency", - "freq_range", - "polarization", - "modulation", - "bod_velocity", - "snr", - "standard", - "related_objitem", - "sigma_parameter" - ) - list_display_links = ("frequency", "id_satellite") - list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem") - - list_filter = ( - HasSigmaParameterFilter, - ("objitem", MultiSelectRelatedDropdownFilter), - ("id_satellite", MultiSelectRelatedDropdownFilter), - ("polarization__name", MultiSelectDropdownFilter), - ("modulation", MultiSelectRelatedDropdownFilter), - ("standard", MultiSelectRelatedDropdownFilter), - ("frequency", NumericRangeFilterBuilder()), - ("freq_range", NumericRangeFilterBuilder()), - ("snr", NumericRangeFilterBuilder()), - ) - - search_fields = ( - "id_satellite__name", - "frequency", - "freq_range", - "bod_velocity", - "snr", - "modulation__name", - "polarization__name", - "standard__name", - "objitem__name", - ) - - ordering = ("-frequency",) - 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() - if sigma_obj: - return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}" - return "-" - sigma_parameter.short_description = "ВЧ sigma" - - -@admin.register(SigmaParameter) -class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): - """ - Админ-панель для модели SigmaParameter. - - Оптимизирована для работы с параметрами Sigma: - - Использует select_related и prefetch_related для оптимизации - - Предоставляет фильтры по основным характеристикам - - Поддерживает импорт/экспорт данных - """ - list_display = ( - "id_satellite", - "frequency", - "transfer_frequency", - "freq_range", - "polarization", - "modulation", - "bod_velocity", - "snr", - "parameter", - "datetime_begin", - "datetime_end", - ) - list_display_links = ("id_satellite",) - list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization") - - readonly_fields = ( - "datetime_begin", - "datetime_end", - "transfer_frequency" - ) - - list_filter = ( - ("id_satellite__name", MultiSelectDropdownFilter), - ("modulation__name", MultiSelectDropdownFilter), - ("standard__name", MultiSelectDropdownFilter), - ("frequency", NumericRangeFilterBuilder()), - ("freq_range", NumericRangeFilterBuilder()), - ("snr", NumericRangeFilterBuilder()), - ("datetime_begin", DateRangeQuickSelectListFilterBuilder()), - ("datetime_end", DateRangeQuickSelectListFilterBuilder()), - ) - - search_fields = ( - "id_satellite__name", - "frequency", - "freq_range", - "bod_velocity", - "snr", - "modulation__name", - "standard__name", - ) - - autocomplete_fields = ("mark",) - ordering = ("-frequency",) - - def get_queryset(self, request): - """Оптимизированный queryset с prefetch_related для mark.""" - qs = super().get_queryset(request) - return qs.prefetch_related("mark") - - -@admin.register(Satellite) -class SatelliteAdmin(BaseAdmin): - """Админ-панель для модели Satellite.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) - - -@admin.register(Mirror) -class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): - """Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) - - -@admin.register(Geo) -class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): - """ - Админ-панель для модели Geo с поддержкой карты Leaflet. - - Оптимизирована для работы с геоданными: - - Использует prefetch_related для оптимизации запросов к mirrors - - Предоставляет фильтры по зеркалам, локации и дате - - Поддерживает импорт/экспорт данных - - Интегрирована с Leaflet для отображения на карте - """ - form = LocationForm - - readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") - - fieldsets = ( - ("Основная информация", { - "fields": ("mirrors", "location", "distance_coords_kup", - "distance_coords_valid", "distance_kup_valid", "timestamp", "comment") - }), - ("Координаты: геолокация", { - "fields": ("longitude_geo", "latitude_geo", "coords") - }), - ("Координаты: Кубсат", { - "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat") - }), - ("Координаты: Оперативный отдел", { - "fields": ("longitude_valid", "latitude_valid", "coords_valid") - }), - ) - - list_display = ( - "formatted_timestamp", - "location", - "mirrors_names", - "geo_coords", - "kupsat_coords", - "valid_coords", - "is_average", - ) - list_display_links = ("formatted_timestamp",) - - list_filter = ( - ("mirrors", MultiSelectRelatedDropdownFilter), - "is_average", - ("location", MultiSelectDropdownFilter), - ("timestamp", DateRangeQuickSelectListFilterBuilder()), - ) - - search_fields = ( - "mirrors__name", - "location", - ) - - autocomplete_fields = ("mirrors",) - ordering = ("-timestamp",) - actions = [show_on_map] - - settings_overrides = { - 'DEFAULT_CENTER': (55.7558, 37.6173), - 'DEFAULT_ZOOM': 12, - } - - def get_queryset(self, request): - """Оптимизированный queryset с prefetch_related для mirrors.""" - qs = super().get_queryset(request) - return qs.prefetch_related("mirrors") - - def mirrors_names(self, obj): - """Отображает список зеркал через запятую.""" - return ", ".join(m.name for m in obj.mirrors.all()) - mirrors_names.short_description = "Зеркала" - - def formatted_timestamp(self, obj): - """Форматирует timestamp в локальное время.""" - if not obj.timestamp: - return "" - local_time = timezone.localtime(obj.timestamp) - return local_time.strftime("%d.%m.%Y %H:%M:%S") - formatted_timestamp.short_description = "Дата и время" - formatted_timestamp.admin_order_field = "timestamp" - - def geo_coords(self, obj): - """Отображает координаты геолокации в формате широта/долгота.""" - if not obj.coords: - return "-" - longitude = obj.coords.coords[0] - latitude = obj.coords.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - geo_coords.short_description = "Координаты геолокации" - - def kupsat_coords(self, obj): - """Отображает координаты Кубсата в формате широта/долгота.""" - if obj.coords_kupsat is None: - return "-" - longitude = obj.coords_kupsat.coords[0] - latitude = obj.coords_kupsat.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - kupsat_coords.short_description = "Координаты Кубсата" - - def valid_coords(self, obj): - """Отображает координаты оперативного отдела в формате широта/долгота.""" - if obj.coords_valid is None: - return "-" - longitude = obj.coords_valid.coords[0] - latitude = obj.coords_valid.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - valid_coords.short_description = "Координаты оперативного отдела" - - - - -@admin.register(ObjItem) -class ObjItemAdmin(BaseAdmin): - """ - Админ-панель для модели ObjItem. - - Оптимизирована для работы с большим количеством объектов: - - Использует select_related и prefetch_related для оптимизации запросов - - Предоставляет фильтры по основным параметрам - - Поддерживает поиск по имени, координатам и частоте - - Включает кастомные actions для отображения на карте - """ - list_display = ( - "name", - "sat_name", - "freq", - "freq_range", - "pol", - "bod_velocity", - "modulation", - "snr", - "geo_coords", - "kupsat_coords", - "valid_coords", - "distance_geo_kup", - "distance_geo_valid", - "distance_kup_valid", - "created_at", - "updated_at", - ) - list_display_links = ("name",) - 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, - ("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()), - ("updated_at", DateRangeQuickSelectListFilterBuilder()), - ) - - search_fields = ( - "name", - "geo_obj__location", - "parameter_obj__frequency", - "parameter_obj__id_satellite__name", - ) - - ordering = ("-updated_at",) - inlines = [GeoInline, ParameterInline] - actions = [show_selected_on_map, export_objects_to_csv] - readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") - - fieldsets = ( - ("Основная информация", { - "fields": ("name",) - }), - ("Метаданные", { - "fields": ("created_at", "created_by", "updated_at", "updated_by"), - "classes": ("collapse",) - }), - ) - - def get_queryset(self, request): - """ - Оптимизированный queryset с использованием select_related. - - Загружает связанные объекты одним запросом для улучшения производительности. - """ - qs = super().get_queryset(request) - return qs.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" - ) - - def sat_name(self, obj): - """Отображает название спутника из связанного параметра.""" - 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 = "parameter_obj__id_satellite__name" - - def freq(self, obj): - """Отображает частоту из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: - return obj.parameter_obj.frequency - return "-" - freq.short_description = "Частота, МГц" - freq.admin_order_field = "parameter_obj__frequency" - - def distance_geo_kup(self, obj): - """Отображает расстояние между геолокацией и Кубсатом.""" - geo = obj.geo_obj - if not geo or geo.distance_coords_kup is None: - return "-" - return round(geo.distance_coords_kup, 3) - distance_geo_kup.short_description = "Гео-куб, км" - - def distance_geo_valid(self, obj): - """Отображает расстояние между геолокацией и оперативным отделом.""" - geo = obj.geo_obj - if not geo or geo.distance_coords_valid is None: - return "-" - return round(geo.distance_coords_valid, 3) - distance_geo_valid.short_description = "Гео-опер, км" - - def distance_kup_valid(self, obj): - """Отображает расстояние между Кубсатом и оперативным отделом.""" - geo = obj.geo_obj - if not geo or geo.distance_kup_valid is None: - return "-" - return round(geo.distance_kup_valid, 3) - distance_kup_valid.short_description = "Куб-опер, км" - - def pol(self, obj): - """Отображает поляризацию из связанного параметра.""" - 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): - """Отображает полосу частот из связанного параметра.""" - 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 = "parameter_obj__freq_range" - - def bod_velocity(self, obj): - """Отображает символьную скорость из связанного параметра.""" - 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): - """Отображает модуляцию из связанного параметра.""" - 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): - """Отображает отношение сигнал/шум из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: - return obj.parameter_obj.snr - return "-" - snr.short_description = "ОСШ" - - def geo_coords(self, obj): - """Отображает координаты геолокации в формате широта/долгота.""" - geo = obj.geo_obj - if not geo or not geo.coords: - return "-" - longitude = geo.coords.coords[0] - latitude = geo.coords.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - geo_coords.short_description = "Координаты геолокации" - geo_coords.admin_order_field = "geo_obj__coords" - - def kupsat_coords(self, obj): - """Отображает координаты Кубсата в формате широта/долгота.""" - geo = obj.geo_obj - if not geo or not geo.coords_kupsat: - return "-" - longitude = geo.coords_kupsat.coords[0] - latitude = geo.coords_kupsat.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - kupsat_coords.short_description = "Координаты Кубсата" - - def valid_coords(self, obj): - """Отображает координаты оперативного отдела в формате широта/долгота.""" - geo = obj.geo_obj - if not geo or not geo.coords_valid: - return "-" - longitude = geo.coords_valid.coords[0] - latitude = geo.coords_valid.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - return f"{lat} {lon}" - valid_coords.short_description = "Координаты оперативного отдела" +# Django imports +from django import forms +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import Group, User +from django.shortcuts import redirect +from django.urls import reverse +from django.utils import timezone + +# Third-party imports +from import_export.admin import ImportExportActionModelAdmin +from leaflet.admin import LeafletGeoAdmin +from more_admin_filters import ( + MultiSelectDropdownFilter, + MultiSelectRelatedDropdownFilter, +) +from rangefilter.filters import ( + DateRangeQuickSelectListFilterBuilder, + NumericRangeFilterBuilder, +) + +from .models import ( + Polarization, + Modulation, + Standard, + SigmaParMark, + SigmaParameter, + Parameter, + Satellite, + Mirror, + Geo, + ObjItem, + CustomUser, + Band, + Source +) +from .filters import ( + GeoKupDistanceFilter, + GeoValidDistanceFilter, + UniqueToggleFilter, + HasSigmaParameterFilter +) + + +admin.site.site_title = "Геолокация" +admin.site.site_header = "Geolocation" +admin.site.index_title = "Geo" + +# Unregister default User and Group since we're customizing them +admin.site.unregister(User) +admin.site.unregister(Group) + + +# ============================================================================ +# Base Admin Classes +# ============================================================================ + +class BaseAdmin(admin.ModelAdmin): + """ + Базовый класс для всех admin моделей. + + Предоставляет общую функциональность: + - Кнопки сохранения сверху и снизу + - Настройка количества элементов на странице + - Автоматическое заполнение полей created_by и updated_by + """ + save_on_top = True + list_per_page = 50 + + def save_model(self, request, obj, form, change): + """ + Автоматически заполняет поля created_by и updated_by при сохранении. + + Args: + request: HTTP запрос + obj: Сохраняемый объект модели + form: Форма с данными + change: True если это редактирование, False если создание + """ + if not change: + # При создании нового объекта устанавливаем created_by + if hasattr(obj, 'created_by') and not obj.created_by_id: + obj.created_by = getattr(request.user, 'customuser', None) + + # При любом сохранении обновляем updated_by + if hasattr(obj, 'updated_by'): + obj.updated_by = getattr(request.user, 'customuser', None) + + super().save_model(request, obj, form, change) + + +class CustomUserInline(admin.StackedInline): + model = CustomUser + can_delete = False + verbose_name_plural = 'Дополнительная информация пользователя' + + +class LocationForm(forms.ModelForm): + latitude_geo = forms.FloatField(required=False, label="Широта") + longitude_geo = forms.FloatField(required=False, label="Долгота") + # latitude_kupsat = forms.FloatField(required=False, label="Широта") + # longitude_kupsat = forms.FloatField(required=False, label="Долгота") + # latitude_valid = forms.FloatField(required=False, label="Широта") + # longitude_valid = forms.FloatField(required=False, label="Долгота") + + class Meta: + model = Geo + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.coords: + self.fields['latitude_geo'].initial = self.instance.coords[1] + self.fields['longitude_geo'].initial = self.instance.coords[0] + # if self.instance and self.instance.coords_kupsat: + # self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1] + # self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0] + # if self.instance and self.instance.coords_valid: + # self.fields['latitude_valid'].initial = self.instance.coords_valid[1] + # self.fields['longitude_valid'].initial = self.instance.coords_valid[0] + + def save(self, commit=True): + instance = super().save(commit=False) + from django.contrib.gis.geos import Point + lat = self.cleaned_data.get('latitude_geo') + lon = self.cleaned_data.get('longitude_geo') + if lat is not None and lon is not None: + instance.coords = Point(lon, lat, srid=4326) + + # lat = self.cleaned_data.get('latitude_kupsat') + # lon = self.cleaned_data.get('longitude_kupsat') + # if lat is not None and lon is not None: + # instance.coords_kupsat = Point(lon, lat, srid=4326) + + # lat = self.cleaned_data.get('latitude_valid') + # lon = self.cleaned_data.get('longitude_valid') + # if lat is not None and lon is not None: + # instance.coords_valid = Point(lon, lat, srid=4326) + + if commit: + instance.save() + return instance + + +class GeoInline(admin.StackedInline): + model = Geo + extra = 0 + verbose_name = "Гео" + verbose_name_plural = "Гео" + form = LocationForm + # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") + prefetch_related = ("mirrors",) + autocomplete_fields = ('mirrors',) + fieldsets = ( + ("Основная информация", { + "fields": ("mirrors", "location", + # "distance_coords_kup", + # "distance_coords_valid", + # "distance_kup_valid", + "timestamp", "comment",) + }), + ("Координаты: геолокация", { + "fields": ("longitude_geo", "latitude_geo", "coords"), + }), + # ("Координаты: Кубсат", { + # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"), + # }), + # ("Координаты: Оперативный отдел", { + # "fields": ("longitude_valid", "latitude_valid", "coords_valid"), + # }), + ) + + +class UserAdmin(BaseUserAdmin): + inlines = [CustomUserInline] + +admin.site.register(User, UserAdmin) + + +# ============================================================================ +# Custom Admin Actions +# ============================================================================ + +@admin.action(description="Показать выбранные на карте") +def show_on_map(modeladmin, request, queryset): + """ + Action для отображения выбранных Geo объектов на карте. + + Оптимизирован для работы с большим количеством объектов: + использует values_list для получения только ID. + """ + selected_ids = queryset.values_list('id', flat=True) + ids_str = ','.join(str(pk) for pk in selected_ids) + return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}') + + +@admin.action(description="Показать выбранные объекты на карте") +def show_selected_on_map(modeladmin, request, queryset): + """ + Action для отображения выбранных ObjItem объектов на карте. + + Оптимизирован для работы с большим количеством объектов: + использует values_list для получения только ID. + """ + selected_ids = queryset.values_list('id', flat=True) + ids_str = ','.join(str(pk) for pk in selected_ids) + return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}') + + +@admin.action(description="Экспортировать выбранные объекты в CSV") +def export_objects_to_csv(modeladmin, request, queryset): + """ + Action для экспорта выбранных ObjItem объектов в CSV формат. + + Оптимизирован с использованием select_related и prefetch_related + для минимизации количества запросов к БД. + """ + import csv + from django.http import HttpResponse + + # Оптимизируем queryset + queryset = queryset.select_related( + 'geo_obj', + 'created_by__user', + 'updated_by__user', + 'parameter_obj', + 'parameter_obj__id_satellite', + 'parameter_obj__polarization', + 'parameter_obj__modulation' + ) + + response = HttpResponse(content_type='text/csv; charset=utf-8') + response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"' + response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel + + writer = csv.writer(response) + writer.writerow([ + 'Название', + 'Спутник', + 'Частота (МГц)', + 'Полоса (МГц)', + 'Поляризация', + 'Модуляция', + 'ОСШ', + 'Координаты геолокации', + 'Координаты Кубсата', + 'Координаты оперативного отдела', + 'Расстояние Гео-Куб (км)', + 'Расстояние Гео-Опер (км)', + 'Дата создания', + 'Дата обновления' + ]) + + for obj in queryset: + param = getattr(obj, 'parameter_obj', None) + geo = obj.geo_obj + + # Форматирование координат + def format_coords(coords): + if not coords: + return "-" + lon, lat = coords.coords[0], coords.coords[1] + lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W" + lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S" + return f"{lat_str} {lon_str}" + + writer.writerow([ + obj.name, + param.id_satellite.name if param and param.id_satellite else "-", + param.frequency if param else "-", + param.freq_range if param else "-", + param.polarization.name if param and param.polarization else "-", + param.modulation.name if param and param.modulation else "-", + param.snr if param else "-", + format_coords(geo) if geo and geo.coords else "-", + format_coords(geo) if geo and geo.coords_kupsat else "-", + format_coords(geo) if geo and geo.coords_valid else "-", + round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-", + round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-", + obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-", + obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-" + ]) + + return response + + +# ============================================================================ +# Inline Admin Classes +# ============================================================================ + +class ParameterInline(admin.StackedInline): + """Inline для редактирования параметра объекта.""" + model = Parameter + extra = 0 + max_num = 1 + can_delete = True + verbose_name = "ВЧ загрузка" + verbose_name_plural = "ВЧ загрузка" + fields = ( + 'id_satellite', + 'frequency', + 'freq_range', + 'polarization', + 'modulation', + 'bod_velocity', + 'snr', + 'standard' + ) + autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard') + + +# ============================================================================ +# Admin Classes +# ============================================================================ + +@admin.register(SigmaParMark) +class SigmaParMarkAdmin(BaseAdmin): + """Админ-панель для модели SigmaParMark.""" + list_display = ("mark", "timestamp") + search_fields = ("mark",) + ordering = ("-timestamp",) + list_filter = ( + ("timestamp", DateRangeQuickSelectListFilterBuilder()), + ) + + +@admin.register(Polarization) +class PolarizationAdmin(BaseAdmin): + """Админ-панель для модели Polarization.""" + list_display = ("name",) + search_fields = ("name",) + ordering = ("name",) + + +@admin.register(Modulation) +class ModulationAdmin(BaseAdmin): + """Админ-панель для модели Modulation.""" + list_display = ("name",) + search_fields = ("name",) + ordering = ("name",) + + +@admin.register(Standard) +class StandardAdmin(BaseAdmin): + """Админ-панель для модели Standard.""" + list_display = ("name",) + search_fields = ("name",) + ordering = ("name",) + + +class SigmaParameterInline(admin.StackedInline): + model = SigmaParameter + extra = 0 + autocomplete_fields = ['mark'] + readonly_fields = ( + "datetime_begin", + "datetime_end", + ) + def has_add_permission(self, request, obj=None): + return False + + +@admin.register(Parameter) +class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): + """ + Админ-панель для модели Parameter. + + Оптимизирована для работы с большим количеством параметров: + - Использует select_related для оптимизации запросов + - Предоставляет фильтры по основным характеристикам + - Поддерживает импорт/экспорт данных + """ + list_display = ( + "id_satellite", + "frequency", + "freq_range", + "polarization", + "modulation", + "bod_velocity", + "snr", + "standard", + "related_objitem", + "sigma_parameter" + ) + list_display_links = ("frequency", "id_satellite") + list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem") + + list_filter = ( + HasSigmaParameterFilter, + ("objitem", MultiSelectRelatedDropdownFilter), + ("id_satellite", MultiSelectRelatedDropdownFilter), + ("polarization__name", MultiSelectDropdownFilter), + ("modulation", MultiSelectRelatedDropdownFilter), + ("standard", MultiSelectRelatedDropdownFilter), + ("frequency", NumericRangeFilterBuilder()), + ("freq_range", NumericRangeFilterBuilder()), + ("snr", NumericRangeFilterBuilder()), + ) + + search_fields = ( + "id_satellite__name", + "frequency", + "freq_range", + "bod_velocity", + "snr", + "modulation__name", + "polarization__name", + "standard__name", + "objitem__name", + ) + + ordering = ("-frequency",) + 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() + if sigma_obj: + return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}" + return "-" + sigma_parameter.short_description = "ВЧ sigma" + + +@admin.register(SigmaParameter) +class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): + """ + Админ-панель для модели SigmaParameter. + + Оптимизирована для работы с параметрами Sigma: + - Использует select_related и prefetch_related для оптимизации + - Предоставляет фильтры по основным характеристикам + - Поддерживает импорт/экспорт данных + """ + list_display = ( + "id_satellite", + "frequency", + "transfer_frequency", + "freq_range", + "polarization", + "modulation", + "bod_velocity", + "snr", + "parameter", + "datetime_begin", + "datetime_end", + ) + list_display_links = ("id_satellite",) + list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization") + + readonly_fields = ( + "datetime_begin", + "datetime_end", + "transfer_frequency" + ) + + list_filter = ( + ("id_satellite__name", MultiSelectDropdownFilter), + ("modulation__name", MultiSelectDropdownFilter), + ("standard__name", MultiSelectDropdownFilter), + ("frequency", NumericRangeFilterBuilder()), + ("freq_range", NumericRangeFilterBuilder()), + ("snr", NumericRangeFilterBuilder()), + ("datetime_begin", DateRangeQuickSelectListFilterBuilder()), + ("datetime_end", DateRangeQuickSelectListFilterBuilder()), + ) + + search_fields = ( + "id_satellite__name", + "frequency", + "freq_range", + "bod_velocity", + "snr", + "modulation__name", + "standard__name", + ) + + autocomplete_fields = ("mark",) + ordering = ("-frequency",) + + def get_queryset(self, request): + """Оптимизированный queryset с prefetch_related для mark.""" + qs = super().get_queryset(request) + return qs.prefetch_related("mark") + + +@admin.register(Satellite) +class SatelliteAdmin(BaseAdmin): + """Админ-панель для модели Satellite.""" + list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at") + search_fields = ("name", "norad") + ordering = ("name",) + filter_horizontal = ("band",) + autocomplete_fields = ("band",) + readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") + + +@admin.register(Mirror) +class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): + """Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" + list_display = ("name",) + search_fields = ("name",) + ordering = ("name",) + + +@admin.register(Geo) +class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): + """ + Админ-панель для модели Geo с поддержкой карты Leaflet. + + Оптимизирована для работы с геоданными: + - Использует prefetch_related для оптимизации запросов к mirrors + - Предоставляет фильтры по зеркалам, локации и дате + - Поддерживает импорт/экспорт данных + - Интегрирована с Leaflet для отображения на карте + """ + form = LocationForm + + # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") + + fieldsets = ( + ("Основная информация", { + "fields": ("mirrors", "location", + # "distance_coords_kup", + # "distance_coords_valid", + # "distance_kup_valid", + "timestamp", "comment", "transponder") + }), + ("Координаты: геолокация", { + "fields": ("longitude_geo", "latitude_geo", "coords") + }), + # ("Координаты: Кубсат", { + # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat") + # }), + # ("Координаты: Оперативный отдел", { + # "fields": ("longitude_valid", "latitude_valid", "coords_valid") + # }), + ) + + list_display = ( + "formatted_timestamp", + "location", + "mirrors_names", + "geo_coords", + # "kupsat_coords", + # "valid_coords", + "is_average", + ) + list_display_links = ("formatted_timestamp",) + + list_filter = ( + ("mirrors", MultiSelectRelatedDropdownFilter), + ("transponder", MultiSelectRelatedDropdownFilter), + "is_average", + ("location", MultiSelectDropdownFilter), + ("timestamp", DateRangeQuickSelectListFilterBuilder()), + ) + + search_fields = ( + "mirrors__name", + "location", + "transponder__name", + ) + + autocomplete_fields = ("mirrors", ) + ordering = ("-timestamp",) + actions = [show_on_map] + + settings_overrides = { + 'DEFAULT_CENTER': (55.7558, 37.6173), + 'DEFAULT_ZOOM': 12, + } + + def get_queryset(self, request): + """Оптимизированный queryset с prefetch_related для mirrors.""" + qs = super().get_queryset(request) + return qs.prefetch_related("mirrors", "transponder") + + def mirrors_names(self, obj): + """Отображает список зеркал через запятую.""" + return ", ".join(m.name for m in obj.mirrors.all()) + mirrors_names.short_description = "Зеркала" + + def formatted_timestamp(self, obj): + """Форматирует timestamp в локальное время.""" + if not obj.timestamp: + return "" + local_time = timezone.localtime(obj.timestamp) + return local_time.strftime("%d.%m.%Y %H:%M:%S") + formatted_timestamp.short_description = "Дата и время" + formatted_timestamp.admin_order_field = "timestamp" + + def geo_coords(self, obj): + """Отображает координаты геолокации в формате широта/долгота.""" + if not obj.coords: + return "-" + longitude = obj.coords.coords[0] + latitude = obj.coords.coords[1] + lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + return f"{lat} {lon}" + geo_coords.short_description = "Координаты геолокации" + + # def kupsat_coords(self, obj): + # """Отображает координаты Кубсата в формате широта/долгота.""" + # if obj.coords_kupsat is None: + # return "-" + # longitude = obj.coords_kupsat.coords[0] + # latitude = obj.coords_kupsat.coords[1] + # lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + # lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + # return f"{lat} {lon}" + # kupsat_coords.short_description = "Координаты Кубсата" + + # def valid_coords(self, obj): + # """Отображает координаты оперативного отдела в формате широта/долгота.""" + # if obj.coords_valid is None: + # return "-" + # longitude = obj.coords_valid.coords[0] + # latitude = obj.coords_valid.coords[1] + # lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + # lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + # return f"{lat} {lon}" + # valid_coords.short_description = "Координаты оперативного отдела" + + + + +@admin.register(ObjItem) +class ObjItemAdmin(BaseAdmin): + """ + Админ-панель для модели ObjItem. + + Оптимизирована для работы с большим количеством объектов: + - Использует select_related и prefetch_related для оптимизации запросов + - Предоставляет фильтры по основным параметрам + - Поддерживает поиск по имени, координатам и частоте + - Включает кастомные actions для отображения на карте + """ + list_display = ( + "name", + "sat_name", + "freq", + "freq_range", + "pol", + "bod_velocity", + "modulation", + "snr", + "geo_coords", + # "kupsat_coords", + # "valid_coords", + # "distance_geo_kup", + # "distance_geo_valid", + # "distance_kup_valid", + "created_at", + "updated_at", + ) + list_display_links = ("name",) + 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, + ("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()), + ("updated_at", DateRangeQuickSelectListFilterBuilder()), + ) + + search_fields = ( + "name", + "geo_obj__location", + "parameter_obj__frequency", + "parameter_obj__id_satellite__name", + ) + + ordering = ("-updated_at",) + inlines = [GeoInline, ParameterInline] + actions = [show_selected_on_map, export_objects_to_csv] + readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") + + fieldsets = ( + ("Основная информация", { + "fields": ("name",) + }), + ("Метаданные", { + "fields": ("created_at", "created_by", "updated_at", "updated_by"), + "classes": ("collapse",) + }), + ) + + def get_queryset(self, request): + """ + Оптимизированный queryset с использованием select_related. + + Загружает связанные объекты одним запросом для улучшения производительности. + """ + qs = super().get_queryset(request) + return qs.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" + ) + + def sat_name(self, obj): + """Отображает название спутника из связанного параметра.""" + 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 = "parameter_obj__id_satellite__name" + + def freq(self, obj): + """Отображает частоту из связанного параметра.""" + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.frequency + return "-" + freq.short_description = "Частота, МГц" + freq.admin_order_field = "parameter_obj__frequency" + + # def distance_geo_kup(self, obj): + # """Отображает расстояние между геолокацией и Кубсатом.""" + # geo = obj.geo_obj + # if not geo or geo.distance_coords_kup is None: + # return "-" + # return round(geo.distance_coords_kup, 3) + # distance_geo_kup.short_description = "Гео-куб, км" + + # def distance_geo_valid(self, obj): + # """Отображает расстояние между геолокацией и оперативным отделом.""" + # geo = obj.geo_obj + # if not geo or geo.distance_coords_valid is None: + # return "-" + # return round(geo.distance_coords_valid, 3) + # distance_geo_valid.short_description = "Гео-опер, км" + + # def distance_kup_valid(self, obj): + # """Отображает расстояние между Кубсатом и оперативным отделом.""" + # geo = obj.geo_obj + # if not geo or geo.distance_kup_valid is None: + # return "-" + # return round(geo.distance_kup_valid, 3) + # distance_kup_valid.short_description = "Куб-опер, км" + + def pol(self, obj): + """Отображает поляризацию из связанного параметра.""" + 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): + """Отображает полосу частот из связанного параметра.""" + 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 = "parameter_obj__freq_range" + + def bod_velocity(self, obj): + """Отображает символьную скорость из связанного параметра.""" + 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): + """Отображает модуляцию из связанного параметра.""" + 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): + """Отображает отношение сигнал/шум из связанного параметра.""" + if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + return obj.parameter_obj.snr + return "-" + snr.short_description = "ОСШ" + + def geo_coords(self, obj): + """Отображает координаты геолокации в формате широта/долгота.""" + geo = obj.geo_obj + if not geo or not geo.coords: + return "-" + longitude = geo.coords.coords[0] + latitude = geo.coords.coords[1] + lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + return f"{lat} {lon}" + geo_coords.short_description = "Координаты геолокации" + geo_coords.admin_order_field = "geo_obj__coords" + + def kupsat_coords(self, obj): + """Отображает координаты Кубсата в формате широта/долгота.""" + geo = obj.geo_obj + if not geo or not geo.coords_kupsat: + return "-" + longitude = geo.coords_kupsat.coords[0] + latitude = geo.coords_kupsat.coords[1] + lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + return f"{lat} {lon}" + kupsat_coords.short_description = "Координаты Кубсата" + + def valid_coords(self, obj): + """Отображает координаты оперативного отдела в формате широта/долгота.""" + geo = obj.geo_obj + if not geo or not geo.coords_valid: + return "-" + longitude = geo.coords_valid.coords[0] + latitude = geo.coords_valid.coords[1] + lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + return f"{lat} {lon}" + valid_coords.short_description = "Координаты оперативного отдела" + + +@admin.register(Band) +class BandAdmin(ImportExportActionModelAdmin, BaseAdmin): + """Админ-панель для модели Band.""" + list_display = ("name", "border_start", "border_end") + search_fields = ("name",) + ordering = ("name",) + + +@admin.register(Source) +class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): + """Админ-панель для модели Source.""" + list_display = ("id", "created_at", "updated_at") + list_filter = ( + ("created_at", DateRangeQuickSelectListFilterBuilder()), + ("updated_at", DateRangeQuickSelectListFilterBuilder()), + ) + ordering = ("-created_at",) + readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") + + fieldsets = ( + ("Координаты: геолокация", { + "fields": ("coords_kupsat", "coords_valid", "coords_reference") + }), + ("Метаданные", { + "fields": ("created_at", "created_by", "updated_at", "updated_by"), + "classes": ("collapse",) + }), + ) diff --git a/dbapp/mainapp/migrations/0001_initial.py b/dbapp/mainapp/migrations/0001_initial.py index 579b6de..272fbe1 100644 --- a/dbapp/mainapp/migrations/0001_initial.py +++ b/dbapp/mainapp/migrations/0001_initial.py @@ -1,204 +1,211 @@ -# Generated by Django 5.2.7 on 2025-10-31 13:36 - -import django.contrib.gis.db.models.fields -import django.contrib.gis.db.models.functions -import django.db.models.deletion -import django.db.models.expressions -import mainapp.models -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Mirror', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')), - ], - options={ - 'verbose_name': 'Зеркало', - 'verbose_name_plural': 'Зеркала', - }, - ), - migrations.CreateModel( - name='Modulation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')), - ], - options={ - 'verbose_name': 'Модуляция', - 'verbose_name_plural': 'Модуляции', - }, - ), - migrations.CreateModel( - name='Polarization', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')), - ], - options={ - 'verbose_name': 'Поляризация', - 'verbose_name_plural': 'Поляризация', - }, - ), - migrations.CreateModel( - name='Satellite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')), - ('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')), - ], - options={ - 'verbose_name': 'Спутник', - 'verbose_name_plural': 'Спутники', - }, - ), - migrations.CreateModel( - name='SigmaParMark', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')), - ('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')), - ], - options={ - 'verbose_name': 'Отметка', - 'verbose_name_plural': 'Отметки', - }, - ), - migrations.CreateModel( - name='Standard', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')), - ], - options={ - 'verbose_name': 'Стандарт', - 'verbose_name_plural': 'Стандарты', - }, - ), - migrations.CreateModel( - name='CustomUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Пользователь', - 'verbose_name_plural': 'Пользователи', - }, - ), - migrations.CreateModel( - name='ObjItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')), - ('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')), - ], - options={ - 'verbose_name': 'Объект', - 'verbose_name_plural': 'Объекты', - }, - ), - migrations.CreateModel( - name='Parameter', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')), - ('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')), - ('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), - ('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')), - ('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')), - ('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')), - ('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')), - ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')), - ('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')), - ('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')), - ], - options={ - 'verbose_name': 'ВЧ загрузка', - 'verbose_name_plural': 'ВЧ загрузки', - }, - ), - migrations.CreateModel( - name='SourceType', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')), - ('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')), - ], - options={ - 'verbose_name': 'Тип источника', - 'verbose_name_plural': 'Типы источников', - }, - ), - migrations.CreateModel( - name='SigmaParameter', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')), - ('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')), - ('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')), - ('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')), - ('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')), - ('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')), - ('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), - ('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')), - ('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')), - ('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')), - ('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')), - ('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')), - ('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')), - ('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')), - ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')), - ('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')), - ('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')), - ], - options={ - 'verbose_name': 'ВЧ sigma', - 'verbose_name_plural': 'ВЧ sigma', - }, - ), - migrations.CreateModel( - name='Geo', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')), - ('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')), - ('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')), - ('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')), - ('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')), - ('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')), - ('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')), - ('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')), - ('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')), - ('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')), - ('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')), - ('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')), - ('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')), - ], - options={ - 'verbose_name': 'Гео', - 'verbose_name_plural': 'Гео', - 'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')], - }, - ), - migrations.AddIndex( - model_name='parameter', - index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'), - ), - migrations.AddIndex( - model_name='parameter', - index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'), - ), - ] +# Generated by Django 5.2.7 on 2025-11-12 14:21 + +import django.contrib.gis.db.models.fields +import django.core.validators +import django.db.models.deletion +import django.db.models.expressions +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('lyngsatapp', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Band', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Название диапазона', max_length=50, unique=True, verbose_name='Название')), + ('border_start', models.FloatField(blank=True, null=True, verbose_name='Нижняя граница диапазона, МГц')), + ('border_end', models.FloatField(blank=True, null=True, verbose_name='Верхняя граница диапазона, МГц')), + ], + options={ + 'verbose_name': 'Диапазон', + 'verbose_name_plural': 'Диапазоны', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Mirror', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала')), + ], + options={ + 'verbose_name': 'Зеркало', + 'verbose_name_plural': 'Зеркала', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Modulation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция')), + ], + options={ + 'verbose_name': 'Модуляция', + 'verbose_name_plural': 'Модуляции', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Parameter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')), + ('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц')), + ('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')), + ('snr', models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ')), + ], + options={ + 'verbose_name': 'ВЧ загрузка', + 'verbose_name_plural': 'ВЧ загрузки', + }, + ), + migrations.CreateModel( + name='Polarization', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация')), + ], + options={ + 'verbose_name': 'Поляризация', + 'verbose_name_plural': 'Поляризация', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Satellite', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника')), + ('norad', models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID')), + ('undersat_point', models.FloatField(blank=True, help_text='Подспутниковая точка в градусах. Восточное полушарие с +, западное с -', null=True, verbose_name='Подспутниковая точка, градусы')), + ('url', models.URLField(blank=True, help_text='Ссылка на сайт, где можно проверить информацию', null=True, verbose_name='Ссылка на источник')), + ('comment', models.TextField(blank=True, help_text='Любой возможный комменатрий', null=True, verbose_name='Комментарий')), + ('launch_date', models.DateField(blank=True, help_text='Дата запуска спутника', null=True, verbose_name='Дата запуска')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')), + ], + options={ + 'verbose_name': 'Спутник', + 'verbose_name_plural': 'Спутники', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='SigmaParameter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте')), + ('status', models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус')), + ('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')), + ('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')), + ('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц')), + ('power', models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм')), + ('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')), + ('snr', models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб')), + ('packets', models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность')), + ('datetime_begin', models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения')), + ('datetime_end', models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения')), + ], + options={ + 'verbose_name': 'ВЧ sigma', + 'verbose_name_plural': 'ВЧ sigma', + }, + ), + migrations.CreateModel( + name='SigmaParMark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mark', models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала')), + ('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время')), + ], + options={ + 'verbose_name': 'Отметка', + 'verbose_name_plural': 'Отметки', + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='Source', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата')), + ('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников')), + ('coords_reference', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, ещё кем-то проверенные (WGS84)', null=True, srid=4326, verbose_name='Координаты справочные')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')), + ], + options={ + 'verbose_name': 'Источник', + 'verbose_name_plural': 'Источники', + }, + ), + migrations.CreateModel( + name='Standard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт')), + ], + options={ + 'verbose_name': 'Стандарт', + 'verbose_name_plural': 'Стандарты', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя')), + ('user', models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Пользователь', + 'verbose_name_plural': 'Пользователи', + 'ordering': ['user__username'], + }, + ), + migrations.CreateModel( + name='Geo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время')), + ('location', models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение')), + ('comment', models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий')), + ('is_average', models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое')), + ('coords', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации')), + ('mirrors', models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')), + ], + options={ + 'verbose_name': 'Гео', + 'verbose_name_plural': 'Гео', + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='ObjItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')), + ('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем')), + ('lyngsat_source', models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat')), + ], + options={ + 'verbose_name': 'Объект', + 'verbose_name_plural': 'Объекты', + 'ordering': ['-updated_at'], + }, + ), + ] diff --git a/dbapp/mainapp/migrations/0002_initial.py b/dbapp/mainapp/migrations/0002_initial.py new file mode 100644 index 0000000..70282d7 --- /dev/null +++ b/dbapp/mainapp/migrations/0002_initial.py @@ -0,0 +1,150 @@ +# Generated by Django 5.2.7 on 2025-11-12 14:21 + +import django.db.models.deletion +import mainapp.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('mainapp', '0001_initial'), + ('mapsapp', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='objitem', + name='transponder', + field=models.ForeignKey(blank=True, help_text='Транспондер, с помощью которого была получена точка', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder', to='mapsapp.transponders', verbose_name='Транспондер'), + ), + migrations.AddField( + model_name='objitem', + name='updated_by', + field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'), + ), + migrations.AddField( + model_name='geo', + name='objitem', + field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'), + ), + migrations.AddField( + model_name='parameter', + name='modulation', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция'), + ), + 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='Объект'), + ), + migrations.AddField( + model_name='parameter', + name='polarization', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация'), + ), + migrations.AddField( + model_name='satellite', + name='band', + field=models.ManyToManyField(blank=True, help_text='Диапазоны работы спутника', related_name='bands', to='mainapp.band', verbose_name='Диапазоны'), + ), + migrations.AddField( + model_name='satellite', + name='created_by', + field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_created', to='mainapp.customuser', verbose_name='Создан пользователем'), + ), + migrations.AddField( + model_name='satellite', + name='updated_by', + field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'), + ), + migrations.AddField( + model_name='parameter', + name='id_satellite', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='id_satellite', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='modulation', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='parameter', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='polarization', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='mark', + field=models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка'), + ), + migrations.AddField( + model_name='source', + name='created_by', + field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_created', to='mainapp.customuser', verbose_name='Создан пользователем'), + ), + migrations.AddField( + model_name='source', + name='updated_by', + field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'), + ), + migrations.AddField( + model_name='objitem', + name='source', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source', to='mainapp.source', verbose_name='ИРИ'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='standard', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт'), + ), + migrations.AddField( + model_name='parameter', + name='standard', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт'), + ), + migrations.AddIndex( + model_name='geo', + index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'), + ), + migrations.AddIndex( + model_name='geo', + index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'), + ), + migrations.AddConstraint( + model_name='geo', + constraint=models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination'), + ), + migrations.AddIndex( + model_name='objitem', + index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'), + ), + migrations.AddIndex( + model_name='objitem', + index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'), + ), + migrations.AddIndex( + model_name='objitem', + index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'), + ), + migrations.AddIndex( + model_name='parameter', + index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'), + ), + migrations.AddIndex( + model_name='parameter', + index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'), + ), + ] diff --git a/dbapp/mainapp/migrations/0002_objitem_created_at_objitem_created_by_and_more.py b/dbapp/mainapp/migrations/0002_objitem_created_at_objitem_created_by_and_more.py deleted file mode 100644 index 218c9df..0000000 --- a/dbapp/mainapp/migrations/0002_objitem_created_at_objitem_created_by_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 13:56 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='objitem', - name='created_at', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'), - ), - migrations.AddField( - model_name='objitem', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'), - ), - migrations.AddField( - model_name='objitem', - name='updated_at', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'), - ), - migrations.AddField( - model_name='objitem', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'), - ), - ] diff --git a/dbapp/mainapp/migrations/0003_alter_objitem_created_at_alter_objitem_updated_at.py b/dbapp/mainapp/migrations/0003_alter_objitem_created_at_alter_objitem_updated_at.py deleted file mode 100644 index 3302785..0000000 --- a/dbapp/mainapp/migrations/0003_alter_objitem_created_at_alter_objitem_updated_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 14:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='objitem', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), - ), - migrations.AlterField( - model_name='objitem', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'), - ), - ] diff --git a/dbapp/mainapp/migrations/0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more.py b/dbapp/mainapp/migrations/0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more.py deleted file mode 100644 index cc6cc39..0000000 --- a/dbapp/mainapp/migrations/0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-01 07:38 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'), - ] - - operations = [ - migrations.RemoveField( - model_name='geo', - name='id_user_add', - ), - migrations.RemoveField( - model_name='objitem', - name='id_user_add', - ), - migrations.RemoveField( - model_name='parameter', - name='id_user_add', - ), - ] diff --git a/dbapp/mainapp/migrations/0005_alter_geo_objitem.py b/dbapp/mainapp/migrations/0005_alter_geo_objitem.py deleted file mode 100644 index db28ddf..0000000 --- a/dbapp/mainapp/migrations/0005_alter_geo_objitem.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-07 19:35 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='geo', - name='objitem', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'), - ), - ] diff --git a/dbapp/mainapp/migrations/0006_alter_customuser_options_alter_geo_options_and_more.py b/dbapp/mainapp/migrations/0006_alter_customuser_options_alter_geo_options_and_more.py deleted file mode 100644 index 61a0af0..0000000 --- a/dbapp/mainapp/migrations/0006_alter_customuser_options_alter_geo_options_and_more.py +++ /dev/null @@ -1,290 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-07 20:58 - -import django.contrib.gis.db.models.fields -import django.contrib.gis.db.models.functions -import django.core.validators -import django.db.models.deletion -import django.db.models.expressions -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0005_alter_geo_objitem'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterModelOptions( - name='customuser', - options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'}, - ), - migrations.AlterModelOptions( - name='geo', - options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'}, - ), - migrations.AlterModelOptions( - name='mirror', - options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'}, - ), - migrations.AlterModelOptions( - name='modulation', - options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'}, - ), - migrations.AlterModelOptions( - name='objitem', - options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'}, - ), - migrations.AlterModelOptions( - name='polarization', - options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'}, - ), - migrations.AlterModelOptions( - name='satellite', - options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'}, - ), - migrations.AlterModelOptions( - name='sigmaparmark', - options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'}, - ), - migrations.AlterModelOptions( - name='sourcetype', - options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'}, - ), - migrations.AlterModelOptions( - name='standard', - options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'}, - ), - migrations.AlterField( - model_name='customuser', - name='role', - field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'), - ), - migrations.AlterField( - model_name='customuser', - name='user', - field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'), - ), - migrations.AlterField( - model_name='geo', - name='comment', - field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'), - ), - migrations.AlterField( - model_name='geo', - name='coords', - field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'), - ), - migrations.AlterField( - model_name='geo', - name='coords_kupsat', - field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'), - ), - migrations.AlterField( - model_name='geo', - name='coords_valid', - field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'), - ), - migrations.AlterField( - model_name='geo', - name='distance_coords_kup', - field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'), - ), - migrations.AlterField( - model_name='geo', - name='distance_kup_valid', - field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'), - ), - migrations.AlterField( - model_name='geo', - name='is_average', - field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'), - ), - migrations.AlterField( - model_name='geo', - name='location', - field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'), - ), - migrations.AlterField( - model_name='geo', - name='mirrors', - field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'), - ), - migrations.AlterField( - model_name='geo', - name='objitem', - field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'), - ), - migrations.AlterField( - model_name='geo', - name='timestamp', - field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'), - ), - migrations.AlterField( - model_name='mirror', - name='name', - field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'), - ), - migrations.AlterField( - model_name='modulation', - name='name', - field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'), - ), - migrations.AlterField( - model_name='objitem', - name='created_at', - field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'), - ), - migrations.AlterField( - model_name='objitem', - name='created_by', - field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'), - ), - migrations.AlterField( - model_name='objitem', - name='name', - field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'), - ), - migrations.AlterField( - model_name='objitem', - name='updated_at', - field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'), - ), - migrations.AlterField( - model_name='objitem', - name='updated_by', - field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'), - ), - migrations.AlterField( - model_name='parameter', - name='bod_velocity', - field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'), - ), - migrations.AlterField( - model_name='parameter', - name='freq_range', - field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'), - ), - migrations.AlterField( - model_name='parameter', - name='frequency', - field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'), - ), - migrations.AlterField( - model_name='parameter', - name='snr', - field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'), - ), - migrations.AlterField( - model_name='polarization', - name='name', - field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'), - ), - migrations.AlterField( - model_name='satellite', - name='name', - field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'), - ), - migrations.AlterField( - model_name='satellite', - name='norad', - field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='bod_velocity', - field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='datetime_begin', - field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='datetime_end', - field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='freq_range', - field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='frequency', - field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='packets', - field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='power', - field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='snr', - field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='status', - field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='transfer', - field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'), - ), - migrations.AlterField( - model_name='sigmaparmark', - name='mark', - field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'), - ), - migrations.AlterField( - model_name='sigmaparmark', - name='timestamp', - field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'), - ), - migrations.AlterField( - model_name='sourcetype', - name='name', - field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'), - ), - migrations.AlterField( - model_name='sourcetype', - name='objitem', - field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'), - ), - migrations.AlterField( - model_name='standard', - name='name', - field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'), - ), - migrations.AddIndex( - model_name='geo', - index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'), - ), - migrations.AddIndex( - model_name='geo', - index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'), - ), - migrations.AddIndex( - model_name='objitem', - index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'), - ), - migrations.AddIndex( - model_name='objitem', - index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'), - ), - migrations.AddIndex( - model_name='objitem', - index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'), - ), - ] diff --git a/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py b/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py deleted file mode 100644 index 73f959f..0000000 --- a/dbapp/mainapp/migrations/0007_remove_parameter_objitems_parameter_objitem.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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/migrations/0008_remove_sourcetype_objitem_objitem_source_type_id_and_more.py b/dbapp/mainapp/migrations/0008_remove_sourcetype_objitem_objitem_source_type_id_and_more.py deleted file mode 100644 index 861348f..0000000 --- a/dbapp/mainapp/migrations/0008_remove_sourcetype_objitem_objitem_source_type_id_and_more.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-11 13:59 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0007_remove_parameter_objitems_parameter_objitem'), - ] - - operations = [ - migrations.RemoveField( - model_name='sourcetype', - name='objitem', - ), - migrations.AddField( - model_name='objitem', - name='source_type_id', - field=models.ForeignKey(blank=True, help_text='Тип источника сигнала', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_sourcetype', to='mainapp.sourcetype', verbose_name='Тип источника'), - ), - migrations.AlterField( - model_name='parameter', - name='bod_velocity', - field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'), - ), - migrations.AlterField( - model_name='parameter', - name='freq_range', - field=models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц'), - ), - migrations.AlterField( - model_name='parameter', - name='frequency', - field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'), - ), - migrations.AlterField( - model_name='parameter', - name='snr', - field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='bod_velocity', - field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='freq_range', - field=models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='frequency', - field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'), - ), - migrations.AlterField( - model_name='sigmaparameter', - name='power', - field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм'), - ), - ] diff --git a/dbapp/mainapp/migrations/0009_remove_sourcetype_add_lyngsat.py b/dbapp/mainapp/migrations/0009_remove_sourcetype_add_lyngsat.py deleted file mode 100644 index dffee81..0000000 --- a/dbapp/mainapp/migrations/0009_remove_sourcetype_add_lyngsat.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-11 19:02 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('lyngsatapp', '0002_alter_lyngsat_last_update'), - ('mainapp', '0008_remove_sourcetype_objitem_objitem_source_type_id_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='objitem', - name='source_type_id', - ), - migrations.AddField( - model_name='objitem', - name='lyngsat_source', - field=models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat'), - ), - migrations.DeleteModel( - name='SourceType', - ), - ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index 9f2454b..42b455b 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -201,6 +201,32 @@ class Standard(models.Model): verbose_name_plural = "Стандарты" ordering = ["name"] +class Band(models.Model): + name = models.CharField( + max_length=50, + unique=True, + verbose_name="Название", + help_text="Название диапазона", + ) + border_start = models.FloatField( + blank=True, + null=True, + verbose_name="Нижняя граница диапазона, МГц" + ) + border_end = models.FloatField( + blank=True, + null=True, + verbose_name="Верхняя граница диапазона, МГц" + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Диапазон" + verbose_name_plural = "Диапазоны" + ordering = ["name"] + class Satellite(models.Model): """ @@ -223,6 +249,66 @@ class Satellite(models.Model): verbose_name="NORAD ID", help_text="Идентификатор NORAD для отслеживания спутника", ) + band = models.ManyToManyField( + Band, + related_name="bands", + verbose_name="Диапазоны", + blank=True, + help_text="Диапазоны работы спутника", + ) + undersat_point = models.FloatField( + blank=True, + null=True, + verbose_name="Подспутниковая точка, градусы", + help_text="Подспутниковая точка в градусах. Восточное полушарие с +, западное с -", + ) + url = models.URLField( + blank=True, + null=True, + verbose_name="Ссылка на источник", + help_text="Ссылка на сайт, где можно проверить информацию", + ) + comment = models.TextField( + blank=True, + null=True, + verbose_name="Комментарий", + help_text="Любой возможный комменатрий", + ) + launch_date = models.DateField( + blank=True, + null=True, + verbose_name="Дата запуска", + help_text="Дата запуска спутника", + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="satellite_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="satellite_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) def __str__(self): return self.name @@ -283,11 +369,73 @@ class ObjItemManager(models.Manager): return self.get_queryset().by_user(user) +class Source(models.Model): + """ + Модель источника сигнала. + """ + + coords_kupsat = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты Кубсата", + help_text="Координаты, полученные от кубсата (WGS84)", + ) + coords_valid = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты оперативников", + help_text="Координаты, предоставленные оперативным отделом (WGS84)", + ) + coords_reference = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты справочные", + help_text="Координаты, ещё кем-то проверенные (WGS84)", + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="source_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="source_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) + + + class Meta: + verbose_name = "Источник" + verbose_name_plural = "Источники" + + class ObjItem(models.Model): """ - Модель объекта (источника сигнала). + Модель точки ГЛ. - Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации и типе источника. + Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации. """ # Основные поля @@ -299,6 +447,22 @@ class ObjItem(models.Model): db_index=True, help_text="Название объекта/источника сигнала", ) + source = models.ForeignKey( + Source, + on_delete=models.CASCADE, + null=True, + verbose_name="ИРИ", + related_name="source", + ) + transponder = models.ForeignKey( + "mapsapp.Transponders", + on_delete=models.SET_NULL, + related_name="transponder", + null=True, + blank=True, + verbose_name="Транспондер", + help_text="Транспондер, с помощью которого была получена точка", + ) # Метаданные created_at = models.DateTimeField( @@ -679,46 +843,32 @@ class Geo(models.Model): verbose_name="Координата геолокации", help_text="Основные координаты геолокации (WGS84)", ) - coords_kupsat = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты Кубсата", - help_text="Координаты, полученные от кубсата (WGS84)", - ) - coords_valid = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты оперативников", - help_text="Координаты, предоставленные оперативным отделом (WGS84)", - ) # Вычисляемые поля - расстояния - distance_coords_kup = models.GeneratedField( - expression=functions.Distance("coords", "coords_kupsat") / 1000, - output_field=models.FloatField(), - db_persist=True, - null=True, - blank=True, - verbose_name="Расстояние между кубсатом и гео, км", - ) - distance_coords_valid = models.GeneratedField( - expression=functions.Distance("coords", "coords_valid") / 1000, - output_field=models.FloatField(), - db_persist=True, - null=True, - blank=True, - verbose_name="Расстояние между гео и оперативным отделом, км", - ) - distance_kup_valid = models.GeneratedField( - expression=functions.Distance("coords_valid", "coords_kupsat") / 1000, - output_field=models.FloatField(), - db_persist=True, - null=True, - blank=True, - verbose_name="Расстояние между кубсатом и оперативным отделом, км", - ) + # distance_coords_kup = models.GeneratedField( + # expression=functions.Distance("coords", "coords_kupsat") / 1000, + # output_field=models.FloatField(), + # db_persist=True, + # null=True, + # blank=True, + # verbose_name="Расстояние между кубсатом и гео, км", + # ) + # distance_coords_valid = models.GeneratedField( + # expression=functions.Distance("coords", "coords_valid") / 1000, + # output_field=models.FloatField(), + # db_persist=True, + # null=True, + # blank=True, + # verbose_name="Расстояние между гео и оперативным отделом, км", + # ) + # distance_kup_valid = models.GeneratedField( + # expression=functions.Distance("coords_valid", "coords_kupsat") / 1000, + # output_field=models.FloatField(), + # db_persist=True, + # null=True, + # blank=True, + # verbose_name="Расстояние между кубсатом и оперативным отделом, км", + # ) # Связи mirrors = models.ManyToManyField( diff --git a/dbapp/mainapp/templates/mainapp/actions.html b/dbapp/mainapp/templates/mainapp/actions.html index f5229dd..a0fd494 100644 --- a/dbapp/mainapp/templates/mainapp/actions.html +++ b/dbapp/mainapp/templates/mainapp/actions.html @@ -95,8 +95,8 @@

Добавление транспондеров

-

Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.

- +

Добавьте список транспондеров в базу данных.

+
Добавить транспондеры diff --git a/dbapp/mapsapp/migrations/0001_initial.py b/dbapp/mapsapp/migrations/0001_initial.py index 452d8d5..643ceae 100644 --- a/dbapp/mapsapp/migrations/0001_initial.py +++ b/dbapp/mapsapp/migrations/0001_initial.py @@ -1,37 +1,44 @@ -# Generated by Django 5.2.7 on 2025-10-31 13:36 - -import django.db.models.deletion -import django.db.models.expressions -import django.db.models.functions.math -import mainapp.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('mainapp', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Transponders', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')), - ('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')), - ('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')), - ('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')), - ('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')), - ('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')), - ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')), - ('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')), - ], - options={ - 'verbose_name': 'Транспондер', - 'verbose_name_plural': 'Транспондеры', - }, - ), - ] +# Generated by Django 5.2.7 on 2025-11-12 14:21 + +import django.db.models.deletion +import django.db.models.expressions +import django.db.models.functions.math +import mainapp.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('mainapp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Transponders', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера')), + ('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')), + ('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')), + ('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')), + ('zone_name', models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны')), + ('snr', models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, verbose_name='Полоса')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')), + ('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')), + ('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_created', to='mainapp.customuser', verbose_name='Создан пользователем')), + ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')), + ('sat_id', models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')), + ('updated_by', models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_updated', to='mainapp.customuser', verbose_name='Изменен пользователем')), + ], + options={ + 'verbose_name': 'Транспондер', + 'verbose_name_plural': 'Транспондеры', + 'ordering': ['sat_id', 'downlink'], + 'indexes': [models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'), models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx')], + }, + ), + ] diff --git a/dbapp/mapsapp/migrations/0002_alter_transponders_options_and_more.py b/dbapp/mapsapp/migrations/0002_alter_transponders_options_and_more.py deleted file mode 100644 index 70a32d8..0000000 --- a/dbapp/mapsapp/migrations/0002_alter_transponders_options_and_more.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-07 20:58 - -import django.core.validators -import django.db.models.deletion -import mainapp.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'), - ('mapsapp', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='transponders', - options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'}, - ), - migrations.AlterField( - model_name='transponders', - name='downlink', - field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'), - ), - migrations.AlterField( - model_name='transponders', - name='frequency_range', - field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'), - ), - migrations.AlterField( - model_name='transponders', - name='name', - field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'), - ), - migrations.AlterField( - model_name='transponders', - name='polarization', - field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'), - ), - migrations.AlterField( - model_name='transponders', - name='sat_id', - field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'), - ), - migrations.AlterField( - model_name='transponders', - name='uplink', - field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'), - ), - migrations.AlterField( - model_name='transponders', - name='zone_name', - field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'), - ), - migrations.AddIndex( - model_name='transponders', - index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'), - ), - migrations.AddIndex( - model_name='transponders', - index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'), - ), - ] diff --git a/dbapp/mapsapp/models.py b/dbapp/mapsapp/models.py index 132ac01..d0c44f9 100644 --- a/dbapp/mapsapp/models.py +++ b/dbapp/mapsapp/models.py @@ -6,7 +6,7 @@ from django.db.models import ExpressionWrapper, F from django.db.models.functions import Abs # Local imports -from mainapp.models import Polarization, Satellite, get_default_polarization +from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser class Transponders(models.Model): @@ -29,22 +29,22 @@ class Transponders(models.Model): blank=True, null=True, verbose_name="Downlink", - validators=[MinValueValidator(0), MaxValueValidator(50000)], - help_text="Частота downlink в МГц (0-50000)" + # validators=[MinValueValidator(0), MaxValueValidator(50000)], + # help_text="Частота downlink в МГц (0-50000)" ) frequency_range = models.FloatField( blank=True, null=True, verbose_name="Полоса", - validators=[MinValueValidator(0), MaxValueValidator(1000)], - help_text="Полоса частот в МГц (0-1000)" + # validators=[MinValueValidator(0), MaxValueValidator(1000)], + # help_text="Полоса частот в МГц (0-1000)" ) uplink = models.FloatField( blank=True, null=True, verbose_name="Uplink", - validators=[MinValueValidator(0), MaxValueValidator(50000)], - help_text="Частота uplink в МГц (0-50000)" + # validators=[MinValueValidator(0), MaxValueValidator(50000)], + # help_text="Частота uplink в МГц (0-50000)" ) zone_name = models.CharField( max_length=255, @@ -54,6 +54,41 @@ class Transponders(models.Model): db_index=True, help_text="Название зоны покрытия транспондера" ) + snr = models.FloatField( + blank=True, + null=True, + verbose_name="Полоса", + # validators=[MinValueValidator(0), MaxValueValidator(1000)], + help_text="Полоса частот в МГц (0-1000)" + ) + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="transponder_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + CustomUser, + on_delete=models.SET_NULL, + related_name="transponder_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) # Связи polarization = models.ForeignKey( @@ -88,17 +123,17 @@ class Transponders(models.Model): verbose_name="Перенос" ) - def clean(self): - """Валидация на уровне модели""" - super().clean() + # def clean(self): + # """Валидация на уровне модели""" + # super().clean() - # Проверка что downlink и uplink заданы - if self.downlink and self.uplink: - # Обычно uplink выше downlink для спутниковой связи - if self.uplink < self.downlink: - raise ValidationError({ - 'uplink': 'Частота uplink обычно выше частоты downlink' - }) + # # Проверка что downlink и uplink заданы + # if self.downlink and self.uplink: + # # Обычно uplink выше downlink для спутниковой связи + # if self.uplink < self.downlink: + # raise ValidationError({ + # 'uplink': 'Частота uplink обычно выше частоты downlink' + # }) def __str__(self): if self.name: diff --git a/dbapp/mapsapp/utils.py b/dbapp/mapsapp/utils.py index 5d2e165..512c867 100644 --- a/dbapp/mapsapp/utils.py +++ b/dbapp/mapsapp/utils.py @@ -1,165 +1,169 @@ -# Standard library imports -import json -import re -from io import BytesIO - -# Third-party imports -import requests - -# Local imports -from mainapp.models import Polarization, Satellite - -from .models import Transponders - -def search_satellite_on_page(data: dict, satellite_name: str): - for pos, value in data.get('page', {}).get('positions').items(): - for name in value['satellites']: - if name['other_names'] is None: - name['other_names'] = '' - if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower(): - return pos, name['id'] - return '', '' - -def get_footprint_data(position: str = 62) -> dict: - """Возвращает словарь с данным по footprint для спутников на выбранной долготе""" - response = requests.get(f"https://www.satbeams.com/footprints?position={position}") - response.raise_for_status() - match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) - if match: - json_str = match.group(1) - try: - data = json.loads(json_str) - return data.get("page", {}).get("footprint_data", {}).get("beams",[]) - except json.JSONDecodeError as e: - print("Ошибка парсинга JSON:", e) - else: - print("Нужных данных не найдено") - return {} - - - -def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict: - """Возвращает словарь с данными по всем спутникам на странице""" - response = requests.get(url) - response.raise_for_status() - match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) - if match: - json_str = match.group(1) - try: - data = json.loads(json_str) - # Файл json на диске для достоверности - with open('data.json', 'w') as jf: - json.dump(data, jf, indent=2) - return data - except json.JSONDecodeError as e: - print("Ошибка парсинга JSON:", e) - else: - print("Нужных данных не найдено") - return {} - - -def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]: - names = [] - for beam in footprint_data: - if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']: - names.append( - { - "name": beam['name'], - "fullname": beam['fullname'][8:] - } - ) - return names - - -def get_band_names(satellite_name: str) -> list[str]: - data = get_all_page_data() - pos, sat_id = search_satellite_on_page(data, satellite_name) - footprints = get_footprint_data(pos) - names = get_names_footprints_for_satellite(footprints, sat_id) - return names - -def parse_transponders_from_json(filepath: str): - with open(filepath, encoding="utf-8") as jf: - data = json.load(jf) - for sat_name, trans_zone in data["satellites"].items(): - for zone, trans in trans_zone.items(): - for tran in trans: - f_b, f_e = tran["freq"][0].split("-") - f = round((float(f_b) + float(f_e))/2, 3) - f_range = round(abs(float(f_e) - float(f_b)), 3) - tran_obj = Transponders.objects.create( - name=tran["name"], - frequency=f, - frequency_range=f_range, - zone_name=zone, - polarization=Polarization.objects.get(name=tran["pol"]), - sat_id=Satellite.objects.get(name__iexact=sat_name) - ) - tran_obj.save() - - -# Third-party imports (additional) -from lxml import etree - -def parse_transponders_from_xml(data_in: BytesIO): - - tree = etree.parse(data_in) - ns = { - 'i': 'http://www.w3.org/2001/XMLSchema-instance', - 'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos', - 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' - } - satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) - for sat in satellites[:]: - name = sat.xpath('./ns:name/text()', namespaces=ns)[0] - if name == 'X' or 'DONT USE' in name: - continue - norad = sat.xpath('./ns:norad/text()', namespaces=ns) - beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) - zones = {} - for zone in beams: - zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' - zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { - "name": zone_name, - "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], - } - transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) - for transponder in transponders: - tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] - downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) - downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) - uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) - uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) - tr_data = zones[tr_id] - # p = tr_data['pol'][0] if tr_data['pol'] else '-' - match tr_data['pol']: - case 'Horizontal': - pol = 'Горизонтальная' - case 'Vertical': - pol = 'Вертикальная' - case 'CircularRight': - pol = 'Правая' - case 'CircularLeft': - pol = 'Левая' - case _: - pol = '-' - tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] - - pol_obj, _ = Polarization.objects.get_or_create(name=pol) - sat_obj, _ = Satellite.objects.get_or_create( - name=name, - defaults={ - "norad": int(norad[0]) if norad else -1 - }) - trans_obj, _ = Transponders.objects.get_or_create( - polarization=pol_obj, - downlink=(downlink_start+downlink_end)/2/1000000, - uplink=(uplink_start+uplink_end)/2/1000000, - frequency_range=abs(downlink_end-downlink_start)/1000000, - name=tr_name, - defaults={ - "zone_name": tr_data['name'], - "sat_id": sat_obj, - } - ) - trans_obj.save() +# Standard library imports +import json +import re +from io import BytesIO + +# Third-party imports +import requests + +# Local imports +from mainapp.models import Polarization, Satellite + +from .models import Transponders + +def search_satellite_on_page(data: dict, satellite_name: str): + for pos, value in data.get('page', {}).get('positions').items(): + for name in value['satellites']: + if name['other_names'] is None: + name['other_names'] = '' + if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower(): + return pos, name['id'] + return '', '' + +def get_footprint_data(position: str = 62) -> dict: + """Возвращает словарь с данным по footprint для спутников на выбранной долготе""" + response = requests.get(f"https://www.satbeams.com/footprints?position={position}") + response.raise_for_status() + match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) + if match: + json_str = match.group(1) + try: + data = json.loads(json_str) + return data.get("page", {}).get("footprint_data", {}).get("beams",[]) + except json.JSONDecodeError as e: + print("Ошибка парсинга JSON:", e) + else: + print("Нужных данных не найдено") + return {} + + + +def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict: + """Возвращает словарь с данными по всем спутникам на странице""" + response = requests.get(url) + response.raise_for_status() + match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) + if match: + json_str = match.group(1) + try: + data = json.loads(json_str) + # Файл json на диске для достоверности + with open('data.json', 'w') as jf: + json.dump(data, jf, indent=2) + return data + except json.JSONDecodeError as e: + print("Ошибка парсинга JSON:", e) + else: + print("Нужных данных не найдено") + return {} + + +def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]: + names = [] + for beam in footprint_data: + if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']: + names.append( + { + "name": beam['name'], + "fullname": beam['fullname'][8:] + } + ) + return names + + +def get_band_names(satellite_name: str) -> list[str]: + data = get_all_page_data() + pos, sat_id = search_satellite_on_page(data, satellite_name) + footprints = get_footprint_data(pos) + names = get_names_footprints_for_satellite(footprints, sat_id) + return names + +def parse_transponders_from_json(filepath: str): + with open(filepath, encoding="utf-8") as jf: + data = json.load(jf) + for sat_name, trans_zone in data["satellites"].items(): + for zone, trans in trans_zone.items(): + for tran in trans: + f_b, f_e = tran["freq"][0].split("-") + f = round((float(f_b) + float(f_e))/2, 3) + f_range = round(abs(float(f_e) - float(f_b)), 3) + tran_obj = Transponders.objects.create( + name=tran["name"], + frequency=f, + frequency_range=f_range, + zone_name=zone, + polarization=Polarization.objects.get(name=tran["pol"]), + sat_id=Satellite.objects.get(name__iexact=sat_name) + ) + tran_obj.save() + + +# Third-party imports (additional) +from lxml import etree + +def parse_transponders_from_xml(data_in: BytesIO, user=None): + + tree = etree.parse(data_in) + ns = { + 'i': 'http://www.w3.org/2001/XMLSchema-instance', + 'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos', + 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' + } + satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) + for sat in satellites[:]: + name = sat.xpath('./ns:name/text()', namespaces=ns)[0] + if name == 'X' or 'DONT USE' in name: + continue + norad = sat.xpath('./ns:norad/text()', namespaces=ns) + beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) + zones = {} + for zone in beams: + zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' + zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { + "name": zone_name, + "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], + } + transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) + for transponder in transponders: + tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] + downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) + downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) + uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) + uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) + tr_data = zones[tr_id] + # p = tr_data['pol'][0] if tr_data['pol'] else '-' + match tr_data['pol']: + case 'Horizontal': + pol = 'Горизонтальная' + case 'Vertical': + pol = 'Вертикальная' + case 'CircularRight': + pol = 'Правая' + case 'CircularLeft': + pol = 'Левая' + case _: + pol = '-' + tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] + + pol_obj, _ = Polarization.objects.get_or_create(name=pol) + sat_obj, _ = Satellite.objects.get_or_create( + name=name, + defaults={ + "norad": int(norad[0]) if norad else -1 + }) + trans_obj, created = Transponders.objects.get_or_create( + polarization=pol_obj, + downlink=(downlink_start+downlink_end)/2/1000000, + uplink=(uplink_start+uplink_end)/2/1000000, + frequency_range=abs(downlink_end-downlink_start)/1000000, + name=tr_name, + defaults={ + "zone_name": tr_data['name'], + "sat_id": sat_obj, + } + ) + if user: + if created: + trans_obj.created_by = user + trans_obj.updated_by = user + trans_obj.save() \ No newline at end of file diff --git a/dbapp/mapsapp/utils.py.backup b/dbapp/mapsapp/utils.py.backup new file mode 100644 index 0000000..5d2e165 --- /dev/null +++ b/dbapp/mapsapp/utils.py.backup @@ -0,0 +1,165 @@ +# Standard library imports +import json +import re +from io import BytesIO + +# Third-party imports +import requests + +# Local imports +from mainapp.models import Polarization, Satellite + +from .models import Transponders + +def search_satellite_on_page(data: dict, satellite_name: str): + for pos, value in data.get('page', {}).get('positions').items(): + for name in value['satellites']: + if name['other_names'] is None: + name['other_names'] = '' + if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower(): + return pos, name['id'] + return '', '' + +def get_footprint_data(position: str = 62) -> dict: + """Возвращает словарь с данным по footprint для спутников на выбранной долготе""" + response = requests.get(f"https://www.satbeams.com/footprints?position={position}") + response.raise_for_status() + match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) + if match: + json_str = match.group(1) + try: + data = json.loads(json_str) + return data.get("page", {}).get("footprint_data", {}).get("beams",[]) + except json.JSONDecodeError as e: + print("Ошибка парсинга JSON:", e) + else: + print("Нужных данных не найдено") + return {} + + + +def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict: + """Возвращает словарь с данными по всем спутникам на странице""" + response = requests.get(url) + response.raise_for_status() + match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) + if match: + json_str = match.group(1) + try: + data = json.loads(json_str) + # Файл json на диске для достоверности + with open('data.json', 'w') as jf: + json.dump(data, jf, indent=2) + return data + except json.JSONDecodeError as e: + print("Ошибка парсинга JSON:", e) + else: + print("Нужных данных не найдено") + return {} + + +def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]: + names = [] + for beam in footprint_data: + if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']: + names.append( + { + "name": beam['name'], + "fullname": beam['fullname'][8:] + } + ) + return names + + +def get_band_names(satellite_name: str) -> list[str]: + data = get_all_page_data() + pos, sat_id = search_satellite_on_page(data, satellite_name) + footprints = get_footprint_data(pos) + names = get_names_footprints_for_satellite(footprints, sat_id) + return names + +def parse_transponders_from_json(filepath: str): + with open(filepath, encoding="utf-8") as jf: + data = json.load(jf) + for sat_name, trans_zone in data["satellites"].items(): + for zone, trans in trans_zone.items(): + for tran in trans: + f_b, f_e = tran["freq"][0].split("-") + f = round((float(f_b) + float(f_e))/2, 3) + f_range = round(abs(float(f_e) - float(f_b)), 3) + tran_obj = Transponders.objects.create( + name=tran["name"], + frequency=f, + frequency_range=f_range, + zone_name=zone, + polarization=Polarization.objects.get(name=tran["pol"]), + sat_id=Satellite.objects.get(name__iexact=sat_name) + ) + tran_obj.save() + + +# Third-party imports (additional) +from lxml import etree + +def parse_transponders_from_xml(data_in: BytesIO): + + tree = etree.parse(data_in) + ns = { + 'i': 'http://www.w3.org/2001/XMLSchema-instance', + 'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos', + 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' + } + satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) + for sat in satellites[:]: + name = sat.xpath('./ns:name/text()', namespaces=ns)[0] + if name == 'X' or 'DONT USE' in name: + continue + norad = sat.xpath('./ns:norad/text()', namespaces=ns) + beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) + zones = {} + for zone in beams: + zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' + zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { + "name": zone_name, + "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], + } + transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) + for transponder in transponders: + tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] + downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) + downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) + uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) + uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) + tr_data = zones[tr_id] + # p = tr_data['pol'][0] if tr_data['pol'] else '-' + match tr_data['pol']: + case 'Horizontal': + pol = 'Горизонтальная' + case 'Vertical': + pol = 'Вертикальная' + case 'CircularRight': + pol = 'Правая' + case 'CircularLeft': + pol = 'Левая' + case _: + pol = '-' + tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] + + pol_obj, _ = Polarization.objects.get_or_create(name=pol) + sat_obj, _ = Satellite.objects.get_or_create( + name=name, + defaults={ + "norad": int(norad[0]) if norad else -1 + }) + trans_obj, _ = Transponders.objects.get_or_create( + polarization=pol_obj, + downlink=(downlink_start+downlink_end)/2/1000000, + uplink=(uplink_start+uplink_end)/2/1000000, + frequency_range=abs(downlink_end-downlink_start)/1000000, + name=tr_name, + defaults={ + "zone_name": tr_data['name'], + "sat_id": sat_obj, + } + ) + trans_obj.save()