Compare commits

..

2 Commits

23 changed files with 4083 additions and 1560 deletions

View File

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

View File

@@ -2,57 +2,55 @@
from django import forms from django import forms
# Local imports # Local imports
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard from .models import (
from .widgets import TagSelectWidget Geo,
Modulation,
ObjItem,
Parameter,
Polarization,
Satellite,
Source,
Standard,
)
from .widgets import CheckboxSelectMultipleWidget
class UploadFileForm(forms.Form): class UploadFileForm(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите файл", label="Выберите файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={"class": "form-file-input"}),
'class': 'form-file-input'
})
) )
class LoadExcelData(forms.Form): class LoadExcelData(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите Excel файл", label="Выберите Excel файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
'class': 'form-control',
'accept': '.xlsx,.xls'
})
) )
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={"class": "form-select"}),
'class': 'form-select'
})
) )
number_input = forms.IntegerField( number_input = forms.IntegerField(
label="Введите число объектов", label="Введите число объектов",
min_value=0, min_value=0,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={"class": "form-control"}),
'class': 'form-control'
})
) )
class LoadCsvData(forms.Form): class LoadCsvData(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите CSV файл", label="Выберите CSV файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
'class': 'form-control',
'accept': '.csv'
})
) )
class UploadVchLoad(UploadFileForm): class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={"class": "form-select"}),
'class': 'form-select'
})
) )
@@ -60,9 +58,7 @@ class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={"class": "form-select"}),
'class': 'form-select'
})
) )
# ku_range = forms.ChoiceField( # ku_range = forms.ChoiceField(
# choices=[(9750.0, '9750'), (10750.0, '10750')], # choices=[(9750.0, '9750'), (10750.0, '10750')],
@@ -74,18 +70,22 @@ class VchLinkForm(forms.Form):
label="Разброс по частоте (не используется)", label="Разброс по частоте (не используется)",
required=False, required=False,
initial=0.0, initial=0.0,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(
'class': 'form-control', attrs={
'placeholder': 'Не используется - погрешность определяется автоматически' "class": "form-control",
}) "placeholder": "Не используется - погрешность определяется автоматически",
}
),
) )
value2 = forms.FloatField( value2 = forms.FloatField(
label="Разброс по полосе (в %)", label="Разброс по полосе (в %)",
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(
'class': 'form-control', attrs={
'placeholder': 'Введите погрешность полосы в процентах', "class": "form-control",
'step': '0.1' "placeholder": "Введите погрешность полосы в процентах",
}) "step": "0.1",
}
),
) )
@@ -106,10 +106,7 @@ class NewEventForm(forms.Form):
# ) # )
file = forms.FileField( file = forms.FileField(
label="Выберите файл", label="Выберите файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
'class': 'form-control',
'accept': '.xlsx,.xls'
})
) )
@@ -117,53 +114,43 @@ class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования""" """Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
REGION_CHOICES = [ REGION_CHOICES = [
('europe', 'Европа'), ("europe", "Европа"),
('asia', 'Азия'), ("asia", "Азия"),
('america', 'Америка'), ("america", "Америка"),
('atlantic', 'Атлантика'), ("atlantic", "Атлантика"),
] ]
satellites = forms.ModelMultipleChoiceField( satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'), queryset=Satellite.objects.all().order_by("name"),
label="Выберите спутники", label="Выберите спутники",
widget=forms.SelectMultiple(attrs={ widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
'class': 'form-select',
'size': '10'
}),
required=True, required=True,
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников" help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников",
) )
regions = forms.MultipleChoiceField( regions = forms.MultipleChoiceField(
choices=REGION_CHOICES, choices=REGION_CHOICES,
label="Выберите регионы", label="Выберите регионы",
widget=forms.SelectMultiple(attrs={ widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "4"}),
'class': 'form-select',
'size': '4'
}),
required=True, required=True,
initial=['europe', 'asia', 'america', 'atlantic'], initial=["europe", "asia", "america", "atlantic"],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов" help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов",
) )
use_cache = forms.BooleanField( use_cache = forms.BooleanField(
label="Использовать кеширование", label="Использовать кеширование",
required=False, required=False,
initial=True, initial=True,
widget=forms.CheckboxInput(attrs={ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
'class': 'form-check-input' help_text="Использовать кешированные данные (ускоряет повторные запросы)",
}),
help_text="Использовать кешированные данные (ускоряет повторные запросы)"
) )
force_refresh = forms.BooleanField( force_refresh = forms.BooleanField(
label="Принудительно обновить данные", label="Принудительно обновить данные",
required=False, required=False,
initial=False, initial=False,
widget=forms.CheckboxInput(attrs={ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
'class': 'form-check-input' help_text="Игнорировать кеш и получить свежие данные с сайта",
}),
help_text="Игнорировать кеш и получить свежие данные с сайта"
) )
@@ -171,26 +158,22 @@ class LinkLyngsatForm(forms.Form):
"""Форма для привязки источников LyngSat к объектам""" """Форма для привязки источников LyngSat к объектам"""
satellites = forms.ModelMultipleChoiceField( satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'), queryset=Satellite.objects.all().order_by("name"),
label="Выберите спутники", label="Выберите спутники",
widget=forms.SelectMultiple(attrs={ widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
'class': 'form-select',
'size': '10'
}),
required=False, required=False,
help_text="Оставьте пустым для обработки всех спутников" help_text="Оставьте пустым для обработки всех спутников",
) )
frequency_tolerance = forms.FloatField( frequency_tolerance = forms.FloatField(
label="Допуск по частоте (МГц)", label="Допуск по частоте (МГц)",
initial=0.5, initial=0.5,
min_value=0, min_value=0,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}),
'class': 'form-control', help_text="Допустимое отклонение частоты при сравнении",
'step': '0.1'
}),
help_text="Допустимое отклонение частоты при сравнении"
) )
class ParameterForm(forms.ModelForm): class ParameterForm(forms.ModelForm):
""" """
Форма для создания и редактирования параметров ВЧ загрузки. Форма для создания и редактирования параметров ВЧ загрузки.
@@ -201,73 +184,88 @@ class ParameterForm(forms.ModelForm):
class Meta: class Meta:
model = Parameter model = Parameter
fields = [ fields = [
'id_satellite', 'frequency', 'freq_range', 'polarization', "id_satellite",
'bod_velocity', 'modulation', 'snr', 'standard' "frequency",
"freq_range",
"polarization",
"bod_velocity",
"modulation",
"snr",
"standard",
] ]
widgets = { widgets = {
'id_satellite': forms.Select(attrs={ "id_satellite": forms.Select(
'class': 'form-select', attrs={"class": "form-select", "required": True}
'required': True ),
}), "frequency": forms.NumberInput(
'frequency': forms.NumberInput(attrs={ attrs={
'class': 'form-control', "class": "form-control",
'step': '0.000001', "step": "0.000001",
'min': '0', "min": "0",
'max': '50000', "max": "50000",
'placeholder': 'Введите частоту в МГц' "placeholder": "Введите частоту в МГц",
}), }
'freq_range': forms.NumberInput(attrs={ ),
'class': 'form-control', "freq_range": forms.NumberInput(
'step': '0.000001', attrs={
'min': '0', "class": "form-control",
'max': '1000', "step": "0.000001",
'placeholder': 'Введите полосу частот в МГц' "min": "0",
}), "max": "1000",
'bod_velocity': forms.NumberInput(attrs={ "placeholder": "Введите полосу частот в МГц",
'class': 'form-control', }
'step': '0.001', ),
'min': '0', "bod_velocity": forms.NumberInput(
'placeholder': 'Введите символьную скорость в БОД' attrs={
}), "class": "form-control",
'snr': forms.NumberInput(attrs={ "step": "0.001",
'class': 'form-control', "min": "0",
'step': '0.001', "placeholder": "Введите символьную скорость в БОД",
'min': '-50', }
'max': '100', ),
'placeholder': 'Введите ОСШ в дБ' "snr": forms.NumberInput(
}), attrs={
'polarization': forms.Select(attrs={'class': 'form-select'}), "class": "form-control",
'modulation': forms.Select(attrs={'class': 'form-select'}), "step": "0.001",
'standard': forms.Select(attrs={'class': 'form-select'}), "min": "-50",
"max": "100",
"placeholder": "Введите ОСШ в дБ",
}
),
"polarization": forms.Select(attrs={"class": "form-select"}),
"modulation": forms.Select(attrs={"class": "form-select"}),
"standard": forms.Select(attrs={"class": "form-select"}),
} }
labels = { labels = {
'id_satellite': 'Спутник', "id_satellite": "Спутник",
'frequency': 'Частота (МГц)', "frequency": "Частота (МГц)",
'freq_range': 'Полоса частот (МГц)', "freq_range": "Полоса частот (МГц)",
'polarization': 'Поляризация', "polarization": "Поляризация",
'bod_velocity': 'Символьная скорость (БОД)', "bod_velocity": "Символьная скорость (БОД)",
'modulation': 'Модуляция', "modulation": "Модуляция",
'snr': 'ОСШ (дБ)', "snr": "ОСШ (дБ)",
'standard': 'Стандарт', "standard": "Стандарт",
} }
help_texts = { help_texts = {
'frequency': 'Частота в диапазоне от 0 до 50000 МГц', "frequency": "Частота в диапазоне от 0 до 50000 МГц",
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц', "freq_range": "Полоса частот в диапазоне от 0 до 1000 МГц",
'bod_velocity': 'Символьная скорость должна быть положительной', "bod_velocity": "Символьная скорость должна быть положительной",
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ', "snr": "Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Динамически загружаем choices для select полей # Динамически загружаем choices для select полей
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name') self.fields["id_satellite"].queryset = Satellite.objects.all().order_by("name")
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name') self.fields["polarization"].queryset = Polarization.objects.all().order_by(
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name') "name"
self.fields['standard'].queryset = Standard.objects.all().order_by('name') )
self.fields["modulation"].queryset = Modulation.objects.all().order_by("name")
self.fields["standard"].queryset = Standard.objects.all().order_by("name")
# Делаем спутник обязательным полем # Делаем спутник обязательным полем
self.fields['id_satellite'].required = True self.fields["id_satellite"].required = True
def clean(self): def clean(self):
""" """
@@ -276,42 +274,54 @@ class ParameterForm(forms.ModelForm):
Проверяет соотношение между частотой, полосой частот и символьной скоростью. Проверяет соотношение между частотой, полосой частот и символьной скоростью.
""" """
cleaned_data = super().clean() cleaned_data = super().clean()
frequency = cleaned_data.get('frequency') frequency = cleaned_data.get("frequency")
freq_range = cleaned_data.get('freq_range') freq_range = cleaned_data.get("freq_range")
bod_velocity = cleaned_data.get('bod_velocity') bod_velocity = cleaned_data.get("bod_velocity")
# Проверка что частота больше полосы частот # Проверка что частота больше полосы частот
if frequency and freq_range: if frequency and freq_range:
if freq_range > frequency: if freq_range > frequency:
self.add_error('freq_range', 'Полоса частот не может быть больше частоты') self.add_error(
"freq_range", "Полоса частот не может быть больше частоты"
)
# Проверка что символьная скорость соответствует полосе частот # Проверка что символьная скорость соответствует полосе частот
if bod_velocity and freq_range: if bod_velocity and freq_range:
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот') self.add_error(
"bod_velocity",
"Символьная скорость не может превышать полосу частот",
)
return cleaned_data return cleaned_data
class GeoForm(forms.ModelForm): class GeoForm(forms.ModelForm):
class Meta: class Meta:
model = Geo model = Geo
fields = ['location', 'comment', 'is_average', 'mirrors'] fields = ["location", "comment", "is_average", "mirrors"]
widgets = { widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}), "location": forms.TextInput(attrs={"class": "form-control"}),
'comment': forms.TextInput(attrs={'class': 'form-control'}), "comment": forms.TextInput(attrs={"class": "form-control"}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}), "is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}), "mirrors": CheckboxSelectMultipleWidget(
attrs={
"id": "id_geo-mirrors",
"placeholder": "Выберите спутники...",
}
),
} }
labels = { labels = {
'location': 'Местоположение', "location": "Местоположение",
'comment': 'Комментарий', "comment": "Комментарий",
'is_average': 'Усреднённое', "is_average": "Усреднённое",
'mirrors': 'Спутники-зеркала, использованные для приёма', "mirrors": "Спутники-зеркала, использованные для приёма",
} }
help_texts = { help_texts = {
'mirrors': 'Начните вводить название спутника для поиска', "mirrors": "Выберите спутники из списка",
} }
class ObjItemForm(forms.ModelForm): class ObjItemForm(forms.ModelForm):
""" """
Форма для создания и редактирования объектов (источников сигнала). Форма для создания и редактирования объектов (источников сигнала).
@@ -322,25 +332,27 @@ class ObjItemForm(forms.ModelForm):
class Meta: class Meta:
model = ObjItem model = ObjItem
fields = ['name'] fields = ["name"]
widgets = { widgets = {
'name': forms.TextInput(attrs={ "name": forms.TextInput(
'class': 'form-control', attrs={
'placeholder': 'Введите название объекта', "class": "form-control",
'maxlength': '100' "placeholder": "Введите название объекта",
}), "maxlength": "100",
}
),
} }
labels = { labels = {
'name': 'Название объекта', "name": "Название объекта",
} }
help_texts = { help_texts = {
'name': 'Уникальное название объекта/источника сигнала', "name": "Уникальное название объекта/источника сигнала",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Делаем поле name необязательным, так как оно может быть пустым # Делаем поле name необязательным, так как оно может быть пустым
self.fields['name'].required = False self.fields["name"].required = False
def clean_name(self): def clean_name(self):
""" """
@@ -348,7 +360,7 @@ class ObjItemForm(forms.ModelForm):
Проверяет что название не состоит только из пробелов. Проверяет что название не состоит только из пробелов.
""" """
name = self.cleaned_data.get('name') name = self.cleaned_data.get("name")
if name: if name:
# Удаляем лишние пробелы # Удаляем лишние пробелы
@@ -356,6 +368,165 @@ class ObjItemForm(forms.ModelForm):
# Проверяем что после удаления пробелов что-то осталось # Проверяем что после удаления пробелов что-то осталось
if not name: if not name:
raise forms.ValidationError('Название не может состоять только из пробелов') raise forms.ValidationError(
"Название не может состоять только из пробелов"
)
return name return name
class SourceForm(forms.ModelForm):
"""Form for editing Source model with 4 coordinate fields."""
# Координаты ГЛ (coords_average)
average_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта ГЛ",
)
average_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота ГЛ",
)
# Координаты Кубсата (coords_kupsat)
kupsat_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта Кубсата",
)
kupsat_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота Кубсата",
)
# Координаты оперативников (coords_valid)
valid_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта оперативников",
)
valid_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота оперативников",
)
# Координаты справочные (coords_reference)
reference_latitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
),
label="Широта справочные",
)
reference_longitude = forms.FloatField(
required=False,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"step": "0.000001",
"placeholder": "Долгота",
}
),
label="Долгота справочные",
)
class Meta:
model = Source
fields = [] # Все поля обрабатываются вручную
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Заполняем поля координат из instance
if self.instance and self.instance.pk:
if self.instance.coords_average:
self.fields[
"average_longitude"
].initial = self.instance.coords_average.x
self.fields["average_latitude"].initial = self.instance.coords_average.y
if self.instance.coords_kupsat:
self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x
self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y
if self.instance.coords_valid:
self.fields["valid_longitude"].initial = self.instance.coords_valid.x
self.fields["valid_latitude"].initial = self.instance.coords_valid.y
if self.instance.coords_reference:
self.fields[
"reference_longitude"
].initial = self.instance.coords_reference.x
self.fields[
"reference_latitude"
].initial = self.instance.coords_reference.y
def save(self, commit=True):
from django.contrib.gis.geos import Point
instance = super().save(commit=False)
# Обработка coords_average
avg_lat = self.cleaned_data.get("average_latitude")
avg_lng = self.cleaned_data.get("average_longitude")
if avg_lat is not None and avg_lng is not None:
instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
else:
instance.coords_average = None
# Обработка coords_kupsat
kup_lat = self.cleaned_data.get("kupsat_latitude")
kup_lng = self.cleaned_data.get("kupsat_longitude")
if kup_lat is not None and kup_lng is not None:
instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326)
else:
instance.coords_kupsat = None
# Обработка coords_valid
val_lat = self.cleaned_data.get("valid_latitude")
val_lng = self.cleaned_data.get("valid_longitude")
if val_lat is not None and val_lng is not None:
instance.coords_valid = Point(val_lng, val_lat, srid=4326)
else:
instance.coords_valid = None
# Обработка coords_reference
ref_lat = self.cleaned_data.get("reference_latitude")
ref_lng = self.cleaned_data.get("reference_longitude")
if ref_lat is not None and ref_lng is not None:
instance.coords_reference = Point(ref_lng, ref_lat, srid=4326)
else:
instance.coords_reference = None
if commit:
instance.save()
return instance

