Добавил транспондеры к ObjItem шаблону

This commit is contained in:
2025-11-14 08:00:23 +03:00
parent 5ab6770809
commit 6a26991dc0
18 changed files with 3286 additions and 1188 deletions

View File

@@ -32,13 +32,13 @@ from .models import (
ObjItem, ObjItem,
CustomUser, CustomUser,
Band, Band,
Source Source,
) )
from .filters import ( from .filters import (
GeoKupDistanceFilter, GeoKupDistanceFilter,
GeoValidDistanceFilter, GeoValidDistanceFilter,
UniqueToggleFilter, UniqueToggleFilter,
HasSigmaParameterFilter HasSigmaParameterFilter,
) )
@@ -55,22 +55,24 @@ admin.site.unregister(Group)
# Base Admin Classes # Base Admin Classes
# ============================================================================ # ============================================================================
class BaseAdmin(admin.ModelAdmin): class BaseAdmin(admin.ModelAdmin):
""" """
Базовый класс для всех admin моделей. Базовый класс для всех admin моделей.
Предоставляет общую функциональность: Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу - Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице - Настройка количества элементов на странице
- Автоматическое заполнение полей created_by и updated_by - Автоматическое заполнение полей created_by и updated_by
""" """
save_on_top = True save_on_top = True
list_per_page = 50 list_per_page = 50
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
""" """
Автоматически заполняет поля created_by и updated_by при сохранении. Автоматически заполняет поля created_by и updated_by при сохранении.
Args: Args:
request: HTTP запрос request: HTTP запрос
obj: Сохраняемый объект модели obj: Сохраняемый объект модели
@@ -79,20 +81,20 @@ class BaseAdmin(admin.ModelAdmin):
""" """
if not change: if not change:
# При создании нового объекта устанавливаем created_by # При создании нового объекта устанавливаем created_by
if hasattr(obj, 'created_by') and not obj.created_by_id: if hasattr(obj, "created_by") and not obj.created_by_id:
obj.created_by = getattr(request.user, 'customuser', None) obj.created_by = getattr(request.user, "customuser", None)
# При любом сохранении обновляем updated_by # При любом сохранении обновляем updated_by
if hasattr(obj, 'updated_by'): if hasattr(obj, "updated_by"):
obj.updated_by = getattr(request.user, 'customuser', None) obj.updated_by = getattr(request.user, "customuser", None)
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
class CustomUserInline(admin.StackedInline): class CustomUserInline(admin.StackedInline):
model = CustomUser model = CustomUser
can_delete = False can_delete = False
verbose_name_plural = 'Дополнительная информация пользователя' verbose_name_plural = "Дополнительная информация пользователя"
class LocationForm(forms.ModelForm): class LocationForm(forms.ModelForm):
@@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm):
class Meta: class Meta:
model = Geo model = Geo
fields = '__all__' fields = "__all__"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.instance and self.instance.coords: if self.instance and self.instance.coords:
self.fields['latitude_geo'].initial = self.instance.coords[1] self.fields["latitude_geo"].initial = self.instance.coords[1]
self.fields['longitude_geo'].initial = self.instance.coords[0] self.fields["longitude_geo"].initial = self.instance.coords[0]
# if self.instance and self.instance.coords_kupsat: # if self.instance and self.instance.coords_kupsat:
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1] # self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0] # self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
@@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=False) instance = super().save(commit=False)
from django.contrib.gis.geos import Point 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: if lat is not None and lon is not None:
instance.coords = Point(lon, lat, srid=4326) instance.coords = Point(lon, lat, srid=4326)
@@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline):
form = LocationForm form = LocationForm
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
prefetch_related = ("mirrors",) prefetch_related = ("mirrors",)
autocomplete_fields = ('mirrors',) autocomplete_fields = ("mirrors",)
fieldsets = ( fieldsets = (
("Основная информация", { (
"fields": ("mirrors", "location", "Основная информация",
# "distance_coords_kup", {
# "distance_coords_valid", "fields": (
# "distance_kup_valid", "mirrors",
"timestamp", "comment",) "location",
}), # "distance_coords_kup",
("Координаты: геолокация", { # "distance_coords_valid",
"fields": ("longitude_geo", "latitude_geo", "coords"), # "distance_kup_valid",
}), "timestamp",
"comment",
)
},
),
(
"Координаты: геолокация",
{
"fields": ("longitude_geo", "latitude_geo", "coords"),
},
),
# ("Координаты: Кубсат", { # ("Координаты: Кубсат", {
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"), # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
# }), # }),
@@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline):
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
inlines = [CustomUserInline] inlines = [CustomUserInline]
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
@@ -181,80 +195,83 @@ admin.site.register(User, UserAdmin)
# Custom Admin Actions # Custom Admin Actions
# ============================================================================ # ============================================================================
@admin.action(description="Показать выбранные на карте") @admin.action(description="Показать выбранные на карте")
def show_on_map(modeladmin, request, queryset): def show_on_map(modeladmin, request, queryset):
""" """
Action для отображения выбранных Geo объектов на карте. Action для отображения выбранных Geo объектов на карте.
Оптимизирован для работы с большим количеством объектов: Оптимизирован для работы с большим количеством объектов:
использует values_list для получения только ID. использует values_list для получения только ID.
""" """
selected_ids = queryset.values_list('id', flat=True) selected_ids = queryset.values_list("id", flat=True)
ids_str = ','.join(str(pk) for pk in selected_ids) ids_str = ",".join(str(pk) for pk in selected_ids)
return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}') return redirect(reverse("mainapp:admin_show_map") + f"?ids={ids_str}")
@admin.action(description="Показать выбранные объекты на карте") @admin.action(description="Показать выбранные объекты на карте")
def show_selected_on_map(modeladmin, request, queryset): def show_selected_on_map(modeladmin, request, queryset):
""" """
Action для отображения выбранных ObjItem объектов на карте. Action для отображения выбранных ObjItem объектов на карте.
Оптимизирован для работы с большим количеством объектов: Оптимизирован для работы с большим количеством объектов:
использует values_list для получения только ID. использует values_list для получения только ID.
""" """
selected_ids = queryset.values_list('id', flat=True) selected_ids = queryset.values_list("id", flat=True)
ids_str = ','.join(str(pk) for pk in selected_ids) ids_str = ",".join(str(pk) for pk in selected_ids)
return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}') return redirect(reverse("mainapp:show_selected_objects_map") + f"?ids={ids_str}")
@admin.action(description="Экспортировать выбранные объекты в CSV") @admin.action(description="Экспортировать выбранные объекты в CSV")
def export_objects_to_csv(modeladmin, request, queryset): def export_objects_to_csv(modeladmin, request, queryset):
""" """
Action для экспорта выбранных ObjItem объектов в CSV формат. Action для экспорта выбранных ObjItem объектов в CSV формат.
Оптимизирован с использованием select_related и prefetch_related Оптимизирован с использованием select_related и prefetch_related
для минимизации количества запросов к БД. для минимизации количества запросов к БД.
""" """
import csv import csv
from django.http import HttpResponse from django.http import HttpResponse
# Оптимизируем queryset # Оптимизируем queryset
queryset = queryset.select_related( queryset = queryset.select_related(
'geo_obj', "geo_obj",
'created_by__user', "created_by__user",
'updated_by__user', "updated_by__user",
'parameter_obj', "parameter_obj",
'parameter_obj__id_satellite', "parameter_obj__id_satellite",
'parameter_obj__polarization', "parameter_obj__polarization",
'parameter_obj__modulation' "parameter_obj__modulation",
) )
response = HttpResponse(content_type='text/csv; charset=utf-8') response = HttpResponse(content_type="text/csv; charset=utf-8")
response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"' response["Content-Disposition"] = 'attachment; filename="objitems_export.csv"'
response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel response.write("\ufeff") # UTF-8 BOM для корректного отображения в Excel
writer = csv.writer(response) writer = csv.writer(response)
writer.writerow([ writer.writerow(
'Название', [
'Спутник', "Название",
'Частота (МГц)', "Спутник",
'Полоса (МГц)', "Частота (МГц)",
'Поляризация', "Полоса (МГц)",
'Модуляция', "Поляризация",
'ОСШ', "Модуляция",
'Координаты геолокации', "ОСШ",
'Координаты Кубсата', "Координаты геолокации",
'Координаты оперативного отдела', "Координаты Кубсата",
'Расстояние Гео-Куб (км)', "Координаты оперативного отдела",
'Расстояние Гео-Опер (км)', "Расстояние Гео-Куб (км)",
'Дата создания', "Расстояние Гео-Опер (км)",
'Дата обновления' "Дата создания",
]) "Дата обновления",
]
)
for obj in queryset: for obj in queryset:
param = getattr(obj, 'parameter_obj', None) param = getattr(obj, "parameter_obj", None)
geo = obj.geo_obj geo = obj.geo_obj
# Форматирование координат # Форматирование координат
def format_coords(coords): def format_coords(coords):
if not 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" 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" lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
return f"{lat_str} {lon_str}" return f"{lat_str} {lon_str}"
writer.writerow([ writer.writerow(
obj.name, [
param.id_satellite.name if param and param.id_satellite else "-", obj.name,
param.frequency if param else "-", param.id_satellite.name if param and param.id_satellite else "-",
param.freq_range if param else "-", param.frequency if param else "-",
param.polarization.name if param and param.polarization else "-", param.freq_range if param else "-",
param.modulation.name if param and param.modulation else "-", param.polarization.name if param and param.polarization else "-",
param.snr if param else "-", param.modulation.name if param and param.modulation else "-",
format_coords(geo) if geo and geo.coords else "-", param.snr if param else "-",
format_coords(geo) if geo and geo.coords_kupsat else "-", format_coords(geo) if geo and geo.coords else "-",
format_coords(geo) if geo and geo.coords_valid else "-", format_coords(geo) if geo and geo.coords_kupsat else "-",
round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-", format_coords(geo) if geo and geo.coords_valid else "-",
round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-", round(geo.distance_coords_kup, 3)
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-", if geo and geo.distance_coords_kup
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-" 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 return response
@@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset):
# Inline Admin Classes # Inline Admin Classes
# ============================================================================ # ============================================================================
class ParameterInline(admin.StackedInline): class ParameterInline(admin.StackedInline):
"""Inline для редактирования параметра объекта.""" """Inline для редактирования параметра объекта."""
model = Parameter model = Parameter
extra = 0 extra = 0
max_num = 1 max_num = 1
@@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline):
verbose_name = "ВЧ загрузка" verbose_name = "ВЧ загрузка"
verbose_name_plural = "ВЧ загрузка" verbose_name_plural = "ВЧ загрузка"
fields = ( fields = (
'id_satellite', "id_satellite",
'frequency', "frequency",
'freq_range', "freq_range",
'polarization', "polarization",
'modulation', "modulation",
'bod_velocity', "bod_velocity",
'snr', "snr",
'standard' "standard",
) )
autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard') autocomplete_fields = ("id_satellite", "polarization", "modulation", "standard")
# ============================================================================ # ============================================================================
# Admin Classes # Admin Classes
# ============================================================================ # ============================================================================
@admin.register(SigmaParMark) @admin.register(SigmaParMark)
class SigmaParMarkAdmin(BaseAdmin): class SigmaParMarkAdmin(BaseAdmin):
"""Админ-панель для модели SigmaParMark.""" """Админ-панель для модели SigmaParMark."""
list_display = ("mark", "timestamp") list_display = ("mark", "timestamp")
search_fields = ("mark",) search_fields = ("mark",)
ordering = ("-timestamp",) ordering = ("-timestamp",)
list_filter = ( list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
("timestamp", DateRangeQuickSelectListFilterBuilder()),
)
@admin.register(Polarization) @admin.register(Polarization)
class PolarizationAdmin(BaseAdmin): class PolarizationAdmin(BaseAdmin):
"""Админ-панель для модели Polarization.""" """Админ-панель для модели Polarization."""
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin):
@admin.register(Modulation) @admin.register(Modulation)
class ModulationAdmin(BaseAdmin): class ModulationAdmin(BaseAdmin):
"""Админ-панель для модели Modulation.""" """Админ-панель для модели Modulation."""
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin):
@admin.register(Standard) @admin.register(Standard)
class StandardAdmin(BaseAdmin): class StandardAdmin(BaseAdmin):
"""Админ-панель для модели Standard.""" """Админ-панель для модели Standard."""
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin):
class SigmaParameterInline(admin.StackedInline): class SigmaParameterInline(admin.StackedInline):
model = SigmaParameter model = SigmaParameter
extra = 0 extra = 0
autocomplete_fields = ['mark'] autocomplete_fields = ["mark"]
readonly_fields = ( readonly_fields = (
"datetime_begin", "datetime_begin",
"datetime_end", "datetime_end",
) )
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
return False return False
@@ -364,12 +393,13 @@ class SigmaParameterInline(admin.StackedInline):
class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
""" """
Админ-панель для модели Parameter. Админ-панель для модели Parameter.
Оптимизирована для работы с большим количеством параметров: Оптимизирована для работы с большим количеством параметров:
- Использует select_related для оптимизации запросов - Использует select_related для оптимизации запросов
- Предоставляет фильтры по основным характеристикам - Предоставляет фильтры по основным характеристикам
- Поддерживает импорт/экспорт данных - Поддерживает импорт/экспорт данных
""" """
list_display = ( list_display = (
"id_satellite", "id_satellite",
"frequency", "frequency",
@@ -380,11 +410,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"snr", "snr",
"standard", "standard",
"related_objitem", "related_objitem",
"sigma_parameter" "sigma_parameter",
) )
list_display_links = ("frequency", "id_satellite") 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 = ( list_filter = (
HasSigmaParameterFilter, HasSigmaParameterFilter,
("objitem", MultiSelectRelatedDropdownFilter), ("objitem", MultiSelectRelatedDropdownFilter),
@@ -396,7 +432,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
("freq_range", NumericRangeFilterBuilder()), ("freq_range", NumericRangeFilterBuilder()),
("snr", NumericRangeFilterBuilder()), ("snr", NumericRangeFilterBuilder()),
) )
search_fields = ( search_fields = (
"id_satellite__name", "id_satellite__name",
"frequency", "frequency",
@@ -408,16 +444,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"standard__name", "standard__name",
"objitem__name", "objitem__name",
) )
ordering = ("-frequency",) ordering = ("-frequency",)
autocomplete_fields = ("objitem",) autocomplete_fields = ("objitem",)
inlines = [SigmaParameterInline] inlines = [SigmaParameterInline]
def related_objitem(self, obj): def related_objitem(self, obj):
"""Отображает связанный ObjItem.""" """Отображает связанный ObjItem."""
if hasattr(obj, 'objitem') and obj.objitem: if hasattr(obj, "objitem") and obj.objitem:
return obj.objitem.name return obj.objitem.name
return "-" return "-"
related_objitem.short_description = "Объект" related_objitem.short_description = "Объект"
related_objitem.admin_order_field = "objitem__name" related_objitem.admin_order_field = "objitem__name"
@@ -427,19 +464,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
if sigma_obj: if sigma_obj:
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}" return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
return "-" return "-"
sigma_parameter.short_description = "ВЧ sigma" sigma_parameter.short_description = "ВЧ sigma"
@admin.register(SigmaParameter) @admin.register(SigmaParameter)
class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
""" """
Админ-панель для модели SigmaParameter. Админ-панель для модели SigmaParameter.
Оптимизирована для работы с параметрами Sigma: Оптимизирована для работы с параметрами Sigma:
- Использует select_related и prefetch_related для оптимизации - Использует select_related и prefetch_related для оптимизации
- Предоставляет фильтры по основным характеристикам - Предоставляет фильтры по основным характеристикам
- Поддерживает импорт/экспорт данных - Поддерживает импорт/экспорт данных
""" """
list_display = ( list_display = (
"id_satellite", "id_satellite",
"frequency", "frequency",
@@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"datetime_end", "datetime_end",
) )
list_display_links = ("id_satellite",) list_display_links = ("id_satellite",)
list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization") list_select_related = (
"modulation",
readonly_fields = ( "standard",
"datetime_begin", "id_satellite",
"datetime_end", "parameter",
"transfer_frequency" "polarization",
) )
readonly_fields = ("datetime_begin", "datetime_end", "transfer_frequency")
list_filter = ( list_filter = (
("id_satellite__name", MultiSelectDropdownFilter), ("id_satellite__name", MultiSelectDropdownFilter),
("modulation__name", MultiSelectDropdownFilter), ("modulation__name", MultiSelectDropdownFilter),
@@ -472,7 +513,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
("datetime_begin", DateRangeQuickSelectListFilterBuilder()), ("datetime_begin", DateRangeQuickSelectListFilterBuilder()),
("datetime_end", DateRangeQuickSelectListFilterBuilder()), ("datetime_end", DateRangeQuickSelectListFilterBuilder()),
) )
search_fields = ( search_fields = (
"id_satellite__name", "id_satellite__name",
"frequency", "frequency",
@@ -484,17 +525,25 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
) )
autocomplete_fields = ("mark",) autocomplete_fields = ("mark",)
ordering = ("-frequency",) ordering = ("-frequency",)
def get_queryset(self, request): def get_queryset(self, request):
"""Оптимизированный queryset с prefetch_related для mark.""" """Оптимизированный queryset с prefetch_related для mark."""
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.prefetch_related("mark") return qs.prefetch_related("mark")
@admin.register(Satellite) @admin.register(Satellite)
class SatelliteAdmin(BaseAdmin): class SatelliteAdmin(BaseAdmin):
"""Админ-панель для модели Satellite.""" """Админ-панель для модели 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") search_fields = ("name", "norad")
ordering = ("name",) ordering = ("name",)
filter_horizontal = ("band",) filter_horizontal = ("band",)
@@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin):
@admin.register(Mirror) @admin.register(Mirror)
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" """Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@@ -514,28 +564,37 @@ class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
""" """
Админ-панель для модели Geo с поддержкой карты Leaflet. Админ-панель для модели Geo с поддержкой карты Leaflet.
Оптимизирована для работы с геоданными: Оптимизирована для работы с геоданными:
- Использует prefetch_related для оптимизации запросов к mirrors - Использует prefetch_related для оптимизации запросов к mirrors
- Предоставляет фильтры по зеркалам, локации и дате - Предоставляет фильтры по зеркалам, локации и дате
- Поддерживает импорт/экспорт данных - Поддерживает импорт/экспорт данных
- Интегрирована с Leaflet для отображения на карте - Интегрирована с Leaflet для отображения на карте
""" """
form = LocationForm form = LocationForm
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
fieldsets = ( fieldsets = (
("Основная информация", { (
"fields": ("mirrors", "location", "Основная информация",
{
"fields": (
"mirrors",
"location",
# "distance_coords_kup", # "distance_coords_kup",
# "distance_coords_valid", # "distance_coords_valid",
# "distance_kup_valid", # "distance_kup_valid",
"timestamp", "comment", "transponder") "timestamp",
}), "comment",
("Координаты: геолокация", { )
"fields": ("longitude_geo", "latitude_geo", "coords") },
}), ),
(
"Координаты: геолокация",
{"fields": ("longitude_geo", "latitude_geo", "coords")},
),
# ("Координаты: Кубсат", { # ("Координаты: Кубсат", {
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat") # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
# }), # }),
@@ -543,7 +602,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
# "fields": ("longitude_valid", "latitude_valid", "coords_valid") # "fields": ("longitude_valid", "latitude_valid", "coords_valid")
# }), # }),
) )
list_display = ( list_display = (
"formatted_timestamp", "formatted_timestamp",
"location", "location",
@@ -554,38 +613,39 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
"is_average", "is_average",
) )
list_display_links = ("formatted_timestamp",) list_display_links = ("formatted_timestamp",)
list_filter = ( list_filter = (
("mirrors", MultiSelectRelatedDropdownFilter), ("mirrors", MultiSelectRelatedDropdownFilter),
("transponder", MultiSelectRelatedDropdownFilter),
"is_average", "is_average",
("location", MultiSelectDropdownFilter), ("location", MultiSelectDropdownFilter),
("timestamp", DateRangeQuickSelectListFilterBuilder()), ("timestamp", DateRangeQuickSelectListFilterBuilder()),
) )
search_fields = ( search_fields = (
"mirrors__name", "mirrors__name",
"location", "location",
"transponder__name",
) )
autocomplete_fields = ("mirrors", ) autocomplete_fields = ("mirrors",)
ordering = ("-timestamp",) ordering = ("-timestamp",)
actions = [show_on_map] actions = [show_on_map]
settings_overrides = { settings_overrides = {
'DEFAULT_CENTER': (55.7558, 37.6173), "DEFAULT_CENTER": (55.7558, 37.6173),
'DEFAULT_ZOOM': 12, "DEFAULT_ZOOM": 12,
} }
def get_queryset(self, request): def get_queryset(self, request):
"""Оптимизированный queryset с prefetch_related для mirrors.""" """Оптимизированный queryset с prefetch_related для mirrors."""
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.prefetch_related("mirrors", "transponder") return qs.prefetch_related(
"mirrors",
)
def mirrors_names(self, obj): def mirrors_names(self, obj):
"""Отображает список зеркал через запятую.""" """Отображает список зеркал через запятую."""
return ", ".join(m.name for m in obj.mirrors.all()) return ", ".join(m.name for m in obj.mirrors.all())
mirrors_names.short_description = "Зеркала" mirrors_names.short_description = "Зеркала"
def formatted_timestamp(self, obj): def formatted_timestamp(self, obj):
@@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
return "" return ""
local_time = timezone.localtime(obj.timestamp) local_time = timezone.localtime(obj.timestamp)
return local_time.strftime("%d.%m.%Y %H:%M:%S") return local_time.strftime("%d.%m.%Y %H:%M:%S")
formatted_timestamp.short_description = "Дата и время" formatted_timestamp.short_description = "Дата и время"
formatted_timestamp.admin_order_field = "timestamp" 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" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
geo_coords.short_description = "Координаты геолокации" geo_coords.short_description = "Координаты геолокации"
# def kupsat_coords(self, obj): # def kupsat_coords(self, obj):
@@ -631,19 +693,18 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
# valid_coords.short_description = "Координаты оперативного отдела" # valid_coords.short_description = "Координаты оперативного отдела"
@admin.register(ObjItem) @admin.register(ObjItem)
class ObjItemAdmin(BaseAdmin): class ObjItemAdmin(BaseAdmin):
""" """
Админ-панель для модели ObjItem. Админ-панель для модели ObjItem.
Оптимизирована для работы с большим количеством объектов: Оптимизирована для работы с большим количеством объектов:
- Использует select_related и prefetch_related для оптимизации запросов - Использует select_related и prefetch_related для оптимизации запросов
- Предоставляет фильтры по основным параметрам - Предоставляет фильтры по основным параметрам
- Поддерживает поиск по имени, координатам и частоте - Поддерживает поиск по имени, координатам и частоте
- Включает кастомные actions для отображения на карте - Включает кастомные actions для отображения на карте
""" """
list_display = ( list_display = (
"name", "name",
"sat_name", "sat_name",
@@ -664,16 +725,16 @@ class ObjItemAdmin(BaseAdmin):
) )
list_display_links = ("name",) list_display_links = ("name",)
list_select_related = ( list_select_related = (
"geo_obj", "geo_obj",
"created_by__user", "created_by__user",
"updated_by__user", "updated_by__user",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
"parameter_obj__modulation", "parameter_obj__modulation",
"parameter_obj__standard" "parameter_obj__standard",
) )
list_filter = ( list_filter = (
UniqueToggleFilter, UniqueToggleFilter,
("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter), ("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter),
@@ -687,33 +748,34 @@ class ObjItemAdmin(BaseAdmin):
("created_at", DateRangeQuickSelectListFilterBuilder()), ("created_at", DateRangeQuickSelectListFilterBuilder()),
("updated_at", DateRangeQuickSelectListFilterBuilder()), ("updated_at", DateRangeQuickSelectListFilterBuilder()),
) )
search_fields = ( search_fields = (
"name", "name",
"geo_obj__location", "geo_obj__location",
"parameter_obj__frequency", "parameter_obj__frequency",
"parameter_obj__id_satellite__name", "parameter_obj__id_satellite__name",
) )
ordering = ("-updated_at",) ordering = ("-updated_at",)
inlines = [GeoInline, ParameterInline] inlines = [GeoInline, ParameterInline]
actions = [show_selected_on_map, export_objects_to_csv] actions = [show_selected_on_map, export_objects_to_csv]
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
fieldsets = ( fieldsets = (
("Основная информация", { ("Основная информация", {"fields": ("name",)}),
"fields": ("name",) (
}), "Метаданные",
("Метаданные", { {
"fields": ("created_at", "created_by", "updated_at", "updated_by"), "fields": ("created_at", "created_by", "updated_at", "updated_by"),
"classes": ("collapse",) "classes": ("collapse",),
}), },
),
) )
def get_queryset(self, request): def get_queryset(self, request):
""" """
Оптимизированный queryset с использованием select_related. Оптимизированный queryset с использованием select_related.
Загружает связанные объекты одним запросом для улучшения производительности. Загружает связанные объекты одним запросом для улучшения производительности.
""" """
qs = super().get_queryset(request) qs = super().get_queryset(request)
@@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin):
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
"parameter_obj__modulation", "parameter_obj__modulation",
"parameter_obj__standard" "parameter_obj__standard",
) )
def sat_name(self, obj): 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: if obj.parameter_obj.id_satellite:
return obj.parameter_obj.id_satellite.name return obj.parameter_obj.id_satellite.name
return "-" return "-"
sat_name.short_description = "Спутник" sat_name.short_description = "Спутник"
sat_name.admin_order_field = "parameter_obj__id_satellite__name" sat_name.admin_order_field = "parameter_obj__id_satellite__name"
def freq(self, obj): 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 obj.parameter_obj.frequency
return "-" return "-"
freq.short_description = "Частота, МГц" freq.short_description = "Частота, МГц"
freq.admin_order_field = "parameter_obj__frequency" freq.admin_order_field = "parameter_obj__frequency"
@@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin):
def pol(self, obj): 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: if obj.parameter_obj.polarization:
return obj.parameter_obj.polarization.name return obj.parameter_obj.polarization.name
return "-" return "-"
pol.short_description = "Поляризация" pol.short_description = "Поляризация"
def freq_range(self, obj): 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 obj.parameter_obj.freq_range
return "-" return "-"
freq_range.short_description = "Полоса, МГц" freq_range.short_description = "Полоса, МГц"
freq_range.admin_order_field = "parameter_obj__freq_range" freq_range.admin_order_field = "parameter_obj__freq_range"
def bod_velocity(self, obj): 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 obj.parameter_obj.bod_velocity
return "-" return "-"
bod_velocity.short_description = "Сим. v, БОД" bod_velocity.short_description = "Сим. v, БОД"
def modulation(self, obj): 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: if obj.parameter_obj.modulation:
return obj.parameter_obj.modulation.name return obj.parameter_obj.modulation.name
return "-" return "-"
modulation.short_description = "Модуляция" modulation.short_description = "Модуляция"
def snr(self, obj): 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 obj.parameter_obj.snr
return "-" return "-"
snr.short_description = "ОСШ" snr.short_description = "ОСШ"
def geo_coords(self, obj): def geo_coords(self, obj):
@@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin):
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
geo_coords.short_description = "Координаты геолокации" geo_coords.short_description = "Координаты геолокации"
geo_coords.admin_order_field = "geo_obj__coords" 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" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
kupsat_coords.short_description = "Координаты Кубсата" kupsat_coords.short_description = "Координаты Кубсата"
def valid_coords(self, obj): def valid_coords(self, obj):
@@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin):
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
valid_coords.short_description = "Координаты оперативного отдела" valid_coords.short_description = "Координаты оперативного отдела"
@admin.register(Band) @admin.register(Band)
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin): class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""Админ-панель для модели Band.""" """Админ-панель для модели Band."""
list_display = ("name", "border_start", "border_end") list_display = ("name", "border_start", "border_end")
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@@ -855,29 +928,44 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
class ObjItemInline(admin.TabularInline): class ObjItemInline(admin.TabularInline):
"""Inline для отображения объектов ObjItem в Source.""" """Inline для отображения объектов ObjItem в Source."""
model = ObjItem model = ObjItem
fk_name = "source" fk_name = "source"
extra = 0 extra = 0
can_delete = False can_delete = False
verbose_name = "Объект" verbose_name = "Объект"
verbose_name_plural = "Объекты" verbose_name_plural = "Объекты"
fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at") fields = (
readonly_fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at") "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): def get_queryset(self, request):
"""Оптимизированный queryset с предзагрузкой связанных объектов.""" """Оптимизированный queryset с предзагрузкой связанных объектов."""
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.select_related( return qs.select_related(
'geo_obj', "geo_obj",
'parameter_obj', "parameter_obj",
'parameter_obj__id_satellite', "parameter_obj__id_satellite",
'parameter_obj__polarization' "parameter_obj__polarization",
) )
def get_geo_coords(self, obj): def get_geo_coords(self, obj):
"""Отображает координаты из связанной модели Geo.""" """Отображает координаты из связанной модели Geo."""
if not obj or not hasattr(obj, 'geo_obj'): if not obj or not hasattr(obj, "geo_obj"):
return "-" return "-"
geo = obj.geo_obj geo = obj.geo_obj
if not geo or not geo.coords: 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" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
get_geo_coords.short_description = "Координаты" get_geo_coords.short_description = "Координаты"
def get_satellite(self, obj): 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 obj.parameter_obj.id_satellite.name
return "-" return "-"
get_satellite.short_description = "Спутник" get_satellite.short_description = "Спутник"
def get_frequency(self, obj): 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 obj.parameter_obj.frequency
return "-" return "-"
get_frequency.short_description = "Частота, МГц" get_frequency.short_description = "Частота, МГц"
def get_polarization(self, obj): 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 obj.parameter_obj.polarization.name
return "-" return "-"
get_polarization.short_description = "Поляризация" get_polarization.short_description = "Поляризация"
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
return False return False
@@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline):
@admin.register(Source) @admin.register(Source)
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
"""Админ-панель для модели Source.""" """Админ-панель для модели Source."""
list_display = ("id", "created_at", "updated_at") list_display = ("id", "created_at", "updated_at")
list_filter = ( list_filter = (
("created_at", DateRangeQuickSelectListFilterBuilder()), ("created_at", DateRangeQuickSelectListFilterBuilder()),
@@ -925,13 +1026,17 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
ordering = ("-created_at",) ordering = ("-created_at",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
inlines = [ObjItemInline] inlines = [ObjItemInline]
fieldsets = ( fieldsets = (
("Координаты: геолокация", { (
"fields": ("coords_kupsat", "coords_valid", "coords_reference") "Координаты: геолокация",
}), {"fields": ("coords_kupsat", "coords_valid", "coords_reference")},
("Метаданные", { ),
"fields": ("created_at", "created_by", "updated_at", "updated_by"), (
"classes": ("collapse",) "Метаданные",
}), {
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
"classes": ("collapse",),
},
),
) )

View File

@@ -9,6 +9,7 @@ from .models import (
Parameter, Parameter,
Polarization, Polarization,
Satellite, Satellite,
Source,
Standard, Standard,
) )
from .widgets import CheckboxSelectMultipleWidget from .widgets import CheckboxSelectMultipleWidget
@@ -305,8 +306,8 @@ class GeoForm(forms.ModelForm):
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}), "is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"mirrors": CheckboxSelectMultipleWidget( "mirrors": CheckboxSelectMultipleWidget(
attrs={ attrs={
'id': 'id_geo-mirrors', "id": "id_geo-mirrors",
'placeholder': 'Выберите спутники...', "placeholder": "Выберите спутники...",
} }
), ),
} }
@@ -372,3 +373,160 @@ class ObjItemForm(forms.ModelForm):
) )
return name 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

View File

@@ -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=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=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=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=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=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=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=6 column_label="Поляризация" 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=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=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=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=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=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=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=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=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=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=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=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=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=19 column_label="Стандарт" checked=False %}
{% 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=20 column_label="Тип источника" 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=21 column_label="Sigma" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
</ul> </ul>
</div> </div>

View File

@@ -32,6 +32,7 @@
</th> </th>
<th scope="col">Имя</th> <th scope="col">Имя</th>
<th scope="col">Спутник</th> <th scope="col">Спутник</th>
<th scope="col">Транспондер</th>
<th scope="col">Част, МГц</th> <th scope="col">Част, МГц</th>
<th scope="col">Полоса, МГц</th> <th scope="col">Полоса, МГц</th>
<th scope="col">Поляризация</th> <th scope="col">Поляризация</th>
@@ -41,8 +42,6 @@
<th scope="col">Время ГЛ</th> <th scope="col">Время ГЛ</th>
<th scope="col">Местоположение</th> <th scope="col">Местоположение</th>
<th scope="col">Геолокация</th> <th scope="col">Геолокация</th>
<th scope="col">Кубсат</th>
<th scope="col">Опер. отд</th>
<th scope="col">Обновлено</th> <th scope="col">Обновлено</th>
<th scope="col">Кем(обн)</th> <th scope="col">Кем(обн)</th>
<th scope="col">Создано</th> <th scope="col">Создано</th>

View File

@@ -70,7 +70,8 @@
<!-- Filter Toggle Button --> <!-- Filter Toggle Button -->
<div> <div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters"> <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры <i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span> <span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button> </button>
@@ -82,10 +83,11 @@
<i class="bi bi-plus-circle"></i> Добавить к <i class="bi bi-plus-circle"></i> Добавить к
</button> </button>
</div> </div>
<!-- Selected Items Counter Button --> <!-- Selected Items Counter Button -->
<div> <div>
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas"> <button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
<i class="bi bi-list-check"></i> Список <i class="bi bi-list-check"></i> Список
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span> <span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
</button> </button>
@@ -112,295 +114,277 @@
</div> </div>
<div class="offcanvas-body"> <div class="offcanvas-body">
<form method="get" id="filter-form"> <form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select --> <!-- Satellite Selection - Multi-select -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Спутник:</label> <label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1"> <div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" <button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', true)">Выбрать</button> onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" <button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', false)">Снять</button> onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div> </div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6"> <select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %} {% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}> <option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }} {{ satellite.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Range Filter -->
<div class="mb-2">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ range_min|default:'' }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm"
placeholder="До" value="{{ range_max|default:'' }}">
</div>
<!-- SNR Filter -->
<div class="mb-2">
<label class="form-label">ОСШ:</label>
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ snr_min|default:'' }}">
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm"
placeholder="До" value="{{ snr_max|default:'' }}">
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Сим. v, БОД:</label>
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ bod_min|default:'' }}">
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm"
placeholder="До" value="{{ bod_max|default:'' }}">
</div>
<!-- Modulation Filter -->
<div class="mb-2">
<label class="form-label">Модуляция:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
{% for mod in modulations %}
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Polarization Filter -->
<div class="mb-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization', false)">Снять</button>
</div>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Kubsat Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1"
value="1" {% if has_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0"
value="0" {% if has_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_0">Нет</label>
</div>
</div>
</div>
<!-- Valid Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты опер. отдела:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1"
value="1" {% if has_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0"
value="0" {% if has_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_0">Нет</label>
</div>
</div>
</div>
<!-- Source Type Filter -->
<div class="mb-2">
<label class="form-label">Тип источника:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1"
value="1" {% if has_source_type == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0"
value="0" {% if has_source_type == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_source_type_0">Нет</label>
</div>
</div>
</div>
<!-- Sigma Filter -->
<div class="mb-2">
<label class="form-label">Sigma:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1"
value="1" {% if has_sigma == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0"
value="0" {% if has_sigma == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_0">Нет</label>
</div>
</div>
</div>
<!-- Date Filter -->
<div class="mb-2">
<label class="form-label">Дата ГЛ:</label>
<div class="mb-2">
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('today')">Сегодня</button>
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('week')">Неделя</button>
</div>
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('month')">Месяц</button>
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('year')">Год</button>
</div>
</div>
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ date_from|default:'' }}">
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
placeholder="До" value="{{ date_to|default:'' }}">
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div> </div>
</div>
</div>
<!-- Main Table --> <!-- Frequency Filter -->
<div class="col-md"> <div class="mb-2">
<div class="card h-100"> <label class="form-label">Частота, МГц:</label>
<div class="card-body p-0"> <input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;"> placeholder="От" value="{{ freq_min|default:'' }}">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
<thead class="table-dark sticky-top"> placeholder="До" value="{{ freq_max|default:'' }}">
<tr> </div>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input"> <!-- Range Filter -->
</th> <div class="mb-2">
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %} <label class="form-label">Полоса, МГц:</label>
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %} <input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1"
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %} placeholder="От" value="{{ range_min|default:'' }}">
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %} <input type="number" step="0.001" name="range_max" class="form-control form-control-sm"
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %} placeholder="До" value="{{ range_max|default:'' }}">
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %} </div>
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %} <!-- SNR Filter -->
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %} <div class="mb-2">
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %} <label class="form-label">ОСШ:</label>
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %} <input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1"
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %} placeholder="От" value="{{ snr_min|default:'' }}">
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %} <input type="number" step="0.001" name="snr_max" class="form-control form-control-sm"
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %} placeholder="До" value="{{ snr_max|default:'' }}">
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %} </div>
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %} <!-- Symbol Rate Filter -->
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %} <div class="mb-2">
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %} <label class="form-label">Сим. v, БОД:</label>
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %} <input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1"
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %} placeholder="От" value="{{ bod_min|default:'' }}">
</tr> <input type="number" step="0.001" name="bod_max" class="form-control form-control-sm"
</thead> placeholder="До" value="{{ bod_max|default:'' }}">
<tbody> </div>
{% for item in processed_objects %}
<tr> <!-- Modulation Filter -->
<td class="text-center"> <div class="mb-2">
<input type="checkbox" class="form-check-input item-checkbox" <label class="form-label">Модуляция:</label>
value="{{ item.id }}"> <div class="d-flex justify-content-between mb-1">
</td> <button type="button" class="btn btn-sm btn-outline-secondary"
<td><a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td> onclick="selectAllOptions('modulation', true)">Выбрать</button>
<td>{{ item.satellite_name }}</td> <button type="button" class="btn btn-sm btn-outline-secondary"
<td>{{ item.frequency }}</td> onclick="selectAllOptions('modulation', false)">Снять</button>
<td>{{ item.freq_range }}</td> </div>
<td>{{ item.polarization }}</td> <select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
<td>{{ item.bod_velocity }}</td> {% for mod in modulations %}
<td>{{ item.modulation }}</td> <option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
<td>{{ item.snr }}</td> {{ mod.name }}
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td> </option>
<td>{{ item.geo_location}}</td> {% endfor %}
<td>{{ item.geo_coords }}</td> </select>
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td> </div>
<td>{{ item.updated_by }}</td>
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td> <!-- Polarization Filter -->
<td>{{ item.obj.created_by }}</td> <div class="mb-2">
<td>{{ item.comment }}</td> <label class="form-label">Поляризация:</label>
<td>{{ item.is_average }}</td> <div class="d-flex justify-content-between mb-1">
<td>{{ item.standard }}</td> <button type="button" class="btn btn-sm btn-outline-secondary"
<td> onclick="selectAllOptions('polarization', true)">Выбрать</button>
{% if item.obj.lyngsat_source %} <button type="button" class="btn btn-sm btn-outline-secondary"
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;"> onclick="selectAllOptions('polarization', false)">Снять</button>
<i class="bi bi-tv"></i> ТВ </div>
</a> <select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% else %} {% for pol in polarizations %}
- <option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
{% endif %} {{ pol.name }}
</td> </option>
<td> {% endfor %}
{% if item.has_sigma %} </select>
<a href="#" class="text-info text-decoration-none" onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;" title="{{ item.sigma_info }}"> </div>
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
</a>
{% else %} <!-- Source Type Filter -->
- <div class="mb-2">
{% endif %} <label class="form-label">Тип источника:</label>
</td> <div>
<td>{{ item.mirrors }}</td> <div class="form-check form-check-inline">
</tr> <input class="form-check-input" type="checkbox" name="has_source_type"
{% empty %} id="has_source_type_1" value="1" {% if has_source_type == '1' %}checked{% endif %}>
<tr> <label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
<td colspan="22" class="text-center py-4"> </div>
{% if selected_satellite_id %} <div class="form-check form-check-inline">
Нет данных для выбранных фильтров <input class="form-check-input" type="checkbox" name="has_source_type"
{% else %} id="has_source_type_0" value="0" {% if has_source_type == '0' %}checked{% endif %}>
Пожалуйста, выберите спутник для отображения данных <label class="form-check-label" for="has_source_type_0">Нет</label>
{% endif %} </div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
<!-- Sigma Filter -->
<div class="mb-2">
<label class="form-label">Sigma:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" value="1"
{% if has_sigma == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" value="0"
{% if has_sigma == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_0">Нет</label>
</div>
</div>
</div>
<!-- Date Filter -->
<div class="mb-2">
<label class="form-label">Дата ГЛ:</label>
<div class="mb-2">
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('today')">Сегодня</button>
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('week')">Неделя</button>
</div>
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('month')">Месяц</button>
<button type="button" class="btn btn-outline-secondary"
onclick="setDateRange('year')">Год</button>
</div>
</div>
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ date_from|default:'' }}">
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm" placeholder="До"
value="{{ date_to|default:'' }}">
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main Table -->
<div class="col-md">
<div class="card h-100">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input">
</th>
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Транспондер" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
</tr>
</thead>
<tbody>
{% for item in processed_objects %}
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
</td>
<td>
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
<td>{{ item.satellite_name }}</td>
<td>
{% if item.obj.transponder %}
<a href="#" class="text-success text-decoration-none"
onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;"
title="Показать данные транспондера">
<i class="bi bi-broadcast"></i> {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }}
</a>
{% else %}
-
{% endif %}
</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
<td>{{ item.geo_location}}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.updated_by }}</td>
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.obj.created_by }}</td>
<td>{{ item.comment }}</td>
<td>{{ item.is_average }}</td>
<td>{{ item.standard }}</td>
<td>
{% if item.obj.lyngsat_source %}
<a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
<i class="bi bi-tv"></i> ТВ
</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if item.has_sigma %}
<a href="#" class="text-info text-decoration-none"
onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;"
title="{{ item.sigma_info }}">
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
</a>
{% else %}
-
{% endif %}
</td>
<td>{{ item.mirrors }}</td>
</tr>
{% empty %}
<tr>
<td colspan="22" class="text-center py-4">
{% if selected_satellite_id %}
Нет данных для выбранных фильтров
{% else %}
Пожалуйста, выберите спутник для отображения данных
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<script> <script>
@@ -720,8 +704,8 @@
// Initialize column visibility - hide creation columns by default // Initialize column visibility - hide creation columns by default
function initColumnVisibility() { function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="14"]'); const creationDateCheckbox = document.querySelector('input[data-column="15"]');
const creationUserCheckbox = document.querySelector('input[data-column="15"]'); const creationUserCheckbox = document.querySelector('input[data-column="16"]');
if (creationDateCheckbox) { if (creationDateCheckbox) {
creationDateCheckbox.checked = false; creationDateCheckbox.checked = false;
toggleColumn(creationDateCheckbox); toggleColumn(creationDateCheckbox);
@@ -731,22 +715,22 @@
creationUserCheckbox.checked = false; creationUserCheckbox.checked = false;
toggleColumn(creationUserCheckbox); toggleColumn(creationUserCheckbox);
} }
// Hide comment, is_average, and standard columns by default // Hide comment, is_average, and standard columns by default
const commentCheckbox = document.querySelector('input[data-column="16"]'); const commentCheckbox = document.querySelector('input[data-column="17"]');
const isAverageCheckbox = document.querySelector('input[data-column="17"]'); const isAverageCheckbox = document.querySelector('input[data-column="18"]');
const standardCheckbox = document.querySelector('input[data-column="18"]'); const standardCheckbox = document.querySelector('input[data-column="19"]');
if (commentCheckbox) { if (commentCheckbox) {
commentCheckbox.checked = false; commentCheckbox.checked = false;
toggleColumn(commentCheckbox); toggleColumn(commentCheckbox);
} }
if (isAverageCheckbox) { if (isAverageCheckbox) {
isAverageCheckbox.checked = false; isAverageCheckbox.checked = false;
toggleColumn(isAverageCheckbox); toggleColumn(isAverageCheckbox);
} }
if (standardCheckbox) { if (standardCheckbox) {
standardCheckbox.checked = false; standardCheckbox.checked = false;
toggleColumn(standardCheckbox); toggleColumn(standardCheckbox);
@@ -785,7 +769,7 @@
// Count checkbox filters // Count checkbox filters
const hasKupsatCheckboxes = document.querySelectorAll('input[name="has_kupsat"]:checked'); const hasKupsatCheckboxes = document.querySelectorAll('input[name="has_kupsat"]:checked');
const hasValidCheckboxes = document.querySelectorAll('input[name="has_valid"]:checked'); const hasValidCheckboxes = document.querySelectorAll('input[name="has_valid"]:checked');
if (hasKupsatCheckboxes.length > 0) { if (hasKupsatCheckboxes.length > 0) {
filterCount++; filterCount++;
} }
@@ -853,7 +837,7 @@
} }
// Function to save selected items to localStorage // Function to save selected items to localStorage
window.saveSelectedItemsToStorage = function() { window.saveSelectedItemsToStorage = function () {
try { try {
localStorage.setItem('selectedItems', JSON.stringify(window.selectedItems)); localStorage.setItem('selectedItems', JSON.stringify(window.selectedItems));
} catch (e) { } catch (e) {
@@ -862,7 +846,7 @@
} }
// Function to update the selected items counter // Function to update the selected items counter
window.updateSelectedCounter = function() { window.updateSelectedCounter = function () {
const counterElement = document.getElementById('selectedCounter'); const counterElement = document.getElementById('selectedCounter');
if (window.selectedItems && window.selectedItems.length > 0) { if (window.selectedItems && window.selectedItems.length > 0) {
counterElement.textContent = window.selectedItems.length; counterElement.textContent = window.selectedItems.length;
@@ -870,7 +854,7 @@
} else { } else {
counterElement.style.display = 'none'; counterElement.style.display = 'none';
} }
// Also update the counter in the offcanvas // Also update the counter in the offcanvas
const offcanvasCounter = document.querySelector('#selectedItemsOffcanvas .offcanvas-header .badge'); const offcanvasCounter = document.querySelector('#selectedItemsOffcanvas .offcanvas-header .badge');
if (offcanvasCounter && window.selectedItems && window.selectedItems.length > 0) { if (offcanvasCounter && window.selectedItems && window.selectedItems.length > 0) {
@@ -885,9 +869,9 @@
updateSelectedCounter(); updateSelectedCounter();
// Function to add selected items to the list // Function to add selected items to the list
window.addSelectedToList = function() { window.addSelectedToList = function () {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked'); const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) { if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один элемент для добавления в список'); alert('Пожалуйста, выберите хотя бы один элемент для добавления в список');
return; return;
@@ -897,31 +881,30 @@
checkedCheckboxes.forEach(checkbox => { checkedCheckboxes.forEach(checkbox => {
const row = checkbox.closest('tr'); const row = checkbox.closest('tr');
const itemId = checkbox.value; const itemId = checkbox.value;
const itemExists = window.selectedItems.some(item => item.id === itemId); const itemExists = window.selectedItems.some(item => item.id === itemId);
if (!itemExists) { if (!itemExists) {
const rowData = { const rowData = {
id: itemId, id: itemId,
name: row.cells[1].textContent, name: row.cells[1].textContent,
satellite: row.cells[2].textContent, satellite: row.cells[2].textContent,
frequency: row.cells[3].textContent, transponder: row.cells[3].textContent,
freq_range: row.cells[4].textContent, frequency: row.cells[4].textContent,
polarization: row.cells[5].textContent, freq_range: row.cells[5].textContent,
bod_velocity: row.cells[6].textContent, polarization: row.cells[6].textContent,
modulation: row.cells[7].textContent, bod_velocity: row.cells[7].textContent,
snr: row.cells[8].textContent, modulation: row.cells[8].textContent,
geo_timestamp: row.cells[9].textContent, snr: row.cells[9].textContent,
geo_location: row.cells[10].textContent, geo_timestamp: row.cells[10].textContent,
geo_coords: row.cells[11].textContent, geo_location: row.cells[11].textContent,
kupsat_coords: row.cells[12].textContent, geo_coords: row.cells[12].textContent,
valid_coords: row.cells[13].textContent, updated_at: row.cells[13].textContent,
updated_at: row.cells[12].textContent, updated_by: row.cells[14].textContent,
updated_by: row.cells[13].textContent, created_at: row.cells[15].textContent,
created_at: row.cells[14].textContent, created_by: row.cells[16].textContent,
created_by: row.cells[15].textContent, mirrors: row.cells[22].textContent
mirrors: row.cells[21].textContent
}; };
window.selectedItems.push(rowData); window.selectedItems.push(rowData);
} }
}); });
@@ -966,6 +949,7 @@
</td> </td>
<td>${item.name}</td> <td>${item.name}</td>
<td>${item.satellite}</td> <td>${item.satellite}</td>
<td>${item.transponder}</td>
<td>${item.frequency}</td> <td>${item.frequency}</td>
<td>${item.freq_range}</td> <td>${item.freq_range}</td>
<td>${item.polarization}</td> <td>${item.polarization}</td>
@@ -975,8 +959,6 @@
<td>${item.geo_timestamp}</td> <td>${item.geo_timestamp}</td>
<td>${item.geo_location}</td> <td>${item.geo_location}</td>
<td>${item.geo_coords}</td> <td>${item.geo_coords}</td>
<td>${item.kupsat_coords}</td>
<td>${item.valid_coords}</td>
<td>${item.updated_at}</td> <td>${item.updated_at}</td>
<td>${item.updated_by}</td> <td>${item.updated_by}</td>
<td>${item.created_at}</td> <td>${item.created_at}</td>
@@ -997,10 +979,10 @@
// Get IDs of items to remove // Get IDs of items to remove
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value); const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
// Remove items from the selectedItems array // Remove items from the selectedItems array
window.selectedItems = window.selectedItems.filter(item => !idsToRemove.includes(item.id)); window.selectedItems = window.selectedItems.filter(item => !idsToRemove.includes(item.id));
// Save selected items to localStorage // Save selected items to localStorage
saveSelectedItemsToStorage(); saveSelectedItemsToStorage();
@@ -1018,7 +1000,7 @@
alert('Пожалуйста, выберите хотя бы один элемент для отправки'); alert('Пожалуйста, выберите хотя бы один элемент для отправки');
return; return;
} }
alert(`Отправка ${selectedCount} элементов... (функция в разработке)`); alert(`Отправка ${selectedCount} элементов... (функция в разработке)`);
// Placeholder for actual send functionality // Placeholder for actual send functionality
} }
@@ -1032,10 +1014,10 @@
} }
// Update the selected items table when the offcanvas is shown // Update the selected items table when the offcanvas is shown
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
const offcanvasElement = document.getElementById('selectedItemsOffcanvas'); const offcanvasElement = document.getElementById('selectedItemsOffcanvas');
if (offcanvasElement) { if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', function() { offcanvasElement.addEventListener('show.bs.offcanvas', function () {
populateSelectedItemsTable(); populateSelectedItemsTable();
}); });
} }
@@ -1056,7 +1038,8 @@
<h5 class="modal-title" id="lyngsatModalLabel"> <h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat <i class="bi bi-tv"></i> Данные источника LyngSat
</h5> </h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
</div> </div>
<div class="modal-body" id="lyngsatModalBody"> <div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4"> <div class="text-center py-4">
@@ -1073,32 +1056,32 @@
</div> </div>
<script> <script>
function showLyngsatModal(lyngsatId) { function showLyngsatModal(lyngsatId) {
// Показываем модальное окно // Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal')); const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show(); modal.show();
// Показываем индикатор загрузки // Показываем индикатор загрузки
const modalBody = document.getElementById('lyngsatModalBody'); const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="text-center py-4"> <div class="text-center py-4">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span> <span class="visually-hidden">Загрузка...</span>
</div> </div>
</div> </div>
`; `;
// Загружаем данные // Загружаем данные
fetch(`/api/lyngsat/${lyngsatId}/`) fetch(`/api/lyngsat/${lyngsatId}/`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Ошибка загрузки данных'); throw new Error('Ошибка загрузки данных');
} }
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
// Формируем HTML с данными // Формируем HTML с данными
let html = ` let html = `
<div class="container-fluid"> <div class="container-fluid">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
@@ -1191,16 +1174,139 @@ function showLyngsatModal(lyngsatId) {
</div> </div>
</div> </div>
`; `;
modalBody.innerHTML = html; modalBody.innerHTML = html;
}) })
.catch(error => { .catch(error => {
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message} <i class="bi bi-exclamation-triangle"></i> ${error.message}
</div> </div>
`; `;
}); });
} }
// Function to show transponder modal
function showTransponderModal(transponderId) {
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
modal.show();
const modalBody = document.getElementById('transponderModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
fetch(`/api/transponder/${transponderId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных транспондера');
}
return response.json();
})
.then(data => {
let html = `
<div class="container-fluid">
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Название:</td>
<td><strong>${data.name || '-'}</strong></td>
</tr>
<tr>
<td class="text-muted">Спутник:</td>
<td><strong>${data.satellite}</strong></td>
</tr>
<tr>
<td class="text-muted">Зона покрытия:</td>
<td>${data.zone_name || '-'}</td>
</tr>
<tr>
<td class="text-muted">Поляризация:</td>
<td><span class="badge bg-info">${data.polarization}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-broadcast"></i> Частотные параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Downlink:</td>
<td><strong>${data.downlink} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Uplink:</td>
<td><strong>${data.uplink || '-'} ${data.uplink ? 'МГц' : ''}</strong></td>
</tr>
<tr>
<td class="text-muted">Полоса:</td>
<td><strong>${data.frequency_range} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Перенос:</td>
<td>${data.transfer || '-'} ${data.transfer ? 'МГц' : ''}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
`;
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</div>
`;
});
}
</script> </script>
<!-- Transponder Data Modal -->
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="transponderModalLabel">
<i class="bi bi-broadcast"></i> Данные транспондера
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="transponderModalBody">
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,10 +1,74 @@
{% extends "mapsapp/map2d_base.html" %} {% extends "mainapp/base.html" %}
{% load static %} {% load static %}
{% block title %}Карта выбранных объектов{% endblock title %} {% block title %}Карта выбранных объектов{% endblock title %}
{% block extra_css %}
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<style>
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px; /* Высота navbar */
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
</style>
{% endblock %}
{% block content %}
<div id="map"></div>
{% endblock content %}
{% block extra_js %} {% block extra_js %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script> <script>
// Цвета для стандартных маркеров (из leaflet-color-markers) // Инициализация карты
let map = L.map('map').setView([55.75, 37.62], 5);
L.control.scale({
imperial: false,
metric: true
}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri'
});
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
const baseLayers = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
// Цвета для маркеров
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue']; var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
var getColorIcon = function(color) { var getColorIcon = function(color) {
return L.icon({ return L.icon({
@@ -19,47 +83,52 @@
var overlays = []; var overlays = [];
// Создаём слои для каждого объекта
{% for group in groups %} {% for group in groups %}
var groupIndex = {{ forloop.counter0 }}; var groupIndex = {{ forloop.counter0 }};
var groupName = '{{ group.name|escapejs }}';
var colorName = markerColors[groupIndex % markerColors.length]; var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName); var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup(); var groupLayer = L.layerGroup();
var subgroup = []; var subgroup = [];
{% for point_data in group.points %} {% for point_data in group.points %}
var pointName = "{{ group.name|escapejs }}"; var pointName = "{{ group.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], { var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon icon: groupIcon
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}"); }).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
groupLayer.addLayer(marker); groupLayer.addLayer(marker);
subgroup.push({ subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}", label: "{{ forloop.counter }} - {{ point_data.frequency|escapejs }}",
layer: marker layer: marker
}); });
{% endfor %} {% endfor %}
overlays.push({ overlays.push({
label: '{{ group.name|escapejs }}', label: groupName,
selectAllCheckbox: true, selectAllCheckbox: true,
children: subgroup, children: subgroup,
layer: groupLayer layer: groupLayer
}); });
{% endfor %} {% endfor %}
// Create the layer control with a custom container that includes a select all checkbox // Корневая группа
var layerControl = L.control.layers.tree(baseLayers, overlays, { const rootGroup = {
label: "Все точки",
selectAllCheckbox: true,
children: overlays,
layer: L.layerGroup()
};
// Создаём tree control
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
collapsed: false, collapsed: false,
autoZIndex: true autoZIndex: true
}); });
// Add the layer control to the map
layerControl.addTo(map); layerControl.addTo(map);
// Calculate map bounds to fit all markers // Подгоняем карту под все маркеры
{% if groups %} {% if groups %}
var groupBounds = L.featureGroup([]); var groupBounds = L.featureGroup([]);
{% for group in groups %} {% for group in groups %}
@@ -67,40 +136,7 @@
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}])); groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding map.fitBounds(groupBounds.getBounds().pad(0.1));
{% else %}
map.setView([55.75, 37.62], 5); // Default view if no markers
{% endif %} {% endif %}
// Add a "Select All" checkbox functionality for all overlays
setTimeout(function() {
// Create a custom "select all" checkbox
var selectAllContainer = document.createElement('div');
selectAllContainer.className = 'leaflet-control-layers-select-all';
selectAllContainer.style.padding = '5px';
selectAllContainer.style.borderBottom = '1px solid #ccc';
selectAllContainer.style.marginBottom = '5px';
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
// Insert the checkbox at the top of the layer control
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
if (layerControlContainer) {
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
}
// Add event listener to the "select all" checkbox
document.getElementById('select-all-overlays').addEventListener('change', function() {
var isChecked = this.checked;
// Iterate through all overlays and toggle visibility
for (var i = 0; i < overlays.length; i++) {
if (isChecked) {
map.addLayer(overlays[i].layer);
} else {
map.removeLayer(overlays[i].layer);
}
}
});
}, 500); // Slight delay to ensure the tree control has been rendered
</script> </script>
{% endblock extra_js %} {% endblock extra_js %}

View File

@@ -0,0 +1,63 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% block title %}Удалить источник #{{ object.id }}{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">Подтверждение удаления</h4>
</div>
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<strong>Внимание!</strong> Вы собираетесь удалить источник.
</div>
<h5>Информация об источнике:</h5>
<ul class="list-group mb-3">
<li class="list-group-item">
<strong>ID:</strong> {{ object.id }}
</li>
<li class="list-group-item">
<strong>Дата создания:</strong>
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</li>
<li class="list-group-item">
<strong>Создан пользователем:</strong>
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</li>
<li class="list-group-item">
<strong>Привязанных объектов:</strong>
<span class="badge bg-primary">{{ objitems_count }}</span>
</li>
</ul>
{% if objitems_count > 0 %}
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-circle-fill"></i>
<strong>Важно!</strong> При удалении источника будут также удалены все {{ objitems_count }} привязанных объектов!
</div>
{% endif %}
<p class="text-muted">Это действие нельзя отменить. Вы уверены, что хотите продолжить?</p>
<form method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Да, удалить
</button>
<a href="{% url 'mainapp:source_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,652 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% load static leaflet_tags %}
{% load l10n %}
{% block title %}Редактировать источник #{{ object.id }}{% endblock %}
{% block extra_css %}
<style>
.form-section {
margin-bottom: 2rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
.form-section-header {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
.btn-action {
margin-right: 0.5rem;
}
.readonly-field {
background-color: #f8f9fa;
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.coord-group {
border: 1px solid #dee2e6;
padding: 0.75rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.coord-group-header {
font-weight: bold;
margin-bottom: 0.5rem;
color: #495057;
}
#map {
height: 500px;
width: 100%;
margin-bottom: 1rem;
}
.map-container {
margin-bottom: 1rem;
}
.map-controls {
display: flex;
gap: 10px;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.map-control-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
background-color: #f8f9fa;
border-radius: 0.25rem;
cursor: pointer;
}
.map-control-btn.active {
background-color: #e9ecef;
border-color: #dee2e6;
}
.map-control-btn.edit {
background-color: #fff3cd;
border-color: #ffeeba;
}
.map-control-btn.save {
background-color: #d1ecf1;
border-color: #bee5eb;
}
.map-control-btn.cancel {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
}
.objitems-table {
width: 100%;
margin-top: 1rem;
}
.objitems-table th {
background-color: #f8f9fa;
font-weight: 600;
padding: 0.75rem;
border: 1px solid #dee2e6;
}
.objitems-table td {
padding: 0.5rem 0.75rem;
border: 1px solid #dee2e6;
}
.objitems-table tbody tr:hover {
background-color: #f8f9fa;
}
.objitem-link {
color: #0d6efd;
text-decoration: none;
}
.objitem-link:hover {
text-decoration: underline;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2>Редактировать источник #{{ object.id }}</h2>
<div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
<a href="{% url 'mainapp:source_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-danger btn-action">Удалить</a>
{% endif %}
<a href="{% url 'mainapp:home' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
</div>
<form method="post" id="source-form">
{% csrf_token %}
<!-- Основная информация -->
<div class="form-section">
<div class="form-section-header">
<h4>Основная информация</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">ID источника:</label>
<div class="readonly-field">{{ object.id }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата создания:</label>
<div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Создан пользователем:</label>
<div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Изменен пользователем:</label>
<div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Блок с картой -->
<div class="form-section">
<div class="form-section-header">
<h4>Карта</h4>
</div>
<div class="map-container">
<div id="map"></div>
</div>
</div>
<!-- Координаты -->
<div class="form-section">
<div class="form-section-header">
<h4>Координаты</h4>
</div>
<!-- Координаты ГЛ -->
<div class="coord-group">
<div class="coord-group-header">Координаты ГЛ (усреднённые)</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_average_latitude" class="form-label">Широта:</label>
{{ form.average_latitude }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_average_longitude" class="form-label">Долгота:</label>
{{ form.average_longitude }}
</div>
</div>
</div>
</div>
<!-- Координаты Кубсата -->
<div class="coord-group">
<div class="coord-group-header">Координаты Кубсата</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
{{ form.kupsat_latitude }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_kupsat_longitude" class="form-label">Долгота:</label>
{{ form.kupsat_longitude }}
</div>
</div>
</div>
</div>
<!-- Координаты оперативников -->
<div class="coord-group">
<div class="coord-group-header">Координаты оперативников</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_valid_latitude" class="form-label">Широта:</label>
{{ form.valid_latitude }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_valid_longitude" class="form-label">Долгота:</label>
{{ form.valid_longitude }}
</div>
</div>
</div>
</div>
<!-- Координаты справочные -->
<div class="coord-group">
<div class="coord-group-header">Координаты справочные</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_reference_latitude" class="form-label">Широта:</label>
{{ form.reference_latitude }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_reference_longitude" class="form-label">Долгота:</label>
{{ form.reference_longitude }}
</div>
</div>
</div>
</div>
</div>
<!-- Привязанные объекты -->
<div class="form-section">
<div class="form-section-header">
<h4>Привязанные объекты ({{ objitems.count }})</h4>
</div>
{% if objitems %}
<div class="table-responsive">
<table class="objitems-table table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Спутник</th>
<th>Частота, МГц</th>
<th>Полоса, МГц</th>
<th>Поляризация</th>
<th>Модуляция</th>
<th>Координаты</th>
<th>Создан</th>
<th>Обновлен</th>
</tr>
</thead>
<tbody>
{% for item in objitems %}
<tr>
<td>
<a href="{% url 'mainapp:objitem_update' item.id %}" class="objitem-link">
{{ item.id }}
</a>
</td>
<td>{{ item.name|default:"-" }}</td>
<td>
{% if item.parameter_obj and item.parameter_obj.id_satellite %}
{{ item.parameter_obj.id_satellite.name }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.parameter_obj %}
{{ item.parameter_obj.frequency|default:"-" }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.parameter_obj %}
{{ item.parameter_obj.freq_range|default:"-" }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.parameter_obj and item.parameter_obj.polarization %}
{{ item.parameter_obj.polarization.name }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.parameter_obj and item.parameter_obj.modulation %}
{{ item.parameter_obj.modulation.name }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.geo_obj and item.geo_obj.coords %}
{{ item.geo_obj.coords.y|floatformat:6 }}, {{ item.geo_obj.coords.x|floatformat:6 }}
{% else %}
-
{% endif %}
</td>
<td>{{ item.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.updated_at|date:"d.m.Y H:i" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">Нет привязанных объектов</p>
{% endif %}
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Подключаем Leaflet -->
{% leaflet_js %}
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Функция для создания иконок маркеров разных цветов
function createMarkerIcon(color) {
return L.icon({
iconUrl: `{% static "leaflet-markers/img/marker-icon-" %}${color}.png`,
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
const editableLayerGroup = new L.FeatureGroup();
map.addLayer(editableLayerGroup);
// Создаем 4 маркера для разных типов координат
const markers = {
average: {
marker: L.marker([55.75, 37.62], {
draggable: false,
icon: createMarkerIcon('blue'),
title: 'Координаты ГЛ'
}).addTo(editableLayerGroup),
latField: 'id_average_latitude',
lngField: 'id_average_longitude',
label: 'Координаты ГЛ'
},
kupsat: {
marker: L.marker([55.75, 37.62], {
draggable: false,
icon: createMarkerIcon('green'),
title: 'Координаты Кубсата'
}).addTo(editableLayerGroup),
latField: 'id_kupsat_latitude',
lngField: 'id_kupsat_longitude',
label: 'Координаты Кубсата'
},
valid: {
marker: L.marker([55.75, 37.62], {
draggable: false,
icon: createMarkerIcon('red'),
title: 'Координаты оперативников'
}).addTo(editableLayerGroup),
latField: 'id_valid_latitude',
lngField: 'id_valid_longitude',
label: 'Координаты оперативников'
},
reference: {
marker: L.marker([55.75, 37.62], {
draggable: false,
icon: createMarkerIcon('yellow'),
title: 'Координаты справочные'
}).addTo(editableLayerGroup),
latField: 'id_reference_latitude',
lngField: 'id_reference_longitude',
label: 'Координаты справочные'
}
};
// Привязываем попапы к маркерам
Object.values(markers).forEach(m => {
m.marker.bindPopup(m.label);
});
// Синхронизация при перетаскивании
Object.entries(markers).forEach(([key, m]) => {
m.marker.on('dragend', function (event) {
const latLng = event.target.getLatLng();
document.getElementById(m.latField).value = latLng.lat.toFixed(6);
document.getElementById(m.lngField).value = latLng.lng.toFixed(6);
});
// Методы для управления
m.marker.enableEditing = function () {
this.dragging.enable();
this.openPopup();
};
m.marker.disableEditing = function () {
this.dragging.disable();
this.closePopup();
};
});
// Устанавливаем начальные координаты из полей формы
function initMarkersFromForm() {
let hasValidCoords = false;
let centerLat = 55.75;
let centerLng = 37.62;
Object.entries(markers).forEach(([key, m]) => {
const lat = parseFloat(document.getElementById(m.latField).value);
const lng = parseFloat(document.getElementById(m.lngField).value);
if (!isNaN(lat) && !isNaN(lng)) {
m.marker.setLatLng([lat, lng]);
if (!hasValidCoords) {
centerLat = lat;
centerLng = lng;
hasValidCoords = true;
}
} else {
// Скрываем маркер если нет координат
m.marker.setOpacity(0);
}
});
// Центрируем карту
map.setView([centerLat, centerLng], hasValidCoords ? 10 : 5);
}
// Настройка формы для синхронизации с маркерами
function setupFormChange(latFieldId, lngFieldId, marker) {
const latField = document.getElementById(latFieldId);
const lngField = document.getElementById(lngFieldId);
[latField, lngField].forEach(field => {
field.addEventListener('change', function () {
const lat = parseFloat(latField.value);
const lng = parseFloat(lngField.value);
if (!isNaN(lat) && !isNaN(lng)) {
marker.setLatLng([lat, lng]);
marker.setOpacity(1);
map.setView(marker.getLatLng(), 10);
} else {
marker.setOpacity(0);
}
});
});
}
// Инициализация
initMarkersFromForm();
Object.values(markers).forEach(m => {
setupFormChange(m.latField, m.lngField, m.marker);
});
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
editControlsDiv.style.position = 'absolute';
editControlsDiv.style.top = '10px';
editControlsDiv.style.right = '10px';
editControlsDiv.style.zIndex = '1000';
editControlsDiv.style.background = 'white';
editControlsDiv.style.padding = '10px';
editControlsDiv.style.borderRadius = '4px';
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
editControlsDiv.innerHTML = `
<div class="map-controls">
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
</div>
`;
map.getContainer().appendChild(editControlsDiv);
let isEditing = false;
const initialPositions = {};
// Сохраняем начальные координаты для отмены
Object.entries(markers).forEach(([key, m]) => {
initialPositions[key] = m.marker.getLatLng();
});
// Включение редактирования
document.getElementById('edit-btn').addEventListener('click', function () {
if (isEditing) return;
isEditing = true;
document.getElementById('edit-btn').classList.add('active');
document.getElementById('save-btn').disabled = false;
document.getElementById('cancel-btn').disabled = false;
// Включаем drag для всех маркеров
Object.values(markers).forEach(m => {
if (m.marker.options.opacity !== 0) {
m.marker.enableEditing();
}
});
// Показываем подсказку
L.popup()
.setLatLng(map.getCenter())
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
.openOn(map);
});
// Сохранение изменений
document.getElementById('save-btn').addEventListener('click', function () {
if (!isEditing) return;
isEditing = false;
document.getElementById('edit-btn').classList.remove('active');
document.getElementById('save-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
// Отключаем редактирование
Object.values(markers).forEach(m => {
m.marker.disableEditing();
});
// Обновляем начальные позиции
Object.entries(markers).forEach(([key, m]) => {
initialPositions[key] = m.marker.getLatLng();
});
map.closePopup();
});
// Отмена изменений
document.getElementById('cancel-btn').addEventListener('click', function () {
if (!isEditing) return;
isEditing = false;
document.getElementById('edit-btn').classList.remove('active');
document.getElementById('save-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
// Возвращаем маркеры на исходные позиции
Object.entries(markers).forEach(([key, m]) => {
m.marker.setLatLng(initialPositions[key]);
m.marker.disableEditing();
// Синхронизируем форму с исходными значениями
document.getElementById(m.latField).value = initialPositions[key].lat.toFixed(6);
document.getElementById(m.lngField).value = initialPositions[key].lng.toFixed(6);
});
map.closePopup();
});
// Легенда
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function () {
const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = `
<h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Координаты ГЛ</div>
<div><span style="color: green; font-weight: bold;">•</span> Координаты Кубсата</div>
<div><span style="color: red; font-weight: bold;">•</span> Координаты оперативников</div>
<div><span style="color: gold; font-weight: bold;">•</span> Координаты справочные</div>
`;
return div;
};
legend.addTo(map);
});
</script>
{% endblock %}

View File

@@ -12,6 +12,16 @@
top: 0; top: 0;
z-index: 10; 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;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -55,11 +65,20 @@
</select> </select>
</div> </div>
<!-- Action buttons -->
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</button>
</div>
<!-- Filter Toggle Button --> <!-- Filter Toggle Button -->
<div> <div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters"> data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры <i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button> </button>
</div> </div>
@@ -185,6 +204,9 @@
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input">
</th>
<th scope="col" class="text-center" style="min-width: 60px;"> <th scope="col" class="text-center" style="min-width: 60px;">
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none"> <a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
ID ID
@@ -229,12 +251,16 @@
{% endif %} {% endif %}
</a> </a>
</th> </th>
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th> <th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for source in processed_sources %} {% for source in processed_sources %}
<tr> <tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox"
value="{{ source.id }}">
</td>
<td class="text-center">{{ source.id }}</td> <td class="text-center">{{ source.id }}</td>
<td>{{ source.coords_average }}</td> <td>{{ source.coords_average }}</td>
<td>{{ source.coords_kupsat }}</td> <td>{{ source.coords_kupsat }}</td>
@@ -244,15 +270,44 @@
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td> <td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td> <td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-sm btn-outline-primary" <div class="btn-group" role="group">
onclick="showSourceDetails({{ source.id }})"> {% if source.objitem_count > 0 %}
<i class="bi bi-eye"></i> Показать <a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
</button> target="_blank"
class="btn btn-sm btn-outline-success"
title="Показать источник с точками на карте">
<i class="bi bi-geo-alt"></i>
<span class="badge bg-success">{{ source.objitem_count }}</span>
</a>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
<i class="bi bi-geo-alt"></i>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="showSourceDetails({{ source.id }})"
title="Показать детали">
<i class="bi bi-eye"></i>
</button>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:source_update' source.id %}"
class="btn btn-sm btn-outline-warning"
title="Редактировать источник">
<i class="bi bi-pencil"></i>
</a>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
<i class="bi bi-pencil"></i>
</button>
{% endif %}
</div>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td> <td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -285,6 +340,10 @@
<table class="table table-striped table-hover table-sm"> <table class="table table-striped table-hover table-sm">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th class="text-center" style="width: 3%;">
<input type="checkbox" id="modal-select-all" class="form-check-input">
</th>
<th class="text-center" style="min-width: 60px;">ID</th>
<th>Имя</th> <th>Имя</th>
<th>Спутник</th> <th>Спутник</th>
<th>Частота, МГц</th> <th>Частота, МГц</th>
@@ -319,6 +378,55 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
let lastCheckedIndex = null;
function updateRowHighlight(checkbox) {
const row = checkbox.closest('tr');
if (checkbox.checked) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
function handleCheckboxClick(e) {
if (e.shiftKey && lastCheckedIndex !== null) {
const checkboxes = document.querySelectorAll('.item-checkbox');
const currentIndex = Array.from(checkboxes).indexOf(e.target);
const startIndex = Math.min(lastCheckedIndex, currentIndex);
const endIndex = Math.max(lastCheckedIndex, currentIndex);
for (let i = startIndex; i <= endIndex; i++) {
checkboxes[i].checked = e.target.checked;
updateRowHighlight(checkboxes[i]);
}
} else {
updateRowHighlight(e.target);
}
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
}
// Function to show selected sources on map
function showSelectedOnMap() {
// Get all checked checkboxes
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
return;
}
// Extract IDs from checked checkboxes
const selectedIds = [];
checkedCheckboxes.forEach(checkbox => {
selectedIds.push(checkbox.value);
});
// Redirect to the map view with selected IDs as query parameter
const url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
window.open(url, '_blank'); // Open in a new tab
}
// Search functionality // Search functionality
function performSearch() { function performSearch() {
const searchValue = document.getElementById('toolbar-search').value.trim(); const searchValue = document.getElementById('toolbar-search').value.trim();
@@ -377,6 +485,103 @@ function updateSort(field) {
window.location.search = urlParams.toString(); window.location.search = urlParams.toString();
} }
// Setup radio-like behavior for filter checkboxes
function setupRadioLikeCheckboxes(name) {
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
if (this.checked) {
checkboxes.forEach(other => {
if (other !== this) {
other.checked = false;
}
});
}
});
});
}
// Filter counter functionality
function updateFilterCounter() {
const form = document.getElementById('filter-form');
const formData = new FormData(form);
let filterCount = 0;
// Count non-empty form fields
for (const [key, value] of formData.entries()) {
if (value && value.trim() !== '') {
filterCount++;
}
}
// Display the filter counter
const counterElement = document.getElementById('filterCounter');
if (counterElement) {
if (filterCount > 0) {
counterElement.textContent = filterCount;
counterElement.style.display = 'inline';
} else {
counterElement.style.display = 'none';
}
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Setup select-all checkbox
const selectAllCheckbox = document.getElementById('select-all');
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
if (selectAllCheckbox && itemCheckboxes.length > 0) {
selectAllCheckbox.addEventListener('change', function () {
itemCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
updateRowHighlight(checkbox);
});
});
itemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
});
// Add shift-click handler
checkbox.addEventListener('click', handleCheckboxClick);
});
}
// Setup radio-like checkboxes for filters
setupRadioLikeCheckboxes('has_coords_average');
setupRadioLikeCheckboxes('has_coords_kupsat');
setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference');
// Update filter counter on page load
updateFilterCounter();
// Add event listeners to form elements to update counter when filters change
const form = document.getElementById('filter-form');
if (form) {
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
inputFields.forEach(input => {
input.addEventListener('input', updateFilterCounter);
input.addEventListener('change', updateFilterCounter);
});
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
checkboxFields.forEach(checkbox => {
checkbox.addEventListener('change', updateFilterCounter);
});
}
// Update counter when offcanvas is shown
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
});
// Show source details in modal // Show source details in modal
function showSourceDetails(sourceId) { function showSourceDetails(sourceId) {
// Update modal title // Update modal title
@@ -420,6 +625,10 @@ function showSourceDetails(sourceId) {
data.objitems.forEach(objitem => { data.objitems.forEach(objitem => {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<td class="text-center">
<input type="checkbox" class="form-check-input modal-item-checkbox" value="${objitem.id}">
</td>
<td class="text-center">${objitem.id}</td>
<td>${objitem.name}</td> <td>${objitem.name}</td>
<td>${objitem.satellite_name}</td> <td>${objitem.satellite_name}</td>
<td>${objitem.frequency}</td> <td>${objitem.frequency}</td>
@@ -434,6 +643,9 @@ function showSourceDetails(sourceId) {
`; `;
tbody.appendChild(row); tbody.appendChild(row);
}); });
// Setup modal select-all checkbox
setupModalSelectAll();
} else { } else {
// Show no data message // Show no data message
document.getElementById('modalNoData').style.display = 'block'; document.getElementById('modalNoData').style.display = 'block';
@@ -449,5 +661,32 @@ function showSourceDetails(sourceId) {
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
}); });
} }
// Setup select-all functionality for modal
function setupModalSelectAll() {
const modalSelectAll = document.getElementById('modal-select-all');
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
if (modalSelectAll && modalItemCheckboxes.length > 0) {
// Remove old event listeners by cloning
const newModalSelectAll = modalSelectAll.cloneNode(true);
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
newModalSelectAll.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = newModalSelectAll.checked;
});
});
modalItemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
document.getElementById('modal-select-all').checked = allChecked;
});
});
}
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,189 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Карта источников{% endblock title %}
{% block extra_css %}
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<style>
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px; /* Высота navbar */
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
.legend {
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
font-size: 11px;
}
.legend h6 {
font-size: 12px;
margin: 0 0 6px 0;
}
.legend-item {
margin: 3px 0;
display: flex;
align-items: center;
}
.legend-marker {
width: 18px;
height: 30px;
margin-right: 6px;
background-size: contain;
background-repeat: no-repeat;
}
</style>
{% endblock %}
{% block content %}
<div id="map"></div>
{% endblock content %}
{% block extra_js %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
// Инициализация карты
let map = L.map('map').setView([55.75, 37.62], 5);
L.control.scale({
imperial: false,
metric: true
}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri'
});
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
const baseLayers = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
// Цвета для маркеров
var markerColors = {
'blue': 'blue',
'orange': 'orange',
'green': 'green',
'violet': 'violet'
};
var getColorIcon = function(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
};
var overlays = [];
// Создаём слои для каждого типа координат
{% for group in groups %}
var groupName = '{{ group.name|escapejs }}';
var colorName = '{{ group.color }}';
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
var subgroup = [];
{% for point_data in group.points %}
var pointName = "{{ point_data.source_id|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName);
groupLayer.addLayer(marker);
subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.source_id|escapejs }}",
layer: marker
});
{% endfor %}
overlays.push({
label: groupName,
selectAllCheckbox: true,
children: subgroup,
layer: groupLayer
});
{% endfor %}
// Корневая группа
const rootGroup = {
label: "Все точки",
selectAllCheckbox: true,
children: overlays,
layer: L.layerGroup()
};
// Создаём tree control
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
collapsed: false,
autoZIndex: true
});
layerControl.addTo(map);
// Подгоняем карту под все маркеры
{% if groups %}
var groupBounds = L.featureGroup([]);
{% for group in groups %}
{% for point_data in group.points %}
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %}
{% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1));
{% endif %}
// Добавляем легенду в левый нижний угол
var legend = L.control({ position: 'bottomleft' });
legend.onAdd = function(map) {
var div = L.DomUtil.create('div', 'legend');
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
{% for group in groups %}
div.innerHTML += `
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
<span>{{ group.name|escapejs }}</span>
</div>
`;
{% endfor %}
return div;
};
legend.addTo(map);
</script>
{% endblock extra_js %}

View File

@@ -0,0 +1,244 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %}
{% block extra_css %}
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<style>
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px; /* Высота navbar */
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
.legend {
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
font-size: 11px;
}
.legend h6 {
font-size: 12px;
margin: 0 0 6px 0;
}
.legend-item {
margin: 3px 0;
display: flex;
align-items: center;
}
.legend-marker {
width: 18px;
height: 30px;
margin-right: 6px;
background-size: contain;
background-repeat: no-repeat;
}
.legend-section {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #ddd;
}
.legend-section:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.legend-section-title {
font-weight: bold;
font-size: 11px;
margin-bottom: 4px;
}
</style>
{% endblock %}
{% block content %}
<div id="map"></div>
{% endblock content %}
{% block extra_js %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
// Инициализация карты
let map = L.map('map').setView([55.75, 37.62], 5);
L.control.scale({
imperial: false,
metric: true
}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri'
});
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
const baseLayers = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
// Функция для создания иконки
var getColorIcon = function(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
};
var sourceOverlays = [];
var glPointLayers = [];
// Создаём слои для координат источника и точек ГЛ
{% for group in groups %}
var groupName = '{{ group.name|escapejs }}';
var colorName = '{{ group.color }}';
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
{% for point_data in group.points %}
{% if point_data.source_id %}
// Это координата источника
var pointName = "{{ point_data.source_id|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName);
groupLayer.addLayer(marker);
{% else %}
// Это точка ГЛ
var pointName = "{{ point_data.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
groupLayer.addLayer(marker);
// Добавляем каждую точку ГЛ отдельно в список
glPointLayers.push({
label: "{{ forloop.counter }} - {{ point_data.name|escapejs }} ({{ point_data.frequency|escapejs }})",
layer: marker
});
{% endif %}
{% endfor %}
// Для координат источника добавляем как отдельный слой без вложенности
{% if group.color in 'blue,orange,green,violet' %}
sourceOverlays.push({
label: groupName,
layer: groupLayer
});
{% endif %}
{% endfor %}
// Создаём иерархию
var treeOverlays = [];
if (sourceOverlays.length > 0) {
treeOverlays.push({
label: "Координаты источника #{{ source_id }}",
selectAllCheckbox: true,
children: sourceOverlays,
layer: L.layerGroup()
});
}
if (glPointLayers.length > 0) {
treeOverlays.push({
label: "Точки ГЛ",
selectAllCheckbox: true,
children: glPointLayers,
layer: L.layerGroup()
});
}
// Создаём tree control
const layerControl = L.control.layers.tree(baseLayers, treeOverlays, {
collapsed: false,
autoZIndex: true
});
layerControl.addTo(map);
// Подгоняем карту под все маркеры
{% if groups %}
var groupBounds = L.featureGroup([]);
{% for group in groups %}
{% for point_data in group.points %}
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %}
{% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1));
{% endif %}
// Добавляем легенду в левый нижний угол
var legend = L.control({ position: 'bottomleft' });
legend.onAdd = function(map) {
var div = L.DomUtil.create('div', 'legend');
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
// Координаты источника
var hasSourceCoords = false;
{% for group in groups %}
{% if group.color in 'blue,orange,green,violet' %}
{% if not hasSourceCoords %}
if (!hasSourceCoords) {
div.innerHTML += '<div class="legend-section-title">Координаты источника:</div>';
hasSourceCoords = true;
}
{% endif %}
div.innerHTML += `
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
<span>{{ group.name|escapejs }}</span>
</div>
`;
{% endif %}
{% endfor %}
// Точки ГЛ (все одним цветом)
{% for group in groups %}
{% if group.color not in 'blue,orange,green,violet' %}
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Точки ГЛ:</div></div>';
div.innerHTML += `
<div class="legend-item">
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
<span>{{ group.name|escapejs }}</span>
</div>
`;
{% endif %}
{% endfor %}
return div;
};
legend.addTo(map);
</script>
{% endblock extra_js %}

View File

@@ -25,9 +25,14 @@ from .views import (
ProcessKubsatView, ProcessKubsatView,
ShowMapView, ShowMapView,
ShowSelectedObjectsMapView, ShowSelectedObjectsMapView,
ShowSourcesMapView,
ShowSourceWithPointsMapView,
SourceListView, SourceListView,
SourceUpdateView,
SourceDeleteView,
SourceObjItemsAPIView, SourceObjItemsAPIView,
SigmaParameterDataAPIView, SigmaParameterDataAPIView,
TransponderDataAPIView,
UploadVchLoadView, UploadVchLoadView,
custom_logout, custom_logout,
) )
@@ -36,6 +41,8 @@ app_name = 'mainapp'
urlpatterns = [ urlpatterns = [
path('', SourceListView.as_view(), name='home'), path('', SourceListView.as_view(), name='home'),
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
path('objitems/', ObjItemListView.as_view(), name='objitem_list'), path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
path('actions/', ActionsPageView.as_view(), name='actions'), path('actions/', ActionsPageView.as_view(), name='actions'),
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'), 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('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', ShowMapView.as_view(), name='admin_show_map'), 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-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/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'), path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', ClusterTestView.as_view(), name='cluster'), path('cluster/', ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
@@ -53,6 +62,7 @@ urlpatterns = [
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'), path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'), path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'), path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'), path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'), path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),

View File

@@ -23,6 +23,7 @@ from .api import (
SigmaParameterDataAPIView, SigmaParameterDataAPIView,
SourceObjItemsAPIView, SourceObjItemsAPIView,
LyngsatTaskStatusAPIView, LyngsatTaskStatusAPIView,
TransponderDataAPIView,
) )
from .lyngsat import ( from .lyngsat import (
LinkLyngsatSourcesView, LinkLyngsatSourcesView,
@@ -30,8 +31,14 @@ from .lyngsat import (
LyngsatTaskStatusView, LyngsatTaskStatusView,
ClearLyngsatCacheView, ClearLyngsatCacheView,
) )
from .source import SourceListView from .source import SourceListView, SourceUpdateView, SourceDeleteView
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView from .map import (
ShowMapView,
ShowSelectedObjectsMapView,
ShowSourcesMapView,
ShowSourceWithPointsMapView,
ClusterTestView,
)
__all__ = [ __all__ = [
# Base # Base
@@ -58,6 +65,7 @@ __all__ = [
'SigmaParameterDataAPIView', 'SigmaParameterDataAPIView',
'SourceObjItemsAPIView', 'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView', 'LyngsatTaskStatusAPIView',
'TransponderDataAPIView',
# LyngSat # LyngSat
'LinkLyngsatSourcesView', 'LinkLyngsatSourcesView',
'FillLyngsatDataView', 'FillLyngsatDataView',
@@ -65,8 +73,12 @@ __all__ = [
'ClearLyngsatCacheView', 'ClearLyngsatCacheView',
# Source # Source
'SourceListView', 'SourceListView',
'SourceUpdateView',
'SourceDeleteView',
# Map # Map
'ShowMapView', 'ShowMapView',
'ShowSelectedObjectsMapView', 'ShowSelectedObjectsMapView',
'ShowSourcesMapView',
'ShowSourceWithPointsMapView',
'ClusterTestView', 'ClusterTestView',
] ]

View File

@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
response_data['status'] = task.state response_data['status'] = task.state
return JsonResponse(response_data) 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)

View File

@@ -1,6 +1,7 @@
""" """
Map related views for displaying objects on maps. Map related views for displaying objects on maps.
""" """
from collections import defaultdict from collections import defaultdict
from django.contrib.admin.views.decorators import staff_member_required 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") @method_decorator(staff_member_required, name="dispatch")
class ShowMapView(RoleRequiredMixin, View): class ShowMapView(RoleRequiredMixin, View):
"""View for displaying objects on map (admin interface).""" """View for displaying objects on map (admin interface)."""
required_roles = ["admin", "moderator"] required_roles = ["admin", "moderator"]
def get(self, request): def get(self, request):
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
or not obj.geo_obj.coords or not obj.geo_obj.coords
): ):
continue continue
param = getattr(obj, 'parameter_obj', None) param = getattr(obj, "parameter_obj", None)
if not param: if not param:
continue continue
points.append( points.append(
@@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View):
) )
else: else:
return redirect("admin") return redirect("admin")
grouped = defaultdict(list) grouped = defaultdict(list)
for p in points: for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]}) grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
@@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View):
class ShowSelectedObjectsMapView(LoginRequiredMixin, View): class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
"""View for displaying selected objects on map.""" """View for displaying selected objects on map."""
def get(self, request): def get(self, request):
ids = request.GET.get("ids", "") ids = request.GET.get("ids", "")
points = [] points = []
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
or not obj.geo_obj.coords or not obj.geo_obj.coords
): ):
continue continue
param = getattr(obj, 'parameter_obj', None) param = getattr(obj, "parameter_obj", None)
if not param: if not param:
continue continue
points.append( points.append(
@@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
return render(request, "mainapp/objitem_map.html", context) 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): class ClusterTestView(LoginRequiredMixin, View):
"""Test view for clustering functionality.""" """Test view for clustering functionality."""
def get(self, request): def get(self, request):
objs = ObjItem.objects.filter( objs = ObjItem.objects.filter(
name__icontains="! Astra 4A 12654,040 [1,962] МГц H" name__icontains="! Astra 4A 12654,040 [1,962] МГц H"

View File

@@ -3,12 +3,15 @@ Source related views.
""" """
from datetime import datetime 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.core.paginator import Paginator
from django.db.models import Count 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 django.views import View
from ..forms import SourceForm
from ..models import Source from ..models import Source
from ..utils import parse_pagination_params from ..utils import parse_pagination_params
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
# Get pagination parameters # Get pagination parameters
page_number, items_per_page = parse_pagination_params(request) page_number, items_per_page = parse_pagination_params(request)
# Get sorting parameters # Get sorting parameters (default to ID ascending)
sort_param = request.GET.get("sort", "-created_at") sort_param = request.GET.get("sort", "id")
# Get filter parameters # Get filter parameters
search_query = request.GET.get("search", "").strip() search_query = request.GET.get("search", "").strip()
@@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
} }
return render(request, "mainapp/source_list.html", context) 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')

View File

@@ -12,54 +12,54 @@ from mainapp.models import Polarization, Satellite, get_default_polarization, Cu
class Transponders(models.Model): class Transponders(models.Model):
""" """
Модель транспондера спутника. Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации. Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
""" """
# Основные поля # Основные поля
name = models.CharField( name = models.CharField(
max_length=30, max_length=30,
null=True, null=True,
blank=True, blank=True,
verbose_name="Название транспондера", verbose_name="Название транспондера",
db_index=True, db_index=True,
help_text="Название транспондера" help_text="Название транспондера",
) )
downlink = models.FloatField( downlink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Downlink", verbose_name="Downlink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота downlink в МГц (0-50000)" # help_text="Частота downlink в МГц (0-50000)"
) )
frequency_range = models.FloatField( frequency_range = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
# help_text="Полоса частот в МГц (0-1000)" # help_text="Полоса частот в МГц (0-1000)"
) )
uplink = models.FloatField( uplink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Uplink", verbose_name="Uplink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота uplink в МГц (0-50000)" # help_text="Частота uplink в МГц (0-50000)"
) )
zone_name = models.CharField( zone_name = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
verbose_name="Название зоны", verbose_name="Название зоны",
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера" help_text="Название зоны покрытия транспондера",
) )
snr = models.FloatField( snr = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)" help_text="Полоса частот в МГц (0-1000)",
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
@@ -89,44 +89,43 @@ class Transponders(models.Model):
verbose_name="Изменен пользователем", verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись", help_text="Пользователь, последним изменивший запись",
) )
# Связи # Связи
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, Polarization,
default=get_default_polarization, default=get_default_polarization,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
related_name="tran_polarizations", related_name="tran_polarizations",
null=True, null=True,
blank=True, blank=True,
verbose_name="Поляризация", verbose_name="Поляризация",
help_text="Поляризация сигнала" help_text="Поляризация сигнала",
) )
sat_id = models.ForeignKey( sat_id = models.ForeignKey(
Satellite, Satellite,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="tran_satellite", related_name="tran_satellite",
verbose_name="Спутник", verbose_name="Спутник",
db_index=True, db_index=True,
help_text="Спутник, которому принадлежит транспондер" help_text="Спутник, которому принадлежит транспондер",
) )
# Вычисляемые поля # Вычисляемые поля
transfer = models.GeneratedField( transfer = models.GeneratedField(
expression=ExpressionWrapper( expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')), Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
output_field=models.FloatField()
), ),
output_field=models.FloatField(), output_field=models.FloatField(),
db_persist=True, db_persist=True,
null=True, null=True,
blank=True, blank=True,
verbose_name="Перенос" verbose_name="Перенос",
) )
# def clean(self): # def clean(self):
# """Валидация на уровне модели""" # """Валидация на уровне модели"""
# super().clean() # super().clean()
# # Проверка что downlink и uplink заданы # # Проверка что downlink и uplink заданы
# if self.downlink and self.uplink: # if self.downlink and self.uplink:
# # Обычно uplink выше downlink для спутниковой связи # # Обычно uplink выше downlink для спутниковой связи
@@ -139,14 +138,12 @@ class Transponders(models.Model):
if self.name: if self.name:
return self.name return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}" return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta: class Meta:
verbose_name = "Транспондер" verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры" verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink'] ordering = ["sat_id", "downlink"]
indexes = [ indexes = [
models.Index(fields=['sat_id', 'downlink']), models.Index(fields=["sat_id", "downlink"]),
models.Index(fields=['sat_id', 'zone_name']), models.Index(fields=["sat_id", "zone_name"]),
] ]

File diff suppressed because it is too large Load Diff