1043 lines
36 KiB
Python
1043 lines
36 KiB
Python
# 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",),
|
||
},
|
||
),
|
||
)
|