View File

@@ -0,0 +1,160 @@
.checkbox-multiselect-wrapper {
position: relative;
width: 100%;
}
.multiselect-input-container {
position: relative;
display: flex;
align-items: center;
min-height: 38px;
border: 1px solid #ced4da;
border-radius: 0.25rem;
background-color: #fff;
cursor: text;
padding: 4px 30px 4px 4px;
flex-wrap: wrap;
gap: 4px;
}
.multiselect-input-container:focus-within {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.multiselect-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 0 0 auto;
}
.multiselect-tag {
display: inline-flex;
align-items: center;
background-color: #e9ecef;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 2px 8px;
font-size: 0.875rem;
line-height: 1.5;
white-space: nowrap;
}
.multiselect-tag-remove {
margin-left: 6px;
cursor: pointer;
color: #6c757d;
font-weight: bold;
border: none;
background: none;
padding: 0;
font-size: 1rem;
line-height: 1;
}
.multiselect-tag-remove:hover {
color: #dc3545;
}
.multiselect-search {
flex: 1 1 auto;
min-width: 120px;
border: none;
outline: none;
padding: 4px;
font-size: 0.875rem;
}
.multiselect-search:focus {
box-shadow: none;
}
.multiselect-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
color: #6c757d;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: none;
}
.multiselect-clear:hover {
color: #dc3545;
}
.multiselect-input-container.has-selections .multiselect-clear {
display: block;
}
.multiselect-dropdown {
position: absolute;
left: 0;
right: 0;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.25rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
max-height: 300px;
overflow-y: auto;
z-index: 1000;
display: none;
}
/* Открытие вверх (по умолчанию) */
.multiselect-dropdown {
bottom: 100%;
margin-bottom: 2px;
}
/* Открытие вниз (если места сверху недостаточно) */
.multiselect-dropdown.dropdown-below {
bottom: auto;
top: 100%;
margin-top: 2px;
margin-bottom: 0;
}
.multiselect-dropdown.show {
display: block;
}
.multiselect-options {
padding: 4px 0;
}
.multiselect-option {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
margin: 0;
transition: background-color 0.15s ease-in-out;
}
.multiselect-option:hover {
background-color: #f8f9fa;
}
.multiselect-option input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
.multiselect-option .option-label {
flex: 1;
user-select: none;
}
.multiselect-option.hidden {
display: none;
}

