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

View File

@@ -9,6 +9,7 @@ from .models import (
Parameter,
Polarization,
Satellite,
Source,
Standard,
)
from .widgets import CheckboxSelectMultipleWidget
@@ -305,8 +306,8 @@ class GeoForm(forms.ModelForm):
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"mirrors": CheckboxSelectMultipleWidget(
attrs={
'id': 'id_geo-mirrors',
'placeholder': 'Выберите спутники...',
"id": "id_geo-mirrors",
"placeholder": "Выберите спутники...",
}
),
}
@@ -372,3 +373,160 @@ class ObjItemForm(forms.ModelForm):
)
return name
class SourceForm(forms.ModelForm):
"""Form for editing Source model with 4 coordinate fields."""
# Координаты ГЛ (coords_average)
average_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта ГЛ",
)
average_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота ГЛ",
)
# Координаты Кубсата (coords_kupsat)
kupsat_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта Кубсата",
)
kupsat_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота Кубсата",
)
# Координаты оперативников (coords_valid)
valid_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта оперативников",
)
valid_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота оперативников",
)
# Координаты справочные (coords_reference)
reference_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта справочные",
)
reference_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота справочные",
)
class Meta:
model = Source
fields = [] # Все поля обрабатываются вручную
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Заполняем поля координат из instance
if self.instance and self.instance.pk:
if self.instance.coords_average:
self.fields[
"average_longitude"
].initial = self.instance.coords_average.x
self.fields["average_latitude"].initial = self.instance.coords_average.y
if self.instance.coords_kupsat:
self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x
self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y
if self.instance.coords_valid:
self.fields["valid_longitude"].initial = self.instance.coords_valid.x
self.fields["valid_latitude"].initial = self.instance.coords_valid.y
if self.instance.coords_reference:
self.fields[
"reference_longitude"
].initial = self.instance.coords_reference.x
self.fields[
"reference_latitude"
].initial = self.instance.coords_reference.y
def save(self, commit=True):
from django.contrib.gis.geos import Point
instance = super().save(commit=False)
# Обработка coords_average
avg_lat = self.cleaned_data.get("average_latitude")
avg_lng = self.cleaned_data.get("average_longitude")
if avg_lat is not None and avg_lng is not None:
instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
else:
instance.coords_average = None
# Обработка coords_kupsat
kup_lat = self.cleaned_data.get("kupsat_latitude")
kup_lng = self.cleaned_data.get("kupsat_longitude")
if kup_lat is not None and kup_lng is not None:
instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326)
else:
instance.coords_kupsat = None
# Обработка coords_valid
val_lat = self.cleaned_data.get("valid_latitude")
val_lng = self.cleaned_data.get("valid_longitude")
if val_lat is not None and val_lng is not None:
instance.coords_valid = Point(val_lng, val_lat, srid=4326)
else:
instance.coords_valid = None
# Обработка coords_reference
ref_lat = self.cleaned_data.get("reference_latitude")
ref_lng = self.cleaned_data.get("reference_longitude")
if ref_lat is not None and ref_lng is not None:
instance.coords_reference = Point(ref_lng, ref_lat, srid=4326)
else:
instance.coords_reference = None
if commit:
instance.save()
return instance

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=1 column_label="Имя" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
</ul>
</div>

View File

@@ -32,6 +32,7 @@
</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>

View File

