Compare commits
2 Commits
8e0d32c307
...
6a26991dc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a26991dc0 | |||
| 5ab6770809 |
@@ -32,13 +32,13 @@ from .models import (
|
||||
ObjItem,
|
||||
CustomUser,
|
||||
Band,
|
||||
Source
|
||||
Source,
|
||||
)
|
||||
from .filters import (
|
||||
GeoKupDistanceFilter,
|
||||
GeoValidDistanceFilter,
|
||||
UniqueToggleFilter,
|
||||
HasSigmaParameterFilter
|
||||
HasSigmaParameterFilter,
|
||||
)
|
||||
|
||||
|
||||
@@ -55,22 +55,24 @@ admin.site.unregister(Group)
|
||||
# Base Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Базовый класс для всех admin моделей.
|
||||
|
||||
|
||||
Предоставляет общую функциональность:
|
||||
- Кнопки сохранения сверху и снизу
|
||||
- Настройка количества элементов на странице
|
||||
- Автоматическое заполнение полей created_by и updated_by
|
||||
"""
|
||||
|
||||
save_on_top = True
|
||||
list_per_page = 50
|
||||
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Автоматически заполняет поля created_by и updated_by при сохранении.
|
||||
|
||||
|
||||
Args:
|
||||
request: HTTP запрос
|
||||
obj: Сохраняемый объект модели
|
||||
@@ -79,20 +81,20 @@ class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
if not change:
|
||||
# При создании нового объекта устанавливаем created_by
|
||||
if hasattr(obj, 'created_by') and not obj.created_by_id:
|
||||
obj.created_by = getattr(request.user, 'customuser', None)
|
||||
|
||||
if hasattr(obj, "created_by") and not obj.created_by_id:
|
||||
obj.created_by = getattr(request.user, "customuser", None)
|
||||
|
||||
# При любом сохранении обновляем updated_by
|
||||
if hasattr(obj, 'updated_by'):
|
||||
obj.updated_by = getattr(request.user, 'customuser', None)
|
||||
|
||||
if hasattr(obj, "updated_by"):
|
||||
obj.updated_by = getattr(request.user, "customuser", None)
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
class CustomUserInline(admin.StackedInline):
|
||||
model = CustomUser
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Дополнительная информация пользователя'
|
||||
verbose_name_plural = "Дополнительная информация пользователя"
|
||||
|
||||
|
||||
class LocationForm(forms.ModelForm):
|
||||
@@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Geo
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.coords:
|
||||
self.fields['latitude_geo'].initial = self.instance.coords[1]
|
||||
self.fields['longitude_geo'].initial = self.instance.coords[0]
|
||||
self.fields["latitude_geo"].initial = self.instance.coords[1]
|
||||
self.fields["longitude_geo"].initial = self.instance.coords[0]
|
||||
# if self.instance and self.instance.coords_kupsat:
|
||||
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
||||
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
||||
@@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
from django.contrib.gis.geos import Point
|
||||
lat = self.cleaned_data.get('latitude_geo')
|
||||
lon = self.cleaned_data.get('longitude_geo')
|
||||
|
||||
lat = self.cleaned_data.get("latitude_geo")
|
||||
lon = self.cleaned_data.get("longitude_geo")
|
||||
if lat is not None and lon is not None:
|
||||
instance.coords = Point(lon, lat, srid=4326)
|
||||
|
||||
@@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline):
|
||||
form = LocationForm
|
||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||
prefetch_related = ("mirrors",)
|
||||
autocomplete_fields = ('mirrors',)
|
||||
autocomplete_fields = ("mirrors",)
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location",
|
||||
# "distance_coords_kup",
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp", "comment",)
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||
}),
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": (
|
||||
"mirrors",
|
||||
"location",
|
||||
# "distance_coords_kup",
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp",
|
||||
"comment",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||
},
|
||||
),
|
||||
# ("Координаты: Кубсат", {
|
||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||
# }),
|
||||
@@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline):
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
inlines = [CustomUserInline]
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
|
||||
@@ -181,80 +195,83 @@ admin.site.register(User, UserAdmin)
|
||||
# Custom Admin Actions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin.action(description="Показать выбранные на карте")
|
||||
def show_on_map(modeladmin, request, queryset):
|
||||
"""
|
||||
Action для отображения выбранных Geo объектов на карте.
|
||||
|
||||
|
||||
Оптимизирован для работы с большим количеством объектов:
|
||||
использует values_list для получения только ID.
|
||||
"""
|
||||
selected_ids = queryset.values_list('id', flat=True)
|
||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}')
|
||||
selected_ids = queryset.values_list("id", flat=True)
|
||||
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse("mainapp:admin_show_map") + f"?ids={ids_str}")
|
||||
|
||||
|
||||
@admin.action(description="Показать выбранные объекты на карте")
|
||||
def show_selected_on_map(modeladmin, request, queryset):
|
||||
"""
|
||||
Action для отображения выбранных ObjItem объектов на карте.
|
||||
|
||||
|
||||
Оптимизирован для работы с большим количеством объектов:
|
||||
использует values_list для получения только ID.
|
||||
"""
|
||||
selected_ids = queryset.values_list('id', flat=True)
|
||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}')
|
||||
selected_ids = queryset.values_list("id", flat=True)
|
||||
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse("mainapp:show_selected_objects_map") + f"?ids={ids_str}")
|
||||
|
||||
|
||||
@admin.action(description="Экспортировать выбранные объекты в CSV")
|
||||
def export_objects_to_csv(modeladmin, request, queryset):
|
||||
"""
|
||||
Action для экспорта выбранных ObjItem объектов в CSV формат.
|
||||
|
||||
|
||||
Оптимизирован с использованием select_related и prefetch_related
|
||||
для минимизации количества запросов к БД.
|
||||
"""
|
||||
import csv
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
# Оптимизируем queryset
|
||||
queryset = queryset.select_related(
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user',
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation'
|
||||
"geo_obj",
|
||||
"created_by__user",
|
||||
"updated_by__user",
|
||||
"parameter_obj",
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
)
|
||||
|
||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||
response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"'
|
||||
response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel
|
||||
|
||||
|
||||
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
||||
response["Content-Disposition"] = 'attachment; filename="objitems_export.csv"'
|
||||
response.write("\ufeff") # UTF-8 BOM для корректного отображения в Excel
|
||||
|
||||
writer = csv.writer(response)
|
||||
writer.writerow([
|
||||
'Название',
|
||||
'Спутник',
|
||||
'Частота (МГц)',
|
||||
'Полоса (МГц)',
|
||||
'Поляризация',
|
||||
'Модуляция',
|
||||
'ОСШ',
|
||||
'Координаты геолокации',
|
||||
'Координаты Кубсата',
|
||||
'Координаты оперативного отдела',
|
||||
'Расстояние Гео-Куб (км)',
|
||||
'Расстояние Гео-Опер (км)',
|
||||
'Дата создания',
|
||||
'Дата обновления'
|
||||
])
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
"Название",
|
||||
"Спутник",
|
||||
"Частота (МГц)",
|
||||
"Полоса (МГц)",
|
||||
"Поляризация",
|
||||
"Модуляция",
|
||||
"ОСШ",
|
||||
"Координаты геолокации",
|
||||
"Координаты Кубсата",
|
||||
"Координаты оперативного отдела",
|
||||
"Расстояние Гео-Куб (км)",
|
||||
"Расстояние Гео-Опер (км)",
|
||||
"Дата создания",
|
||||
"Дата обновления",
|
||||
]
|
||||
)
|
||||
|
||||
for obj in queryset:
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
geo = obj.geo_obj
|
||||
|
||||
|
||||
# Форматирование координат
|
||||
def format_coords(coords):
|
||||
if not coords:
|
||||
@@ -263,24 +280,30 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
|
||||
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||
return f"{lat_str} {lon_str}"
|
||||
|
||||
writer.writerow([
|
||||
obj.name,
|
||||
param.id_satellite.name if param and param.id_satellite else "-",
|
||||
param.frequency if param else "-",
|
||||
param.freq_range if param else "-",
|
||||
param.polarization.name if param and param.polarization else "-",
|
||||
param.modulation.name if param and param.modulation else "-",
|
||||
param.snr if param else "-",
|
||||
format_coords(geo) if geo and geo.coords else "-",
|
||||
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
||||
format_coords(geo) if geo and geo.coords_valid else "-",
|
||||
round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-",
|
||||
round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-",
|
||||
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
||||
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-"
|
||||
])
|
||||
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
obj.name,
|
||||
param.id_satellite.name if param and param.id_satellite else "-",
|
||||
param.frequency if param else "-",
|
||||
param.freq_range if param else "-",
|
||||
param.polarization.name if param and param.polarization else "-",
|
||||
param.modulation.name if param and param.modulation else "-",
|
||||
param.snr if param else "-",
|
||||
format_coords(geo) if geo and geo.coords else "-",
|
||||
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
||||
format_coords(geo) if geo and geo.coords_valid else "-",
|
||||
round(geo.distance_coords_kup, 3)
|
||||
if geo and geo.distance_coords_kup
|
||||
else "-",
|
||||
round(geo.distance_coords_valid, 3)
|
||||
if geo and geo.distance_coords_valid
|
||||
else "-",
|
||||
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
||||
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-",
|
||||
]
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
# Inline Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ParameterInline(admin.StackedInline):
|
||||
"""Inline для редактирования параметра объекта."""
|
||||
|
||||
model = Parameter
|
||||
extra = 0
|
||||
max_num = 1
|
||||
@@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline):
|
||||
verbose_name = "ВЧ загрузка"
|
||||
verbose_name_plural = "ВЧ загрузка"
|
||||
fields = (
|
||||
'id_satellite',
|
||||
'frequency',
|
||||
'freq_range',
|
||||
'polarization',
|
||||
'modulation',
|
||||
'bod_velocity',
|
||||
'snr',
|
||||
'standard'
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
"freq_range",
|
||||
"polarization",
|
||||
"modulation",
|
||||
"bod_velocity",
|
||||
"snr",
|
||||
"standard",
|
||||
)
|
||||
autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard')
|
||||
autocomplete_fields = ("id_satellite", "polarization", "modulation", "standard")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin.register(SigmaParMark)
|
||||
class SigmaParMarkAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели SigmaParMark."""
|
||||
|
||||
list_display = ("mark", "timestamp")
|
||||
search_fields = ("mark",)
|
||||
ordering = ("-timestamp",)
|
||||
list_filter = (
|
||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
|
||||
|
||||
|
||||
@admin.register(Polarization)
|
||||
class PolarizationAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Polarization."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin):
|
||||
@admin.register(Modulation)
|
||||
class ModulationAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Modulation."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin):
|
||||
@admin.register(Standard)
|
||||
class StandardAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Standard."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin):
|
||||
class SigmaParameterInline(admin.StackedInline):
|
||||
model = SigmaParameter
|
||||
extra = 0
|
||||
autocomplete_fields = ['mark']
|
||||
autocomplete_fields = ["mark"]
|
||||
readonly_fields = (
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
)
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -364,12 +393,13 @@ class SigmaParameterInline(admin.StackedInline):
|
||||
class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели Parameter.
|
||||
|
||||
|
||||
Оптимизирована для работы с большим количеством параметров:
|
||||
- Использует select_related для оптимизации запросов
|
||||
- Предоставляет фильтры по основным характеристикам
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
@@ -380,11 +410,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"snr",
|
||||
"standard",
|
||||
"related_objitem",
|
||||
"sigma_parameter"
|
||||
"sigma_parameter",
|
||||
)
|
||||
list_display_links = ("frequency", "id_satellite")
|
||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem")
|
||||
|
||||
list_select_related = (
|
||||
"polarization",
|
||||
"modulation",
|
||||
"standard",
|
||||
"id_satellite",
|
||||
"objitem",
|
||||
)
|
||||
|
||||
list_filter = (
|
||||
HasSigmaParameterFilter,
|
||||
("objitem", MultiSelectRelatedDropdownFilter),
|
||||
@@ -396,7 +432,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
("freq_range", NumericRangeFilterBuilder()),
|
||||
("snr", NumericRangeFilterBuilder()),
|
||||
)
|
||||
|
||||
|
||||
search_fields = (
|
||||
"id_satellite__name",
|
||||
"frequency",
|
||||
@@ -408,16 +444,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"standard__name",
|
||||
"objitem__name",
|
||||
)
|
||||
|
||||
|
||||
ordering = ("-frequency",)
|
||||
autocomplete_fields = ("objitem",)
|
||||
inlines = [SigmaParameterInline]
|
||||
|
||||
def related_objitem(self, obj):
|
||||
"""Отображает связанный ObjItem."""
|
||||
if hasattr(obj, 'objitem') and obj.objitem:
|
||||
if hasattr(obj, "objitem") and obj.objitem:
|
||||
return obj.objitem.name
|
||||
return "-"
|
||||
|
||||
related_objitem.short_description = "Объект"
|
||||
related_objitem.admin_order_field = "objitem__name"
|
||||
|
||||
@@ -427,19 +464,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
if sigma_obj:
|
||||
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
||||
return "-"
|
||||
|
||||
sigma_parameter.short_description = "ВЧ sigma"
|
||||
|
||||
|
||||
|
||||
@admin.register(SigmaParameter)
|
||||
class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели SigmaParameter.
|
||||
|
||||
|
||||
Оптимизирована для работы с параметрами Sigma:
|
||||
- Использует select_related и prefetch_related для оптимизации
|
||||
- Предоставляет фильтры по основным характеристикам
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
@@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"datetime_end",
|
||||
)
|
||||
list_display_links = ("id_satellite",)
|
||||
list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization")
|
||||
|
||||
readonly_fields = (
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
"transfer_frequency"
|
||||
list_select_related = (
|
||||
"modulation",
|
||||
"standard",
|
||||
"id_satellite",
|
||||
"parameter",
|
||||
"polarization",
|
||||
)
|
||||
|
||||
|
||||
readonly_fields = ("datetime_begin", "datetime_end", "transfer_frequency")
|
||||
|
||||
list_filter = (
|
||||
("id_satellite__name", MultiSelectDropdownFilter),
|
||||
("modulation__name", MultiSelectDropdownFilter),
|
||||
@@ -472,7 +513,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
("datetime_begin", DateRangeQuickSelectListFilterBuilder()),
|
||||
("datetime_end", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
|
||||
|
||||
search_fields = (
|
||||
"id_satellite__name",
|
||||
"frequency",
|
||||
@@ -484,17 +525,25 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
)
|
||||
autocomplete_fields = ("mark",)
|
||||
ordering = ("-frequency",)
|
||||
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Оптимизированный queryset с prefetch_related для mark."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.prefetch_related("mark")
|
||||
|
||||
|
||||
|
||||
@admin.register(Satellite)
|
||||
class SatelliteAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Satellite."""
|
||||
list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at")
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"norad",
|
||||
"undersat_point",
|
||||
"launch_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
search_fields = ("name", "norad")
|
||||
ordering = ("name",)
|
||||
filter_horizontal = ("band",)
|
||||
@@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin):
|
||||
@admin.register(Mirror)
|
||||
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -514,28 +564,37 @@ class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели Geo с поддержкой карты Leaflet.
|
||||
|
||||
|
||||
Оптимизирована для работы с геоданными:
|
||||
- Использует prefetch_related для оптимизации запросов к mirrors
|
||||
- Предоставляет фильтры по зеркалам, локации и дате
|
||||
- Поддерживает импорт/экспорт данных
|
||||
- Интегрирована с Leaflet для отображения на карте
|
||||
"""
|
||||
|
||||
form = LocationForm
|
||||
|
||||
|
||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||
|
||||
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location",
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": (
|
||||
"mirrors",
|
||||
"location",
|
||||
# "distance_coords_kup",
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp", "comment", "transponder")
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords")
|
||||
}),
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp",
|
||||
"comment",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{"fields": ("longitude_geo", "latitude_geo", "coords")},
|
||||
),
|
||||
# ("Координаты: Кубсат", {
|
||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
||||
# }),
|
||||
@@ -543,7 +602,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
# "fields": ("longitude_valid", "latitude_valid", "coords_valid")
|
||||
# }),
|
||||
)
|
||||
|
||||
|
||||
list_display = (
|
||||
"formatted_timestamp",
|
||||
"location",
|
||||
@@ -554,38 +613,39 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
"is_average",
|
||||
)
|
||||
list_display_links = ("formatted_timestamp",)
|
||||
|
||||
|
||||
list_filter = (
|
||||
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||
("transponder", MultiSelectRelatedDropdownFilter),
|
||||
"is_average",
|
||||
("location", MultiSelectDropdownFilter),
|
||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
|
||||
|
||||
search_fields = (
|
||||
"mirrors__name",
|
||||
"location",
|
||||
"transponder__name",
|
||||
)
|
||||
|
||||
autocomplete_fields = ("mirrors", )
|
||||
|
||||
autocomplete_fields = ("mirrors",)
|
||||
ordering = ("-timestamp",)
|
||||
actions = [show_on_map]
|
||||
|
||||
|
||||
settings_overrides = {
|
||||
'DEFAULT_CENTER': (55.7558, 37.6173),
|
||||
'DEFAULT_ZOOM': 12,
|
||||
"DEFAULT_CENTER": (55.7558, 37.6173),
|
||||
"DEFAULT_ZOOM": 12,
|
||||
}
|
||||
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.prefetch_related("mirrors", "transponder")
|
||||
return qs.prefetch_related(
|
||||
"mirrors",
|
||||
)
|
||||
|
||||
def mirrors_names(self, obj):
|
||||
"""Отображает список зеркал через запятую."""
|
||||
return ", ".join(m.name for m in obj.mirrors.all())
|
||||
|
||||
mirrors_names.short_description = "Зеркала"
|
||||
|
||||
def formatted_timestamp(self, obj):
|
||||
@@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
return ""
|
||||
local_time = timezone.localtime(obj.timestamp)
|
||||
return local_time.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
formatted_timestamp.short_description = "Дата и время"
|
||||
formatted_timestamp.admin_order_field = "timestamp"
|
||||
|
||||
@@ -606,6 +667,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
geo_coords.short_description = "Координаты геолокации"
|
||||
|
||||
# def kupsat_coords(self, obj):
|
||||
@@ -631,19 +693,18 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
# valid_coords.short_description = "Координаты оперативного отдела"
|
||||
|
||||
|
||||
|
||||
|
||||
@admin.register(ObjItem)
|
||||
class ObjItemAdmin(BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели ObjItem.
|
||||
|
||||
|
||||
Оптимизирована для работы с большим количеством объектов:
|
||||
- Использует select_related и prefetch_related для оптимизации запросов
|
||||
- Предоставляет фильтры по основным параметрам
|
||||
- Поддерживает поиск по имени, координатам и частоте
|
||||
- Включает кастомные actions для отображения на карте
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"sat_name",
|
||||
@@ -664,16 +725,16 @@ class ObjItemAdmin(BaseAdmin):
|
||||
)
|
||||
list_display_links = ("name",)
|
||||
list_select_related = (
|
||||
"geo_obj",
|
||||
"created_by__user",
|
||||
"geo_obj",
|
||||
"created_by__user",
|
||||
"updated_by__user",
|
||||
"parameter_obj",
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
"parameter_obj__standard"
|
||||
"parameter_obj__standard",
|
||||
)
|
||||
|
||||
|
||||
list_filter = (
|
||||
UniqueToggleFilter,
|
||||
("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||
@@ -687,33 +748,34 @@ class ObjItemAdmin(BaseAdmin):
|
||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
|
||||
|
||||
search_fields = (
|
||||
"name",
|
||||
"geo_obj__location",
|
||||
"parameter_obj__frequency",
|
||||
"parameter_obj__id_satellite__name",
|
||||
)
|
||||
|
||||
|
||||
ordering = ("-updated_at",)
|
||||
inlines = [GeoInline, ParameterInline]
|
||||
actions = [show_selected_on_map, export_objects_to_csv]
|
||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||
|
||||
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("name",)
|
||||
}),
|
||||
("Метаданные", {
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",)
|
||||
}),
|
||||
("Основная информация", {"fields": ("name",)}),
|
||||
(
|
||||
"Метаданные",
|
||||
{
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""
|
||||
Оптимизированный queryset с использованием select_related.
|
||||
|
||||
|
||||
Загружает связанные объекты одним запросом для улучшения производительности.
|
||||
"""
|
||||
qs = super().get_queryset(request)
|
||||
@@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin):
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
"parameter_obj__standard"
|
||||
"parameter_obj__standard",
|
||||
)
|
||||
|
||||
def sat_name(self, obj):
|
||||
"""Отображает название спутника из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.id_satellite:
|
||||
return obj.parameter_obj.id_satellite.name
|
||||
return "-"
|
||||
|
||||
sat_name.short_description = "Спутник"
|
||||
sat_name.admin_order_field = "parameter_obj__id_satellite__name"
|
||||
|
||||
def freq(self, obj):
|
||||
"""Отображает частоту из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.frequency
|
||||
return "-"
|
||||
|
||||
freq.short_description = "Частота, МГц"
|
||||
freq.admin_order_field = "parameter_obj__frequency"
|
||||
|
||||
@@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin):
|
||||
|
||||
def pol(self, obj):
|
||||
"""Отображает поляризацию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.polarization:
|
||||
return obj.parameter_obj.polarization.name
|
||||
return "-"
|
||||
|
||||
pol.short_description = "Поляризация"
|
||||
|
||||
def freq_range(self, obj):
|
||||
"""Отображает полосу частот из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.freq_range
|
||||
return "-"
|
||||
|
||||
freq_range.short_description = "Полоса, МГц"
|
||||
freq_range.admin_order_field = "parameter_obj__freq_range"
|
||||
|
||||
def bod_velocity(self, obj):
|
||||
"""Отображает символьную скорость из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.bod_velocity
|
||||
return "-"
|
||||
|
||||
bod_velocity.short_description = "Сим. v, БОД"
|
||||
|
||||
def modulation(self, obj):
|
||||
"""Отображает модуляцию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.modulation:
|
||||
return obj.parameter_obj.modulation.name
|
||||
return "-"
|
||||
|
||||
modulation.short_description = "Модуляция"
|
||||
|
||||
def snr(self, obj):
|
||||
"""Отображает отношение сигнал/шум из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.snr
|
||||
return "-"
|
||||
|
||||
snr.short_description = "ОСШ"
|
||||
|
||||
def geo_coords(self, obj):
|
||||
@@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
geo_coords.short_description = "Координаты геолокации"
|
||||
geo_coords.admin_order_field = "geo_obj__coords"
|
||||
|
||||
@@ -830,6 +900,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
kupsat_coords.short_description = "Координаты Кубсата"
|
||||
|
||||
def valid_coords(self, obj):
|
||||
@@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
valid_coords.short_description = "Координаты оперативного отдела"
|
||||
|
||||
|
||||
@admin.register(Band)
|
||||
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Band."""
|
||||
|
||||
list_display = ("name", "border_start", "border_end")
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -855,29 +928,44 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
|
||||
class ObjItemInline(admin.TabularInline):
|
||||
"""Inline для отображения объектов ObjItem в Source."""
|
||||
|
||||
model = ObjItem
|
||||
fk_name = "source"
|
||||
extra = 0
|
||||
can_delete = False
|
||||
verbose_name = "Объект"
|
||||
verbose_name_plural = "Объекты"
|
||||
|
||||
fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
||||
readonly_fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
||||
|
||||
|
||||
fields = (
|
||||
"name",
|
||||
"get_geo_coords",
|
||||
"get_satellite",
|
||||
"get_frequency",
|
||||
"get_polarization",
|
||||
"updated_at",
|
||||
)
|
||||
readonly_fields = (
|
||||
"name",
|
||||
"get_geo_coords",
|
||||
"get_satellite",
|
||||
"get_frequency",
|
||||
"get_polarization",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Оптимизированный queryset с предзагрузкой связанных объектов."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related(
|
||||
'geo_obj',
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization'
|
||||
"geo_obj",
|
||||
"parameter_obj",
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
)
|
||||
|
||||
|
||||
def get_geo_coords(self, obj):
|
||||
"""Отображает координаты из связанной модели Geo."""
|
||||
if not obj or not hasattr(obj, 'geo_obj'):
|
||||
if not obj or not hasattr(obj, "geo_obj"):
|
||||
return "-"
|
||||
geo = obj.geo_obj
|
||||
if not geo or not geo.coords:
|
||||
@@ -887,29 +975,41 @@ class ObjItemInline(admin.TabularInline):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
get_geo_coords.short_description = "Координаты"
|
||||
|
||||
|
||||
def get_satellite(self, obj):
|
||||
"""Отображает спутник из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.id_satellite:
|
||||
if (
|
||||
hasattr(obj, "parameter_obj")
|
||||
and obj.parameter_obj
|
||||
and obj.parameter_obj.id_satellite
|
||||
):
|
||||
return obj.parameter_obj.id_satellite.name
|
||||
return "-"
|
||||
|
||||
get_satellite.short_description = "Спутник"
|
||||
|
||||
|
||||
def get_frequency(self, obj):
|
||||
"""Отображает частоту из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.frequency
|
||||
return "-"
|
||||
|
||||
get_frequency.short_description = "Частота, МГц"
|
||||
|
||||
|
||||
def get_polarization(self, obj):
|
||||
"""Отображает поляризацию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.polarization:
|
||||
if (
|
||||
hasattr(obj, "parameter_obj")
|
||||
and obj.parameter_obj
|
||||
and obj.parameter_obj.polarization
|
||||
):
|
||||
return obj.parameter_obj.polarization.name
|
||||
return "-"
|
||||
|
||||
get_polarization.short_description = "Поляризация"
|
||||
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline):
|
||||
@admin.register(Source)
|
||||
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Source."""
|
||||
|
||||
list_display = ("id", "created_at", "updated_at")
|
||||
list_filter = (
|
||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
@@ -925,13 +1026,17 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
ordering = ("-created_at",)
|
||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||
inlines = [ObjItemInline]
|
||||
|
||||
|
||||
fieldsets = (
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("coords_kupsat", "coords_valid", "coords_reference")
|
||||
}),
|
||||
("Метаданные", {
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",)
|
||||
}),
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{"fields": ("coords_kupsat", "coords_valid", "coords_reference")},
|
||||
),
|
||||
(
|
||||
"Метаданные",
|
||||
{
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,57 +2,55 @@
|
||||
from django import forms
|
||||
|
||||
# Local imports
|
||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
||||
from .widgets import TagSelectWidget
|
||||
from .models import (
|
||||
Geo,
|
||||
Modulation,
|
||||
ObjItem,
|
||||
Parameter,
|
||||
Polarization,
|
||||
Satellite,
|
||||
Source,
|
||||
Standard,
|
||||
)
|
||||
from .widgets import CheckboxSelectMultipleWidget
|
||||
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-file-input'
|
||||
})
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={"class": "form-file-input"}),
|
||||
)
|
||||
|
||||
|
||||
class LoadExcelData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите Excel файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.xlsx,.xls'
|
||||
})
|
||||
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||
)
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
number_input = forms.IntegerField(
|
||||
label="Введите число объектов",
|
||||
min_value=0,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control'
|
||||
})
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
|
||||
class LoadCsvData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите CSV файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.csv'
|
||||
})
|
||||
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
||||
)
|
||||
|
||||
|
||||
class UploadVchLoad(UploadFileForm):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
|
||||
|
||||
@@ -60,9 +58,7 @@ class VchLinkForm(forms.Form):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
# ku_range = forms.ChoiceField(
|
||||
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||
@@ -74,18 +70,22 @@ class VchLinkForm(forms.Form):
|
||||
label="Разброс по частоте (не используется)",
|
||||
required=False,
|
||||
initial=0.0,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Не используется - погрешность определяется автоматически'
|
||||
})
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "Не используется - погрешность определяется автоматически",
|
||||
}
|
||||
),
|
||||
)
|
||||
value2 = forms.FloatField(
|
||||
label="Разброс по полосе (в %)",
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите погрешность полосы в процентах',
|
||||
'step': '0.1'
|
||||
})
|
||||
label="Разброс по полосе (в %)",
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "Введите погрешность полосы в процентах",
|
||||
"step": "0.1",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -106,256 +106,427 @@ class NewEventForm(forms.Form):
|
||||
# )
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.xlsx,.xls'
|
||||
})
|
||||
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||
)
|
||||
|
||||
|
||||
class FillLyngsatDataForm(forms.Form):
|
||||
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
|
||||
|
||||
|
||||
REGION_CHOICES = [
|
||||
('europe', 'Европа'),
|
||||
('asia', 'Азия'),
|
||||
('america', 'Америка'),
|
||||
('atlantic', 'Атлантика'),
|
||||
("europe", "Европа"),
|
||||
("asia", "Азия"),
|
||||
("america", "Америка"),
|
||||
("atlantic", "Атлантика"),
|
||||
]
|
||||
|
||||
|
||||
satellites = forms.ModelMultipleChoiceField(
|
||||
queryset=Satellite.objects.all().order_by('name'),
|
||||
queryset=Satellite.objects.all().order_by("name"),
|
||||
label="Выберите спутники",
|
||||
widget=forms.SelectMultiple(attrs={
|
||||
'class': 'form-select',
|
||||
'size': '10'
|
||||
}),
|
||||
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||
required=True,
|
||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
|
||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников",
|
||||
)
|
||||
|
||||
|
||||
regions = forms.MultipleChoiceField(
|
||||
choices=REGION_CHOICES,
|
||||
label="Выберите регионы",
|
||||
widget=forms.SelectMultiple(attrs={
|
||||
'class': 'form-select',
|
||||
'size': '4'
|
||||
}),
|
||||
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "4"}),
|
||||
required=True,
|
||||
initial=['europe', 'asia', 'america', 'atlantic'],
|
||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
|
||||
initial=["europe", "asia", "america", "atlantic"],
|
||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов",
|
||||
)
|
||||
|
||||
|
||||
use_cache = forms.BooleanField(
|
||||
label="Использовать кеширование",
|
||||
required=False,
|
||||
initial=True,
|
||||
widget=forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
help_text="Использовать кешированные данные (ускоряет повторные запросы)"
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Использовать кешированные данные (ускоряет повторные запросы)",
|
||||
)
|
||||
|
||||
|
||||
force_refresh = forms.BooleanField(
|
||||
label="Принудительно обновить данные",
|
||||
required=False,
|
||||
initial=False,
|
||||
widget=forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
help_text="Игнорировать кеш и получить свежие данные с сайта"
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Игнорировать кеш и получить свежие данные с сайта",
|
||||
)
|
||||
|
||||
|
||||
class LinkLyngsatForm(forms.Form):
|
||||
"""Форма для привязки источников LyngSat к объектам"""
|
||||
|
||||
|
||||
satellites = forms.ModelMultipleChoiceField(
|
||||
queryset=Satellite.objects.all().order_by('name'),
|
||||
queryset=Satellite.objects.all().order_by("name"),
|
||||
label="Выберите спутники",
|
||||
widget=forms.SelectMultiple(attrs={
|
||||
'class': 'form-select',
|
||||
'size': '10'
|
||||
}),
|
||||
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||
required=False,
|
||||
help_text="Оставьте пустым для обработки всех спутников"
|
||||
help_text="Оставьте пустым для обработки всех спутников",
|
||||
)
|
||||
|
||||
|
||||
frequency_tolerance = forms.FloatField(
|
||||
label="Допуск по частоте (МГц)",
|
||||
initial=0.5,
|
||||
min_value=0,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.1'
|
||||
}),
|
||||
help_text="Допустимое отклонение частоты при сравнении"
|
||||
widget=forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}),
|
||||
help_text="Допустимое отклонение частоты при сравнении",
|
||||
)
|
||||
|
||||
|
||||
class ParameterForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||
|
||||
|
||||
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Parameter
|
||||
fields = [
|
||||
'id_satellite', 'frequency', 'freq_range', 'polarization',
|
||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
"freq_range",
|
||||
"polarization",
|
||||
"bod_velocity",
|
||||
"modulation",
|
||||
"snr",
|
||||
"standard",
|
||||
]
|
||||
widgets = {
|
||||
'id_satellite': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
'required': True
|
||||
}),
|
||||
'frequency': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.000001',
|
||||
'min': '0',
|
||||
'max': '50000',
|
||||
'placeholder': 'Введите частоту в МГц'
|
||||
}),
|
||||
'freq_range': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.000001',
|
||||
'min': '0',
|
||||
'max': '1000',
|
||||
'placeholder': 'Введите полосу частот в МГц'
|
||||
}),
|
||||
'bod_velocity': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.001',
|
||||
'min': '0',
|
||||
'placeholder': 'Введите символьную скорость в БОД'
|
||||
}),
|
||||
'snr': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.001',
|
||||
'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'}),
|
||||
"id_satellite": forms.Select(
|
||||
attrs={"class": "form-select", "required": True}
|
||||
),
|
||||
"frequency": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"min": "0",
|
||||
"max": "50000",
|
||||
"placeholder": "Введите частоту в МГц",
|
||||
}
|
||||
),
|
||||
"freq_range": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"min": "0",
|
||||
"max": "1000",
|
||||
"placeholder": "Введите полосу частот в МГц",
|
||||
}
|
||||
),
|
||||
"bod_velocity": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.001",
|
||||
"min": "0",
|
||||
"placeholder": "Введите символьную скорость в БОД",
|
||||
}
|
||||
),
|
||||
"snr": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.001",
|
||||
"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 = {
|
||||
'id_satellite': 'Спутник',
|
||||
'frequency': 'Частота (МГц)',
|
||||
'freq_range': 'Полоса частот (МГц)',
|
||||
'polarization': 'Поляризация',
|
||||
'bod_velocity': 'Символьная скорость (БОД)',
|
||||
'modulation': 'Модуляция',
|
||||
'snr': 'ОСШ (дБ)',
|
||||
'standard': 'Стандарт',
|
||||
"id_satellite": "Спутник",
|
||||
"frequency": "Частота (МГц)",
|
||||
"freq_range": "Полоса частот (МГц)",
|
||||
"polarization": "Поляризация",
|
||||
"bod_velocity": "Символьная скорость (БОД)",
|
||||
"modulation": "Модуляция",
|
||||
"snr": "ОСШ (дБ)",
|
||||
"standard": "Стандарт",
|
||||
}
|
||||
help_texts = {
|
||||
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
|
||||
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
|
||||
'bod_velocity': 'Символьная скорость должна быть положительной',
|
||||
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
|
||||
"frequency": "Частота в диапазоне от 0 до 50000 МГц",
|
||||
"freq_range": "Полоса частот в диапазоне от 0 до 1000 МГц",
|
||||
"bod_velocity": "Символьная скорость должна быть положительной",
|
||||
"snr": "Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# Динамически загружаем choices для select полей
|
||||
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
||||
self.fields['polarization'].queryset = Polarization.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"].queryset = Satellite.objects.all().order_by("name")
|
||||
self.fields["polarization"].queryset = Polarization.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):
|
||||
"""
|
||||
Дополнительная валидация формы.
|
||||
|
||||
|
||||
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
frequency = cleaned_data.get('frequency')
|
||||
freq_range = cleaned_data.get('freq_range')
|
||||
bod_velocity = cleaned_data.get('bod_velocity')
|
||||
|
||||
frequency = cleaned_data.get("frequency")
|
||||
freq_range = cleaned_data.get("freq_range")
|
||||
bod_velocity = cleaned_data.get("bod_velocity")
|
||||
|
||||
# Проверка что частота больше полосы частот
|
||||
if frequency and freq_range:
|
||||
if freq_range > frequency:
|
||||
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
|
||||
|
||||
self.add_error(
|
||||
"freq_range", "Полоса частот не может быть больше частоты"
|
||||
)
|
||||
|
||||
# Проверка что символьная скорость соответствует полосе частот
|
||||
if bod_velocity and freq_range:
|
||||
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
||||
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
|
||||
|
||||
self.add_error(
|
||||
"bod_velocity",
|
||||
"Символьная скорость не может превышать полосу частот",
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class GeoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Geo
|
||||
fields = ['location', 'comment', 'is_average', 'mirrors']
|
||||
fields = ["location", "comment", "is_average", "mirrors"]
|
||||
widgets = {
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}),
|
||||
"location": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"comment": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"mirrors": CheckboxSelectMultipleWidget(
|
||||
attrs={
|
||||
"id": "id_geo-mirrors",
|
||||
"placeholder": "Выберите спутники...",
|
||||
}
|
||||
),
|
||||
}
|
||||
labels = {
|
||||
'location': 'Местоположение',
|
||||
'comment': 'Комментарий',
|
||||
'is_average': 'Усреднённое',
|
||||
'mirrors': 'Спутники-зеркала, использованные для приёма',
|
||||
"location": "Местоположение",
|
||||
"comment": "Комментарий",
|
||||
"is_average": "Усреднённое",
|
||||
"mirrors": "Спутники-зеркала, использованные для приёма",
|
||||
}
|
||||
help_texts = {
|
||||
'mirrors': 'Начните вводить название спутника для поиска',
|
||||
"mirrors": "Выберите спутники из списка",
|
||||
}
|
||||
|
||||
|
||||
class ObjItemForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования объектов (источников сигнала).
|
||||
|
||||
|
||||
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
|
||||
через ParameterForm с использованием OneToOne связи.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ObjItem
|
||||
fields = ['name']
|
||||
fields = ["name"]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите название объекта',
|
||||
'maxlength': '100'
|
||||
}),
|
||||
"name": forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "Введите название объекта",
|
||||
"maxlength": "100",
|
||||
}
|
||||
),
|
||||
}
|
||||
labels = {
|
||||
'name': 'Название объекта',
|
||||
"name": "Название объекта",
|
||||
}
|
||||
help_texts = {
|
||||
'name': 'Уникальное название объекта/источника сигнала',
|
||||
"name": "Уникальное название объекта/источника сигнала",
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Делаем поле name необязательным, так как оно может быть пустым
|
||||
self.fields['name'].required = False
|
||||
|
||||
self.fields["name"].required = False
|
||||
|
||||
def clean_name(self):
|
||||
"""
|
||||
Валидация поля name.
|
||||
|
||||
|
||||
Проверяет что название не состоит только из пробелов.
|
||||
"""
|
||||
name = self.cleaned_data.get('name')
|
||||
|
||||
name = self.cleaned_data.get("name")
|
||||
|
||||
if name:
|
||||
# Удаляем лишние пробелы
|
||||
name = name.strip()
|
||||
|
||||
|
||||
# Проверяем что после удаления пробелов что-то осталось
|
||||
if not name:
|
||||
raise forms.ValidationError('Название не может состоять только из пробелов')
|
||||
|
||||
return name
|
||||
raise forms.ValidationError(
|
||||
"Название не может состоять только из пробелов"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
160
dbapp/mainapp/static/css/checkbox-select-multiple.css
Normal file
160
dbapp/mainapp/static/css/checkbox-select-multiple.css
Normal 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;
|
||||
}
|
||||
120
dbapp/mainapp/static/js/checkbox-select-multiple.js
Normal file
120
dbapp/mainapp/static/js/checkbox-select-multiple.js
Normal 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();
|
||||
}
|
||||
@@ -22,24 +22,25 @@
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@
|
||||
</th>
|
||||
<th scope="col">Имя</th>
|
||||
<th scope="col">Спутник</th>
|
||||
<th scope="col">Транспондер</th>
|
||||
<th scope="col">Част, МГц</th>
|
||||
<th scope="col">Полоса, МГц</th>
|
||||
<th scope="col">Поляризация</th>
|
||||
@@ -41,8 +42,6 @@
|
||||
<th scope="col">Время ГЛ</th>
|
||||
<th scope="col">Местоположение</th>
|
||||
<th scope="col">Геолокация</th>
|
||||
<th scope="col">Кубсат</th>
|
||||
<th scope="col">Опер. отд</th>
|
||||
<th scope="col">Обновлено</th>
|
||||
<th scope="col">Кем(обн)</th>
|
||||
<th scope="col">Создано</th>
|
||||
|
||||
@@ -6,27 +6,93 @@
|
||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.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; }
|
||||
.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; }
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
@@ -34,25 +100,43 @@
|
||||
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));
|
||||
}
|
||||
|
||||
/* 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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,10 +149,12 @@
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||
{% 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 %}
|
||||
<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>
|
||||
@@ -186,16 +272,16 @@
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_geo_latitude" name="geo_latitude"
|
||||
<input type="number" step="0.000001" class="form-control" id="id_geo_latitude"
|
||||
name="geo_latitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_geo_longitude" name="geo_longitude"
|
||||
<input type="number" step="0.000001" class="form-control" id="id_geo_longitude"
|
||||
name="geo_longitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,15 +304,13 @@
|
||||
<div class="datetime-group">
|
||||
<div>
|
||||
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
||||
<input type="date" class="form-control"
|
||||
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 %}">
|
||||
<input type="date" class="form-control" 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 %}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_timestamp_time" class="form-label">Время:</label>
|
||||
<input type="time" class="form-control"
|
||||
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 %}">
|
||||
<input type="time" class="form-control" 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 %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,201 +337,204 @@
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
<!-- Подключаем кастомный виджет для мультивыбора -->
|
||||
<script src="{% static 'js/checkbox-select-multiple.js' %}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация карты
|
||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Функция для создания иконки маркера
|
||||
function createMarkerIcon() {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.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);
|
||||
|
||||
// Маркер геолокации
|
||||
const marker = L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(),
|
||||
title: 'Геолокация'
|
||||
}).addTo(editableLayerGroup);
|
||||
marker.bindPopup('Геолокация');
|
||||
|
||||
// Синхронизация при изменении формы
|
||||
function syncFromForm() {
|
||||
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
|
||||
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
}
|
||||
}
|
||||
|
||||
// Синхронизация при перетаскивании (только если активировано)
|
||||
marker.on('dragend', function(event) {
|
||||
const latLng = event.target.getLatLng();
|
||||
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
|
||||
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
|
||||
});
|
||||
|
||||
// Добавляем методы для управления
|
||||
marker.enableEditing = function() {
|
||||
this.dragging.enable();
|
||||
this.openPopup();
|
||||
};
|
||||
|
||||
marker.disableEditing = function() {
|
||||
this.dragging.disable();
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
marker.syncFromForm = syncFromForm;
|
||||
|
||||
// Устанавливаем начальные координаты из полей формы
|
||||
function initMarkersFromForm() {
|
||||
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
||||
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
||||
marker.setLatLng([geoLat, geoLng]);
|
||||
|
||||
// Центрируем карту на маркере
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
}
|
||||
|
||||
// Настройка формы для синхронизации с маркером
|
||||
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]);
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
}
|
||||
// Функция для создания иконки маркера
|
||||
function createMarkerIcon() {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.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);
|
||||
|
||||
// Инициализация
|
||||
initMarkersFromForm();
|
||||
// Настройка формы для синхронизации с маркером
|
||||
setupFormChange('id_geo_latitude', 'id_geo_longitude', 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 = `
|
||||
// Маркер геолокации
|
||||
const marker = L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(),
|
||||
title: 'Геолокация'
|
||||
}).addTo(editableLayerGroup);
|
||||
marker.bindPopup('Геолокация');
|
||||
|
||||
// Синхронизация при изменении формы
|
||||
function syncFromForm() {
|
||||
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
|
||||
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
}
|
||||
}
|
||||
|
||||
// Синхронизация при перетаскивании (только если активировано)
|
||||
marker.on('dragend', function (event) {
|
||||
const latLng = event.target.getLatLng();
|
||||
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
|
||||
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
|
||||
});
|
||||
|
||||
// Добавляем методы для управления
|
||||
marker.enableEditing = function () {
|
||||
this.dragging.enable();
|
||||
this.openPopup();
|
||||
};
|
||||
|
||||
marker.disableEditing = function () {
|
||||
this.dragging.disable();
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
marker.syncFromForm = syncFromForm;
|
||||
|
||||
// Устанавливаем начальные координаты из полей формы
|
||||
function initMarkersFromForm() {
|
||||
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
||||
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
||||
marker.setLatLng([geoLat, geoLng]);
|
||||
|
||||
// Центрируем карту на маркере
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
}
|
||||
|
||||
// Настройка формы для синхронизации с маркером
|
||||
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]);
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
initMarkersFromForm();
|
||||
// Настройка формы для синхронизации с маркером
|
||||
setupFormChange('id_geo_latitude', 'id_geo_longitude', 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);
|
||||
map.getContainer().appendChild(editControlsDiv);
|
||||
|
||||
let isEditing = false;
|
||||
let isEditing = false;
|
||||
|
||||
// Сохраняем начальные координаты для отмены
|
||||
const initialPosition = marker.getLatLng();
|
||||
// Сохраняем начальные координаты для отмены
|
||||
const initialPosition = marker.getLatLng();
|
||||
|
||||
// Включение редактирования
|
||||
document.getElementById('edit-btn').addEventListener('click', function() {
|
||||
if (isEditing) return;
|
||||
// Включение редактирования
|
||||
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;
|
||||
isEditing = true;
|
||||
document.getElementById('edit-btn').classList.add('active');
|
||||
document.getElementById('save-btn').disabled = false;
|
||||
document.getElementById('cancel-btn').disabled = false;
|
||||
|
||||
// Включаем drag для маркера
|
||||
marker.enableEditing();
|
||||
// Включаем drag для маркера
|
||||
marker.enableEditing();
|
||||
|
||||
// Показываем подсказку
|
||||
L.popup()
|
||||
.setLatLng(map.getCenter())
|
||||
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
|
||||
.openOn(map);
|
||||
});
|
||||
// Показываем подсказку
|
||||
L.popup()
|
||||
.setLatLng(map.getCenter())
|
||||
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
|
||||
.openOn(map);
|
||||
});
|
||||
|
||||
// Сохранение изменений
|
||||
document.getElementById('save-btn').addEventListener('click', function() {
|
||||
if (!isEditing) return;
|
||||
// Сохранение изменений
|
||||
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;
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Отключаем редактирование
|
||||
marker.disableEditing();
|
||||
// Отключаем редактирование
|
||||
marker.disableEditing();
|
||||
|
||||
// Обновляем начальную позицию
|
||||
initialPosition.lat = marker.getLatLng().lat;
|
||||
initialPosition.lng = marker.getLatLng().lng;
|
||||
// Обновляем начальную позицию
|
||||
initialPosition.lat = marker.getLatLng().lat;
|
||||
initialPosition.lng = marker.getLatLng().lng;
|
||||
|
||||
// Убираем попап подсказки
|
||||
map.closePopup();
|
||||
});
|
||||
// Убираем попап подсказки
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Отмена изменений
|
||||
document.getElementById('cancel-btn').addEventListener('click', function() {
|
||||
if (!isEditing) return;
|
||||
// Отмена изменений
|
||||
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;
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Возвращаем маркер на исходную позицию
|
||||
marker.setLatLng(initialPosition);
|
||||
// Возвращаем маркер на исходную позицию
|
||||
marker.setLatLng(initialPosition);
|
||||
|
||||
// Отключаем редактирование
|
||||
marker.disableEditing();
|
||||
// Отключаем редактирование
|
||||
marker.disableEditing();
|
||||
|
||||
// Синхронизируем форму с исходным значением
|
||||
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
|
||||
document.getElementById('id_geo_longitude').value = initialPosition.lng.toFixed(6);
|
||||
map.closePopup();
|
||||
});
|
||||
// Синхронизируем форму с исходным значением
|
||||
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
|
||||
document.getElementById('id_geo_longitude').value = initialPosition.lng.toFixed(6);
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Легенда
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
// Легенда
|
||||
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 = `
|
||||
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>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
});
|
||||
legend.addTo(map);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -70,7 +70,8 @@
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<i class="bi bi-funnel"></i> Фильтры
|
||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||
</button>
|
||||
@@ -82,10 +83,11 @@
|
||||
<i class="bi bi-plus-circle"></i> Добавить к
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Selected Items Counter Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||
<i class="bi bi-list-check"></i> Список
|
||||
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
|
||||
</button>
|
||||
@@ -112,295 +114,277 @@
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<form method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Frequency Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Частота, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ freq_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ freq_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Symbol Rate Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Сим. v, БОД:</label>
|
||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ bod_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ bod_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Modulation Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('modulation', false)">Снять</button>
|
||||
</div>
|
||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for mod in modulations %}
|
||||
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
|
||||
{{ mod.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Polarization Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||
</div>
|
||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for pol in polarizations %}
|
||||
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||
{{ pol.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Kubsat Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты Кубсата:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1"
|
||||
value="1" {% if has_kupsat == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0"
|
||||
value="0" {% if has_kupsat == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valid Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты опер. отдела:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1"
|
||||
value="1" {% if has_valid == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0"
|
||||
value="0" {% if has_valid == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source Type Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Тип источника:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1"
|
||||
value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0"
|
||||
value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sigma Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Sigma:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1"
|
||||
value="1" {% if has_sigma == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0"
|
||||
value="0" {% if has_sigma == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата ГЛ:</label>
|
||||
<div class="mb-2">
|
||||
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('today')">Сегодня</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('week')">Неделя</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('month')">Месяц</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('year')">Год</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="col-md">
|
||||
<div class="card h-100">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in processed_objects %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ item.id }}">
|
||||
</td>
|
||||
<td><a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>{{ item.frequency }}</td>
|
||||
<td>{{ item.freq_range }}</td>
|
||||
<td>{{ item.polarization }}</td>
|
||||
<td>{{ item.bod_velocity }}</td>
|
||||
<td>{{ item.modulation }}</td>
|
||||
<td>{{ item.snr }}</td>
|
||||
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.geo_location}}</td>
|
||||
<td>{{ item.geo_coords }}</td>
|
||||
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.updated_by }}</td>
|
||||
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.obj.created_by }}</td>
|
||||
<td>{{ item.comment }}</td>
|
||||
<td>{{ item.is_average }}</td>
|
||||
<td>{{ item.standard }}</td>
|
||||
<td>
|
||||
{% if item.obj.lyngsat_source %}
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
||||
<i class="bi bi-tv"></i> ТВ
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.has_sigma %}
|
||||
<a href="#" class="text-info text-decoration-none" onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;" title="{{ item.sigma_info }}">
|
||||
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.mirrors }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="22" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
Пожалуйста, выберите спутник для отображения данных
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Frequency Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Частота, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ freq_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ freq_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Symbol Rate Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Сим. v, БОД:</label>
|
||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ bod_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ bod_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Modulation Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('modulation', false)">Снять</button>
|
||||
</div>
|
||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for mod in modulations %}
|
||||
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
|
||||
{{ mod.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Polarization Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||
</div>
|
||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for pol in polarizations %}
|
||||
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||
{{ pol.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Source Type Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Тип источника:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||
id="has_source_type_1" value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||
id="has_source_type_0" value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sigma Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Sigma:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" value="1"
|
||||
{% if has_sigma == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" value="0"
|
||||
{% if has_sigma == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата ГЛ:</label>
|
||||
<div class="mb-2">
|
||||
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('today')">Сегодня</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('week')">Неделя</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('month')">Месяц</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="setDateRange('year')">Год</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm" placeholder="До"
|
||||
value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="col-md">
|
||||
<div class="card h-100">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Транспондер" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in processed_objects %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>
|
||||
{% if item.obj.transponder %}
|
||||
<a href="#" class="text-success text-decoration-none"
|
||||
onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;"
|
||||
title="Показать данные транспондера">
|
||||
<i class="bi bi-broadcast"></i> {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.frequency }}</td>
|
||||
<td>{{ item.freq_range }}</td>
|
||||
<td>{{ item.polarization }}</td>
|
||||
<td>{{ item.bod_velocity }}</td>
|
||||
<td>{{ item.modulation }}</td>
|
||||
<td>{{ item.snr }}</td>
|
||||
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.geo_location}}</td>
|
||||
<td>{{ item.geo_coords }}</td>
|
||||
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.updated_by }}</td>
|
||||
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.obj.created_by }}</td>
|
||||
<td>{{ item.comment }}</td>
|
||||
<td>{{ item.is_average }}</td>
|
||||
<td>{{ item.standard }}</td>
|
||||
<td>
|
||||
{% if item.obj.lyngsat_source %}
|
||||
<a href="#" class="text-primary text-decoration-none"
|
||||
onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
||||
<i class="bi bi-tv"></i> ТВ
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.has_sigma %}
|
||||
<a href="#" class="text-info text-decoration-none"
|
||||
onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;"
|
||||
title="{{ item.sigma_info }}">
|
||||
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.mirrors }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="22" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
Пожалуйста, выберите спутник для отображения данных
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
@@ -720,8 +704,8 @@
|
||||
|
||||
// Initialize column visibility - hide creation columns by default
|
||||
function initColumnVisibility() {
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="16"]');
|
||||
if (creationDateCheckbox) {
|
||||
creationDateCheckbox.checked = false;
|
||||
toggleColumn(creationDateCheckbox);
|
||||
@@ -731,22 +715,22 @@
|
||||
creationUserCheckbox.checked = false;
|
||||
toggleColumn(creationUserCheckbox);
|
||||
}
|
||||
|
||||
|
||||
// Hide comment, is_average, and standard columns by default
|
||||
const commentCheckbox = document.querySelector('input[data-column="16"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="18"]');
|
||||
|
||||
const commentCheckbox = document.querySelector('input[data-column="17"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="18"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="19"]');
|
||||
|
||||
if (commentCheckbox) {
|
||||
commentCheckbox.checked = false;
|
||||
toggleColumn(commentCheckbox);
|
||||
}
|
||||
|
||||
|
||||
if (isAverageCheckbox) {
|
||||
isAverageCheckbox.checked = false;
|
||||
toggleColumn(isAverageCheckbox);
|
||||
}
|
||||
|
||||
|
||||
if (standardCheckbox) {
|
||||
standardCheckbox.checked = false;
|
||||
toggleColumn(standardCheckbox);
|
||||
@@ -785,7 +769,7 @@
|
||||
// Count checkbox filters
|
||||
const hasKupsatCheckboxes = document.querySelectorAll('input[name="has_kupsat"]:checked');
|
||||
const hasValidCheckboxes = document.querySelectorAll('input[name="has_valid"]:checked');
|
||||
|
||||
|
||||
if (hasKupsatCheckboxes.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
@@ -853,7 +837,7 @@
|
||||
}
|
||||
|
||||
// Function to save selected items to localStorage
|
||||
window.saveSelectedItemsToStorage = function() {
|
||||
window.saveSelectedItemsToStorage = function () {
|
||||
try {
|
||||
localStorage.setItem('selectedItems', JSON.stringify(window.selectedItems));
|
||||
} catch (e) {
|
||||
@@ -862,7 +846,7 @@
|
||||
}
|
||||
|
||||
// Function to update the selected items counter
|
||||
window.updateSelectedCounter = function() {
|
||||
window.updateSelectedCounter = function () {
|
||||
const counterElement = document.getElementById('selectedCounter');
|
||||
if (window.selectedItems && window.selectedItems.length > 0) {
|
||||
counterElement.textContent = window.selectedItems.length;
|
||||
@@ -870,7 +854,7 @@
|
||||
} else {
|
||||
counterElement.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
// Also update the counter in the offcanvas
|
||||
const offcanvasCounter = document.querySelector('#selectedItemsOffcanvas .offcanvas-header .badge');
|
||||
if (offcanvasCounter && window.selectedItems && window.selectedItems.length > 0) {
|
||||
@@ -885,9 +869,9 @@
|
||||
updateSelectedCounter();
|
||||
|
||||
// Function to add selected items to the list
|
||||
window.addSelectedToList = function() {
|
||||
window.addSelectedToList = function () {
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один элемент для добавления в список');
|
||||
return;
|
||||
@@ -897,31 +881,30 @@
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
const row = checkbox.closest('tr');
|
||||
const itemId = checkbox.value;
|
||||
|
||||
|
||||
const itemExists = window.selectedItems.some(item => item.id === itemId);
|
||||
if (!itemExists) {
|
||||
const rowData = {
|
||||
id: itemId,
|
||||
name: row.cells[1].textContent,
|
||||
satellite: row.cells[2].textContent,
|
||||
frequency: row.cells[3].textContent,
|
||||
freq_range: row.cells[4].textContent,
|
||||
polarization: row.cells[5].textContent,
|
||||
bod_velocity: row.cells[6].textContent,
|
||||
modulation: row.cells[7].textContent,
|
||||
snr: row.cells[8].textContent,
|
||||
geo_timestamp: row.cells[9].textContent,
|
||||
geo_location: row.cells[10].textContent,
|
||||
geo_coords: row.cells[11].textContent,
|
||||
kupsat_coords: row.cells[12].textContent,
|
||||
valid_coords: row.cells[13].textContent,
|
||||
updated_at: row.cells[12].textContent,
|
||||
updated_by: row.cells[13].textContent,
|
||||
created_at: row.cells[14].textContent,
|
||||
created_by: row.cells[15].textContent,
|
||||
mirrors: row.cells[21].textContent
|
||||
transponder: row.cells[3].textContent,
|
||||
frequency: row.cells[4].textContent,
|
||||
freq_range: row.cells[5].textContent,
|
||||
polarization: row.cells[6].textContent,
|
||||
bod_velocity: row.cells[7].textContent,
|
||||
modulation: row.cells[8].textContent,
|
||||
snr: row.cells[9].textContent,
|
||||
geo_timestamp: row.cells[10].textContent,
|
||||
geo_location: row.cells[11].textContent,
|
||||
geo_coords: row.cells[12].textContent,
|
||||
updated_at: row.cells[13].textContent,
|
||||
updated_by: row.cells[14].textContent,
|
||||
created_at: row.cells[15].textContent,
|
||||
created_by: row.cells[16].textContent,
|
||||
mirrors: row.cells[22].textContent
|
||||
};
|
||||
|
||||
|
||||
window.selectedItems.push(rowData);
|
||||
}
|
||||
});
|
||||
@@ -966,6 +949,7 @@
|
||||
</td>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.satellite}</td>
|
||||
<td>${item.transponder}</td>
|
||||
<td>${item.frequency}</td>
|
||||
<td>${item.freq_range}</td>
|
||||
<td>${item.polarization}</td>
|
||||
@@ -975,8 +959,6 @@
|
||||
<td>${item.geo_timestamp}</td>
|
||||
<td>${item.geo_location}</td>
|
||||
<td>${item.geo_coords}</td>
|
||||
<td>${item.kupsat_coords}</td>
|
||||
<td>${item.valid_coords}</td>
|
||||
<td>${item.updated_at}</td>
|
||||
<td>${item.updated_by}</td>
|
||||
<td>${item.created_at}</td>
|
||||
@@ -997,10 +979,10 @@
|
||||
|
||||
// Get IDs of items to remove
|
||||
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
|
||||
|
||||
|
||||
// Remove items from the selectedItems array
|
||||
window.selectedItems = window.selectedItems.filter(item => !idsToRemove.includes(item.id));
|
||||
|
||||
|
||||
// Save selected items to localStorage
|
||||
saveSelectedItemsToStorage();
|
||||
|
||||
@@ -1018,7 +1000,7 @@
|
||||
alert('Пожалуйста, выберите хотя бы один элемент для отправки');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
alert(`Отправка ${selectedCount} элементов... (функция в разработке)`);
|
||||
// Placeholder for actual send functionality
|
||||
}
|
||||
@@ -1032,10 +1014,10 @@
|
||||
}
|
||||
|
||||
// Update the selected items table when the offcanvas is shown
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const offcanvasElement = document.getElementById('selectedItemsOffcanvas');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', function() {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', function () {
|
||||
populateSelectedItemsTable();
|
||||
});
|
||||
}
|
||||
@@ -1056,7 +1038,8 @@
|
||||
<h5 class="modal-title" id="lyngsatModalLabel">
|
||||
<i class="bi bi-tv"></i> Данные источника LyngSat
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||
aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="lyngsatModalBody">
|
||||
<div class="text-center py-4">
|
||||
@@ -1073,32 +1056,32 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showLyngsatModal(lyngsatId) {
|
||||
// Показываем модальное окно
|
||||
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
|
||||
modal.show();
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
const modalBody = document.getElementById('lyngsatModalBody');
|
||||
modalBody.innerHTML = `
|
||||
function showLyngsatModal(lyngsatId) {
|
||||
// Показываем модальное окно
|
||||
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
|
||||
modal.show();
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
const modalBody = document.getElementById('lyngsatModalBody');
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Загружаем данные
|
||||
fetch(`/api/lyngsat/${lyngsatId}/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Формируем HTML с данными
|
||||
let html = `
|
||||
|
||||
// Загружаем данные
|
||||
fetch(`/api/lyngsat/${lyngsatId}/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Формируем HTML с данными
|
||||
let html = `
|
||||
<div class="container-fluid">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
@@ -1191,16 +1174,139 @@ function showLyngsatModal(lyngsatId) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalBody.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
modalBody.innerHTML = `
|
||||
|
||||
modalBody.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
modalBody.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to show transponder modal
|
||||
function showTransponderModal(transponderId) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
|
||||
modal.show();
|
||||
|
||||
const modalBody = document.getElementById('transponderModalBody');
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/api/transponder/${transponderId}/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных транспондера');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
let html = `
|
||||
<div class="container-fluid">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 40%;">Название:</td>
|
||||
<td><strong>${data.name || '-'}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Спутник:</td>
|
||||
<td><strong>${data.satellite}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Зона покрытия:</td>
|
||||
<td>${data.zone_name || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Поляризация:</td>
|
||||
<td><span class="badge bg-info">${data.polarization}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="bi bi-broadcast"></i> Частотные параметры</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 40%;">Downlink:</td>
|
||||
<td><strong>${data.downlink} МГц</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Uplink:</td>
|
||||
<td><strong>${data.uplink || '-'} ${data.uplink ? 'МГц' : ''}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Полоса:</td>
|
||||
<td><strong>${data.frequency_range} МГц</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Перенос:</td>
|
||||
<td>${data.transfer || '-'} ${data.transfer ? 'МГц' : ''}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modalBody.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
modalBody.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Transponder Data Modal -->
|
||||
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title" id="transponderModalLabel">
|
||||
<i class="bi bi-broadcast"></i> Данные транспондера
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="transponderModalBody">
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,10 +1,74 @@
|
||||
{% extends "mapsapp/map2d_base.html" %}
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Карта выбранных объектов{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 5);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <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 © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Цвета для маркеров
|
||||
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
@@ -19,47 +83,52 @@
|
||||
|
||||
var overlays = [];
|
||||
|
||||
// Создаём слои для каждого объекта
|
||||
{% for group in groups %}
|
||||
var groupIndex = {{ forloop.counter0 }};
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = markerColors[groupIndex % markerColors.length];
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ group.name|escapejs }}";
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
|
||||
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency|escapejs }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: '{{ group.name|escapejs }}',
|
||||
label: groupName,
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Create the layer control with a custom container that includes a select all checkbox
|
||||
var layerControl = L.control.layers.tree(baseLayers, overlays, {
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
|
||||
// Add the layer control to the map
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Calculate map bounds to fit all markers
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
@@ -67,40 +136,7 @@
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
|
||||
{% else %}
|
||||
map.setView([55.75, 37.62], 5); // Default view if no markers
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Add a "Select All" checkbox functionality for all overlays
|
||||
setTimeout(function() {
|
||||
// Create a custom "select all" checkbox
|
||||
var selectAllContainer = document.createElement('div');
|
||||
selectAllContainer.className = 'leaflet-control-layers-select-all';
|
||||
selectAllContainer.style.padding = '5px';
|
||||
selectAllContainer.style.borderBottom = '1px solid #ccc';
|
||||
selectAllContainer.style.marginBottom = '5px';
|
||||
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
|
||||
|
||||
// Insert the checkbox at the top of the layer control
|
||||
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
|
||||
if (layerControlContainer) {
|
||||
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
|
||||
}
|
||||
|
||||
// Add event listener to the "select all" checkbox
|
||||
document.getElementById('select-all-overlays').addEventListener('change', function() {
|
||||
var isChecked = this.checked;
|
||||
|
||||
// Iterate through all overlays and toggle visibility
|
||||
for (var i = 0; i < overlays.length; i++) {
|
||||
if (isChecked) {
|
||||
map.addLayer(overlays[i].layer);
|
||||
} else {
|
||||
map.removeLayer(overlays[i].layer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500); // Slight delay to ensure the tree control has been rendered
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
63
dbapp/mainapp/templates/mainapp/source_confirm_delete.html
Normal file
63
dbapp/mainapp/templates/mainapp/source_confirm_delete.html
Normal 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 %}
|
||||
652
dbapp/mainapp/templates/mainapp/source_form.html
Normal file
652
dbapp/mainapp/templates/mainapp/source_form.html
Normal 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: '© <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 %}
|
||||
@@ -12,6 +12,16 @@
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.btn-group .badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
.btn-group .btn {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -55,11 +65,20 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<i class="bi bi-funnel"></i> Фильтры
|
||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +204,9 @@
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 60px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||
ID
|
||||
@@ -229,12 +251,16 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th>
|
||||
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for source in processed_sources %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ source.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td>{{ source.coords_average }}</td>
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
@@ -244,15 +270,44 @@
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})">
|
||||
<i class="bi bi-eye"></i> Показать
|
||||
</button>
|
||||
<div class="btn-group" role="group">
|
||||
{% if source.objitem_count > 0 %}
|
||||
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-success"
|
||||
title="Показать источник с точками на карте">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})"
|
||||
title="Показать детали">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:source_update' source.id %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать источник">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -285,6 +340,10 @@
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="modal-select-all" class="form-check-input">
|
||||
</th>
|
||||
<th class="text-center" style="min-width: 60px;">ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Спутник</th>
|
||||
<th>Частота, МГц</th>
|
||||
@@ -319,6 +378,55 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
updateRowHighlight(checkboxes[i]);
|
||||
}
|
||||
} else {
|
||||
updateRowHighlight(e.target);
|
||||
}
|
||||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||
}
|
||||
|
||||
// Function to show selected sources on map
|
||||
function showSelectedOnMap() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Redirect to the map view with selected IDs as query parameter
|
||||
const url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.open(url, '_blank'); // Open in a new tab
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
@@ -377,6 +485,103 @@ function updateSort(field) {
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Setup radio-like behavior for filter checkboxes
|
||||
function setupRadioLikeCheckboxes(name) {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
checkboxes.forEach(other => {
|
||||
if (other !== this) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const formData = new FormData(form);
|
||||
let filterCount = 0;
|
||||
|
||||
// Count non-empty form fields
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Display the filter counter
|
||||
const counterElement = document.getElementById('filterCounter');
|
||||
if (counterElement) {
|
||||
if (filterCount > 0) {
|
||||
counterElement.textContent = filterCount;
|
||||
counterElement.style.display = 'inline';
|
||||
} else {
|
||||
counterElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Setup select-all checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
});
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
|
||||
// Add shift-click handler
|
||||
checkbox.addEventListener('click', handleCheckboxClick);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup radio-like checkboxes for filters
|
||||
setupRadioLikeCheckboxes('has_coords_average');
|
||||
setupRadioLikeCheckboxes('has_coords_kupsat');
|
||||
setupRadioLikeCheckboxes('has_coords_valid');
|
||||
setupRadioLikeCheckboxes('has_coords_reference');
|
||||
|
||||
// Update filter counter on page load
|
||||
updateFilterCounter();
|
||||
|
||||
// Add event listeners to form elements to update counter when filters change
|
||||
const form = document.getElementById('filter-form');
|
||||
if (form) {
|
||||
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||
inputFields.forEach(input => {
|
||||
input.addEventListener('input', updateFilterCounter);
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxFields.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
}
|
||||
|
||||
// Update counter when offcanvas is shown
|
||||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
});
|
||||
|
||||
// Show source details in modal
|
||||
function showSourceDetails(sourceId) {
|
||||
// Update modal title
|
||||
@@ -420,6 +625,10 @@ function showSourceDetails(sourceId) {
|
||||
data.objitems.forEach(objitem => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input modal-item-checkbox" value="${objitem.id}">
|
||||
</td>
|
||||
<td class="text-center">${objitem.id}</td>
|
||||
<td>${objitem.name}</td>
|
||||
<td>${objitem.satellite_name}</td>
|
||||
<td>${objitem.frequency}</td>
|
||||
@@ -434,6 +643,9 @@ function showSourceDetails(sourceId) {
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Setup modal select-all checkbox
|
||||
setupModalSelectAll();
|
||||
} else {
|
||||
// Show no data message
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
@@ -449,5 +661,32 @@ function showSourceDetails(sourceId) {
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Setup select-all functionality for modal
|
||||
function setupModalSelectAll() {
|
||||
const modalSelectAll = document.getElementById('modal-select-all');
|
||||
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
|
||||
if (modalSelectAll && modalItemCheckboxes.length > 0) {
|
||||
// Remove old event listeners by cloning
|
||||
const newModalSelectAll = modalSelectAll.cloneNode(true);
|
||||
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
|
||||
|
||||
newModalSelectAll.addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = newModalSelectAll.checked;
|
||||
});
|
||||
});
|
||||
|
||||
modalItemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
|
||||
document.getElementById('modal-select-all').checked = allChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
189
dbapp/mainapp/templates/mainapp/source_map.html
Normal file
189
dbapp/mainapp/templates/mainapp/source_map.html
Normal 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: '© <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 © 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 %}
|
||||
244
dbapp/mainapp/templates/mainapp/source_with_points_map.html
Normal file
244
dbapp/mainapp/templates/mainapp/source_with_points_map.html
Normal 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: '© <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 © 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 %}
|
||||
@@ -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>
|
||||
@@ -25,9 +25,14 @@ from .views import (
|
||||
ProcessKubsatView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
SourceListView,
|
||||
SourceUpdateView,
|
||||
SourceDeleteView,
|
||||
SourceObjItemsAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
TransponderDataAPIView,
|
||||
UploadVchLoadView,
|
||||
custom_logout,
|
||||
)
|
||||
@@ -36,6 +41,8 @@ app_name = 'mainapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', SourceListView.as_view(), name='home'),
|
||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
@@ -45,6 +52,8 @@ urlpatterns = [
|
||||
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
||||
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||
@@ -53,6 +62,7 @@ urlpatterns = [
|
||||
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||
|
||||
@@ -23,6 +23,7 @@ from .api import (
|
||||
SigmaParameterDataAPIView,
|
||||
SourceObjItemsAPIView,
|
||||
LyngsatTaskStatusAPIView,
|
||||
TransponderDataAPIView,
|
||||
)
|
||||
from .lyngsat import (
|
||||
LinkLyngsatSourcesView,
|
||||
@@ -30,8 +31,14 @@ from .lyngsat import (
|
||||
LyngsatTaskStatusView,
|
||||
ClearLyngsatCacheView,
|
||||
)
|
||||
from .source import SourceListView
|
||||
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView
|
||||
from .map import (
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
ClusterTestView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
@@ -58,6 +65,7 @@ __all__ = [
|
||||
'SigmaParameterDataAPIView',
|
||||
'SourceObjItemsAPIView',
|
||||
'LyngsatTaskStatusAPIView',
|
||||
'TransponderDataAPIView',
|
||||
# LyngSat
|
||||
'LinkLyngsatSourcesView',
|
||||
'FillLyngsatDataView',
|
||||
@@ -65,8 +73,12 @@ __all__ = [
|
||||
'ClearLyngsatCacheView',
|
||||
# Source
|
||||
'SourceListView',
|
||||
'SourceUpdateView',
|
||||
'SourceDeleteView',
|
||||
# Map
|
||||
'ShowMapView',
|
||||
'ShowSelectedObjectsMapView',
|
||||
'ShowSourcesMapView',
|
||||
'ShowSourceWithPointsMapView',
|
||||
'ClusterTestView',
|
||||
]
|
||||
|
||||
@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
|
||||
response_data['status'] = task.state
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
class TransponderDataAPIView(LoginRequiredMixin, View):
|
||||
"""API endpoint for getting Transponder data."""
|
||||
|
||||
def get(self, request, transponder_id):
|
||||
from mapsapp.models import Transponders
|
||||
|
||||
try:
|
||||
transponder = Transponders.objects.select_related(
|
||||
'sat_id',
|
||||
'polarization'
|
||||
).get(id=transponder_id)
|
||||
|
||||
data = {
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or '-',
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
|
||||
'polarization': transponder.polarization.name if transponder.polarization else '-',
|
||||
'zone_name': transponder.zone_name or '-',
|
||||
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
except Transponders.DoesNotExist:
|
||||
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Map related views for displaying objects on maps.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
@@ -18,7 +19,7 @@ from ..models import ObjItem
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
class ShowMapView(RoleRequiredMixin, View):
|
||||
"""View for displaying objects on map (admin interface)."""
|
||||
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get(self, request):
|
||||
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
points.append(
|
||||
@@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View):
|
||||
)
|
||||
else:
|
||||
return redirect("admin")
|
||||
|
||||
|
||||
grouped = defaultdict(list)
|
||||
for p in points:
|
||||
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
|
||||
@@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View):
|
||||
|
||||
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying selected objects on map."""
|
||||
|
||||
|
||||
def get(self, request):
|
||||
ids = request.GET.get("ids", "")
|
||||
points = []
|
||||
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
points.append(
|
||||
@@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/objitem_map.html", context)
|
||||
|
||||
|
||||
class ShowSourcesMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying selected sources on map."""
|
||||
|
||||
def get(self, request):
|
||||
from ..models import Source
|
||||
|
||||
ids = request.GET.get("ids", "")
|
||||
groups = []
|
||||
|
||||
if ids:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
sources = Source.objects.filter(id__in=id_list)
|
||||
|
||||
# Define coordinate types with their labels and colors
|
||||
coord_types = [
|
||||
("coords_average", "Усредненные координаты", "blue"),
|
||||
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||
("coords_valid", "Координаты оперативников", "green"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
# Group points by coordinate type
|
||||
for coord_field, label, color in coord_types:
|
||||
points = []
|
||||
for source in sources:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
# coords is a Point object with x (longitude) and y (latitude)
|
||||
points.append(
|
||||
{
|
||||
"point": (coords.x, coords.y), # (lon, lat)
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
)
|
||||
|
||||
if points:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": points,
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
}
|
||||
return render(request, "mainapp/source_map.html", context)
|
||||
|
||||
|
||||
class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying a single source with all its related ObjItem points."""
|
||||
|
||||
def get(self, request, source_id):
|
||||
from ..models import Source
|
||||
|
||||
try:
|
||||
source = Source.objects.prefetch_related(
|
||||
"source_objitems",
|
||||
"source_objitems__parameter_obj",
|
||||
"source_objitems__geo_obj",
|
||||
).get(id=source_id)
|
||||
except Source.DoesNotExist:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
groups = []
|
||||
|
||||
# Цвета для разных типов координат источника
|
||||
source_coord_types = [
|
||||
("coords_average", "Усредненные координаты", "blue"),
|
||||
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||
("coords_valid", "Координаты оперативников", "green"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
# Добавляем координаты источника
|
||||
for coord_field, label, color in source_coord_types:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": [
|
||||
{
|
||||
"point": (coords.x, coords.y),
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
],
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
|
||||
# Добавляем все точки ГЛ одной группой
|
||||
gl_points = source.source_objitems.select_related(
|
||||
"parameter_obj", "geo_obj"
|
||||
).all()
|
||||
|
||||
# Собираем все точки ГЛ в одну группу
|
||||
all_gl_points = []
|
||||
for obj in gl_points:
|
||||
if (
|
||||
not hasattr(obj, "geo_obj")
|
||||
or not obj.geo_obj
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
|
||||
all_gl_points.append(
|
||||
{
|
||||
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||
"name": obj.name,
|
||||
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||
}
|
||||
)
|
||||
|
||||
# Добавляем все точки ГЛ одним цветом (красный)
|
||||
if all_gl_points:
|
||||
groups.append(
|
||||
{"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
|
||||
)
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
"source_id": source_id,
|
||||
}
|
||||
return render(request, "mainapp/source_with_points_map.html", context)
|
||||
|
||||
|
||||
class ClusterTestView(LoginRequiredMixin, View):
|
||||
"""Test view for clustering functionality."""
|
||||
|
||||
|
||||
def get(self, request):
|
||||
objs = ObjItem.objects.filter(
|
||||
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"
|
||||
|
||||
@@ -3,12 +3,15 @@ Source related views.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# Get pagination parameters
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
|
||||
# Get sorting parameters
|
||||
sort_param = request.GET.get("sort", "-created_at")
|
||||
# Get sorting parameters (default to ID ascending)
|
||||
sort_param = request.GET.get("sort", "id")
|
||||
|
||||
# Get filter parameters
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
@@ -37,10 +40,9 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
date_to = request.GET.get("date_to", "").strip()
|
||||
|
||||
# Get all Source objects with query optimization
|
||||
sources = Source.objects.select_related(
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).prefetch_related(
|
||||
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
|
||||
sources = Source.objects.prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__geo_obj'
|
||||
@@ -164,8 +166,6 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'objitem_count': objitem_count,
|
||||
'created_at': source.created_at,
|
||||
'updated_at': source.updated_at,
|
||||
'created_by': source.created_by,
|
||||
'updated_by': source.updated_by,
|
||||
})
|
||||
|
||||
# Prepare context for template
|
||||
@@ -188,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
}
|
||||
|
||||
return render(request, "mainapp/source_list.html", context)
|
||||
|
||||
|
||||
|
||||
class AdminModeratorMixin(UserPassesTestMixin):
|
||||
"""Mixin to restrict access to admin and moderator roles only."""
|
||||
|
||||
def test_func(self):
|
||||
return (
|
||||
self.request.user.is_authenticated and
|
||||
hasattr(self.request.user, 'customuser') and
|
||||
self.request.user.customuser.role in ['admin', 'moderator']
|
||||
)
|
||||
|
||||
def handle_no_permission(self):
|
||||
messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
|
||||
return redirect('mainapp:home')
|
||||
|
||||
|
||||
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
||||
|
||||
def get(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
form = SourceForm(instance=source)
|
||||
|
||||
# Get related ObjItems ordered by creation date
|
||||
objitems = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation',
|
||||
'parameter_obj__standard',
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).order_by('created_at')
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'form': form,
|
||||
'objitems': objitems,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
def post(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
form = SourceForm(request.POST, instance=source)
|
||||
|
||||
if form.is_valid():
|
||||
source = form.save(commit=False)
|
||||
# Set updated_by to current user
|
||||
if hasattr(request.user, 'customuser'):
|
||||
source.updated_by = request.user.customuser
|
||||
source.save()
|
||||
|
||||
messages.success(request, f'Источник #{source.id} успешно обновлен.')
|
||||
|
||||
# Redirect back with query params if present
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:source_update', pk=source.id)
|
||||
|
||||
# If form is invalid, re-render with errors
|
||||
objitems = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation',
|
||||
'parameter_obj__standard',
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).order_by('created_at')
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'form': form,
|
||||
'objitems': objitems,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
|
||||
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for deleting Source."""
|
||||
|
||||
def get(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'objitems_count': source.source_objitems.count(),
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_confirm_delete.html', context)
|
||||
|
||||
def post(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
source_id = source.id
|
||||
|
||||
try:
|
||||
source.delete()
|
||||
messages.success(request, f'Источник #{source_id} успешно удален.')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при удалении источника: {str(e)}')
|
||||
return redirect('mainapp:source_update', pk=pk)
|
||||
|
||||
# Redirect to source list
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
20
dbapp/mainapp/widgets.py
Normal file
20
dbapp/mainapp/widgets.py
Normal 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',)
|
||||
@@ -12,54 +12,54 @@ from mainapp.models import Polarization, Satellite, get_default_polarization, Cu
|
||||
class Transponders(models.Model):
|
||||
"""
|
||||
Модель транспондера спутника.
|
||||
|
||||
|
||||
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
|
||||
"""
|
||||
|
||||
|
||||
# Основные поля
|
||||
name = models.CharField(
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Название транспондера",
|
||||
db_index=True,
|
||||
help_text="Название транспондера"
|
||||
help_text="Название транспондера",
|
||||
)
|
||||
downlink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Downlink",
|
||||
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
# help_text="Частота downlink в МГц (0-50000)"
|
||||
)
|
||||
frequency_range = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Полоса",
|
||||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||
# help_text="Полоса частот в МГц (0-1000)"
|
||||
)
|
||||
uplink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Uplink",
|
||||
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
# help_text="Частота uplink в МГц (0-50000)"
|
||||
)
|
||||
zone_name = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Название зоны",
|
||||
db_index=True,
|
||||
help_text="Название зоны покрытия транспондера"
|
||||
help_text="Название зоны покрытия транспондера",
|
||||
)
|
||||
snr = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Полоса",
|
||||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||
help_text="Полоса частот в МГц (0-1000)"
|
||||
help_text="Полоса частот в МГц (0-1000)",
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
@@ -89,44 +89,43 @@ class Transponders(models.Model):
|
||||
verbose_name="Изменен пользователем",
|
||||
help_text="Пользователь, последним изменивший запись",
|
||||
)
|
||||
|
||||
|
||||
# Связи
|
||||
polarization = models.ForeignKey(
|
||||
Polarization,
|
||||
default=get_default_polarization,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="tran_polarizations",
|
||||
null=True,
|
||||
blank=True,
|
||||
Polarization,
|
||||
default=get_default_polarization,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="tran_polarizations",
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Поляризация",
|
||||
help_text="Поляризация сигнала"
|
||||
help_text="Поляризация сигнала",
|
||||
)
|
||||
sat_id = models.ForeignKey(
|
||||
Satellite,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="tran_satellite",
|
||||
Satellite,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="tran_satellite",
|
||||
verbose_name="Спутник",
|
||||
db_index=True,
|
||||
help_text="Спутник, которому принадлежит транспондер"
|
||||
help_text="Спутник, которому принадлежит транспондер",
|
||||
)
|
||||
|
||||
|
||||
# Вычисляемые поля
|
||||
transfer = models.GeneratedField(
|
||||
expression=ExpressionWrapper(
|
||||
Abs(F('downlink') - F('uplink')),
|
||||
output_field=models.FloatField()
|
||||
Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
|
||||
),
|
||||
output_field=models.FloatField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Перенос"
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Перенос",
|
||||
)
|
||||
|
||||
# def clean(self):
|
||||
# """Валидация на уровне модели"""
|
||||
# super().clean()
|
||||
|
||||
|
||||
# # Проверка что downlink и uplink заданы
|
||||
# if self.downlink and self.uplink:
|
||||
# # Обычно uplink выше downlink для спутниковой связи
|
||||
@@ -139,14 +138,12 @@ class Transponders(models.Model):
|
||||
if self.name:
|
||||
return self.name
|
||||
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Транспондер"
|
||||
verbose_name_plural = "Транспондеры"
|
||||
ordering = ['sat_id', 'downlink']
|
||||
ordering = ["sat_id", "downlink"]
|
||||
indexes = [
|
||||
models.Index(fields=['sat_id', 'downlink']),
|
||||
models.Index(fields=['sat_id', 'zone_name']),
|
||||
models.Index(fields=["sat_id", "downlink"]),
|
||||
models.Index(fields=["sat_id", "zone_name"]),
|
||||
]
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user