# 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, SourceType, 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' ).prefetch_related( 'parameters_obj__id_satellite', 'parameters_obj__polarization', 'parameters_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 = next(iter(obj.parameters_obj.all()), 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 ParameterObjItemInline(admin.StackedInline): model = ObjItem.parameters_obj.through extra = 0 max_num = 1 verbose_name = "ВЧ загрузка" verbose_name_plural = "ВЧ загрузки" # ============================================================================ # 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(SourceType) class SourceTypeAdmin(BaseAdmin): """Админ-панель для модели SourceType.""" 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", "sigma_parameter" ) list_display_links = ("frequency", "id_satellite") list_select_related = ("polarization", "modulation", "standard", "id_satellite") list_filter = ( HasSigmaParameterFilter, ("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", ) ordering = ("-frequency",) autocomplete_fields = ("objitems",) inlines = [SigmaParameterInline] 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") list_filter = ( UniqueToggleFilter, ("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter), ("parameters_obj__frequency", NumericRangeFilterBuilder()), ("parameters_obj__freq_range", NumericRangeFilterBuilder()), ("parameters_obj__snr", NumericRangeFilterBuilder()), ("parameters_obj__modulation", MultiSelectRelatedDropdownFilter), ("parameters_obj__polarization", MultiSelectRelatedDropdownFilter), GeoKupDistanceFilter, GeoValidDistanceFilter, ("created_at", DateRangeQuickSelectListFilterBuilder()), ("updated_at", DateRangeQuickSelectListFilterBuilder()), ) search_fields = ( "name", "geo_obj__location", "parameters_obj__frequency", "parameters_obj__id_satellite__name", ) ordering = ("-updated_at",) inlines = [ParameterObjItemInline, GeoInline] 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 и prefetch_related. Загружает связанные объекты одним запросом для улучшения производительности. """ qs = super().get_queryset(request) return qs.select_related( "geo_obj", "created_by__user", "updated_by__user" ).prefetch_related( "parameters_obj__id_satellite", "parameters_obj__polarization", "parameters_obj__modulation", "parameters_obj__standard" ) def sat_name(self, obj): """Отображает название спутника из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param and param.id_satellite: return param.id_satellite.name return "-" sat_name.short_description = "Спутник" sat_name.admin_order_field = "parameters_obj__id_satellite__name" def freq(self, obj): """Отображает частоту из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param: return param.frequency return "-" freq.short_description = "Частота, МГц" freq.admin_order_field = "parameters_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): """Отображает поляризацию из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param and param.polarization: return param.polarization.name return "-" pol.short_description = "Поляризация" def freq_range(self, obj): """Отображает полосу частот из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param: return param.freq_range return "-" freq_range.short_description = "Полоса, МГц" freq_range.admin_order_field = "parameters_obj__freq_range" def bod_velocity(self, obj): """Отображает символьную скорость из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param: return param.bod_velocity return "-" bod_velocity.short_description = "Сим. v, БОД" def modulation(self, obj): """Отображает модуляцию из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param and param.modulation: return param.modulation.name return "-" modulation.short_description = "Модуляция" def snr(self, obj): """Отображает отношение сигнал/шум из связанного параметра.""" param = next(iter(obj.parameters_obj.all()), None) if param: return param.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 = "Координаты оперативного отдела"