@@ -70,7 +70,8 @@
<!-- Filter Toggle Button -->
<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> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
@@ -82,10 +83,11 @@
<i class="bi bi-plus-circle"></i> Добавить к
</button>
</div>
<!-- Selected Items Counter Button -->
<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> Список
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
</button>
@@ -112,295 +114,277 @@
</div>
<div class="offcanvas-body">
<form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select -->
<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('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</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>
<!-- Satellite Selection - Multi-select -->
<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('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</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="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>{{ 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>
<!-- 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>
<!-- 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>
<!-- 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>
<script>
@@ -720,8 +704,8 @@
// Initialize column visibility - hide creation columns by default
function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
const creationDateCheckbox = document.querySelector('input[data-column="15"]');
const creationUserCheckbox = document.querySelector('input[data-column="16"]');
if (creationDateCheckbox) {
creationDateCheckbox.checked = false;
toggleColumn(creationDateCheckbox);
@@ -731,22 +715,22 @@
creationUserCheckbox.checked = false;
toggleColumn(creationUserCheckbox);
}
// Hide comment, is_average, and standard columns by default
const commentCheckbox = document.querySelector('input[data-column="16"]');
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
const standardCheckbox = document.querySelector('input[data-column="18"]');
const commentCheckbox = document.querySelector('input[data-column="17"]');
const isAverageCheckbox = document.querySelector('input[data-column="18"]');
const standardCheckbox = document.querySelector('input[data-column="19"]');
if (commentCheckbox) {
commentCheckbox.checked = false;
toggleColumn(commentCheckbox);
}
if (isAverageCheckbox) {
isAverageCheckbox.checked = false;
toggleColumn(isAverageCheckbox);
}
if (standardCheckbox) {
standardCheckbox.checked = false;
toggleColumn(standardCheckbox);
@@ -785,7 +769,7 @@
// Count checkbox filters
const hasKupsatCheckboxes = document.querySelectorAll('input[name="has_kupsat"]:checked');
const hasValidCheckboxes = document.querySelectorAll('input[name="has_valid"]:checked');
if (hasKupsatCheckboxes.length > 0) {
filterCount++;
}
@@ -853,7 +837,7 @@
}
// Function to save selected items to localStorage
window.saveSelectedItemsToStorage = function() {
window.saveSelectedItemsToStorage = function () {
try {
localStorage.setItem('selectedItems', JSON.stringify(window.selectedItems));
} catch (e) {
@@ -862,7 +846,7 @@
}
// Function to update the selected items counter
window.updateSelectedCounter = function() {
window.updateSelectedCounter = function () {
const counterElement = document.getElementById('selectedCounter');
if (window.selectedItems && window.selectedItems.length > 0) {
counterElement.textContent = window.selectedItems.length;
@@ -870,7 +854,7 @@
} else {
counterElement.style.display = 'none';
}
// Also update the counter in the offcanvas
const offcanvasCounter = document.querySelector('#selectedItemsOffcanvas .offcanvas-header .badge');
if (offcanvasCounter && window.selectedItems && window.selectedItems.length > 0) {
@@ -885,9 +869,9 @@
updateSelectedCounter();
// Function to add selected items to the list
window.addSelectedToList = function() {
window.addSelectedToList = function () {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один элемент для добавления в список');
return;
@@ -897,31 +881,30 @@
checkedCheckboxes.forEach(checkbox => {
const row = checkbox.closest('tr');
const itemId = checkbox.value;
const itemExists = window.selectedItems.some(item => item.id === itemId);
if (!itemExists) {
const rowData = {
id: itemId,
name: row.cells[1].textContent,
satellite: row.cells[2].textContent,
frequency: row.cells[3].textContent,
freq_range: row.cells[4].textContent,
polarization: row.cells[5].textContent,
bod_velocity: row.cells[6].textContent,
modulation: row.cells[7].textContent,
snr: row.cells[8].textContent,
geo_timestamp: row.cells[9].textContent,
geo_location: row.cells[10].textContent,
geo_coords: row.cells[11].textContent,
kupsat_coords: row.cells[12].textContent,
valid_coords: row.cells[13].textContent,
updated_at: row.cells[12].textContent,
updated_by: row.cells[13].textContent,
created_at: row.cells[14].textContent,
created_by: row.cells[15].textContent,
mirrors: row.cells[21].textContent
transponder: row.cells[3].textContent,
frequency: row.cells[4].textContent,
freq_range: row.cells[5].textContent,
polarization: row.cells[6].textContent,
bod_velocity: row.cells[7].textContent,
modulation: row.cells[8].textContent,
snr: row.cells[9].textContent,
geo_timestamp: row.cells[10].textContent,
geo_location: row.cells[11].textContent,
geo_coords: row.cells[12].textContent,
updated_at: row.cells[13].textContent,
updated_by: row.cells[14].textContent,
created_at: row.cells[15].textContent,
created_by: row.cells[16].textContent,
mirrors: row.cells[22].textContent
};
window.selectedItems.push(rowData);
}
});
@@ -966,6 +949,7 @@
</td>
<td>${item.name}</td>
<td>${item.satellite}</td>
<td>${item.transponder}</td>
<td>${item.frequency}</td>
<td>${item.freq_range}</td>
<td>${item.polarization}</td>
@@ -975,8 +959,6 @@
<td>${item.geo_timestamp}</td>
<td>${item.geo_location}</td>
<td>${item.geo_coords}</td>
<td>${item.kupsat_coords}</td>
<td>${item.valid_coords}</td>
<td>${item.updated_at}</td>
<td>${item.updated_by}</td>
<td>${item.created_at}</td>
@@ -997,10 +979,10 @@
// Get IDs of items to remove
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
// Remove items from the selectedItems array
window.selectedItems = window.selectedItems.filter(item => !idsToRemove.includes(item.id));
// Save selected items to localStorage
saveSelectedItemsToStorage();
@@ -1018,7 +1000,7 @@
alert('Пожалуйста, выберите хотя бы один элемент для отправки');
return;
}
alert(`Отправка ${selectedCount} элементов... (функция в разработке)`);
// Placeholder for actual send functionality
}
@@ -1032,10 +1014,10 @@
}
// Update the selected items table when the offcanvas is shown
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
const offcanvasElement = document.getElementById('selectedItemsOffcanvas');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', function() {
offcanvasElement.addEventListener('show.bs.offcanvas', function () {
populateSelectedItemsTable();
});
}
@@ -1056,7 +1038,8 @@
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat
</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 class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4">
@@ -1073,32 +1056,32 @@
</div>
<script>
function showLyngsatModal(lyngsatId) {
// Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
// Показываем индикатор загрузки
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = `
function showLyngsatModal(lyngsatId) {
// Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
// Показываем индикатор загрузки
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
// Загружаем данные
fetch(`/api/lyngsat/${lyngsatId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
// Формируем HTML с данными
let html = `
// Загружаем данные
fetch(`/api/lyngsat/${lyngsatId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
// Формируем HTML с данными
let html = `
<div class="container-fluid">
<div class="row g-3">
<div class="col-md-6">
@@ -1191,16 +1174,139 @@ function showLyngsatModal(lyngsatId) {
</div>
</div>
`;
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</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>
<!-- 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 %}

View File

@@ -1,10 +1,74 @@
{% extends "mapsapp/map2d_base.html" %}
{% 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;
}
</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>
// Цвета для стандартных маркеров (из 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 getColorIcon = function(color) {
return L.icon({
@@ -19,47 +83,52 @@
var overlays = [];
// Создаём слои для каждого объекта
{% for group in groups %}
var groupIndex = {{ forloop.counter0 }};
var groupName = '{{ group.name|escapejs }}';
var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
var subgroup = [];
{% for point_data in group.points %}
var pointName = "{{ group.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);
subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
label: "{{ forloop.counter }} - {{ point_data.frequency|escapejs }}",
layer: marker
});
{% endfor %}
overlays.push({
label: '{{ group.name|escapejs }}',
label: groupName,
selectAllCheckbox: true,
children: subgroup,
layer: groupLayer
});
{% 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,
autoZIndex: true
});
// Add the layer control to the map
layerControl.addTo(map);
// Calculate map bounds to fit all markers
// Подгоняем карту под все маркеры
{% if groups %}
var groupBounds = L.featureGroup([]);
{% for group in groups %}
@@ -67,40 +136,7 @@
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %}
{% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
{% else %}
map.setView([55.75, 37.62], 5); // Default view if no markers
map.fitBounds(groupBounds.getBounds().pad(0.1));
{% 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>
{% 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;
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>
{% endblock %}
@@ -55,11 +65,20 @@
</select>
</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 -->
<div>
<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> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
@@ -185,6 +204,9 @@
<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>
<th scope="col" class="text-center" style="min-width: 60px;">
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
ID
@@ -229,12 +251,16 @@
{% endif %}
</a>
</th>
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th>
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
{% for source in processed_sources %}
<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>{{ source.coords_average }}</td>
<td>{{ source.coords_kupsat }}</td>
@@ -244,15 +270,44 @@
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="showSourceDetails({{ source.id }})">
<i class="bi bi-eye"></i> Показать
</button>
<div class="btn-group" role="group">
{% if source.objitem_count > 0 %}
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
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>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
</tr>
{% endfor %}
</tbody>
@@ -285,6 +340,10 @@
<table class="table table-striped table-hover table-sm">
<thead class="table-light sticky-top">
<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>
@@ -319,6 +378,55 @@
{% block extra_js %}
<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
function performSearch() {
const searchValue = document.getElementById('toolbar-search').value.trim();
@@ -377,6 +485,103 @@ function updateSort(field) {
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
function showSourceDetails(sourceId) {
// Update modal title
@@ -420,6 +625,10 @@ function showSourceDetails(sourceId) {
data.objitems.forEach(objitem => {
const row = document.createElement('tr');
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.satellite_name}</td>
<td>${objitem.frequency}</td>
@@ -434,6 +643,9 @@ function showSourceDetails(sourceId) {
`;
tbody.appendChild(row);
});
// Setup modal select-all checkbox
setupModalSelectAll();
} else {
// Show no data message
document.getElementById('modalNoData').style.display = 'block';
@@ -449,5 +661,32 @@ function showSourceDetails(sourceId) {
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>
{% 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,
ShowMapView,
ShowSelectedObjectsMapView,
ShowSourcesMapView,
ShowSourceWithPointsMapView,
SourceListView,
SourceUpdateView,
SourceDeleteView,
SourceObjItemsAPIView,
SigmaParameterDataAPIView,
TransponderDataAPIView,
UploadVchLoadView,
custom_logout,
)
@@ -36,6 +41,8 @@ app_name = 'mainapp'
urlpatterns = [
path('', SourceListView.as_view(), name='home'),
path('source/<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('actions/', ActionsPageView.as_view(), name='actions'),
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
@@ -45,6 +52,8 @@ urlpatterns = [
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
@@ -53,6 +62,7 @@ urlpatterns = [
path('api/lyngsat/<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/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('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),

View File

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

View File

@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
response_data['status'] = task.state
return JsonResponse(response_data)
class TransponderDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting Transponder data."""
def get(self, request, transponder_id):
from mapsapp.models import Transponders
try:
transponder = Transponders.objects.select_related(
'sat_id',
'polarization'
).get(id=transponder_id)
data = {
'id': transponder.id,
'name': transponder.name or '-',
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
'polarization': transponder.polarization.name if transponder.polarization else '-',
'zone_name': transponder.zone_name or '-',
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
}
return JsonResponse(data)
except Transponders.DoesNotExist:
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

View File

@@ -1,6 +1,7 @@
"""
Map related views for displaying objects on maps.
"""
from collections import defaultdict
from django.contrib.admin.views.decorators import staff_member_required
@@ -18,7 +19,7 @@ from ..models import ObjItem
@method_decorator(staff_member_required, name="dispatch")
class ShowMapView(RoleRequiredMixin, View):
"""View for displaying objects on map (admin interface)."""
required_roles = ["admin", "moderator"]
def get(self, request):
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View):
)
else:
return redirect("admin")
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
@@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View):
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
"""View for displaying selected objects on map."""
def get(self, request):
ids = request.GET.get("ids", "")
points = []
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
return render(request, "mainapp/objitem_map.html", context)
class ShowSourcesMapView(LoginRequiredMixin, View):
"""View for displaying selected sources on map."""
def get(self, request):
from ..models import Source
ids = request.GET.get("ids", "")
groups = []
if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
sources = Source.objects.filter(id__in=id_list)
# Define coordinate types with their labels and colors
coord_types = [
("coords_average", "Усредненные координаты", "blue"),
("coords_kupsat", "Координаты Кубсата", "orange"),
("coords_valid", "Координаты оперативников", "green"),
("coords_reference", "Координаты справочные", "violet"),
]
# Group points by coordinate type
for coord_field, label, color in coord_types:
points = []
for source in sources:
coords = getattr(source, coord_field)
if coords:
# coords is a Point object with x (longitude) and y (latitude)
points.append(
{
"point": (coords.x, coords.y), # (lon, lat)
"source_id": f"Источник #{source.id}",
}
)
if points:
groups.append(
{
"name": label,
"points": points,
"color": color,
}
)
else:
return redirect("mainapp:home")
context = {
"groups": groups,
}
return render(request, "mainapp/source_map.html", context)
class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
"""View for displaying a single source with all its related ObjItem points."""
def get(self, request, source_id):
from ..models import Source
try:
source = Source.objects.prefetch_related(
"source_objitems",
"source_objitems__parameter_obj",
"source_objitems__geo_obj",
).get(id=source_id)
except Source.DoesNotExist:
return redirect("mainapp:home")
groups = []
# Цвета для разных типов координат источника
source_coord_types = [
("coords_average", "Усредненные координаты", "blue"),
("coords_kupsat", "Координаты Кубсата", "orange"),
("coords_valid", "Координаты оперативников", "green"),
("coords_reference", "Координаты справочные", "violet"),
]
# Добавляем координаты источника
for coord_field, label, color in source_coord_types:
coords = getattr(source, coord_field)
if coords:
groups.append(
{
"name": label,
"points": [
{
"point": (coords.x, coords.y),
"source_id": f"Источник #{source.id}",
}
],
"color": color,
}
)
# Добавляем все точки ГЛ одной группой
gl_points = source.source_objitems.select_related(
"parameter_obj", "geo_obj"
).all()
# Собираем все точки ГЛ в одну группу
all_gl_points = []
for obj in gl_points:
if (
not hasattr(obj, "geo_obj")
or not obj.geo_obj
or not obj.geo_obj.coords
):
continue
param = getattr(obj, "parameter_obj", None)
if not param:
continue
all_gl_points.append(
{
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
"name": obj.name,
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
}
)
# Добавляем все точки ГЛ одним цветом (красный)
if all_gl_points:
groups.append(
{"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
)
context = {
"groups": groups,
"source_id": source_id,
}
return render(request, "mainapp/source_with_points_map.html", context)
class ClusterTestView(LoginRequiredMixin, View):
"""Test view for clustering functionality."""
def get(self, request):
objs = ObjItem.objects.filter(
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"

View File

@@ -3,12 +3,15 @@ Source related views.
"""
from datetime import datetime
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.paginator import Paginator
from django.db.models import Count
from django.shortcuts import render
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views import View
from ..forms import SourceForm
from ..models import Source
from ..utils import parse_pagination_params
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
# Get pagination parameters
page_number, items_per_page = parse_pagination_params(request)
# Get sorting parameters
sort_param = request.GET.get("sort", "-created_at")
# Get sorting parameters (default to ID ascending)
sort_param = request.GET.get("sort", "id")
# Get filter parameters
search_query = request.GET.get("search", "").strip()
@@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
}
return render(request, "mainapp/source_list.html", context)
class AdminModeratorMixin(UserPassesTestMixin):
"""Mixin to restrict access to admin and moderator roles only."""
def test_func(self):
return (
self.request.user.is_authenticated and
hasattr(self.request.user, 'customuser') and
self.request.user.customuser.role in ['admin', 'moderator']
)
def handle_no_permission(self):
messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
return redirect('mainapp:home')
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
"""View for editing Source with 4 coordinate fields and related ObjItems."""
def get(self, request, pk):
source = get_object_or_404(Source, pk=pk)
form = SourceForm(instance=source)
# Get related ObjItems ordered by creation date
objitems = source.source_objitems.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj',
'created_by__user',
'updated_by__user'
).order_by('created_at')
context = {
'object': source,
'form': form,
'objitems': objitems,
'full_width_page': True,
}
return render(request, 'mainapp/source_form.html', context)
def post(self, request, pk):
source = get_object_or_404(Source, pk=pk)
form = SourceForm(request.POST, instance=source)
if form.is_valid():
source = form.save(commit=False)
# Set updated_by to current user
if hasattr(request.user, 'customuser'):
source.updated_by = request.user.customuser
source.save()
messages.success(request, f'Источник #{source.id} успешно обновлен.')
# Redirect back with query params if present
if request.GET.urlencode():
return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
return redirect('mainapp:source_update', pk=source.id)
# If form is invalid, re-render with errors
objitems = source.source_objitems.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj',
'created_by__user',
'updated_by__user'
).order_by('created_at')
context = {
'object': source,
'form': form,
'objitems': objitems,
'full_width_page': True,
}
return render(request, 'mainapp/source_form.html', context)
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
"""View for deleting Source."""
def get(self, request, pk):
source = get_object_or_404(Source, pk=pk)
context = {
'object': source,
'objitems_count': source.source_objitems.count(),
}
return render(request, 'mainapp/source_confirm_delete.html', context)
def post(self, request, pk):
source = get_object_or_404(Source, pk=pk)
source_id = source.id
try:
source.delete()
messages.success(request, f'Источник #{source_id} успешно удален.')
except Exception as e:
messages.error(request, f'Ошибка при удалении источника: {str(e)}')
return redirect('mainapp:source_update', pk=pk)
# Redirect to source list
if request.GET.urlencode():
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
return redirect('mainapp:home')

View File

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

File diff suppressed because it is too large Load Diff