View File

@@ -0,0 +1,120 @@
/**
* Checkbox Select Multiple Widget
* Provides a multi-select dropdown with checkboxes and tag display
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialize all checkbox multiselect widgets
document.querySelectorAll('.checkbox-multiselect-wrapper').forEach(function(wrapper) {
initCheckboxMultiselect(wrapper);
});
});
function initCheckboxMultiselect(wrapper) {
const widgetId = wrapper.dataset.widgetId;
const inputContainer = wrapper.querySelector('.multiselect-input-container');
const searchInput = wrapper.querySelector('.multiselect-search');
const dropdown = wrapper.querySelector('.multiselect-dropdown');
const tagsContainer = wrapper.querySelector('.multiselect-tags');
const clearButton = wrapper.querySelector('.multiselect-clear');
const checkboxes = wrapper.querySelectorAll('input[type="checkbox"]');
// Show dropdown when clicking on input container
inputContainer.addEventListener('click', function(e) {
if (e.target !== clearButton) {
positionDropdown();
dropdown.classList.add('show');
searchInput.focus();
}
});
// Position dropdown (up or down based on available space)
function positionDropdown() {
const rect = inputContainer.getBoundingClientRect();
const spaceAbove = rect.top;
const spaceBelow = window.innerHeight - rect.bottom;
const dropdownHeight = 300; // max-height from CSS
// If more space below and enough space, open downward
if (spaceBelow > spaceAbove && spaceBelow >= dropdownHeight) {
dropdown.classList.add('dropdown-below');
} else {
dropdown.classList.remove('dropdown-below');
}
}
// Hide dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Search functionality
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const options = wrapper.querySelectorAll('.multiselect-option');
options.forEach(function(option) {
const label = option.querySelector('.option-label').textContent.toLowerCase();
if (label.includes(searchTerm)) {
option.classList.remove('hidden');
} else {
option.classList.add('hidden');
}
});
});
// Handle checkbox changes
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
updateTags();
});
});
// Clear all button
clearButton.addEventListener('click', function(e) {
e.stopPropagation();
checkboxes.forEach(function(checkbox) {
checkbox.checked = false;
});
updateTags();
});
// Update tags display
function updateTags() {
tagsContainer.innerHTML = '';
let hasSelections = false;
checkboxes.forEach(function(checkbox) {
if (checkbox.checked) {
hasSelections = true;
const tag = document.createElement('div');
tag.className = 'multiselect-tag';
tag.innerHTML = `
<span>${checkbox.dataset.label}</span>
<button type="button" class="multiselect-tag-remove" data-value="${checkbox.value}">×</button>
`;
// Remove tag on click
tag.querySelector('.multiselect-tag-remove').addEventListener('click', function(e) {
e.stopPropagation();
checkbox.checked = false;
updateTags();
});
tagsContainer.appendChild(tag);
}
});
// Show/hide clear button
if (hasSelections) {
inputContainer.classList.add('has-selections');
} else {
inputContainer.classList.remove('has-selections');
}
}
// Initialize tags on load
updateTags();
}

View File

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

View File

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

View File

@@ -6,27 +6,93 @@
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %} {% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.css' %}">
<style> <style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; } .form-section {
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; } margin-bottom: 2rem;
.btn-action { margin-right: 0.5rem; } border: 1px solid #dee2e6;
.dynamic-form { border: 1px dashed #ced4da; padding: 1rem; margin-top: 1rem; border-radius: 0.25rem; } border-radius: 0.25rem;
.dynamic-form-header { display: flex; justify-content: space-between; align-items: center; } padding: 1rem;
.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; } .form-section-header {
.form-check-input { margin-top: 0.25rem; } border-bottom: 1px solid #dee2e6;
.datetime-group { display: flex; gap: 1rem; } padding-bottom: 0.5rem;
.datetime-group > div { flex: 1; } margin-bottom: 1rem;
#map { height: 500px; width: 100%; margin-bottom: 1rem; } }
.map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; } .btn-action {
margin-right: 0.5rem;
}
.dynamic-form {
border: 1px dashed #ced4da;
padding: 1rem;
margin-top: 1rem;
border-radius: 0.25rem;
}
.dynamic-form-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.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;
}
.form-check-input {
margin-top: 0.25rem;
}
.datetime-group {
display: flex;
gap: 1rem;
}
.datetime-group>div {
flex: 1;
}
#map {
height: 500px;
width: 100%;
margin-bottom: 1rem;
}
.map-container {
margin-bottom: 1rem;
}
.coord-sync-group {
border: 1px solid #dee2e6;
padding: 0.75rem;
border-radius: 0.25rem;
}
.map-controls { .map-controls {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 1rem; margin-bottom: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.map-control-btn { .map-control-btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border: 1px solid #ced4da; border: 1px solid #ced4da;
@@ -34,25 +100,43 @@
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;
} }
.map-control-btn.active { .map-control-btn.active {
background-color: #e9ecef; background-color: #e9ecef;
border-color: #dee2e6; border-color: #dee2e6;
} }
.map-control-btn.edit { .map-control-btn.edit {
background-color: #fff3cd; background-color: #fff3cd;
border-color: #ffeeba; border-color: #ffeeba;
} }
.map-control-btn.save { .map-control-btn.save {
background-color: #d1ecf1; background-color: #d1ecf1;
border-color: #bee5eb; border-color: #bee5eb;
} }
.map-control-btn.cancel { .map-control-btn.cancel {
background-color: #f8d7da; background-color: #f8d7da;
border-color: #f5c6cb; border-color: #f5c6cb;
} }
.leaflet-marker-icon { .leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3)); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
} }
/* Select2 custom styling */
.select2-container--default .select2-selection--multiple {
border: 1px solid #ced4da;
border-radius: 0.25rem;
min-height: 38px;
}
.select2-container--default.select2-container--focus .select2-selection--multiple {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
</style> </style>
{% endblock %} {% endblock %}
@@ -65,10 +149,12 @@
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button> <button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
{% if object %} {% if object %}
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a> <a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-danger btn-action">Удалить</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a> <a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-secondary btn-action">Назад</a>
</div> </div>
</div> </div>
</div> </div>
@@ -186,16 +272,16 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="id_geo_latitude" class="form-label">Широта:</label> <label for="id_geo_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control" <input type="number" step="0.000001" class="form-control" id="id_geo_latitude"
id="id_geo_latitude" name="geo_latitude" name="geo_latitude"
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}"> value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="id_geo_longitude" class="form-label">Долгота:</label> <label for="id_geo_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control" <input type="number" step="0.000001" class="form-control" id="id_geo_longitude"
id="id_geo_longitude" name="geo_longitude" name="geo_longitude"
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}"> value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
</div> </div>
</div> </div>
@@ -218,14 +304,12 @@
<div class="datetime-group"> <div class="datetime-group">
<div> <div>
<label for="id_timestamp_date" class="form-label">Дата:</label> <label for="id_timestamp_date" class="form-label">Дата:</label>
<input type="date" class="form-control" <input type="date" class="form-control" id="id_timestamp_date" name="timestamp_date"
id="id_timestamp_date" name="timestamp_date"
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}"> value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
</div> </div>
<div> <div>
<label for="id_timestamp_time" class="form-label">Время:</label> <label for="id_timestamp_time" class="form-label">Время:</label>
<input type="time" class="form-control" <input type="time" class="form-control" id="id_timestamp_time" name="timestamp_time"
id="id_timestamp_time" name="timestamp_time"
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}"> value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
</div> </div>
</div> </div>
@@ -253,6 +337,9 @@
{% leaflet_css %} {% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script> <script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<!-- Подключаем кастомный виджет для мультивыбора -->
<script src="{% static 'js/checkbox-select-multiple.js' %}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Инициализация карты // Инициализация карты

View File

@@ -70,7 +70,8 @@
<!-- Filter Toggle Button --> <!-- Filter Toggle Button -->
<div> <div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters"> <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры <i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span> <span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button> </button>
@@ -85,7 +86,8 @@
<!-- Selected Items Counter Button --> <!-- Selected Items Counter Button -->
<div> <div>
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas"> <button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
<i class="bi bi-list-check"></i> Список <i class="bi bi-list-check"></i> Список
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span> <span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
</button> </button>
@@ -202,52 +204,19 @@
</select> </select>
</div> </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 --> <!-- Source Type Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Тип источника:</label> <label class="form-label">Тип источника:</label>
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1" <input class="form-check-input" type="checkbox" name="has_source_type"
value="1" {% if has_source_type == '1' %}checked{% endif %}> 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> <label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0" <input class="form-check-input" type="checkbox" name="has_source_type"
value="0" {% if has_source_type == '0' %}checked{% endif %}> 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> <label class="form-check-label" for="has_source_type_0">Нет</label>
</div> </div>
</div> </div>
@@ -258,13 +227,13 @@
<label class="form-label">Sigma:</label> <label class="form-label">Sigma:</label>
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" <input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" value="1"
value="1" {% if has_sigma == '1' %}checked{% endif %}> {% if has_sigma == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_1">Есть</label> <label class="form-check-label" for="has_sigma_1">Есть</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" <input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" value="0"
value="0" {% if has_sigma == '0' %}checked{% endif %}> {% if has_sigma == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_0">Нет</label> <label class="form-check-label" for="has_sigma_0">Нет</label>
</div> </div>
</div> </div>
@@ -289,8 +258,8 @@
</div> </div>
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1" <input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ date_from|default:'' }}"> placeholder="От" value="{{ date_from|default:'' }}">
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm" <input type="date" name="date_to" id="date_to" class="form-control form-control-sm" placeholder="До"
placeholder="До" value="{{ date_to|default:'' }}"> value="{{ date_to|default:'' }}">
</div> </div>
<!-- Apply Filters and Reset Buttons --> <!-- Apply Filters and Reset Buttons -->
@@ -316,6 +285,7 @@
</th> </th>
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %} {% 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="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="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="freq_range" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
@@ -341,11 +311,22 @@
{% for item in processed_objects %} {% for item in processed_objects %}
<tr> <tr>
<td class="text-center"> <td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox" <input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
value="{{ item.id }}">
</td> </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>
<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.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.frequency }}</td>
<td>{{ item.freq_range }}</td> <td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td> <td>{{ item.polarization }}</td>
@@ -364,7 +345,8 @@
<td>{{ item.standard }}</td> <td>{{ item.standard }}</td>
<td> <td>
{% if item.obj.lyngsat_source %} {% if item.obj.lyngsat_source %}
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;"> <a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
<i class="bi bi-tv"></i> ТВ <i class="bi bi-tv"></i> ТВ
</a> </a>
{% else %} {% else %}
@@ -373,7 +355,9 @@
</td> </td>
<td> <td>
{% if item.has_sigma %} {% 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 }}"> <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 }} <i class="bi bi-graph-up"></i> {{ item.sigma_info }}
</a> </a>
{% else %} {% else %}
@@ -720,8 +704,8 @@
// Initialize column visibility - hide creation columns by default // Initialize column visibility - hide creation columns by default
function initColumnVisibility() { function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="14"]'); const creationDateCheckbox = document.querySelector('input[data-column="15"]');
const creationUserCheckbox = document.querySelector('input[data-column="15"]'); const creationUserCheckbox = document.querySelector('input[data-column="16"]');
if (creationDateCheckbox) { if (creationDateCheckbox) {
creationDateCheckbox.checked = false; creationDateCheckbox.checked = false;
toggleColumn(creationDateCheckbox); toggleColumn(creationDateCheckbox);
@@ -733,9 +717,9 @@
} }
// Hide comment, is_average, and standard columns by default // Hide comment, is_average, and standard columns by default
const commentCheckbox = document.querySelector('input[data-column="16"]'); const commentCheckbox = document.querySelector('input[data-column="17"]');
const isAverageCheckbox = document.querySelector('input[data-column="17"]'); const isAverageCheckbox = document.querySelector('input[data-column="18"]');
const standardCheckbox = document.querySelector('input[data-column="18"]'); const standardCheckbox = document.querySelector('input[data-column="19"]');
if (commentCheckbox) { if (commentCheckbox) {
commentCheckbox.checked = false; commentCheckbox.checked = false;
@@ -904,22 +888,21 @@
id: itemId, id: itemId,
name: row.cells[1].textContent, name: row.cells[1].textContent,
satellite: row.cells[2].textContent, satellite: row.cells[2].textContent,
frequency: row.cells[3].textContent, transponder: row.cells[3].textContent,
freq_range: row.cells[4].textContent, frequency: row.cells[4].textContent,
polarization: row.cells[5].textContent, freq_range: row.cells[5].textContent,
bod_velocity: row.cells[6].textContent, polarization: row.cells[6].textContent,
modulation: row.cells[7].textContent, bod_velocity: row.cells[7].textContent,
snr: row.cells[8].textContent, modulation: row.cells[8].textContent,
geo_timestamp: row.cells[9].textContent, snr: row.cells[9].textContent,
geo_location: row.cells[10].textContent, geo_timestamp: row.cells[10].textContent,
geo_coords: row.cells[11].textContent, geo_location: row.cells[11].textContent,
kupsat_coords: row.cells[12].textContent, geo_coords: row.cells[12].textContent,
valid_coords: row.cells[13].textContent, updated_at: row.cells[13].textContent,
updated_at: row.cells[12].textContent, updated_by: row.cells[14].textContent,
updated_by: row.cells[13].textContent, created_at: row.cells[15].textContent,
created_at: row.cells[14].textContent, created_by: row.cells[16].textContent,
created_by: row.cells[15].textContent, mirrors: row.cells[22].textContent
mirrors: row.cells[21].textContent
}; };
window.selectedItems.push(rowData); window.selectedItems.push(rowData);
@@ -966,6 +949,7 @@
</td> </td>
<td>${item.name}</td> <td>${item.name}</td>
<td>${item.satellite}</td> <td>${item.satellite}</td>
<td>${item.transponder}</td>
<td>${item.frequency}</td> <td>${item.frequency}</td>
<td>${item.freq_range}</td> <td>${item.freq_range}</td>
<td>${item.polarization}</td> <td>${item.polarization}</td>
@@ -975,8 +959,6 @@
<td>${item.geo_timestamp}</td> <td>${item.geo_timestamp}</td>
<td>${item.geo_location}</td> <td>${item.geo_location}</td>
<td>${item.geo_coords}</td> <td>${item.geo_coords}</td>
<td>${item.kupsat_coords}</td>
<td>${item.valid_coords}</td>
<td>${item.updated_at}</td> <td>${item.updated_at}</td>
<td>${item.updated_by}</td> <td>${item.updated_by}</td>
<td>${item.created_at}</td> <td>${item.created_at}</td>
@@ -1056,7 +1038,8 @@
<h5 class="modal-title" id="lyngsatModalLabel"> <h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat <i class="bi bi-tv"></i> Данные источника LyngSat
</h5> </h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
</div> </div>
<div class="modal-body" id="lyngsatModalBody"> <div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4"> <div class="text-center py-4">
@@ -1202,5 +1185,128 @@ function showLyngsatModal(lyngsatId) {
`; `;
}); });
} }
// Function to show transponder modal
function showTransponderModal(transponderId) {
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
modal.show();
const modalBody = document.getElementById('transponderModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
fetch(`/api/transponder/${transponderId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных транспондера');
}
return response.json();
})
.then(data => {
let html = `
<div class="container-fluid">
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Название:</td>
<td><strong>${data.name || '-'}</strong></td>
</tr>
<tr>
<td class="text-muted">Спутник:</td>
<td><strong>${data.satellite}</strong></td>
</tr>
<tr>
<td class="text-muted">Зона покрытия:</td>
<td>${data.zone_name || '-'}</td>
</tr>
<tr>
<td class="text-muted">Поляризация:</td>
<td><span class="badge bg-info">${data.polarization}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-broadcast"></i> Частотные параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Downlink:</td>
<td><strong>${data.downlink} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Uplink:</td>
<td><strong>${data.uplink || '-'} ${data.uplink ? 'МГц' : ''}</strong></td>
</tr>
<tr>
<td class="text-muted">Полоса:</td>
<td><strong>${data.frequency_range} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Перенос:</td>
<td>${data.transfer || '-'} ${data.transfer ? 'МГц' : ''}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
`;
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</div>
`;
});
}
</script> </script>
<!-- Transponder Data Modal -->
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="transponderModalLabel">
<i class="bi bi-broadcast"></i> Данные транспондера
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="transponderModalBody">
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
{% load static %}
<div class="checkbox-multiselect-wrapper" data-widget-id="{{ widget.attrs.id }}">
<div class="multiselect-input-container">
<div class="multiselect-tags" id="{{ widget.attrs.id }}_tags"></div>
<input type="text"
class="multiselect-search form-control"
placeholder="{{ widget.attrs.placeholder|default:'Выберите элементы...' }}"
id="{{ widget.attrs.id }}_search"
autocomplete="off">
<button type="button" class="multiselect-clear" id="{{ widget.attrs.id }}_clear" title="Очистить все">×</button>
</div>
<div class="multiselect-dropdown" id="{{ widget.attrs.id }}_dropdown">
<div class="multiselect-options">
{% for group_name, group_choices, group_index in widget.optgroups %}
{% for option in group_choices %}
<label class="multiselect-option">
<input type="checkbox"
name="{{ widget.name }}"
value="{{ option.value }}"
{% if option.selected %}checked{% endif %}
data-label="{{ option.label }}">
<span class="option-label">{{ option.label }}</span>
</label>
{% endfor %}
{% endfor %}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

