diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py
index c78fca1..5071fe2 100644
--- a/dbapp/mainapp/admin.py
+++ b/dbapp/mainapp/admin.py
@@ -32,13 +32,13 @@ from .models import (
ObjItem,
CustomUser,
Band,
- Source
+ Source,
)
from .filters import (
GeoKupDistanceFilter,
GeoValidDistanceFilter,
UniqueToggleFilter,
- HasSigmaParameterFilter
+ HasSigmaParameterFilter,
)
@@ -55,22 +55,24 @@ 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: Сохраняемый объект модели
@@ -79,20 +81,20 @@ class BaseAdmin(admin.ModelAdmin):
"""
if not change:
# При создании нового объекта устанавливаем created_by
- if hasattr(obj, 'created_by') and not obj.created_by_id:
- obj.created_by = getattr(request.user, 'customuser', None)
-
+ 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)
-
+ 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 = 'Дополнительная информация пользователя'
+ verbose_name_plural = "Дополнительная информация пользователя"
class LocationForm(forms.ModelForm):
@@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm):
class Meta:
model = Geo
- fields = '__all__'
+ 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]
+ 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]
@@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm):
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')
+
+ 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)
@@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline):
form = LocationForm
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
prefetch_related = ("mirrors",)
- autocomplete_fields = ('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": (
+ "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"),
# }),
@@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline):
class UserAdmin(BaseUserAdmin):
inlines = [CustomUserInline]
+
admin.site.register(User, UserAdmin)
@@ -181,80 +195,83 @@ 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}')
+ 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}')
+ 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'
+ "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
-
+
+ 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([
- 'Название',
- 'Спутник',
- 'Частота (МГц)',
- 'Полоса (МГц)',
- 'Поляризация',
- 'Модуляция',
- 'ОСШ',
- 'Координаты геолокации',
- 'Координаты Кубсата',
- 'Координаты оперативного отдела',
- 'Расстояние Гео-Куб (км)',
- 'Расстояние Гео-Опер (км)',
- 'Дата создания',
- 'Дата обновления'
- ])
-
+ writer.writerow(
+ [
+ "Название",
+ "Спутник",
+ "Частота (МГц)",
+ "Полоса (МГц)",
+ "Поляризация",
+ "Модуляция",
+ "ОСШ",
+ "Координаты геолокации",
+ "Координаты Кубсата",
+ "Координаты оперативного отдела",
+ "Расстояние Гео-Куб (км)",
+ "Расстояние Гео-Опер (км)",
+ "Дата создания",
+ "Дата обновления",
+ ]
+ )
+
for obj in queryset:
- param = getattr(obj, 'parameter_obj', None)
+ param = getattr(obj, "parameter_obj", None)
geo = obj.geo_obj
-
+
# Форматирование координат
def format_coords(coords):
if not coords:
@@ -263,24 +280,30 @@ def export_objects_to_csv(modeladmin, request, queryset):
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 "-"
- ])
-
+
+ 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
@@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset):
# Inline Admin Classes
# ============================================================================
+
class ParameterInline(admin.StackedInline):
"""Inline для редактирования параметра объекта."""
+
model = Parameter
extra = 0
max_num = 1
@@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline):
verbose_name = "ВЧ загрузка"
verbose_name_plural = "ВЧ загрузка"
fields = (
- 'id_satellite',
- 'frequency',
- 'freq_range',
- 'polarization',
- 'modulation',
- 'bod_velocity',
- 'snr',
- 'standard'
+ "id_satellite",
+ "frequency",
+ "freq_range",
+ "polarization",
+ "modulation",
+ "bod_velocity",
+ "snr",
+ "standard",
)
- autocomplete_fields = ('id_satellite', 'polarization', 'modulation', '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()),
- )
+ list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
@admin.register(Polarization)
class PolarizationAdmin(BaseAdmin):
"""Админ-панель для модели Polarization."""
+
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin):
@admin.register(Modulation)
class ModulationAdmin(BaseAdmin):
"""Админ-панель для модели Modulation."""
+
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin):
@admin.register(Standard)
class StandardAdmin(BaseAdmin):
"""Админ-панель для модели Standard."""
+
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin):
class SigmaParameterInline(admin.StackedInline):
model = SigmaParameter
extra = 0
- autocomplete_fields = ['mark']
+ autocomplete_fields = ["mark"]
readonly_fields = (
- "datetime_begin",
- "datetime_end",
+ "datetime_begin",
+ "datetime_end",
)
+
def has_add_permission(self, request, obj=None):
return False
@@ -364,12 +393,13 @@ class SigmaParameterInline(admin.StackedInline):
class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""
Админ-панель для модели Parameter.
-
+
Оптимизирована для работы с большим количеством параметров:
- Использует select_related для оптимизации запросов
- Предоставляет фильтры по основным характеристикам
- Поддерживает импорт/экспорт данных
"""
+
list_display = (
"id_satellite",
"frequency",
@@ -380,11 +410,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"snr",
"standard",
"related_objitem",
- "sigma_parameter"
+ "sigma_parameter",
)
list_display_links = ("frequency", "id_satellite")
- list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem")
-
+ list_select_related = (
+ "polarization",
+ "modulation",
+ "standard",
+ "id_satellite",
+ "objitem",
+ )
+
list_filter = (
HasSigmaParameterFilter,
("objitem", MultiSelectRelatedDropdownFilter),
@@ -396,7 +432,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
("freq_range", NumericRangeFilterBuilder()),
("snr", NumericRangeFilterBuilder()),
)
-
+
search_fields = (
"id_satellite__name",
"frequency",
@@ -408,16 +444,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"standard__name",
"objitem__name",
)
-
+
ordering = ("-frequency",)
autocomplete_fields = ("objitem",)
inlines = [SigmaParameterInline]
def related_objitem(self, obj):
"""Отображает связанный ObjItem."""
- if hasattr(obj, 'objitem') and 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"
@@ -427,19 +464,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
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",
@@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"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_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),
@@ -472,7 +513,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
("datetime_begin", DateRangeQuickSelectListFilterBuilder()),
("datetime_end", DateRangeQuickSelectListFilterBuilder()),
)
-
+
search_fields = (
"id_satellite__name",
"frequency",
@@ -484,17 +525,25 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
)
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")
+
+ list_display = (
+ "name",
+ "norad",
+ "undersat_point",
+ "launch_date",
+ "created_at",
+ "updated_at",
+ )
search_fields = ("name", "norad")
ordering = ("name",)
filter_horizontal = ("band",)
@@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin):
@admin.register(Mirror)
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
+
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@@ -514,28 +564,37 @@ class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
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",
+ (
+ "Основная информация",
+ {
+ "fields": (
+ "mirrors",
+ "location",
# "distance_coords_kup",
- # "distance_coords_valid",
- # "distance_kup_valid",
- "timestamp", "comment", "transponder")
- }),
- ("Координаты: геолокация", {
- "fields": ("longitude_geo", "latitude_geo", "coords")
- }),
+ # "distance_coords_valid",
+ # "distance_kup_valid",
+ "timestamp",
+ "comment",
+ )
+ },
+ ),
+ (
+ "Координаты: геолокация",
+ {"fields": ("longitude_geo", "latitude_geo", "coords")},
+ ),
# ("Координаты: Кубсат", {
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
# }),
@@ -543,7 +602,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
# "fields": ("longitude_valid", "latitude_valid", "coords_valid")
# }),
)
-
+
list_display = (
"formatted_timestamp",
"location",
@@ -554,38 +613,39 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
"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", )
+
+ autocomplete_fields = ("mirrors",)
ordering = ("-timestamp",)
actions = [show_on_map]
-
+
settings_overrides = {
- 'DEFAULT_CENTER': (55.7558, 37.6173),
- 'DEFAULT_ZOOM': 12,
+ "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")
+ 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):
@@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
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"
@@ -606,6 +667,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
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):
@@ -631,19 +693,18 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
# valid_coords.short_description = "Координаты оперативного отдела"
-
-
@admin.register(ObjItem)
class ObjItemAdmin(BaseAdmin):
"""
Админ-панель для модели ObjItem.
-
+
Оптимизирована для работы с большим количеством объектов:
- Использует select_related и prefetch_related для оптимизации запросов
- Предоставляет фильтры по основным параметрам
- Поддерживает поиск по имени, координатам и частоте
- Включает кастомные actions для отображения на карте
"""
+
list_display = (
"name",
"sat_name",
@@ -664,16 +725,16 @@ class ObjItemAdmin(BaseAdmin):
)
list_display_links = ("name",)
list_select_related = (
- "geo_obj",
- "created_by__user",
+ "geo_obj",
+ "created_by__user",
"updated_by__user",
"parameter_obj",
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
- "parameter_obj__standard"
+ "parameter_obj__standard",
)
-
+
list_filter = (
UniqueToggleFilter,
("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter),
@@ -687,33 +748,34 @@ class ObjItemAdmin(BaseAdmin):
("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",)
- }),
+ ("Основная информация", {"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)
@@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin):
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
- "parameter_obj__standard"
+ "parameter_obj__standard",
)
def sat_name(self, obj):
"""Отображает название спутника из связанного параметра."""
- if hasattr(obj, 'parameter_obj') and obj.parameter_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:
+ 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"
@@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin):
def pol(self, obj):
"""Отображает поляризацию из связанного параметра."""
- if hasattr(obj, 'parameter_obj') and obj.parameter_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:
+ 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:
+ 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 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:
+ if hasattr(obj, "parameter_obj") and obj.parameter_obj:
return obj.parameter_obj.snr
return "-"
+
snr.short_description = "ОСШ"
def geo_coords(self, obj):
@@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin):
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"
@@ -830,6 +900,7 @@ class ObjItemAdmin(BaseAdmin):
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):
@@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin):
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",)
@@ -855,29 +928,44 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
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")
-
+
+ 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'
+ "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'):
+ if not obj or not hasattr(obj, "geo_obj"):
return "-"
geo = obj.geo_obj
if not geo or not geo.coords:
@@ -887,29 +975,41 @@ class ObjItemInline(admin.TabularInline):
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:
+ 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:
+ 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:
+ 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
@@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline):
@admin.register(Source)
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
"""Админ-панель для модели Source."""
+
list_display = ("id", "created_at", "updated_at")
list_filter = (
("created_at", DateRangeQuickSelectListFilterBuilder()),
@@ -925,13 +1026,17 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
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",)
- }),
+ (
+ "Координаты: геолокация",
+ {"fields": ("coords_kupsat", "coords_valid", "coords_reference")},
+ ),
+ (
+ "Метаданные",
+ {
+ "fields": ("created_at", "created_by", "updated_at", "updated_by"),
+ "classes": ("collapse",),
+ },
+ ),
)
diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py
index 27850c9..a2ccd03 100644
--- a/dbapp/mainapp/forms.py
+++ b/dbapp/mainapp/forms.py
@@ -9,6 +9,7 @@ from .models import (
Parameter,
Polarization,
Satellite,
+ Source,
Standard,
)
from .widgets import CheckboxSelectMultipleWidget
@@ -305,8 +306,8 @@ class GeoForm(forms.ModelForm):
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"mirrors": CheckboxSelectMultipleWidget(
attrs={
- 'id': 'id_geo-mirrors',
- 'placeholder': 'Выберите спутники...',
+ "id": "id_geo-mirrors",
+ "placeholder": "Выберите спутники...",
}
),
}
@@ -372,3 +373,160 @@ class ObjItemForm(forms.ModelForm):
)
return name
+
+
+class SourceForm(forms.ModelForm):
+ """Form for editing Source model with 4 coordinate fields."""
+
+ # Координаты ГЛ (coords_average)
+ average_latitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
+ ),
+ label="Широта ГЛ",
+ )
+ average_longitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={
+ "class": "form-control",
+ "step": "0.000001",
+ "placeholder": "Долгота",
+ }
+ ),
+ label="Долгота ГЛ",
+ )
+
+ # Координаты Кубсата (coords_kupsat)
+ kupsat_latitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
+ ),
+ label="Широта Кубсата",
+ )
+ kupsat_longitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={
+ "class": "form-control",
+ "step": "0.000001",
+ "placeholder": "Долгота",
+ }
+ ),
+ label="Долгота Кубсата",
+ )
+
+ # Координаты оперативников (coords_valid)
+ valid_latitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
+ ),
+ label="Широта оперативников",
+ )
+ valid_longitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={
+ "class": "form-control",
+ "step": "0.000001",
+ "placeholder": "Долгота",
+ }
+ ),
+ label="Долгота оперативников",
+ )
+
+ # Координаты справочные (coords_reference)
+ reference_latitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
+ ),
+ label="Широта справочные",
+ )
+ reference_longitude = forms.FloatField(
+ required=False,
+ widget=forms.NumberInput(
+ attrs={
+ "class": "form-control",
+ "step": "0.000001",
+ "placeholder": "Долгота",
+ }
+ ),
+ label="Долгота справочные",
+ )
+
+ class Meta:
+ model = Source
+ fields = [] # Все поля обрабатываются вручную
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Заполняем поля координат из instance
+ if self.instance and self.instance.pk:
+ if self.instance.coords_average:
+ self.fields[
+ "average_longitude"
+ ].initial = self.instance.coords_average.x
+ self.fields["average_latitude"].initial = self.instance.coords_average.y
+
+ if self.instance.coords_kupsat:
+ self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x
+ self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y
+
+ if self.instance.coords_valid:
+ self.fields["valid_longitude"].initial = self.instance.coords_valid.x
+ self.fields["valid_latitude"].initial = self.instance.coords_valid.y
+
+ if self.instance.coords_reference:
+ self.fields[
+ "reference_longitude"
+ ].initial = self.instance.coords_reference.x
+ self.fields[
+ "reference_latitude"
+ ].initial = self.instance.coords_reference.y
+
+ def save(self, commit=True):
+ from django.contrib.gis.geos import Point
+
+ instance = super().save(commit=False)
+
+ # Обработка coords_average
+ avg_lat = self.cleaned_data.get("average_latitude")
+ avg_lng = self.cleaned_data.get("average_longitude")
+ if avg_lat is not None and avg_lng is not None:
+ instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
+ else:
+ instance.coords_average = None
+
+ # Обработка coords_kupsat
+ kup_lat = self.cleaned_data.get("kupsat_latitude")
+ kup_lng = self.cleaned_data.get("kupsat_longitude")
+ if kup_lat is not None and kup_lng is not None:
+ instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326)
+ else:
+ instance.coords_kupsat = None
+
+ # Обработка coords_valid
+ val_lat = self.cleaned_data.get("valid_latitude")
+ val_lng = self.cleaned_data.get("valid_longitude")
+ if val_lat is not None and val_lng is not None:
+ instance.coords_valid = Point(val_lng, val_lat, srid=4326)
+ else:
+ instance.coords_valid = None
+
+ # Обработка coords_reference
+ ref_lat = self.cleaned_data.get("reference_latitude")
+ ref_lng = self.cleaned_data.get("reference_longitude")
+ if ref_lat is not None and ref_lng is not None:
+ instance.coords_reference = Point(ref_lng, ref_lat, srid=4326)
+ else:
+ instance.coords_reference = None
+
+ if commit:
+ instance.save()
+
+ return instance
diff --git a/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html b/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html
index adfefa7..bde10ab 100644
--- a/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html
+++ b/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html
@@ -22,24 +22,25 @@
{% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
- {% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %}
+ {% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
\ No newline at end of file
diff --git a/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html b/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html
index 2f580cb..84b4d43 100644
--- a/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html
+++ b/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html
@@ -32,6 +32,7 @@
Имя
Спутник
+ Транспондер
Част, МГц
Полоса, МГц
Поляризация
@@ -41,8 +42,6 @@
Время ГЛ
Местоположение
Геолокация
- Кубсат
- Опер. отд
Обновлено
Кем(обн)
Создано
diff --git a/dbapp/mainapp/templates/mainapp/objitem_list.html b/dbapp/mainapp/templates/mainapp/objitem_list.html
index ebb67f5..6b5a639 100644
--- a/dbapp/mainapp/templates/mainapp/objitem_list.html
+++ b/dbapp/mainapp/templates/mainapp/objitem_list.html
@@ -70,7 +70,8 @@
-
+
Фильтры
0
@@ -82,10 +83,11 @@
Добавить к
-
+
-
+
Список
0
@@ -112,295 +114,277 @@
+
+
+
Спутник:
+
+ Выбрать
+ Снять
+
+
+ {% for satellite in satellites %}
+
+ {{ satellite.name }}
+
+ {% endfor %}
+
-
-
-
-
-
-
-
-
+
+
+ Частота, МГц:
+
+
+
+
+
+
+ Полоса, МГц:
+
+
+
+
+
+
+ ОСШ:
+
+
+
+
+
+
+ Сим. v, БОД:
+
+
+
+
+
+
+
Модуляция:
+
+ Выбрать
+ Снять
+
+
+ {% for mod in modulations %}
+
+ {{ mod.name }}
+
+ {% endfor %}
+
+
+
+
+
+
Поляризация:
+
+ Выбрать
+ Снять
+
+
+ {% for pol in polarizations %}
+
+ {{ pol.name }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
Дата ГЛ:
+
+
+ Сегодня
+ Неделя
+
+
+ Месяц
+ Год
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/dbapp/mainapp/templates/mainapp/objitem_map.html b/dbapp/mainapp/templates/mainapp/objitem_map.html
index b8262d7..f0427eb 100644
--- a/dbapp/mainapp/templates/mainapp/objitem_map.html
+++ b/dbapp/mainapp/templates/mainapp/objitem_map.html
@@ -1,10 +1,74 @@
-{% extends "mapsapp/map2d_base.html" %}
+{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Карта выбранных объектов{% endblock title %}
+{% block extra_css %}
+
+
+
+
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock content %}
+
{% block extra_js %}
+
+
+
+
+
{% endblock extra_js %}
\ No newline at end of file
diff --git a/dbapp/mainapp/templates/mainapp/source_confirm_delete.html b/dbapp/mainapp/templates/mainapp/source_confirm_delete.html
new file mode 100644
index 0000000..e4b0485
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/source_confirm_delete.html
@@ -0,0 +1,63 @@
+{% extends 'mainapp/base.html' %}
+{% load static %}
+
+{% block title %}Удалить источник #{{ object.id }}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ Внимание! Вы собираетесь удалить источник.
+
+
+
Информация об источнике:
+
+
+ ID: {{ object.id }}
+
+
+ Дата создания:
+ {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
+
+
+ Создан пользователем:
+ {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
+
+
+ Привязанных объектов:
+ {{ objitems_count }}
+
+
+
+ {% if objitems_count > 0 %}
+
+
+ Важно! При удалении источника будут также удалены все {{ objitems_count }} привязанных объектов!
+
+ {% endif %}
+
+
Это действие нельзя отменить. Вы уверены, что хотите продолжить?
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/source_form.html b/dbapp/mainapp/templates/mainapp/source_form.html
new file mode 100644
index 0000000..8143a1b
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/source_form.html
@@ -0,0 +1,652 @@
+{% extends 'mainapp/base.html' %}
+{% load static %}
+{% load static leaflet_tags %}
+{% load l10n %}
+
+{% block title %}Редактировать источник #{{ object.id }}{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
Редактировать источник #{{ object.id }}
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
Сохранить
+
Удалить
+ {% endif %}
+
Назад
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_js %}
+{{ block.super }}
+
+{% leaflet_js %}
+{% leaflet_css %}
+
+
+
+{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html
index bf04666..753061f 100644
--- a/dbapp/mainapp/templates/mainapp/source_list.html
+++ b/dbapp/mainapp/templates/mainapp/source_list.html
@@ -12,6 +12,16 @@
top: 0;
z-index: 10;
}
+ .btn-group .badge {
+ position: absolute;
+ top: -5px;
+ right: -5px;
+ font-size: 0.65rem;
+ padding: 0.2em 0.4em;
+ }
+ .btn-group .btn {
+ position: relative;
+ }
{% endblock %}
@@ -55,11 +65,20 @@
+
+
+
+ Карта
+
+
+
Фильтры
+ 0
@@ -185,6 +204,9 @@
+
+
+
ID
@@ -229,12 +251,16 @@
{% endif %}
- Детали
+ Действия
{% for source in processed_sources %}
+
+
+
{{ source.id }}
{{ source.coords_average }}
{{ source.coords_kupsat }}
@@ -244,15 +270,44 @@
{{ source.created_at|date:"d.m.Y H:i" }}
{{ source.updated_at|date:"d.m.Y H:i" }}
-
- Показать
-
+
+ {% if source.objitem_count > 0 %}
+
+
+ {{ source.objitem_count }}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
+
+
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+
{% empty %}
- Нет данных для отображения
+ Нет данных для отображения
{% endfor %}
@@ -285,6 +340,10 @@
+
+
+
+ ID
Имя
Спутник
Частота, МГц
@@ -319,6 +378,55 @@
{% block extra_js %}
{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/source_map.html b/dbapp/mainapp/templates/mainapp/source_map.html
new file mode 100644
index 0000000..6fca671
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/source_map.html
@@ -0,0 +1,189 @@
+{% extends "mainapp/base.html" %}
+{% load static %}
+{% block title %}Карта источников{% endblock title %}
+
+{% block extra_css %}
+
+
+
+
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock content %}
+
+{% block extra_js %}
+
+
+
+
+
+
+{% endblock extra_js %}
diff --git a/dbapp/mainapp/templates/mainapp/source_with_points_map.html b/dbapp/mainapp/templates/mainapp/source_with_points_map.html
new file mode 100644
index 0000000..5c58082
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/source_with_points_map.html
@@ -0,0 +1,244 @@
+{% extends "mainapp/base.html" %}
+{% load static %}
+{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %}
+
+{% block extra_css %}
+
+
+
+
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock content %}
+
+{% block extra_js %}
+
+
+
+
+
+
+{% endblock extra_js %}
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index 902155d..f847111 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -25,9 +25,14 @@ from .views import (
ProcessKubsatView,
ShowMapView,
ShowSelectedObjectsMapView,
+ ShowSourcesMapView,
+ ShowSourceWithPointsMapView,
SourceListView,
+ SourceUpdateView,
+ SourceDeleteView,
SourceObjItemsAPIView,
SigmaParameterDataAPIView,
+ TransponderDataAPIView,
UploadVchLoadView,
custom_logout,
)
@@ -36,6 +41,8 @@ app_name = 'mainapp'
urlpatterns = [
path('', SourceListView.as_view(), name='home'),
+ path('source//edit/', SourceUpdateView.as_view(), name='source_update'),
+ path('source//delete/', SourceDeleteView.as_view(), name='source_delete'),
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
path('actions/', ActionsPageView.as_view(), name='actions'),
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
@@ -45,6 +52,8 @@ urlpatterns = [
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
+ path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
+ path('show-source-with-points-map//', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
@@ -53,6 +62,7 @@ urlpatterns = [
path('api/lyngsat//', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('api/sigma-parameter//', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('api/source//objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
+ path('api/transponder//', TransponderDataAPIView.as_view(), name='transponder_data_api'),
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
path('object//edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py
index 6b829e9..f1b3300 100644
--- a/dbapp/mainapp/views/__init__.py
+++ b/dbapp/mainapp/views/__init__.py
@@ -23,6 +23,7 @@ from .api import (
SigmaParameterDataAPIView,
SourceObjItemsAPIView,
LyngsatTaskStatusAPIView,
+ TransponderDataAPIView,
)
from .lyngsat import (
LinkLyngsatSourcesView,
@@ -30,8 +31,14 @@ from .lyngsat import (
LyngsatTaskStatusView,
ClearLyngsatCacheView,
)
-from .source import SourceListView
-from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
+from .source import SourceListView, SourceUpdateView, SourceDeleteView
+from .map import (
+ ShowMapView,
+ ShowSelectedObjectsMapView,
+ ShowSourcesMapView,
+ ShowSourceWithPointsMapView,
+ ClusterTestView,
+)
__all__ = [
# Base
@@ -58,6 +65,7 @@ __all__ = [
'SigmaParameterDataAPIView',
'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView',
+ 'TransponderDataAPIView',
# LyngSat
'LinkLyngsatSourcesView',
'FillLyngsatDataView',
@@ -65,8 +73,12 @@ __all__ = [
'ClearLyngsatCacheView',
# Source
'SourceListView',
+ 'SourceUpdateView',
+ 'SourceDeleteView',
# Map
'ShowMapView',
'ShowSelectedObjectsMapView',
+ 'ShowSourcesMapView',
+ 'ShowSourceWithPointsMapView',
'ClusterTestView',
]
diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py
index dd54e87..489aa70 100644
--- a/dbapp/mainapp/views/api.py
+++ b/dbapp/mainapp/views/api.py
@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
response_data['status'] = task.state
return JsonResponse(response_data)
+
+
+class TransponderDataAPIView(LoginRequiredMixin, View):
+ """API endpoint for getting Transponder data."""
+
+ def get(self, request, transponder_id):
+ from mapsapp.models import Transponders
+
+ try:
+ transponder = Transponders.objects.select_related(
+ 'sat_id',
+ 'polarization'
+ ).get(id=transponder_id)
+
+ data = {
+ 'id': transponder.id,
+ 'name': transponder.name or '-',
+ 'satellite': transponder.sat_id.name if transponder.sat_id else '-',
+ 'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
+ 'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
+ 'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
+ 'polarization': transponder.polarization.name if transponder.polarization else '-',
+ 'zone_name': transponder.zone_name or '-',
+ 'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
+ }
+
+ return JsonResponse(data)
+ except Transponders.DoesNotExist:
+ return JsonResponse({'error': 'Транспондер не найден'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
diff --git a/dbapp/mainapp/views/map.py b/dbapp/mainapp/views/map.py
index 59f2577..c1a2089 100644
--- a/dbapp/mainapp/views/map.py
+++ b/dbapp/mainapp/views/map.py
@@ -1,6 +1,7 @@
"""
Map related views for displaying objects on maps.
"""
+
from collections import defaultdict
from django.contrib.admin.views.decorators import staff_member_required
@@ -18,7 +19,7 @@ from ..models import ObjItem
@method_decorator(staff_member_required, name="dispatch")
class ShowMapView(RoleRequiredMixin, View):
"""View for displaying objects on map (admin interface)."""
-
+
required_roles = ["admin", "moderator"]
def get(self, request):
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
- param = getattr(obj, 'parameter_obj', None)
+ param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View):
)
else:
return redirect("admin")
-
+
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
@@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View):
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
"""View for displaying selected objects on map."""
-
+
def get(self, request):
ids = request.GET.get("ids", "")
points = []
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
- param = getattr(obj, 'parameter_obj', None)
+ param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
return render(request, "mainapp/objitem_map.html", context)
+class ShowSourcesMapView(LoginRequiredMixin, View):
+ """View for displaying selected sources on map."""
+
+ def get(self, request):
+ from ..models import Source
+
+ ids = request.GET.get("ids", "")
+ groups = []
+
+ if ids:
+ id_list = [int(x) for x in ids.split(",") if x.isdigit()]
+ sources = Source.objects.filter(id__in=id_list)
+
+ # Define coordinate types with their labels and colors
+ coord_types = [
+ ("coords_average", "Усредненные координаты", "blue"),
+ ("coords_kupsat", "Координаты Кубсата", "orange"),
+ ("coords_valid", "Координаты оперативников", "green"),
+ ("coords_reference", "Координаты справочные", "violet"),
+ ]
+
+ # Group points by coordinate type
+ for coord_field, label, color in coord_types:
+ points = []
+ for source in sources:
+ coords = getattr(source, coord_field)
+ if coords:
+ # coords is a Point object with x (longitude) and y (latitude)
+ points.append(
+ {
+ "point": (coords.x, coords.y), # (lon, lat)
+ "source_id": f"Источник #{source.id}",
+ }
+ )
+
+ if points:
+ groups.append(
+ {
+ "name": label,
+ "points": points,
+ "color": color,
+ }
+ )
+ else:
+ return redirect("mainapp:home")
+
+ context = {
+ "groups": groups,
+ }
+ return render(request, "mainapp/source_map.html", context)
+
+
+class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
+ """View for displaying a single source with all its related ObjItem points."""
+
+ def get(self, request, source_id):
+ from ..models import Source
+
+ try:
+ source = Source.objects.prefetch_related(
+ "source_objitems",
+ "source_objitems__parameter_obj",
+ "source_objitems__geo_obj",
+ ).get(id=source_id)
+ except Source.DoesNotExist:
+ return redirect("mainapp:home")
+
+ groups = []
+
+ # Цвета для разных типов координат источника
+ source_coord_types = [
+ ("coords_average", "Усредненные координаты", "blue"),
+ ("coords_kupsat", "Координаты Кубсата", "orange"),
+ ("coords_valid", "Координаты оперативников", "green"),
+ ("coords_reference", "Координаты справочные", "violet"),
+ ]
+
+ # Добавляем координаты источника
+ for coord_field, label, color in source_coord_types:
+ coords = getattr(source, coord_field)
+ if coords:
+ groups.append(
+ {
+ "name": label,
+ "points": [
+ {
+ "point": (coords.x, coords.y),
+ "source_id": f"Источник #{source.id}",
+ }
+ ],
+ "color": color,
+ }
+ )
+
+ # Добавляем все точки ГЛ одной группой
+ gl_points = source.source_objitems.select_related(
+ "parameter_obj", "geo_obj"
+ ).all()
+
+ # Собираем все точки ГЛ в одну группу
+ all_gl_points = []
+ for obj in gl_points:
+ if (
+ not hasattr(obj, "geo_obj")
+ or not obj.geo_obj
+ or not obj.geo_obj.coords
+ ):
+ continue
+ param = getattr(obj, "parameter_obj", None)
+ if not param:
+ continue
+
+ all_gl_points.append(
+ {
+ "point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
+ "name": obj.name,
+ "frequency": f"{param.frequency} [{param.freq_range}] МГц",
+ }
+ )
+
+ # Добавляем все точки ГЛ одним цветом (красный)
+ if all_gl_points:
+ groups.append(
+ {"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
+ )
+
+ context = {
+ "groups": groups,
+ "source_id": source_id,
+ }
+ return render(request, "mainapp/source_with_points_map.html", context)
+
+
class ClusterTestView(LoginRequiredMixin, View):
"""Test view for clustering functionality."""
-
+
def get(self, request):
objs = ObjItem.objects.filter(
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"
diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py
index f3737fa..31d0e86 100644
--- a/dbapp/mainapp/views/source.py
+++ b/dbapp/mainapp/views/source.py
@@ -3,12 +3,15 @@ Source related views.
"""
from datetime import datetime
-from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.paginator import Paginator
from django.db.models import Count
-from django.shortcuts import render
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
from django.views import View
+from ..forms import SourceForm
from ..models import Source
from ..utils import parse_pagination_params
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
# Get pagination parameters
page_number, items_per_page = parse_pagination_params(request)
- # Get sorting parameters
- sort_param = request.GET.get("sort", "-created_at")
+ # Get sorting parameters (default to ID ascending)
+ sort_param = request.GET.get("sort", "id")
# Get filter parameters
search_query = request.GET.get("search", "").strip()
@@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
}
return render(request, "mainapp/source_list.html", context)
+
+
+
+class AdminModeratorMixin(UserPassesTestMixin):
+ """Mixin to restrict access to admin and moderator roles only."""
+
+ def test_func(self):
+ return (
+ self.request.user.is_authenticated and
+ hasattr(self.request.user, 'customuser') and
+ self.request.user.customuser.role in ['admin', 'moderator']
+ )
+
+ def handle_no_permission(self):
+ messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
+ return redirect('mainapp:home')
+
+
+class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
+ """View for editing Source with 4 coordinate fields and related ObjItems."""
+
+ def get(self, request, pk):
+ source = get_object_or_404(Source, pk=pk)
+ form = SourceForm(instance=source)
+
+ # Get related ObjItems ordered by creation date
+ objitems = source.source_objitems.select_related(
+ 'parameter_obj',
+ 'parameter_obj__id_satellite',
+ 'parameter_obj__polarization',
+ 'parameter_obj__modulation',
+ 'parameter_obj__standard',
+ 'geo_obj',
+ 'created_by__user',
+ 'updated_by__user'
+ ).order_by('created_at')
+
+ context = {
+ 'object': source,
+ 'form': form,
+ 'objitems': objitems,
+ 'full_width_page': True,
+ }
+
+ return render(request, 'mainapp/source_form.html', context)
+
+ def post(self, request, pk):
+ source = get_object_or_404(Source, pk=pk)
+ form = SourceForm(request.POST, instance=source)
+
+ if form.is_valid():
+ source = form.save(commit=False)
+ # Set updated_by to current user
+ if hasattr(request.user, 'customuser'):
+ source.updated_by = request.user.customuser
+ source.save()
+
+ messages.success(request, f'Источник #{source.id} успешно обновлен.')
+
+ # Redirect back with query params if present
+ if request.GET.urlencode():
+ return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
+ return redirect('mainapp:source_update', pk=source.id)
+
+ # If form is invalid, re-render with errors
+ objitems = source.source_objitems.select_related(
+ 'parameter_obj',
+ 'parameter_obj__id_satellite',
+ 'parameter_obj__polarization',
+ 'parameter_obj__modulation',
+ 'parameter_obj__standard',
+ 'geo_obj',
+ 'created_by__user',
+ 'updated_by__user'
+ ).order_by('created_at')
+
+ context = {
+ 'object': source,
+ 'form': form,
+ 'objitems': objitems,
+ 'full_width_page': True,
+ }
+
+ return render(request, 'mainapp/source_form.html', context)
+
+
+class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
+ """View for deleting Source."""
+
+ def get(self, request, pk):
+ source = get_object_or_404(Source, pk=pk)
+
+ context = {
+ 'object': source,
+ 'objitems_count': source.source_objitems.count(),
+ }
+
+ return render(request, 'mainapp/source_confirm_delete.html', context)
+
+ def post(self, request, pk):
+ source = get_object_or_404(Source, pk=pk)
+ source_id = source.id
+
+ try:
+ source.delete()
+ messages.success(request, f'Источник #{source_id} успешно удален.')
+ except Exception as e:
+ messages.error(request, f'Ошибка при удалении источника: {str(e)}')
+ return redirect('mainapp:source_update', pk=pk)
+
+ # Redirect to source list
+ if request.GET.urlencode():
+ return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
+ return redirect('mainapp:home')
diff --git a/dbapp/mapsapp/models.py b/dbapp/mapsapp/models.py
index d0c44f9..aa241c9 100644
--- a/dbapp/mapsapp/models.py
+++ b/dbapp/mapsapp/models.py
@@ -12,54 +12,54 @@ from mainapp.models import Polarization, Satellite, get_default_polarization, Cu
class Transponders(models.Model):
"""
Модель транспондера спутника.
-
+
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
"""
-
+
# Основные поля
name = models.CharField(
- max_length=30,
- null=True,
- blank=True,
+ max_length=30,
+ null=True,
+ blank=True,
verbose_name="Название транспондера",
db_index=True,
- help_text="Название транспондера"
+ help_text="Название транспондера",
)
downlink = models.FloatField(
- blank=True,
- null=True,
+ blank=True,
+ null=True,
verbose_name="Downlink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота downlink в МГц (0-50000)"
)
frequency_range = models.FloatField(
- blank=True,
- null=True,
+ blank=True,
+ null=True,
verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
# help_text="Полоса частот в МГц (0-1000)"
)
uplink = models.FloatField(
- blank=True,
- null=True,
+ blank=True,
+ null=True,
verbose_name="Uplink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота uplink в МГц (0-50000)"
)
zone_name = models.CharField(
- max_length=255,
- blank=True,
- null=True,
+ max_length=255,
+ blank=True,
+ null=True,
verbose_name="Название зоны",
db_index=True,
- help_text="Название зоны покрытия транспондера"
+ help_text="Название зоны покрытия транспондера",
)
snr = models.FloatField(
- blank=True,
- null=True,
+ blank=True,
+ null=True,
verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
- help_text="Полоса частот в МГц (0-1000)"
+ help_text="Полоса частот в МГц (0-1000)",
)
created_at = models.DateTimeField(
auto_now_add=True,
@@ -89,44 +89,43 @@ class Transponders(models.Model):
verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись",
)
-
+
# Связи
polarization = models.ForeignKey(
- Polarization,
- default=get_default_polarization,
- on_delete=models.SET_DEFAULT,
- related_name="tran_polarizations",
- null=True,
- blank=True,
+ Polarization,
+ default=get_default_polarization,
+ on_delete=models.SET_DEFAULT,
+ related_name="tran_polarizations",
+ null=True,
+ blank=True,
verbose_name="Поляризация",
- help_text="Поляризация сигнала"
+ help_text="Поляризация сигнала",
)
sat_id = models.ForeignKey(
- Satellite,
- on_delete=models.PROTECT,
- related_name="tran_satellite",
+ Satellite,
+ on_delete=models.PROTECT,
+ related_name="tran_satellite",
verbose_name="Спутник",
db_index=True,
- help_text="Спутник, которому принадлежит транспондер"
+ help_text="Спутник, которому принадлежит транспондер",
)
-
+
# Вычисляемые поля
transfer = models.GeneratedField(
expression=ExpressionWrapper(
- Abs(F('downlink') - F('uplink')),
- output_field=models.FloatField()
+ Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
- null=True,
- blank=True,
- verbose_name="Перенос"
+ null=True,
+ blank=True,
+ verbose_name="Перенос",
)
# def clean(self):
# """Валидация на уровне модели"""
# super().clean()
-
+
# # Проверка что downlink и uplink заданы
# if self.downlink and self.uplink:
# # Обычно uplink выше downlink для спутниковой связи
@@ -139,14 +138,12 @@ class Transponders(models.Model):
if self.name:
return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
-
+
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"
- ordering = ['sat_id', 'downlink']
+ ordering = ["sat_id", "downlink"]
indexes = [
- models.Index(fields=['sat_id', 'downlink']),
- models.Index(fields=['sat_id', 'zone_name']),
+ models.Index(fields=["sat_id", "downlink"]),
+ models.Index(fields=["sat_id", "zone_name"]),
]
-
-
diff --git a/dbapp/mapsapp/templates/mapsapp/map2d.html b/dbapp/mapsapp/templates/mapsapp/map2d.html
index 76054de..84c56ca 100644
--- a/dbapp/mapsapp/templates/mapsapp/map2d.html
+++ b/dbapp/mapsapp/templates/mapsapp/map2d.html
@@ -2,560 +2,565 @@
{% load static %}
{% block content %}
-
-
Объекты из базы
-
- — Выберите объект —
- {% for sat in sats %}
- {{ sat.name }}
- {% endfor %}
-
-
Все точки
-
Точки транспондеров
-
Очистить маркеры
+
+
Объекты из базы
+
+ — Выберите объект —
+ {% for sat in sats %}
+ {{ sat.name }}
+ {% endfor %}
+
+
Все
+ точки
+
Точки
+ транспондеров
+
Очистить маркеры
-
+
-
{% endblock content %}
{% block extra_js %}
{% endblock extra_js %}
\ No newline at end of file