# 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", ) }, ), ( "Координаты: геолокация", {"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 = "Координаты оперативного отдела" @admin.register(Band) class BandAdmin(ImportExportActionModelAdmin, BaseAdmin): """Админ-панель для модели Band.""" list_display = ("name", "border_start", "border_end") search_fields = ("name",) ordering = ("name",) class ObjItemInline(admin.TabularInline): """Inline для отображения объектов ObjItem в Source.""" model = ObjItem fk_name = "source" extra = 0 can_delete = False verbose_name = "Объект" verbose_name_plural = "Объекты" fields = ( "name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at", ) readonly_fields = ( "name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at", ) def get_queryset(self, request): """Оптимизированный queryset с предзагрузкой связанных объектов.""" qs = super().get_queryset(request) return qs.select_related( "geo_obj", "parameter_obj", "parameter_obj__id_satellite", "parameter_obj__polarization", ) def get_geo_coords(self, obj): """Отображает координаты из связанной модели Geo.""" if not obj or not hasattr(obj, "geo_obj"): return "-" 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}" get_geo_coords.short_description = "Координаты" def get_satellite(self, obj): """Отображает спутник из связанного параметра.""" if ( hasattr(obj, "parameter_obj") and obj.parameter_obj and obj.parameter_obj.id_satellite ): return obj.parameter_obj.id_satellite.name return "-" get_satellite.short_description = "Спутник" def get_frequency(self, obj): """Отображает частоту из связанного параметра.""" if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.frequency return "-" get_frequency.short_description = "Частота, МГц" def get_polarization(self, obj): """Отображает поляризацию из связанного параметра.""" if ( hasattr(obj, "parameter_obj") and obj.parameter_obj and obj.parameter_obj.polarization ): return obj.parameter_obj.polarization.name return "-" get_polarization.short_description = "Поляризация" def has_add_permission(self, request, obj=None): return False @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") inlines = [ObjItemInline] fieldsets = ( ( "Координаты: геолокация", {"fields": ("coords_kupsat", "coords_valid", "coords_reference")}, ), ( "Метаданные", { "fields": ("created_at", "created_by", "updated_at", "updated_by"), "classes": ("collapse",), }, ), )