20
dbapp/mainapp/widgets.py Normal file
View File

@@ -0,0 +1,20 @@
"""
Custom widgets for forms.
"""
from django import forms
from django.utils.safestring import mark_safe
class CheckboxSelectMultipleWidget(forms.CheckboxSelectMultiple):
"""
Custom widget that displays selected items as tags in an input field
with a dropdown containing checkboxes for selection.
"""
template_name = 'mainapp/widgets/checkbox_select_multiple.html'
class Media:
css = {
'all': ('css/checkbox-select-multiple.css',)
}
js = ('js/checkbox-select-multiple.js',)

View File

@@ -23,7 +23,7 @@ class Transponders(models.Model):
blank=True, blank=True,
verbose_name="Название транспондера", verbose_name="Название транспондера",
db_index=True, db_index=True,
help_text="Название транспондера" help_text="Название транспондера",
) )
downlink = models.FloatField( downlink = models.FloatField(
blank=True, blank=True,
@@ -52,14 +52,14 @@ class Transponders(models.Model):
null=True, null=True,
verbose_name="Название зоны", verbose_name="Название зоны",
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера" help_text="Название зоны покрытия транспондера",
) )
snr = models.FloatField( snr = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)" help_text="Полоса частот в МГц (0-1000)",
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
@@ -99,7 +99,7 @@ class Transponders(models.Model):
null=True, null=True,
blank=True, blank=True,
verbose_name="Поляризация", verbose_name="Поляризация",
help_text="Поляризация сигнала" help_text="Поляризация сигнала",
) )
sat_id = models.ForeignKey( sat_id = models.ForeignKey(
Satellite, Satellite,
@@ -107,20 +107,19 @@ class Transponders(models.Model):
related_name="tran_satellite", related_name="tran_satellite",
verbose_name="Спутник", verbose_name="Спутник",
db_index=True, db_index=True,
help_text="Спутник, которому принадлежит транспондер" help_text="Спутник, которому принадлежит транспондер",
) )
# Вычисляемые поля # Вычисляемые поля
transfer = models.GeneratedField( transfer = models.GeneratedField(
expression=ExpressionWrapper( expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')), Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
output_field=models.FloatField()
), ),
output_field=models.FloatField(), output_field=models.FloatField(),
db_persist=True, db_persist=True,
null=True, null=True,
blank=True, blank=True,
verbose_name="Перенос" verbose_name="Перенос",
) )
# def clean(self): # def clean(self):
@@ -143,10 +142,8 @@ class Transponders(models.Model):
class Meta: class Meta:
verbose_name = "Транспондер" verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры" verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink'] ordering = ["sat_id", "downlink"]
indexes = [ indexes = [
models.Index(fields=['sat_id', 'downlink']), models.Index(fields=["sat_id", "downlink"]),
models.Index(fields=['sat_id', 'zone_name']), models.Index(fields=["sat_id", "zone_name"]),
] ]

View File

@@ -2,7 +2,8 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="db-objects-panel" style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;"> <div class="db-objects-panel"
style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<div class="panel-title">Объекты из базы</div> <div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select"> <select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option> <option value="">— Выберите объект —</option>
@@ -10,14 +11,18 @@
<option value="{{ sat.id }}">{{ sat.name }}</option> <option value="{{ sat.id }}">{{ sat.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все точки</button> <button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки транспондеров</button> точки</button>
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()" style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button> <button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки
транспондеров</button>
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()"
style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
</div> </div>
<div class="footprint-control" style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;"> <div class="footprint-control"
style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<div class="panel-title">Области покрытия</div> <div class="panel-title">Области покрытия</div>
<div class="footprint-actions"> <div class="footprint-actions">
<button id="showAllFootprints">Показать все</button> <button id="showAllFootprints">Показать все</button>