Compare commits
2 Commits
8e0d32c307
...
6a26991dc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a26991dc0 | |||
| 5ab6770809 |
@@ -32,13 +32,13 @@ from .models import (
|
|||||||
ObjItem,
|
ObjItem,
|
||||||
CustomUser,
|
CustomUser,
|
||||||
Band,
|
Band,
|
||||||
Source
|
Source,
|
||||||
)
|
)
|
||||||
from .filters import (
|
from .filters import (
|
||||||
GeoKupDistanceFilter,
|
GeoKupDistanceFilter,
|
||||||
GeoValidDistanceFilter,
|
GeoValidDistanceFilter,
|
||||||
UniqueToggleFilter,
|
UniqueToggleFilter,
|
||||||
HasSigmaParameterFilter
|
HasSigmaParameterFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ admin.site.unregister(Group)
|
|||||||
# Base Admin Classes
|
# Base Admin Classes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class BaseAdmin(admin.ModelAdmin):
|
class BaseAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Базовый класс для всех admin моделей.
|
Базовый класс для всех admin моделей.
|
||||||
@@ -64,6 +65,7 @@ class BaseAdmin(admin.ModelAdmin):
|
|||||||
- Настройка количества элементов на странице
|
- Настройка количества элементов на странице
|
||||||
- Автоматическое заполнение полей created_by и updated_by
|
- Автоматическое заполнение полей created_by и updated_by
|
||||||
"""
|
"""
|
||||||
|
|
||||||
save_on_top = True
|
save_on_top = True
|
||||||
list_per_page = 50
|
list_per_page = 50
|
||||||
|
|
||||||
@@ -79,12 +81,12 @@ class BaseAdmin(admin.ModelAdmin):
|
|||||||
"""
|
"""
|
||||||
if not change:
|
if not change:
|
||||||
# При создании нового объекта устанавливаем created_by
|
# При создании нового объекта устанавливаем created_by
|
||||||
if hasattr(obj, 'created_by') and not obj.created_by_id:
|
if hasattr(obj, "created_by") and not obj.created_by_id:
|
||||||
obj.created_by = getattr(request.user, 'customuser', None)
|
obj.created_by = getattr(request.user, "customuser", None)
|
||||||
|
|
||||||
# При любом сохранении обновляем updated_by
|
# При любом сохранении обновляем updated_by
|
||||||
if hasattr(obj, 'updated_by'):
|
if hasattr(obj, "updated_by"):
|
||||||
obj.updated_by = getattr(request.user, 'customuser', None)
|
obj.updated_by = getattr(request.user, "customuser", None)
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ class BaseAdmin(admin.ModelAdmin):
|
|||||||
class CustomUserInline(admin.StackedInline):
|
class CustomUserInline(admin.StackedInline):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
can_delete = False
|
can_delete = False
|
||||||
verbose_name_plural = 'Дополнительная информация пользователя'
|
verbose_name_plural = "Дополнительная информация пользователя"
|
||||||
|
|
||||||
|
|
||||||
class LocationForm(forms.ModelForm):
|
class LocationForm(forms.ModelForm):
|
||||||
@@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Geo
|
model = Geo
|
||||||
fields = '__all__'
|
fields = "__all__"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.instance and self.instance.coords:
|
if self.instance and self.instance.coords:
|
||||||
self.fields['latitude_geo'].initial = self.instance.coords[1]
|
self.fields["latitude_geo"].initial = self.instance.coords[1]
|
||||||
self.fields['longitude_geo'].initial = self.instance.coords[0]
|
self.fields["longitude_geo"].initial = self.instance.coords[0]
|
||||||
# if self.instance and self.instance.coords_kupsat:
|
# if self.instance and self.instance.coords_kupsat:
|
||||||
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
||||||
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
||||||
@@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm):
|
|||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit=False)
|
instance = super().save(commit=False)
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
lat = self.cleaned_data.get('latitude_geo')
|
|
||||||
lon = self.cleaned_data.get('longitude_geo')
|
lat = self.cleaned_data.get("latitude_geo")
|
||||||
|
lon = self.cleaned_data.get("longitude_geo")
|
||||||
if lat is not None and lon is not None:
|
if lat is not None and lon is not None:
|
||||||
instance.coords = Point(lon, lat, srid=4326)
|
instance.coords = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
@@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline):
|
|||||||
form = LocationForm
|
form = LocationForm
|
||||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
prefetch_related = ("mirrors",)
|
prefetch_related = ("mirrors",)
|
||||||
autocomplete_fields = ('mirrors',)
|
autocomplete_fields = ("mirrors",)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
(
|
||||||
"fields": ("mirrors", "location",
|
"Основная информация",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"mirrors",
|
||||||
|
"location",
|
||||||
# "distance_coords_kup",
|
# "distance_coords_kup",
|
||||||
# "distance_coords_valid",
|
# "distance_coords_valid",
|
||||||
# "distance_kup_valid",
|
# "distance_kup_valid",
|
||||||
"timestamp", "comment",)
|
"timestamp",
|
||||||
}),
|
"comment",
|
||||||
("Координаты: геолокация", {
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Координаты: геолокация",
|
||||||
|
{
|
||||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
# ("Координаты: Кубсат", {
|
# ("Координаты: Кубсат", {
|
||||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||||
# }),
|
# }),
|
||||||
@@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline):
|
|||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
inlines = [CustomUserInline]
|
inlines = [CustomUserInline]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
|
||||||
|
|
||||||
@@ -181,6 +195,7 @@ admin.site.register(User, UserAdmin)
|
|||||||
# Custom Admin Actions
|
# Custom Admin Actions
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Показать выбранные на карте")
|
@admin.action(description="Показать выбранные на карте")
|
||||||
def show_on_map(modeladmin, request, queryset):
|
def show_on_map(modeladmin, request, queryset):
|
||||||
"""
|
"""
|
||||||
@@ -189,9 +204,9 @@ def show_on_map(modeladmin, request, queryset):
|
|||||||
Оптимизирован для работы с большим количеством объектов:
|
Оптимизирован для работы с большим количеством объектов:
|
||||||
использует values_list для получения только ID.
|
использует values_list для получения только ID.
|
||||||
"""
|
"""
|
||||||
selected_ids = queryset.values_list('id', flat=True)
|
selected_ids = queryset.values_list("id", flat=True)
|
||||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||||
return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}')
|
return redirect(reverse("mainapp:admin_show_map") + f"?ids={ids_str}")
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Показать выбранные объекты на карте")
|
@admin.action(description="Показать выбранные объекты на карте")
|
||||||
@@ -202,9 +217,9 @@ def show_selected_on_map(modeladmin, request, queryset):
|
|||||||
Оптимизирован для работы с большим количеством объектов:
|
Оптимизирован для работы с большим количеством объектов:
|
||||||
использует values_list для получения только ID.
|
использует values_list для получения только ID.
|
||||||
"""
|
"""
|
||||||
selected_ids = queryset.values_list('id', flat=True)
|
selected_ids = queryset.values_list("id", flat=True)
|
||||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||||
return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}')
|
return redirect(reverse("mainapp:show_selected_objects_map") + f"?ids={ids_str}")
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Экспортировать выбранные объекты в CSV")
|
@admin.action(description="Экспортировать выбранные объекты в CSV")
|
||||||
@@ -220,39 +235,41 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
|
|
||||||
# Оптимизируем queryset
|
# Оптимизируем queryset
|
||||||
queryset = queryset.select_related(
|
queryset = queryset.select_related(
|
||||||
'geo_obj',
|
"geo_obj",
|
||||||
'created_by__user',
|
"created_by__user",
|
||||||
'updated_by__user',
|
"updated_by__user",
|
||||||
'parameter_obj',
|
"parameter_obj",
|
||||||
'parameter_obj__id_satellite',
|
"parameter_obj__id_satellite",
|
||||||
'parameter_obj__polarization',
|
"parameter_obj__polarization",
|
||||||
'parameter_obj__modulation'
|
"parameter_obj__modulation",
|
||||||
)
|
)
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
||||||
response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"'
|
response["Content-Disposition"] = 'attachment; filename="objitems_export.csv"'
|
||||||
response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel
|
response.write("\ufeff") # UTF-8 BOM для корректного отображения в Excel
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
writer.writerow([
|
writer.writerow(
|
||||||
'Название',
|
[
|
||||||
'Спутник',
|
"Название",
|
||||||
'Частота (МГц)',
|
"Спутник",
|
||||||
'Полоса (МГц)',
|
"Частота (МГц)",
|
||||||
'Поляризация',
|
"Полоса (МГц)",
|
||||||
'Модуляция',
|
"Поляризация",
|
||||||
'ОСШ',
|
"Модуляция",
|
||||||
'Координаты геолокации',
|
"ОСШ",
|
||||||
'Координаты Кубсата',
|
"Координаты геолокации",
|
||||||
'Координаты оперативного отдела',
|
"Координаты Кубсата",
|
||||||
'Расстояние Гео-Куб (км)',
|
"Координаты оперативного отдела",
|
||||||
'Расстояние Гео-Опер (км)',
|
"Расстояние Гео-Куб (км)",
|
||||||
'Дата создания',
|
"Расстояние Гео-Опер (км)",
|
||||||
'Дата обновления'
|
"Дата создания",
|
||||||
])
|
"Дата обновления",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
param = getattr(obj, 'parameter_obj', None)
|
param = getattr(obj, "parameter_obj", None)
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
|
|
||||||
# Форматирование координат
|
# Форматирование координат
|
||||||
@@ -264,7 +281,8 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||||
return f"{lat_str} {lon_str}"
|
return f"{lat_str} {lon_str}"
|
||||||
|
|
||||||
writer.writerow([
|
writer.writerow(
|
||||||
|
[
|
||||||
obj.name,
|
obj.name,
|
||||||
param.id_satellite.name if param and param.id_satellite else "-",
|
param.id_satellite.name if param and param.id_satellite else "-",
|
||||||
param.frequency if param else "-",
|
param.frequency if param else "-",
|
||||||
@@ -275,11 +293,16 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
format_coords(geo) if geo and geo.coords else "-",
|
format_coords(geo) if geo and geo.coords else "-",
|
||||||
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
||||||
format_coords(geo) if geo and geo.coords_valid else "-",
|
format_coords(geo) if geo and geo.coords_valid else "-",
|
||||||
round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-",
|
round(geo.distance_coords_kup, 3)
|
||||||
round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-",
|
if geo and geo.distance_coords_kup
|
||||||
|
else "-",
|
||||||
|
round(geo.distance_coords_valid, 3)
|
||||||
|
if geo and geo.distance_coords_valid
|
||||||
|
else "-",
|
||||||
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
||||||
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-"
|
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
# Inline Admin Classes
|
# Inline Admin Classes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class ParameterInline(admin.StackedInline):
|
class ParameterInline(admin.StackedInline):
|
||||||
"""Inline для редактирования параметра объекта."""
|
"""Inline для редактирования параметра объекта."""
|
||||||
|
|
||||||
model = Parameter
|
model = Parameter
|
||||||
extra = 0
|
extra = 0
|
||||||
max_num = 1
|
max_num = 1
|
||||||
@@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline):
|
|||||||
verbose_name = "ВЧ загрузка"
|
verbose_name = "ВЧ загрузка"
|
||||||
verbose_name_plural = "ВЧ загрузка"
|
verbose_name_plural = "ВЧ загрузка"
|
||||||
fields = (
|
fields = (
|
||||||
'id_satellite',
|
"id_satellite",
|
||||||
'frequency',
|
"frequency",
|
||||||
'freq_range',
|
"freq_range",
|
||||||
'polarization',
|
"polarization",
|
||||||
'modulation',
|
"modulation",
|
||||||
'bod_velocity',
|
"bod_velocity",
|
||||||
'snr',
|
"snr",
|
||||||
'standard'
|
"standard",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard')
|
autocomplete_fields = ("id_satellite", "polarization", "modulation", "standard")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Admin Classes
|
# Admin Classes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SigmaParMark)
|
@admin.register(SigmaParMark)
|
||||||
class SigmaParMarkAdmin(BaseAdmin):
|
class SigmaParMarkAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели SigmaParMark."""
|
"""Админ-панель для модели SigmaParMark."""
|
||||||
|
|
||||||
list_display = ("mark", "timestamp")
|
list_display = ("mark", "timestamp")
|
||||||
search_fields = ("mark",)
|
search_fields = ("mark",)
|
||||||
ordering = ("-timestamp",)
|
ordering = ("-timestamp",)
|
||||||
list_filter = (
|
list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
|
||||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Polarization)
|
@admin.register(Polarization)
|
||||||
class PolarizationAdmin(BaseAdmin):
|
class PolarizationAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели Polarization."""
|
"""Админ-панель для модели Polarization."""
|
||||||
|
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin):
|
|||||||
@admin.register(Modulation)
|
@admin.register(Modulation)
|
||||||
class ModulationAdmin(BaseAdmin):
|
class ModulationAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели Modulation."""
|
"""Админ-панель для модели Modulation."""
|
||||||
|
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin):
|
|||||||
@admin.register(Standard)
|
@admin.register(Standard)
|
||||||
class StandardAdmin(BaseAdmin):
|
class StandardAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели Standard."""
|
"""Админ-панель для модели Standard."""
|
||||||
|
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin):
|
|||||||
class SigmaParameterInline(admin.StackedInline):
|
class SigmaParameterInline(admin.StackedInline):
|
||||||
model = SigmaParameter
|
model = SigmaParameter
|
||||||
extra = 0
|
extra = 0
|
||||||
autocomplete_fields = ['mark']
|
autocomplete_fields = ["mark"]
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"datetime_begin",
|
"datetime_begin",
|
||||||
"datetime_end",
|
"datetime_end",
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_add_permission(self, request, obj=None):
|
def has_add_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -370,6 +399,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
- Предоставляет фильтры по основным характеристикам
|
- Предоставляет фильтры по основным характеристикам
|
||||||
- Поддерживает импорт/экспорт данных
|
- Поддерживает импорт/экспорт данных
|
||||||
"""
|
"""
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"id_satellite",
|
"id_satellite",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -380,10 +410,16 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
"snr",
|
"snr",
|
||||||
"standard",
|
"standard",
|
||||||
"related_objitem",
|
"related_objitem",
|
||||||
"sigma_parameter"
|
"sigma_parameter",
|
||||||
)
|
)
|
||||||
list_display_links = ("frequency", "id_satellite")
|
list_display_links = ("frequency", "id_satellite")
|
||||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem")
|
list_select_related = (
|
||||||
|
"polarization",
|
||||||
|
"modulation",
|
||||||
|
"standard",
|
||||||
|
"id_satellite",
|
||||||
|
"objitem",
|
||||||
|
)
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
HasSigmaParameterFilter,
|
HasSigmaParameterFilter,
|
||||||
@@ -415,9 +451,10 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
|
|
||||||
def related_objitem(self, obj):
|
def related_objitem(self, obj):
|
||||||
"""Отображает связанный ObjItem."""
|
"""Отображает связанный ObjItem."""
|
||||||
if hasattr(obj, 'objitem') and obj.objitem:
|
if hasattr(obj, "objitem") and obj.objitem:
|
||||||
return obj.objitem.name
|
return obj.objitem.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
related_objitem.short_description = "Объект"
|
related_objitem.short_description = "Объект"
|
||||||
related_objitem.admin_order_field = "objitem__name"
|
related_objitem.admin_order_field = "objitem__name"
|
||||||
|
|
||||||
@@ -427,6 +464,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
if sigma_obj:
|
if sigma_obj:
|
||||||
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
sigma_parameter.short_description = "ВЧ sigma"
|
sigma_parameter.short_description = "ВЧ sigma"
|
||||||
|
|
||||||
|
|
||||||
@@ -440,6 +478,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
- Предоставляет фильтры по основным характеристикам
|
- Предоставляет фильтры по основным характеристикам
|
||||||
- Поддерживает импорт/экспорт данных
|
- Поддерживает импорт/экспорт данных
|
||||||
"""
|
"""
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"id_satellite",
|
"id_satellite",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
"datetime_end",
|
"datetime_end",
|
||||||
)
|
)
|
||||||
list_display_links = ("id_satellite",)
|
list_display_links = ("id_satellite",)
|
||||||
list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization")
|
list_select_related = (
|
||||||
|
"modulation",
|
||||||
readonly_fields = (
|
"standard",
|
||||||
"datetime_begin",
|
"id_satellite",
|
||||||
"datetime_end",
|
"parameter",
|
||||||
"transfer_frequency"
|
"polarization",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly_fields = ("datetime_begin", "datetime_end", "transfer_frequency")
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("id_satellite__name", MultiSelectDropdownFilter),
|
("id_satellite__name", MultiSelectDropdownFilter),
|
||||||
("modulation__name", MultiSelectDropdownFilter),
|
("modulation__name", MultiSelectDropdownFilter),
|
||||||
@@ -494,7 +535,15 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
@admin.register(Satellite)
|
@admin.register(Satellite)
|
||||||
class SatelliteAdmin(BaseAdmin):
|
class SatelliteAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели Satellite."""
|
"""Админ-панель для модели Satellite."""
|
||||||
list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at")
|
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"norad",
|
||||||
|
"undersat_point",
|
||||||
|
"launch_date",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
search_fields = ("name", "norad")
|
search_fields = ("name", "norad")
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
filter_horizontal = ("band",)
|
filter_horizontal = ("band",)
|
||||||
@@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin):
|
|||||||
@admin.register(Mirror)
|
@admin.register(Mirror)
|
||||||
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
||||||
|
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -521,21 +571,30 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
- Поддерживает импорт/экспорт данных
|
- Поддерживает импорт/экспорт данных
|
||||||
- Интегрирована с Leaflet для отображения на карте
|
- Интегрирована с Leaflet для отображения на карте
|
||||||
"""
|
"""
|
||||||
|
|
||||||
form = LocationForm
|
form = LocationForm
|
||||||
|
|
||||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
(
|
||||||
"fields": ("mirrors", "location",
|
"Основная информация",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"mirrors",
|
||||||
|
"location",
|
||||||
# "distance_coords_kup",
|
# "distance_coords_kup",
|
||||||
# "distance_coords_valid",
|
# "distance_coords_valid",
|
||||||
# "distance_kup_valid",
|
# "distance_kup_valid",
|
||||||
"timestamp", "comment", "transponder")
|
"timestamp",
|
||||||
}),
|
"comment",
|
||||||
("Координаты: геолокация", {
|
)
|
||||||
"fields": ("longitude_geo", "latitude_geo", "coords")
|
},
|
||||||
}),
|
),
|
||||||
|
(
|
||||||
|
"Координаты: геолокация",
|
||||||
|
{"fields": ("longitude_geo", "latitude_geo", "coords")},
|
||||||
|
),
|
||||||
# ("Координаты: Кубсат", {
|
# ("Координаты: Кубсат", {
|
||||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
||||||
# }),
|
# }),
|
||||||
@@ -557,7 +616,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("mirrors", MultiSelectRelatedDropdownFilter),
|
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||||
("transponder", MultiSelectRelatedDropdownFilter),
|
|
||||||
"is_average",
|
"is_average",
|
||||||
("location", MultiSelectDropdownFilter),
|
("location", MultiSelectDropdownFilter),
|
||||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
@@ -566,7 +624,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
search_fields = (
|
search_fields = (
|
||||||
"mirrors__name",
|
"mirrors__name",
|
||||||
"location",
|
"location",
|
||||||
"transponder__name",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
autocomplete_fields = ("mirrors",)
|
autocomplete_fields = ("mirrors",)
|
||||||
@@ -574,18 +631,21 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
actions = [show_on_map]
|
actions = [show_on_map]
|
||||||
|
|
||||||
settings_overrides = {
|
settings_overrides = {
|
||||||
'DEFAULT_CENTER': (55.7558, 37.6173),
|
"DEFAULT_CENTER": (55.7558, 37.6173),
|
||||||
'DEFAULT_ZOOM': 12,
|
"DEFAULT_ZOOM": 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.prefetch_related("mirrors", "transponder")
|
return qs.prefetch_related(
|
||||||
|
"mirrors",
|
||||||
|
)
|
||||||
|
|
||||||
def mirrors_names(self, obj):
|
def mirrors_names(self, obj):
|
||||||
"""Отображает список зеркал через запятую."""
|
"""Отображает список зеркал через запятую."""
|
||||||
return ", ".join(m.name for m in obj.mirrors.all())
|
return ", ".join(m.name for m in obj.mirrors.all())
|
||||||
|
|
||||||
mirrors_names.short_description = "Зеркала"
|
mirrors_names.short_description = "Зеркала"
|
||||||
|
|
||||||
def formatted_timestamp(self, obj):
|
def formatted_timestamp(self, obj):
|
||||||
@@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
return ""
|
return ""
|
||||||
local_time = timezone.localtime(obj.timestamp)
|
local_time = timezone.localtime(obj.timestamp)
|
||||||
return local_time.strftime("%d.%m.%Y %H:%M:%S")
|
return local_time.strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
|
||||||
formatted_timestamp.short_description = "Дата и время"
|
formatted_timestamp.short_description = "Дата и время"
|
||||||
formatted_timestamp.admin_order_field = "timestamp"
|
formatted_timestamp.admin_order_field = "timestamp"
|
||||||
|
|
||||||
@@ -606,6 +667,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
|
|
||||||
geo_coords.short_description = "Координаты геолокации"
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
|
|
||||||
# def kupsat_coords(self, obj):
|
# def kupsat_coords(self, obj):
|
||||||
@@ -631,8 +693,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
# valid_coords.short_description = "Координаты оперативного отдела"
|
# valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ObjItem)
|
@admin.register(ObjItem)
|
||||||
class ObjItemAdmin(BaseAdmin):
|
class ObjItemAdmin(BaseAdmin):
|
||||||
"""
|
"""
|
||||||
@@ -644,6 +704,7 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
- Поддерживает поиск по имени, координатам и частоте
|
- Поддерживает поиск по имени, координатам и частоте
|
||||||
- Включает кастомные actions для отображения на карте
|
- Включает кастомные actions для отображения на карте
|
||||||
"""
|
"""
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
"sat_name",
|
"sat_name",
|
||||||
@@ -671,7 +732,7 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
"parameter_obj__id_satellite",
|
"parameter_obj__id_satellite",
|
||||||
"parameter_obj__polarization",
|
"parameter_obj__polarization",
|
||||||
"parameter_obj__modulation",
|
"parameter_obj__modulation",
|
||||||
"parameter_obj__standard"
|
"parameter_obj__standard",
|
||||||
)
|
)
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
@@ -701,13 +762,14 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
("Основная информация", {"fields": ("name",)}),
|
||||||
"fields": ("name",)
|
(
|
||||||
}),
|
"Метаданные",
|
||||||
("Метаданные", {
|
{
|
||||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||||
"classes": ("collapse",)
|
"classes": ("collapse",),
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
@@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
"parameter_obj__id_satellite",
|
"parameter_obj__id_satellite",
|
||||||
"parameter_obj__polarization",
|
"parameter_obj__polarization",
|
||||||
"parameter_obj__modulation",
|
"parameter_obj__modulation",
|
||||||
"parameter_obj__standard"
|
"parameter_obj__standard",
|
||||||
)
|
)
|
||||||
|
|
||||||
def sat_name(self, obj):
|
def sat_name(self, obj):
|
||||||
"""Отображает название спутника из связанного параметра."""
|
"""Отображает название спутника из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
if obj.parameter_obj.id_satellite:
|
if obj.parameter_obj.id_satellite:
|
||||||
return obj.parameter_obj.id_satellite.name
|
return obj.parameter_obj.id_satellite.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
sat_name.short_description = "Спутник"
|
sat_name.short_description = "Спутник"
|
||||||
sat_name.admin_order_field = "parameter_obj__id_satellite__name"
|
sat_name.admin_order_field = "parameter_obj__id_satellite__name"
|
||||||
|
|
||||||
def freq(self, obj):
|
def freq(self, obj):
|
||||||
"""Отображает частоту из связанного параметра."""
|
"""Отображает частоту из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
return obj.parameter_obj.frequency
|
return obj.parameter_obj.frequency
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
freq.short_description = "Частота, МГц"
|
freq.short_description = "Частота, МГц"
|
||||||
freq.admin_order_field = "parameter_obj__frequency"
|
freq.admin_order_field = "parameter_obj__frequency"
|
||||||
|
|
||||||
@@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
|
|
||||||
def pol(self, obj):
|
def pol(self, obj):
|
||||||
"""Отображает поляризацию из связанного параметра."""
|
"""Отображает поляризацию из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
if obj.parameter_obj.polarization:
|
if obj.parameter_obj.polarization:
|
||||||
return obj.parameter_obj.polarization.name
|
return obj.parameter_obj.polarization.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
pol.short_description = "Поляризация"
|
pol.short_description = "Поляризация"
|
||||||
|
|
||||||
def freq_range(self, obj):
|
def freq_range(self, obj):
|
||||||
"""Отображает полосу частот из связанного параметра."""
|
"""Отображает полосу частот из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
return obj.parameter_obj.freq_range
|
return obj.parameter_obj.freq_range
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
freq_range.short_description = "Полоса, МГц"
|
freq_range.short_description = "Полоса, МГц"
|
||||||
freq_range.admin_order_field = "parameter_obj__freq_range"
|
freq_range.admin_order_field = "parameter_obj__freq_range"
|
||||||
|
|
||||||
def bod_velocity(self, obj):
|
def bod_velocity(self, obj):
|
||||||
"""Отображает символьную скорость из связанного параметра."""
|
"""Отображает символьную скорость из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
return obj.parameter_obj.bod_velocity
|
return obj.parameter_obj.bod_velocity
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
bod_velocity.short_description = "Сим. v, БОД"
|
bod_velocity.short_description = "Сим. v, БОД"
|
||||||
|
|
||||||
def modulation(self, obj):
|
def modulation(self, obj):
|
||||||
"""Отображает модуляцию из связанного параметра."""
|
"""Отображает модуляцию из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
if obj.parameter_obj.modulation:
|
if obj.parameter_obj.modulation:
|
||||||
return obj.parameter_obj.modulation.name
|
return obj.parameter_obj.modulation.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
modulation.short_description = "Модуляция"
|
modulation.short_description = "Модуляция"
|
||||||
|
|
||||||
def snr(self, obj):
|
def snr(self, obj):
|
||||||
"""Отображает отношение сигнал/шум из связанного параметра."""
|
"""Отображает отношение сигнал/шум из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
return obj.parameter_obj.snr
|
return obj.parameter_obj.snr
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
snr.short_description = "ОСШ"
|
snr.short_description = "ОСШ"
|
||||||
|
|
||||||
def geo_coords(self, obj):
|
def geo_coords(self, obj):
|
||||||
@@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
|
|
||||||
geo_coords.short_description = "Координаты геолокации"
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
geo_coords.admin_order_field = "geo_obj__coords"
|
geo_coords.admin_order_field = "geo_obj__coords"
|
||||||
|
|
||||||
@@ -830,6 +900,7 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
|
|
||||||
kupsat_coords.short_description = "Координаты Кубсата"
|
kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
def valid_coords(self, obj):
|
def valid_coords(self, obj):
|
||||||
@@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
|
|
||||||
valid_coords.short_description = "Координаты оперативного отдела"
|
valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Band)
|
@admin.register(Band)
|
||||||
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
"""Админ-панель для модели Band."""
|
"""Админ-панель для модели Band."""
|
||||||
|
|
||||||
list_display = ("name", "border_start", "border_end")
|
list_display = ("name", "border_start", "border_end")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -855,6 +928,7 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
|
|
||||||
class ObjItemInline(admin.TabularInline):
|
class ObjItemInline(admin.TabularInline):
|
||||||
"""Inline для отображения объектов ObjItem в Source."""
|
"""Inline для отображения объектов ObjItem в Source."""
|
||||||
|
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
fk_name = "source"
|
fk_name = "source"
|
||||||
extra = 0
|
extra = 0
|
||||||
@@ -862,22 +936,36 @@ class ObjItemInline(admin.TabularInline):
|
|||||||
verbose_name = "Объект"
|
verbose_name = "Объект"
|
||||||
verbose_name_plural = "Объекты"
|
verbose_name_plural = "Объекты"
|
||||||
|
|
||||||
fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
fields = (
|
||||||
readonly_fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
"name",
|
||||||
|
"get_geo_coords",
|
||||||
|
"get_satellite",
|
||||||
|
"get_frequency",
|
||||||
|
"get_polarization",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
readonly_fields = (
|
||||||
|
"name",
|
||||||
|
"get_geo_coords",
|
||||||
|
"get_satellite",
|
||||||
|
"get_frequency",
|
||||||
|
"get_polarization",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""Оптимизированный queryset с предзагрузкой связанных объектов."""
|
"""Оптимизированный queryset с предзагрузкой связанных объектов."""
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.select_related(
|
return qs.select_related(
|
||||||
'geo_obj',
|
"geo_obj",
|
||||||
'parameter_obj',
|
"parameter_obj",
|
||||||
'parameter_obj__id_satellite',
|
"parameter_obj__id_satellite",
|
||||||
'parameter_obj__polarization'
|
"parameter_obj__polarization",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_geo_coords(self, obj):
|
def get_geo_coords(self, obj):
|
||||||
"""Отображает координаты из связанной модели Geo."""
|
"""Отображает координаты из связанной модели Geo."""
|
||||||
if not obj or not hasattr(obj, 'geo_obj'):
|
if not obj or not hasattr(obj, "geo_obj"):
|
||||||
return "-"
|
return "-"
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or not geo.coords:
|
if not geo or not geo.coords:
|
||||||
@@ -887,27 +975,39 @@ class ObjItemInline(admin.TabularInline):
|
|||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
|
|
||||||
get_geo_coords.short_description = "Координаты"
|
get_geo_coords.short_description = "Координаты"
|
||||||
|
|
||||||
def get_satellite(self, obj):
|
def get_satellite(self, obj):
|
||||||
"""Отображает спутник из связанного параметра."""
|
"""Отображает спутник из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.id_satellite:
|
if (
|
||||||
|
hasattr(obj, "parameter_obj")
|
||||||
|
and obj.parameter_obj
|
||||||
|
and obj.parameter_obj.id_satellite
|
||||||
|
):
|
||||||
return obj.parameter_obj.id_satellite.name
|
return obj.parameter_obj.id_satellite.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
get_satellite.short_description = "Спутник"
|
get_satellite.short_description = "Спутник"
|
||||||
|
|
||||||
def get_frequency(self, obj):
|
def get_frequency(self, obj):
|
||||||
"""Отображает частоту из связанного параметра."""
|
"""Отображает частоту из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||||
return obj.parameter_obj.frequency
|
return obj.parameter_obj.frequency
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
get_frequency.short_description = "Частота, МГц"
|
get_frequency.short_description = "Частота, МГц"
|
||||||
|
|
||||||
def get_polarization(self, obj):
|
def get_polarization(self, obj):
|
||||||
"""Отображает поляризацию из связанного параметра."""
|
"""Отображает поляризацию из связанного параметра."""
|
||||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.polarization:
|
if (
|
||||||
|
hasattr(obj, "parameter_obj")
|
||||||
|
and obj.parameter_obj
|
||||||
|
and obj.parameter_obj.polarization
|
||||||
|
):
|
||||||
return obj.parameter_obj.polarization.name
|
return obj.parameter_obj.polarization.name
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
get_polarization.short_description = "Поляризация"
|
get_polarization.short_description = "Поляризация"
|
||||||
|
|
||||||
def has_add_permission(self, request, obj=None):
|
def has_add_permission(self, request, obj=None):
|
||||||
@@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline):
|
|||||||
@admin.register(Source)
|
@admin.register(Source)
|
||||||
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||||
"""Админ-панель для модели Source."""
|
"""Админ-панель для модели Source."""
|
||||||
|
|
||||||
list_display = ("id", "created_at", "updated_at")
|
list_display = ("id", "created_at", "updated_at")
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
@@ -927,11 +1028,15 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
inlines = [ObjItemInline]
|
inlines = [ObjItemInline]
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Координаты: геолокация", {
|
(
|
||||||
"fields": ("coords_kupsat", "coords_valid", "coords_reference")
|
"Координаты: геолокация",
|
||||||
}),
|
{"fields": ("coords_kupsat", "coords_valid", "coords_reference")},
|
||||||
("Метаданные", {
|
),
|
||||||
|
(
|
||||||
|
"Метаданные",
|
||||||
|
{
|
||||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||||
"classes": ("collapse",)
|
"classes": ("collapse",),
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,57 +2,55 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
# Local imports
|
# Local imports
|
||||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
from .models import (
|
||||||
from .widgets import TagSelectWidget
|
Geo,
|
||||||
|
Modulation,
|
||||||
|
ObjItem,
|
||||||
|
Parameter,
|
||||||
|
Polarization,
|
||||||
|
Satellite,
|
||||||
|
Source,
|
||||||
|
Standard,
|
||||||
|
)
|
||||||
|
from .widgets import CheckboxSelectMultipleWidget
|
||||||
|
|
||||||
|
|
||||||
class UploadFileForm(forms.Form):
|
class UploadFileForm(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите файл",
|
label="Выберите файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-file-input"}),
|
||||||
'class': 'form-file-input'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LoadExcelData(forms.Form):
|
class LoadExcelData(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите Excel файл",
|
label="Выберите Excel файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.xlsx,.xls'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
number_input = forms.IntegerField(
|
number_input = forms.IntegerField(
|
||||||
label="Введите число объектов",
|
label="Введите число объектов",
|
||||||
min_value=0,
|
min_value=0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||||
'class': 'form-control'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LoadCsvData(forms.Form):
|
class LoadCsvData(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите CSV файл",
|
label="Выберите CSV файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.csv'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadVchLoad(UploadFileForm):
|
class UploadVchLoad(UploadFileForm):
|
||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,9 +58,7 @@ class VchLinkForm(forms.Form):
|
|||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
# ku_range = forms.ChoiceField(
|
# ku_range = forms.ChoiceField(
|
||||||
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||||
@@ -74,18 +70,22 @@ class VchLinkForm(forms.Form):
|
|||||||
label="Разброс по частоте (не используется)",
|
label="Разброс по частоте (не используется)",
|
||||||
required=False,
|
required=False,
|
||||||
initial=0.0,
|
initial=0.0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Не используется - погрешность определяется автоматически'
|
"class": "form-control",
|
||||||
})
|
"placeholder": "Не используется - погрешность определяется автоматически",
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
value2 = forms.FloatField(
|
value2 = forms.FloatField(
|
||||||
label="Разброс по полосе (в %)",
|
label="Разброс по полосе (в %)",
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Введите погрешность полосы в процентах',
|
"class": "form-control",
|
||||||
'step': '0.1'
|
"placeholder": "Введите погрешность полосы в процентах",
|
||||||
})
|
"step": "0.1",
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -106,10 +106,7 @@ class NewEventForm(forms.Form):
|
|||||||
# )
|
# )
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите файл",
|
label="Выберите файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.xlsx,.xls'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -117,53 +114,43 @@ class FillLyngsatDataForm(forms.Form):
|
|||||||
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
|
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
|
||||||
|
|
||||||
REGION_CHOICES = [
|
REGION_CHOICES = [
|
||||||
('europe', 'Европа'),
|
("europe", "Европа"),
|
||||||
('asia', 'Азия'),
|
("asia", "Азия"),
|
||||||
('america', 'Америка'),
|
("america", "Америка"),
|
||||||
('atlantic', 'Атлантика'),
|
("atlantic", "Атлантика"),
|
||||||
]
|
]
|
||||||
|
|
||||||
satellites = forms.ModelMultipleChoiceField(
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
queryset=Satellite.objects.all().order_by('name'),
|
queryset=Satellite.objects.all().order_by("name"),
|
||||||
label="Выберите спутники",
|
label="Выберите спутники",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '10'
|
|
||||||
}),
|
|
||||||
required=True,
|
required=True,
|
||||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников",
|
||||||
)
|
)
|
||||||
|
|
||||||
regions = forms.MultipleChoiceField(
|
regions = forms.MultipleChoiceField(
|
||||||
choices=REGION_CHOICES,
|
choices=REGION_CHOICES,
|
||||||
label="Выберите регионы",
|
label="Выберите регионы",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "4"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '4'
|
|
||||||
}),
|
|
||||||
required=True,
|
required=True,
|
||||||
initial=['europe', 'asia', 'america', 'atlantic'],
|
initial=["europe", "asia", "america", "atlantic"],
|
||||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов",
|
||||||
)
|
)
|
||||||
|
|
||||||
use_cache = forms.BooleanField(
|
use_cache = forms.BooleanField(
|
||||||
label="Использовать кеширование",
|
label="Использовать кеширование",
|
||||||
required=False,
|
required=False,
|
||||||
initial=True,
|
initial=True,
|
||||||
widget=forms.CheckboxInput(attrs={
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'class': 'form-check-input'
|
help_text="Использовать кешированные данные (ускоряет повторные запросы)",
|
||||||
}),
|
|
||||||
help_text="Использовать кешированные данные (ускоряет повторные запросы)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
force_refresh = forms.BooleanField(
|
force_refresh = forms.BooleanField(
|
||||||
label="Принудительно обновить данные",
|
label="Принудительно обновить данные",
|
||||||
required=False,
|
required=False,
|
||||||
initial=False,
|
initial=False,
|
||||||
widget=forms.CheckboxInput(attrs={
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'class': 'form-check-input'
|
help_text="Игнорировать кеш и получить свежие данные с сайта",
|
||||||
}),
|
|
||||||
help_text="Игнорировать кеш и получить свежие данные с сайта"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,26 +158,22 @@ class LinkLyngsatForm(forms.Form):
|
|||||||
"""Форма для привязки источников LyngSat к объектам"""
|
"""Форма для привязки источников LyngSat к объектам"""
|
||||||
|
|
||||||
satellites = forms.ModelMultipleChoiceField(
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
queryset=Satellite.objects.all().order_by('name'),
|
queryset=Satellite.objects.all().order_by("name"),
|
||||||
label="Выберите спутники",
|
label="Выберите спутники",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '10'
|
|
||||||
}),
|
|
||||||
required=False,
|
required=False,
|
||||||
help_text="Оставьте пустым для обработки всех спутников"
|
help_text="Оставьте пустым для обработки всех спутников",
|
||||||
)
|
)
|
||||||
|
|
||||||
frequency_tolerance = forms.FloatField(
|
frequency_tolerance = forms.FloatField(
|
||||||
label="Допуск по частоте (МГц)",
|
label="Допуск по частоте (МГц)",
|
||||||
initial=0.5,
|
initial=0.5,
|
||||||
min_value=0,
|
min_value=0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}),
|
||||||
'class': 'form-control',
|
help_text="Допустимое отклонение частоты при сравнении",
|
||||||
'step': '0.1'
|
|
||||||
}),
|
|
||||||
help_text="Допустимое отклонение частоты при сравнении"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ParameterForm(forms.ModelForm):
|
class ParameterForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||||
@@ -201,73 +184,88 @@ class ParameterForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Parameter
|
model = Parameter
|
||||||
fields = [
|
fields = [
|
||||||
'id_satellite', 'frequency', 'freq_range', 'polarization',
|
"id_satellite",
|
||||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"polarization",
|
||||||
|
"bod_velocity",
|
||||||
|
"modulation",
|
||||||
|
"snr",
|
||||||
|
"standard",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'id_satellite': forms.Select(attrs={
|
"id_satellite": forms.Select(
|
||||||
'class': 'form-select',
|
attrs={"class": "form-select", "required": True}
|
||||||
'required': True
|
),
|
||||||
}),
|
"frequency": forms.NumberInput(
|
||||||
'frequency': forms.NumberInput(attrs={
|
attrs={
|
||||||
'class': 'form-control',
|
"class": "form-control",
|
||||||
'step': '0.000001',
|
"step": "0.000001",
|
||||||
'min': '0',
|
"min": "0",
|
||||||
'max': '50000',
|
"max": "50000",
|
||||||
'placeholder': 'Введите частоту в МГц'
|
"placeholder": "Введите частоту в МГц",
|
||||||
}),
|
}
|
||||||
'freq_range': forms.NumberInput(attrs={
|
),
|
||||||
'class': 'form-control',
|
"freq_range": forms.NumberInput(
|
||||||
'step': '0.000001',
|
attrs={
|
||||||
'min': '0',
|
"class": "form-control",
|
||||||
'max': '1000',
|
"step": "0.000001",
|
||||||
'placeholder': 'Введите полосу частот в МГц'
|
"min": "0",
|
||||||
}),
|
"max": "1000",
|
||||||
'bod_velocity': forms.NumberInput(attrs={
|
"placeholder": "Введите полосу частот в МГц",
|
||||||
'class': 'form-control',
|
}
|
||||||
'step': '0.001',
|
),
|
||||||
'min': '0',
|
"bod_velocity": forms.NumberInput(
|
||||||
'placeholder': 'Введите символьную скорость в БОД'
|
attrs={
|
||||||
}),
|
"class": "form-control",
|
||||||
'snr': forms.NumberInput(attrs={
|
"step": "0.001",
|
||||||
'class': 'form-control',
|
"min": "0",
|
||||||
'step': '0.001',
|
"placeholder": "Введите символьную скорость в БОД",
|
||||||
'min': '-50',
|
}
|
||||||
'max': '100',
|
),
|
||||||
'placeholder': 'Введите ОСШ в дБ'
|
"snr": forms.NumberInput(
|
||||||
}),
|
attrs={
|
||||||
'polarization': forms.Select(attrs={'class': 'form-select'}),
|
"class": "form-control",
|
||||||
'modulation': forms.Select(attrs={'class': 'form-select'}),
|
"step": "0.001",
|
||||||
'standard': forms.Select(attrs={'class': 'form-select'}),
|
"min": "-50",
|
||||||
|
"max": "100",
|
||||||
|
"placeholder": "Введите ОСШ в дБ",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"polarization": forms.Select(attrs={"class": "form-select"}),
|
||||||
|
"modulation": forms.Select(attrs={"class": "form-select"}),
|
||||||
|
"standard": forms.Select(attrs={"class": "form-select"}),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'id_satellite': 'Спутник',
|
"id_satellite": "Спутник",
|
||||||
'frequency': 'Частота (МГц)',
|
"frequency": "Частота (МГц)",
|
||||||
'freq_range': 'Полоса частот (МГц)',
|
"freq_range": "Полоса частот (МГц)",
|
||||||
'polarization': 'Поляризация',
|
"polarization": "Поляризация",
|
||||||
'bod_velocity': 'Символьная скорость (БОД)',
|
"bod_velocity": "Символьная скорость (БОД)",
|
||||||
'modulation': 'Модуляция',
|
"modulation": "Модуляция",
|
||||||
'snr': 'ОСШ (дБ)',
|
"snr": "ОСШ (дБ)",
|
||||||
'standard': 'Стандарт',
|
"standard": "Стандарт",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
|
"frequency": "Частота в диапазоне от 0 до 50000 МГц",
|
||||||
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
|
"freq_range": "Полоса частот в диапазоне от 0 до 1000 МГц",
|
||||||
'bod_velocity': 'Символьная скорость должна быть положительной',
|
"bod_velocity": "Символьная скорость должна быть положительной",
|
||||||
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
|
"snr": "Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Динамически загружаем choices для select полей
|
# Динамически загружаем choices для select полей
|
||||||
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
self.fields["id_satellite"].queryset = Satellite.objects.all().order_by("name")
|
||||||
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
|
self.fields["polarization"].queryset = Polarization.objects.all().order_by(
|
||||||
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name')
|
"name"
|
||||||
self.fields['standard'].queryset = Standard.objects.all().order_by('name')
|
)
|
||||||
|
self.fields["modulation"].queryset = Modulation.objects.all().order_by("name")
|
||||||
|
self.fields["standard"].queryset = Standard.objects.all().order_by("name")
|
||||||
|
|
||||||
# Делаем спутник обязательным полем
|
# Делаем спутник обязательным полем
|
||||||
self.fields['id_satellite'].required = True
|
self.fields["id_satellite"].required = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
@@ -276,42 +274,54 @@ class ParameterForm(forms.ModelForm):
|
|||||||
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
||||||
"""
|
"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
frequency = cleaned_data.get('frequency')
|
frequency = cleaned_data.get("frequency")
|
||||||
freq_range = cleaned_data.get('freq_range')
|
freq_range = cleaned_data.get("freq_range")
|
||||||
bod_velocity = cleaned_data.get('bod_velocity')
|
bod_velocity = cleaned_data.get("bod_velocity")
|
||||||
|
|
||||||
# Проверка что частота больше полосы частот
|
# Проверка что частота больше полосы частот
|
||||||
if frequency and freq_range:
|
if frequency and freq_range:
|
||||||
if freq_range > frequency:
|
if freq_range > frequency:
|
||||||
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
|
self.add_error(
|
||||||
|
"freq_range", "Полоса частот не может быть больше частоты"
|
||||||
|
)
|
||||||
|
|
||||||
# Проверка что символьная скорость соответствует полосе частот
|
# Проверка что символьная скорость соответствует полосе частот
|
||||||
if bod_velocity and freq_range:
|
if bod_velocity and freq_range:
|
||||||
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
||||||
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
|
self.add_error(
|
||||||
|
"bod_velocity",
|
||||||
|
"Символьная скорость не может превышать полосу частот",
|
||||||
|
)
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class GeoForm(forms.ModelForm):
|
class GeoForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Geo
|
model = Geo
|
||||||
fields = ['location', 'comment', 'is_average', 'mirrors']
|
fields = ["location", "comment", "is_average", "mirrors"]
|
||||||
widgets = {
|
widgets = {
|
||||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
"location": forms.TextInput(attrs={"class": "form-control"}),
|
||||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
"comment": forms.TextInput(attrs={"class": "form-control"}),
|
||||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}),
|
"mirrors": CheckboxSelectMultipleWidget(
|
||||||
|
attrs={
|
||||||
|
"id": "id_geo-mirrors",
|
||||||
|
"placeholder": "Выберите спутники...",
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'location': 'Местоположение',
|
"location": "Местоположение",
|
||||||
'comment': 'Комментарий',
|
"comment": "Комментарий",
|
||||||
'is_average': 'Усреднённое',
|
"is_average": "Усреднённое",
|
||||||
'mirrors': 'Спутники-зеркала, использованные для приёма',
|
"mirrors": "Спутники-зеркала, использованные для приёма",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mirrors': 'Начните вводить название спутника для поиска',
|
"mirrors": "Выберите спутники из списка",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ObjItemForm(forms.ModelForm):
|
class ObjItemForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Форма для создания и редактирования объектов (источников сигнала).
|
Форма для создания и редактирования объектов (источников сигнала).
|
||||||
@@ -322,25 +332,27 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
fields = ['name']
|
fields = ["name"]
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(attrs={
|
"name": forms.TextInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Введите название объекта',
|
"class": "form-control",
|
||||||
'maxlength': '100'
|
"placeholder": "Введите название объекта",
|
||||||
}),
|
"maxlength": "100",
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Название объекта',
|
"name": "Название объекта",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': 'Уникальное название объекта/источника сигнала',
|
"name": "Уникальное название объекта/источника сигнала",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Делаем поле name необязательным, так как оно может быть пустым
|
# Делаем поле name необязательным, так как оно может быть пустым
|
||||||
self.fields['name'].required = False
|
self.fields["name"].required = False
|
||||||
|
|
||||||
def clean_name(self):
|
def clean_name(self):
|
||||||
"""
|
"""
|
||||||
@@ -348,7 +360,7 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
Проверяет что название не состоит только из пробелов.
|
Проверяет что название не состоит только из пробелов.
|
||||||
"""
|
"""
|
||||||
name = self.cleaned_data.get('name')
|
name = self.cleaned_data.get("name")
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
# Удаляем лишние пробелы
|
# Удаляем лишние пробелы
|
||||||
@@ -356,6 +368,165 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Проверяем что после удаления пробелов что-то осталось
|
# Проверяем что после удаления пробелов что-то осталось
|
||||||
if not name:
|
if not name:
|
||||||
raise forms.ValidationError('Название не может состоять только из пробелов')
|
raise forms.ValidationError(
|
||||||
|
"Название не может состоять только из пробелов"
|
||||||
|
)
|
||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class SourceForm(forms.ModelForm):
|
||||||
|
"""Form for editing Source model with 4 coordinate fields."""
|
||||||
|
|
||||||
|
# Координаты ГЛ (coords_average)
|
||||||
|
average_latitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||||
|
),
|
||||||
|
label="Широта ГЛ",
|
||||||
|
)
|
||||||
|
average_longitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"step": "0.000001",
|
||||||
|
"placeholder": "Долгота",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
label="Долгота ГЛ",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Координаты Кубсата (coords_kupsat)
|
||||||
|
kupsat_latitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||||
|
),
|
||||||
|
label="Широта Кубсата",
|
||||||
|
)
|
||||||
|
kupsat_longitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"step": "0.000001",
|
||||||
|
"placeholder": "Долгота",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
label="Долгота Кубсата",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Координаты оперативников (coords_valid)
|
||||||
|
valid_latitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||||
|
),
|
||||||
|
label="Широта оперативников",
|
||||||
|
)
|
||||||
|
valid_longitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"step": "0.000001",
|
||||||
|
"placeholder": "Долгота",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
label="Долгота оперативников",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Координаты справочные (coords_reference)
|
||||||
|
reference_latitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||||
|
),
|
||||||
|
label="Широта справочные",
|
||||||
|
)
|
||||||
|
reference_longitude = forms.FloatField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"step": "0.000001",
|
||||||
|
"placeholder": "Долгота",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
label="Долгота справочные",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Source
|
||||||
|
fields = [] # Все поля обрабатываются вручную
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Заполняем поля координат из instance
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
if self.instance.coords_average:
|
||||||
|
self.fields[
|
||||||
|
"average_longitude"
|
||||||
|
].initial = self.instance.coords_average.x
|
||||||
|
self.fields["average_latitude"].initial = self.instance.coords_average.y
|
||||||
|
|
||||||
|
if self.instance.coords_kupsat:
|
||||||
|
self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x
|
||||||
|
self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y
|
||||||
|
|
||||||
|
if self.instance.coords_valid:
|
||||||
|
self.fields["valid_longitude"].initial = self.instance.coords_valid.x
|
||||||
|
self.fields["valid_latitude"].initial = self.instance.coords_valid.y
|
||||||
|
|
||||||
|
if self.instance.coords_reference:
|
||||||
|
self.fields[
|
||||||
|
"reference_longitude"
|
||||||
|
].initial = self.instance.coords_reference.x
|
||||||
|
self.fields[
|
||||||
|
"reference_latitude"
|
||||||
|
].initial = self.instance.coords_reference.y
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
|
||||||
|
# Обработка coords_average
|
||||||
|
avg_lat = self.cleaned_data.get("average_latitude")
|
||||||
|
avg_lng = self.cleaned_data.get("average_longitude")
|
||||||
|
if avg_lat is not None and avg_lng is not None:
|
||||||
|
instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
|
||||||
|
else:
|
||||||
|
instance.coords_average = None
|
||||||
|
|
||||||
|
# Обработка coords_kupsat
|
||||||
|
kup_lat = self.cleaned_data.get("kupsat_latitude")
|
||||||
|
kup_lng = self.cleaned_data.get("kupsat_longitude")
|
||||||
|
if kup_lat is not None and kup_lng is not None:
|
||||||
|
instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326)
|
||||||
|
else:
|
||||||
|
instance.coords_kupsat = None
|
||||||
|
|
||||||
|
# Обработка coords_valid
|
||||||
|
val_lat = self.cleaned_data.get("valid_latitude")
|
||||||
|
val_lng = self.cleaned_data.get("valid_longitude")
|
||||||
|
if val_lat is not None and val_lng is not None:
|
||||||
|
instance.coords_valid = Point(val_lng, val_lat, srid=4326)
|
||||||
|
else:
|
||||||
|
instance.coords_valid = None
|
||||||
|
|
||||||
|
# Обработка coords_reference
|
||||||
|
ref_lat = self.cleaned_data.get("reference_latitude")
|
||||||
|
ref_lng = self.cleaned_data.get("reference_longitude")
|
||||||
|
if ref_lat is not None and ref_lng is not None:
|
||||||
|
instance.coords_reference = Point(ref_lng, ref_lat, srid=4326)
|
||||||
|
else:
|
||||||
|
instance.coords_reference = None
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|||||||
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=0 column_label="Выбрать" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %}
|
||||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %}
|
||||||
|
{% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th scope="col">Имя</th>
|
<th scope="col">Имя</th>
|
||||||
<th scope="col">Спутник</th>
|
<th scope="col">Спутник</th>
|
||||||
|
<th scope="col">Транспондер</th>
|
||||||
<th scope="col">Част, МГц</th>
|
<th scope="col">Част, МГц</th>
|
||||||
<th scope="col">Полоса, МГц</th>
|
<th scope="col">Полоса, МГц</th>
|
||||||
<th scope="col">Поляризация</th>
|
<th scope="col">Поляризация</th>
|
||||||
@@ -41,8 +42,6 @@
|
|||||||
<th scope="col">Время ГЛ</th>
|
<th scope="col">Время ГЛ</th>
|
||||||
<th scope="col">Местоположение</th>
|
<th scope="col">Местоположение</th>
|
||||||
<th scope="col">Геолокация</th>
|
<th scope="col">Геолокация</th>
|
||||||
<th scope="col">Кубсат</th>
|
|
||||||
<th scope="col">Опер. отд</th>
|
|
||||||
<th scope="col">Обновлено</th>
|
<th scope="col">Обновлено</th>
|
||||||
<th scope="col">Кем(обн)</th>
|
<th scope="col">Кем(обн)</th>
|
||||||
<th scope="col">Создано</th>
|
<th scope="col">Создано</th>
|
||||||
|
|||||||
@@ -6,27 +6,93 @@
|
|||||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.css' %}">
|
||||||
<style>
|
<style>
|
||||||
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
|
.form-section {
|
||||||
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
margin-bottom: 2rem;
|
||||||
.btn-action { margin-right: 0.5rem; }
|
border: 1px solid #dee2e6;
|
||||||
.dynamic-form { border: 1px dashed #ced4da; padding: 1rem; margin-top: 1rem; border-radius: 0.25rem; }
|
border-radius: 0.25rem;
|
||||||
.dynamic-form-header { display: flex; justify-content: space-between; align-items: center; }
|
padding: 1rem;
|
||||||
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
}
|
||||||
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
|
||||||
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
|
.form-section-header {
|
||||||
.form-check-input { margin-top: 0.25rem; }
|
border-bottom: 1px solid #dee2e6;
|
||||||
.datetime-group { display: flex; gap: 1rem; }
|
padding-bottom: 0.5rem;
|
||||||
.datetime-group > div { flex: 1; }
|
margin-bottom: 1rem;
|
||||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
}
|
||||||
.map-container { margin-bottom: 1rem; }
|
|
||||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
.btn-action {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-form {
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-field {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-group {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-group-header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-group>div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-sync-group {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.map-controls {
|
.map-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn {
|
.map-control-btn {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid #ced4da;
|
||||||
@@ -34,25 +100,43 @@
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.active {
|
.map-control-btn.active {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
border-color: #dee2e6;
|
border-color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.edit {
|
.map-control-btn.edit {
|
||||||
background-color: #fff3cd;
|
background-color: #fff3cd;
|
||||||
border-color: #ffeeba;
|
border-color: #ffeeba;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.save {
|
.map-control-btn.save {
|
||||||
background-color: #d1ecf1;
|
background-color: #d1ecf1;
|
||||||
border-color: #bee5eb;
|
border-color: #bee5eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.cancel {
|
.map-control-btn.cancel {
|
||||||
background-color: #f8d7da;
|
background-color: #f8d7da;
|
||||||
border-color: #f5c6cb;
|
border-color: #f5c6cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
.leaflet-marker-icon {
|
||||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Select2 custom styling */
|
||||||
|
.select2-container--default .select2-selection--multiple {
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: #86b7fe;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -65,10 +149,12 @@
|
|||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
{% if object %}
|
{% if object %}
|
||||||
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
|
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
class="btn btn-danger btn-action">Удалить</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
class="btn btn-secondary btn-action">Назад</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,16 +272,16 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control" id="id_geo_latitude"
|
||||||
id="id_geo_latitude" name="geo_latitude"
|
name="geo_latitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control" id="id_geo_longitude"
|
||||||
id="id_geo_longitude" name="geo_longitude"
|
name="geo_longitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,14 +304,12 @@
|
|||||||
<div class="datetime-group">
|
<div class="datetime-group">
|
||||||
<div>
|
<div>
|
||||||
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
||||||
<input type="date" class="form-control"
|
<input type="date" class="form-control" id="id_timestamp_date" name="timestamp_date"
|
||||||
id="id_timestamp_date" name="timestamp_date"
|
|
||||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="id_timestamp_time" class="form-label">Время:</label>
|
<label for="id_timestamp_time" class="form-label">Время:</label>
|
||||||
<input type="time" class="form-control"
|
<input type="time" class="form-control" id="id_timestamp_time" name="timestamp_time"
|
||||||
id="id_timestamp_time" name="timestamp_time"
|
|
||||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +337,9 @@
|
|||||||
{% leaflet_css %}
|
{% leaflet_css %}
|
||||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Подключаем кастомный виджет для мультивыбора -->
|
||||||
|
<script src="{% static 'js/checkbox-select-multiple.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Инициализация карты
|
// Инициализация карты
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
|
|
||||||
<!-- Filter Toggle Button -->
|
<!-- Filter Toggle Button -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||||
<i class="bi bi-funnel"></i> Фильтры
|
<i class="bi bi-funnel"></i> Фильтры
|
||||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -85,7 +86,8 @@
|
|||||||
|
|
||||||
<!-- Selected Items Counter Button -->
|
<!-- Selected Items Counter Button -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||||
<i class="bi bi-list-check"></i> Список
|
<i class="bi bi-list-check"></i> Список
|
||||||
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
|
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -202,52 +204,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kubsat Coordinates Filter -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Координаты Кубсата:</label>
|
|
||||||
<div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1"
|
|
||||||
value="1" {% if has_kupsat == '1' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0"
|
|
||||||
value="0" {% if has_kupsat == '0' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Valid Coordinates Filter -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Координаты опер. отдела:</label>
|
|
||||||
<div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1"
|
|
||||||
value="1" {% if has_valid == '1' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0"
|
|
||||||
value="0" {% if has_valid == '0' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source Type Filter -->
|
<!-- Source Type Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Тип источника:</label>
|
<label class="form-label">Тип источника:</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1"
|
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||||
value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
id="has_source_type_1" value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
|
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0"
|
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||||
value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
id="has_source_type_0" value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_source_type_0">Нет</label>
|
<label class="form-check-label" for="has_source_type_0">Нет</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,13 +227,13 @@
|
|||||||
<label class="form-label">Sigma:</label>
|
<label class="form-label">Sigma:</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1"
|
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" value="1"
|
||||||
value="1" {% if has_sigma == '1' %}checked{% endif %}>
|
{% if has_sigma == '1' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_sigma_1">Есть</label>
|
<label class="form-check-label" for="has_sigma_1">Есть</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0"
|
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" value="0"
|
||||||
value="0" {% if has_sigma == '0' %}checked{% endif %}>
|
{% if has_sigma == '0' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_sigma_0">Нет</label>
|
<label class="form-check-label" for="has_sigma_0">Нет</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,8 +258,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||||
placeholder="От" value="{{ date_from|default:'' }}">
|
placeholder="От" value="{{ date_from|default:'' }}">
|
||||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm" placeholder="До"
|
||||||
placeholder="До" value="{{ date_to|default:'' }}">
|
value="{{ date_to|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apply Filters and Reset Buttons -->
|
<!-- Apply Filters and Reset Buttons -->
|
||||||
@@ -316,6 +285,7 @@
|
|||||||
</th>
|
</th>
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Транспондер" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
||||||
@@ -341,11 +311,22 @@
|
|||||||
{% for item in processed_objects %}
|
{% for item in processed_objects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<input type="checkbox" class="form-check-input item-checkbox"
|
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||||
value="{{ item.id }}">
|
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
<td>
|
||||||
|
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||||
<td>{{ item.satellite_name }}</td>
|
<td>{{ item.satellite_name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if item.obj.transponder %}
|
||||||
|
<a href="#" class="text-success text-decoration-none"
|
||||||
|
onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;"
|
||||||
|
title="Показать данные транспондера">
|
||||||
|
<i class="bi bi-broadcast"></i> {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ item.frequency }}</td>
|
<td>{{ item.frequency }}</td>
|
||||||
<td>{{ item.freq_range }}</td>
|
<td>{{ item.freq_range }}</td>
|
||||||
<td>{{ item.polarization }}</td>
|
<td>{{ item.polarization }}</td>
|
||||||
@@ -364,7 +345,8 @@
|
|||||||
<td>{{ item.standard }}</td>
|
<td>{{ item.standard }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.obj.lyngsat_source %}
|
{% if item.obj.lyngsat_source %}
|
||||||
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
<a href="#" class="text-primary text-decoration-none"
|
||||||
|
onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
||||||
<i class="bi bi-tv"></i> ТВ
|
<i class="bi bi-tv"></i> ТВ
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -373,7 +355,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.has_sigma %}
|
{% if item.has_sigma %}
|
||||||
<a href="#" class="text-info text-decoration-none" onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;" title="{{ item.sigma_info }}">
|
<a href="#" class="text-info text-decoration-none"
|
||||||
|
onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;"
|
||||||
|
title="{{ item.sigma_info }}">
|
||||||
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
|
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -720,8 +704,8 @@
|
|||||||
|
|
||||||
// Initialize column visibility - hide creation columns by default
|
// Initialize column visibility - hide creation columns by default
|
||||||
function initColumnVisibility() {
|
function initColumnVisibility() {
|
||||||
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
|
const creationDateCheckbox = document.querySelector('input[data-column="15"]');
|
||||||
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
|
const creationUserCheckbox = document.querySelector('input[data-column="16"]');
|
||||||
if (creationDateCheckbox) {
|
if (creationDateCheckbox) {
|
||||||
creationDateCheckbox.checked = false;
|
creationDateCheckbox.checked = false;
|
||||||
toggleColumn(creationDateCheckbox);
|
toggleColumn(creationDateCheckbox);
|
||||||
@@ -733,9 +717,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide comment, is_average, and standard columns by default
|
// Hide comment, is_average, and standard columns by default
|
||||||
const commentCheckbox = document.querySelector('input[data-column="16"]');
|
const commentCheckbox = document.querySelector('input[data-column="17"]');
|
||||||
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
|
const isAverageCheckbox = document.querySelector('input[data-column="18"]');
|
||||||
const standardCheckbox = document.querySelector('input[data-column="18"]');
|
const standardCheckbox = document.querySelector('input[data-column="19"]');
|
||||||
|
|
||||||
if (commentCheckbox) {
|
if (commentCheckbox) {
|
||||||
commentCheckbox.checked = false;
|
commentCheckbox.checked = false;
|
||||||
@@ -904,22 +888,21 @@
|
|||||||
id: itemId,
|
id: itemId,
|
||||||
name: row.cells[1].textContent,
|
name: row.cells[1].textContent,
|
||||||
satellite: row.cells[2].textContent,
|
satellite: row.cells[2].textContent,
|
||||||
frequency: row.cells[3].textContent,
|
transponder: row.cells[3].textContent,
|
||||||
freq_range: row.cells[4].textContent,
|
frequency: row.cells[4].textContent,
|
||||||
polarization: row.cells[5].textContent,
|
freq_range: row.cells[5].textContent,
|
||||||
bod_velocity: row.cells[6].textContent,
|
polarization: row.cells[6].textContent,
|
||||||
modulation: row.cells[7].textContent,
|
bod_velocity: row.cells[7].textContent,
|
||||||
snr: row.cells[8].textContent,
|
modulation: row.cells[8].textContent,
|
||||||
geo_timestamp: row.cells[9].textContent,
|
snr: row.cells[9].textContent,
|
||||||
geo_location: row.cells[10].textContent,
|
geo_timestamp: row.cells[10].textContent,
|
||||||
geo_coords: row.cells[11].textContent,
|
geo_location: row.cells[11].textContent,
|
||||||
kupsat_coords: row.cells[12].textContent,
|
geo_coords: row.cells[12].textContent,
|
||||||
valid_coords: row.cells[13].textContent,
|
updated_at: row.cells[13].textContent,
|
||||||
updated_at: row.cells[12].textContent,
|
updated_by: row.cells[14].textContent,
|
||||||
updated_by: row.cells[13].textContent,
|
created_at: row.cells[15].textContent,
|
||||||
created_at: row.cells[14].textContent,
|
created_by: row.cells[16].textContent,
|
||||||
created_by: row.cells[15].textContent,
|
mirrors: row.cells[22].textContent
|
||||||
mirrors: row.cells[21].textContent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.selectedItems.push(rowData);
|
window.selectedItems.push(rowData);
|
||||||
@@ -966,6 +949,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>${item.name}</td>
|
<td>${item.name}</td>
|
||||||
<td>${item.satellite}</td>
|
<td>${item.satellite}</td>
|
||||||
|
<td>${item.transponder}</td>
|
||||||
<td>${item.frequency}</td>
|
<td>${item.frequency}</td>
|
||||||
<td>${item.freq_range}</td>
|
<td>${item.freq_range}</td>
|
||||||
<td>${item.polarization}</td>
|
<td>${item.polarization}</td>
|
||||||
@@ -975,8 +959,6 @@
|
|||||||
<td>${item.geo_timestamp}</td>
|
<td>${item.geo_timestamp}</td>
|
||||||
<td>${item.geo_location}</td>
|
<td>${item.geo_location}</td>
|
||||||
<td>${item.geo_coords}</td>
|
<td>${item.geo_coords}</td>
|
||||||
<td>${item.kupsat_coords}</td>
|
|
||||||
<td>${item.valid_coords}</td>
|
|
||||||
<td>${item.updated_at}</td>
|
<td>${item.updated_at}</td>
|
||||||
<td>${item.updated_by}</td>
|
<td>${item.updated_by}</td>
|
||||||
<td>${item.created_at}</td>
|
<td>${item.created_at}</td>
|
||||||
@@ -1056,7 +1038,8 @@
|
|||||||
<h5 class="modal-title" id="lyngsatModalLabel">
|
<h5 class="modal-title" id="lyngsatModalLabel">
|
||||||
<i class="bi bi-tv"></i> Данные источника LyngSat
|
<i class="bi bi-tv"></i> Данные источника LyngSat
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||||
|
aria-label="Закрыть"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="lyngsatModalBody">
|
<div class="modal-body" id="lyngsatModalBody">
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
@@ -1202,5 +1185,128 @@ function showLyngsatModal(lyngsatId) {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to show transponder modal
|
||||||
|
function showTransponderModal(transponderId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
const modalBody = document.getElementById('transponderModalBody');
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border text-success" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(`/api/transponder/${transponderId}/`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка загрузки данных транспондера');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
let html = `
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm table-borderless mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted" style="width: 40%;">Название:</td>
|
||||||
|
<td><strong>${data.name || '-'}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Спутник:</td>
|
||||||
|
<td><strong>${data.satellite}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Зона покрытия:</td>
|
||||||
|
<td>${data.zone_name || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Поляризация:</td>
|
||||||
|
<td><span class="badge bg-info">${data.polarization}</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<strong><i class="bi bi-broadcast"></i> Частотные параметры</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm table-borderless mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted" style="width: 40%;">Downlink:</td>
|
||||||
|
<td><strong>${data.downlink} МГц</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Uplink:</td>
|
||||||
|
<td><strong>${data.uplink || '-'} ${data.uplink ? 'МГц' : ''}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Полоса:</td>
|
||||||
|
<td><strong>${data.frequency_range} МГц</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Перенос:</td>
|
||||||
|
<td>${data.transfer || '-'} ${data.transfer ? 'МГц' : ''}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
modalBody.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> ${error.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Transponder Data Modal -->
|
||||||
|
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title" id="transponderModalLabel">
|
||||||
|
<i class="bi bi-broadcast"></i> Данные транспондера
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="transponderModalBody">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border text-success" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,10 +1,74 @@
|
|||||||
{% extends "mapsapp/map2d_base.html" %}
|
{% extends "mainapp/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}Карта выбранных объектов{% endblock title %}
|
{% block title %}Карта выбранных объектов{% endblock title %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<!-- Leaflet CSS -->
|
||||||
|
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||||
|
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||||
|
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
position: fixed;
|
||||||
|
top: 56px; /* Высота navbar */
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="map"></div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
<!-- Leaflet JavaScript -->
|
||||||
|
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||||
|
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||||
|
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
// Инициализация карты
|
||||||
|
let map = L.map('map').setView([55.75, 37.62], 5);
|
||||||
|
L.control.scale({
|
||||||
|
imperial: false,
|
||||||
|
metric: true
|
||||||
|
}).addTo(map);
|
||||||
|
map.attributionControl.setPrefix(false);
|
||||||
|
|
||||||
|
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <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 markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||||
var getColorIcon = function(color) {
|
var getColorIcon = function(color) {
|
||||||
return L.icon({
|
return L.icon({
|
||||||
@@ -19,47 +83,52 @@
|
|||||||
|
|
||||||
var overlays = [];
|
var overlays = [];
|
||||||
|
|
||||||
|
// Создаём слои для каждого объекта
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
var groupIndex = {{ forloop.counter0 }};
|
var groupIndex = {{ forloop.counter0 }};
|
||||||
|
var groupName = '{{ group.name|escapejs }}';
|
||||||
var colorName = markerColors[groupIndex % markerColors.length];
|
var colorName = markerColors[groupIndex % markerColors.length];
|
||||||
var groupIcon = getColorIcon(colorName);
|
var groupIcon = getColorIcon(colorName);
|
||||||
|
|
||||||
var groupLayer = L.layerGroup();
|
var groupLayer = L.layerGroup();
|
||||||
|
|
||||||
var subgroup = [];
|
var subgroup = [];
|
||||||
{% for point_data in group.points %}
|
{% for point_data in group.points %}
|
||||||
var pointName = "{{ group.name|escapejs }}";
|
var pointName = "{{ group.name|escapejs }}";
|
||||||
|
|
||||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||||
icon: groupIcon
|
icon: groupIcon
|
||||||
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
|
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
|
||||||
|
|
||||||
groupLayer.addLayer(marker);
|
groupLayer.addLayer(marker);
|
||||||
|
|
||||||
subgroup.push({
|
subgroup.push({
|
||||||
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
label: "{{ forloop.counter }} - {{ point_data.frequency|escapejs }}",
|
||||||
layer: marker
|
layer: marker
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
overlays.push({
|
overlays.push({
|
||||||
label: '{{ group.name|escapejs }}',
|
label: groupName,
|
||||||
selectAllCheckbox: true,
|
selectAllCheckbox: true,
|
||||||
children: subgroup,
|
children: subgroup,
|
||||||
layer: groupLayer
|
layer: groupLayer
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
// Create the layer control with a custom container that includes a select all checkbox
|
// Корневая группа
|
||||||
var layerControl = L.control.layers.tree(baseLayers, overlays, {
|
const rootGroup = {
|
||||||
|
label: "Все точки",
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: overlays,
|
||||||
|
layer: L.layerGroup()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Создаём tree control
|
||||||
|
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
autoZIndex: true
|
autoZIndex: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the layer control to the map
|
|
||||||
layerControl.addTo(map);
|
layerControl.addTo(map);
|
||||||
|
|
||||||
// Calculate map bounds to fit all markers
|
// Подгоняем карту под все маркеры
|
||||||
{% if groups %}
|
{% if groups %}
|
||||||
var groupBounds = L.featureGroup([]);
|
var groupBounds = L.featureGroup([]);
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
@@ -67,40 +136,7 @@
|
|||||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
|
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||||
{% else %}
|
|
||||||
map.setView([55.75, 37.62], 5); // Default view if no markers
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
// Add a "Select All" checkbox functionality for all overlays
|
|
||||||
setTimeout(function() {
|
|
||||||
// Create a custom "select all" checkbox
|
|
||||||
var selectAllContainer = document.createElement('div');
|
|
||||||
selectAllContainer.className = 'leaflet-control-layers-select-all';
|
|
||||||
selectAllContainer.style.padding = '5px';
|
|
||||||
selectAllContainer.style.borderBottom = '1px solid #ccc';
|
|
||||||
selectAllContainer.style.marginBottom = '5px';
|
|
||||||
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
|
|
||||||
|
|
||||||
// Insert the checkbox at the top of the layer control
|
|
||||||
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
|
|
||||||
if (layerControlContainer) {
|
|
||||||
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listener to the "select all" checkbox
|
|
||||||
document.getElementById('select-all-overlays').addEventListener('change', function() {
|
|
||||||
var isChecked = this.checked;
|
|
||||||
|
|
||||||
// Iterate through all overlays and toggle visibility
|
|
||||||
for (var i = 0; i < overlays.length; i++) {
|
|
||||||
if (isChecked) {
|
|
||||||
map.addLayer(overlays[i].layer);
|
|
||||||
} else {
|
|
||||||
map.removeLayer(overlays[i].layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500); // Slight delay to ensure the tree control has been rendered
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock extra_js %}
|
{% endblock extra_js %}
|
||||||
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;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
.btn-group .badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
}
|
||||||
|
.btn-group .btn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -55,11 +65,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||||
|
onclick="showSelectedOnMap()">
|
||||||
|
<i class="bi bi-map"></i> Карта
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filter Toggle Button -->
|
<!-- Filter Toggle Button -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||||
<i class="bi bi-funnel"></i> Фильтры
|
<i class="bi bi-funnel"></i> Фильтры
|
||||||
|
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,6 +204,9 @@
|
|||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
|
<input type="checkbox" id="select-all" class="form-check-input">
|
||||||
|
</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 60px;">
|
<th scope="col" class="text-center" style="min-width: 60px;">
|
||||||
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||||
ID
|
ID
|
||||||
@@ -229,12 +251,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th>
|
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for source in processed_sources %}
|
{% for source in processed_sources %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="text-center">
|
||||||
|
<input type="checkbox" class="form-check-input item-checkbox"
|
||||||
|
value="{{ source.id }}">
|
||||||
|
</td>
|
||||||
<td class="text-center">{{ source.id }}</td>
|
<td class="text-center">{{ source.id }}</td>
|
||||||
<td>{{ source.coords_average }}</td>
|
<td>{{ source.coords_average }}</td>
|
||||||
<td>{{ source.coords_kupsat }}</td>
|
<td>{{ source.coords_kupsat }}</td>
|
||||||
@@ -244,15 +270,44 @@
|
|||||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
<div class="btn-group" role="group">
|
||||||
onclick="showSourceDetails({{ source.id }})">
|
{% if source.objitem_count > 0 %}
|
||||||
<i class="bi bi-eye"></i> Показать
|
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-success"
|
||||||
|
title="Показать источник с точками на карте">
|
||||||
|
<i class="bi bi-geo-alt"></i>
|
||||||
|
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
|
||||||
|
<i class="bi bi-geo-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="showSourceDetails({{ source.id }})"
|
||||||
|
title="Показать детали">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
|
<a href="{% url 'mainapp:source_update' source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-warning"
|
||||||
|
title="Редактировать источник">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
|
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -285,6 +340,10 @@
|
|||||||
<table class="table table-striped table-hover table-sm">
|
<table class="table table-striped table-hover table-sm">
|
||||||
<thead class="table-light sticky-top">
|
<thead class="table-light sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="text-center" style="width: 3%;">
|
||||||
|
<input type="checkbox" id="modal-select-all" class="form-check-input">
|
||||||
|
</th>
|
||||||
|
<th class="text-center" style="min-width: 60px;">ID</th>
|
||||||
<th>Имя</th>
|
<th>Имя</th>
|
||||||
<th>Спутник</th>
|
<th>Спутник</th>
|
||||||
<th>Частота, МГц</th>
|
<th>Частота, МГц</th>
|
||||||
@@ -319,6 +378,55 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
|
let lastCheckedIndex = null;
|
||||||
|
|
||||||
|
function updateRowHighlight(checkbox) {
|
||||||
|
const row = checkbox.closest('tr');
|
||||||
|
if (checkbox.checked) {
|
||||||
|
row.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
row.classList.remove('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckboxClick(e) {
|
||||||
|
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||||
|
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||||
|
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||||
|
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
checkboxes[i].checked = e.target.checked;
|
||||||
|
updateRowHighlight(checkboxes[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateRowHighlight(e.target);
|
||||||
|
}
|
||||||
|
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show selected sources on map
|
||||||
|
function showSelectedOnMap() {
|
||||||
|
// Get all checked checkboxes
|
||||||
|
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
|
if (checkedCheckboxes.length === 0) {
|
||||||
|
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract IDs from checked checkboxes
|
||||||
|
const selectedIds = [];
|
||||||
|
checkedCheckboxes.forEach(checkbox => {
|
||||||
|
selectedIds.push(checkbox.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to the map view with selected IDs as query parameter
|
||||||
|
const url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||||||
|
window.open(url, '_blank'); // Open in a new tab
|
||||||
|
}
|
||||||
|
|
||||||
// Search functionality
|
// Search functionality
|
||||||
function performSearch() {
|
function performSearch() {
|
||||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||||
@@ -377,6 +485,103 @@ function updateSort(field) {
|
|||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup radio-like behavior for filter checkboxes
|
||||||
|
function setupRadioLikeCheckboxes(name) {
|
||||||
|
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
if (this.checked) {
|
||||||
|
checkboxes.forEach(other => {
|
||||||
|
if (other !== this) {
|
||||||
|
other.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter counter functionality
|
||||||
|
function updateFilterCounter() {
|
||||||
|
const form = document.getElementById('filter-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
let filterCount = 0;
|
||||||
|
|
||||||
|
// Count non-empty form fields
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
filterCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the filter counter
|
||||||
|
const counterElement = document.getElementById('filterCounter');
|
||||||
|
if (counterElement) {
|
||||||
|
if (filterCount > 0) {
|
||||||
|
counterElement.textContent = filterCount;
|
||||||
|
counterElement.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
counterElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Setup select-all checkbox
|
||||||
|
const selectAllCheckbox = document.getElementById('select-all');
|
||||||
|
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
|
||||||
|
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||||
|
selectAllCheckbox.addEventListener('change', function () {
|
||||||
|
itemCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = selectAllCheckbox.checked;
|
||||||
|
updateRowHighlight(checkbox);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
itemCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||||
|
selectAllCheckbox.checked = allChecked;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add shift-click handler
|
||||||
|
checkbox.addEventListener('click', handleCheckboxClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup radio-like checkboxes for filters
|
||||||
|
setupRadioLikeCheckboxes('has_coords_average');
|
||||||
|
setupRadioLikeCheckboxes('has_coords_kupsat');
|
||||||
|
setupRadioLikeCheckboxes('has_coords_valid');
|
||||||
|
setupRadioLikeCheckboxes('has_coords_reference');
|
||||||
|
|
||||||
|
// Update filter counter on page load
|
||||||
|
updateFilterCounter();
|
||||||
|
|
||||||
|
// Add event listeners to form elements to update counter when filters change
|
||||||
|
const form = document.getElementById('filter-form');
|
||||||
|
if (form) {
|
||||||
|
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||||
|
inputFields.forEach(input => {
|
||||||
|
input.addEventListener('input', updateFilterCounter);
|
||||||
|
input.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||||
|
checkboxFields.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update counter when offcanvas is shown
|
||||||
|
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||||
|
if (offcanvasElement) {
|
||||||
|
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show source details in modal
|
// Show source details in modal
|
||||||
function showSourceDetails(sourceId) {
|
function showSourceDetails(sourceId) {
|
||||||
// Update modal title
|
// Update modal title
|
||||||
@@ -420,6 +625,10 @@ function showSourceDetails(sourceId) {
|
|||||||
data.objitems.forEach(objitem => {
|
data.objitems.forEach(objitem => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
|
<td class="text-center">
|
||||||
|
<input type="checkbox" class="form-check-input modal-item-checkbox" value="${objitem.id}">
|
||||||
|
</td>
|
||||||
|
<td class="text-center">${objitem.id}</td>
|
||||||
<td>${objitem.name}</td>
|
<td>${objitem.name}</td>
|
||||||
<td>${objitem.satellite_name}</td>
|
<td>${objitem.satellite_name}</td>
|
||||||
<td>${objitem.frequency}</td>
|
<td>${objitem.frequency}</td>
|
||||||
@@ -434,6 +643,9 @@ function showSourceDetails(sourceId) {
|
|||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup modal select-all checkbox
|
||||||
|
setupModalSelectAll();
|
||||||
} else {
|
} else {
|
||||||
// Show no data message
|
// Show no data message
|
||||||
document.getElementById('modalNoData').style.display = 'block';
|
document.getElementById('modalNoData').style.display = 'block';
|
||||||
@@ -449,5 +661,32 @@ function showSourceDetails(sourceId) {
|
|||||||
errorDiv.style.display = 'block';
|
errorDiv.style.display = 'block';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup select-all functionality for modal
|
||||||
|
function setupModalSelectAll() {
|
||||||
|
const modalSelectAll = document.getElementById('modal-select-all');
|
||||||
|
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||||
|
|
||||||
|
if (modalSelectAll && modalItemCheckboxes.length > 0) {
|
||||||
|
// Remove old event listeners by cloning
|
||||||
|
const newModalSelectAll = modalSelectAll.cloneNode(true);
|
||||||
|
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
|
||||||
|
|
||||||
|
newModalSelectAll.addEventListener('change', function() {
|
||||||
|
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = newModalSelectAll.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modalItemCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||||
|
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
|
||||||
|
document.getElementById('modal-select-all').checked = allChecked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
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,
|
ProcessKubsatView,
|
||||||
ShowMapView,
|
ShowMapView,
|
||||||
ShowSelectedObjectsMapView,
|
ShowSelectedObjectsMapView,
|
||||||
|
ShowSourcesMapView,
|
||||||
|
ShowSourceWithPointsMapView,
|
||||||
SourceListView,
|
SourceListView,
|
||||||
|
SourceUpdateView,
|
||||||
|
SourceDeleteView,
|
||||||
SourceObjItemsAPIView,
|
SourceObjItemsAPIView,
|
||||||
SigmaParameterDataAPIView,
|
SigmaParameterDataAPIView,
|
||||||
|
TransponderDataAPIView,
|
||||||
UploadVchLoadView,
|
UploadVchLoadView,
|
||||||
custom_logout,
|
custom_logout,
|
||||||
)
|
)
|
||||||
@@ -36,6 +41,8 @@ app_name = 'mainapp'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', SourceListView.as_view(), name='home'),
|
path('', SourceListView.as_view(), name='home'),
|
||||||
|
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||||
|
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||||
@@ -45,6 +52,8 @@ urlpatterns = [
|
|||||||
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||||
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||||
|
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
||||||
|
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
||||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||||
@@ -53,6 +62,7 @@ urlpatterns = [
|
|||||||
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||||
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
|
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .api import (
|
|||||||
SigmaParameterDataAPIView,
|
SigmaParameterDataAPIView,
|
||||||
SourceObjItemsAPIView,
|
SourceObjItemsAPIView,
|
||||||
LyngsatTaskStatusAPIView,
|
LyngsatTaskStatusAPIView,
|
||||||
|
TransponderDataAPIView,
|
||||||
)
|
)
|
||||||
from .lyngsat import (
|
from .lyngsat import (
|
||||||
LinkLyngsatSourcesView,
|
LinkLyngsatSourcesView,
|
||||||
@@ -30,8 +31,14 @@ from .lyngsat import (
|
|||||||
LyngsatTaskStatusView,
|
LyngsatTaskStatusView,
|
||||||
ClearLyngsatCacheView,
|
ClearLyngsatCacheView,
|
||||||
)
|
)
|
||||||
from .source import SourceListView
|
from .source import SourceListView, SourceUpdateView, SourceDeleteView
|
||||||
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
|
from .map import (
|
||||||
|
ShowMapView,
|
||||||
|
ShowSelectedObjectsMapView,
|
||||||
|
ShowSourcesMapView,
|
||||||
|
ShowSourceWithPointsMapView,
|
||||||
|
ClusterTestView,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Base
|
# Base
|
||||||
@@ -58,6 +65,7 @@ __all__ = [
|
|||||||
'SigmaParameterDataAPIView',
|
'SigmaParameterDataAPIView',
|
||||||
'SourceObjItemsAPIView',
|
'SourceObjItemsAPIView',
|
||||||
'LyngsatTaskStatusAPIView',
|
'LyngsatTaskStatusAPIView',
|
||||||
|
'TransponderDataAPIView',
|
||||||
# LyngSat
|
# LyngSat
|
||||||
'LinkLyngsatSourcesView',
|
'LinkLyngsatSourcesView',
|
||||||
'FillLyngsatDataView',
|
'FillLyngsatDataView',
|
||||||
@@ -65,8 +73,12 @@ __all__ = [
|
|||||||
'ClearLyngsatCacheView',
|
'ClearLyngsatCacheView',
|
||||||
# Source
|
# Source
|
||||||
'SourceListView',
|
'SourceListView',
|
||||||
|
'SourceUpdateView',
|
||||||
|
'SourceDeleteView',
|
||||||
# Map
|
# Map
|
||||||
'ShowMapView',
|
'ShowMapView',
|
||||||
'ShowSelectedObjectsMapView',
|
'ShowSelectedObjectsMapView',
|
||||||
|
'ShowSourcesMapView',
|
||||||
|
'ShowSourceWithPointsMapView',
|
||||||
'ClusterTestView',
|
'ClusterTestView',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
|
|||||||
response_data['status'] = task.state
|
response_data['status'] = task.state
|
||||||
|
|
||||||
return JsonResponse(response_data)
|
return JsonResponse(response_data)
|
||||||
|
|
||||||
|
|
||||||
|
class TransponderDataAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting Transponder data."""
|
||||||
|
|
||||||
|
def get(self, request, transponder_id):
|
||||||
|
from mapsapp.models import Transponders
|
||||||
|
|
||||||
|
try:
|
||||||
|
transponder = Transponders.objects.select_related(
|
||||||
|
'sat_id',
|
||||||
|
'polarization'
|
||||||
|
).get(id=transponder_id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'id': transponder.id,
|
||||||
|
'name': transponder.name or '-',
|
||||||
|
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
|
||||||
|
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
|
||||||
|
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
|
||||||
|
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
|
||||||
|
'polarization': transponder.polarization.name if transponder.polarization else '-',
|
||||||
|
'zone_name': transponder.zone_name or '-',
|
||||||
|
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(data)
|
||||||
|
except Transponders.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Map related views for displaying objects on maps.
|
Map related views for displaying objects on maps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
|
|||||||
or not obj.geo_obj.coords
|
or not obj.geo_obj.coords
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
param = getattr(obj, 'parameter_obj', None)
|
param = getattr(obj, "parameter_obj", None)
|
||||||
if not param:
|
if not param:
|
||||||
continue
|
continue
|
||||||
points.append(
|
points.append(
|
||||||
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
|||||||
or not obj.geo_obj.coords
|
or not obj.geo_obj.coords
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
param = getattr(obj, 'parameter_obj', None)
|
param = getattr(obj, "parameter_obj", None)
|
||||||
if not param:
|
if not param:
|
||||||
continue
|
continue
|
||||||
points.append(
|
points.append(
|
||||||
@@ -121,6 +122,139 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
|||||||
return render(request, "mainapp/objitem_map.html", context)
|
return render(request, "mainapp/objitem_map.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowSourcesMapView(LoginRequiredMixin, View):
|
||||||
|
"""View for displaying selected sources on map."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from ..models import Source
|
||||||
|
|
||||||
|
ids = request.GET.get("ids", "")
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if ids:
|
||||||
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
|
sources = Source.objects.filter(id__in=id_list)
|
||||||
|
|
||||||
|
# Define coordinate types with their labels and colors
|
||||||
|
coord_types = [
|
||||||
|
("coords_average", "Усредненные координаты", "blue"),
|
||||||
|
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||||
|
("coords_valid", "Координаты оперативников", "green"),
|
||||||
|
("coords_reference", "Координаты справочные", "violet"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Group points by coordinate type
|
||||||
|
for coord_field, label, color in coord_types:
|
||||||
|
points = []
|
||||||
|
for source in sources:
|
||||||
|
coords = getattr(source, coord_field)
|
||||||
|
if coords:
|
||||||
|
# coords is a Point object with x (longitude) and y (latitude)
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"point": (coords.x, coords.y), # (lon, lat)
|
||||||
|
"source_id": f"Источник #{source.id}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if points:
|
||||||
|
groups.append(
|
||||||
|
{
|
||||||
|
"name": label,
|
||||||
|
"points": points,
|
||||||
|
"color": color,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return redirect("mainapp:home")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
return render(request, "mainapp/source_map.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
||||||
|
"""View for displaying a single source with all its related ObjItem points."""
|
||||||
|
|
||||||
|
def get(self, request, source_id):
|
||||||
|
from ..models import Source
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = Source.objects.prefetch_related(
|
||||||
|
"source_objitems",
|
||||||
|
"source_objitems__parameter_obj",
|
||||||
|
"source_objitems__geo_obj",
|
||||||
|
).get(id=source_id)
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
return redirect("mainapp:home")
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
# Цвета для разных типов координат источника
|
||||||
|
source_coord_types = [
|
||||||
|
("coords_average", "Усредненные координаты", "blue"),
|
||||||
|
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||||
|
("coords_valid", "Координаты оперативников", "green"),
|
||||||
|
("coords_reference", "Координаты справочные", "violet"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Добавляем координаты источника
|
||||||
|
for coord_field, label, color in source_coord_types:
|
||||||
|
coords = getattr(source, coord_field)
|
||||||
|
if coords:
|
||||||
|
groups.append(
|
||||||
|
{
|
||||||
|
"name": label,
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"point": (coords.x, coords.y),
|
||||||
|
"source_id": f"Источник #{source.id}",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color": color,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем все точки ГЛ одной группой
|
||||||
|
gl_points = source.source_objitems.select_related(
|
||||||
|
"parameter_obj", "geo_obj"
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Собираем все точки ГЛ в одну группу
|
||||||
|
all_gl_points = []
|
||||||
|
for obj in gl_points:
|
||||||
|
if (
|
||||||
|
not hasattr(obj, "geo_obj")
|
||||||
|
or not obj.geo_obj
|
||||||
|
or not obj.geo_obj.coords
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
param = getattr(obj, "parameter_obj", None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_gl_points.append(
|
||||||
|
{
|
||||||
|
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||||
|
"name": obj.name,
|
||||||
|
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем все точки ГЛ одним цветом (красный)
|
||||||
|
if all_gl_points:
|
||||||
|
groups.append(
|
||||||
|
{"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"groups": groups,
|
||||||
|
"source_id": source_id,
|
||||||
|
}
|
||||||
|
return render(request, "mainapp/source_with_points_map.html", context)
|
||||||
|
|
||||||
|
|
||||||
class ClusterTestView(LoginRequiredMixin, View):
|
class ClusterTestView(LoginRequiredMixin, View):
|
||||||
"""Test view for clustering functionality."""
|
"""Test view for clustering functionality."""
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ Source related views.
|
|||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
|
from ..forms import SourceForm
|
||||||
from ..models import Source
|
from ..models import Source
|
||||||
from ..utils import parse_pagination_params
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
# Get pagination parameters
|
# Get pagination parameters
|
||||||
page_number, items_per_page = parse_pagination_params(request)
|
page_number, items_per_page = parse_pagination_params(request)
|
||||||
|
|
||||||
# Get sorting parameters
|
# Get sorting parameters (default to ID ascending)
|
||||||
sort_param = request.GET.get("sort", "-created_at")
|
sort_param = request.GET.get("sort", "id")
|
||||||
|
|
||||||
# Get filter parameters
|
# Get filter parameters
|
||||||
search_query = request.GET.get("search", "").strip()
|
search_query = request.GET.get("search", "").strip()
|
||||||
@@ -37,10 +40,9 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
date_to = request.GET.get("date_to", "").strip()
|
date_to = request.GET.get("date_to", "").strip()
|
||||||
|
|
||||||
# Get all Source objects with query optimization
|
# Get all Source objects with query optimization
|
||||||
sources = Source.objects.select_related(
|
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||||
'created_by__user',
|
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
|
||||||
'updated_by__user'
|
sources = Source.objects.prefetch_related(
|
||||||
).prefetch_related(
|
|
||||||
'source_objitems',
|
'source_objitems',
|
||||||
'source_objitems__parameter_obj',
|
'source_objitems__parameter_obj',
|
||||||
'source_objitems__geo_obj'
|
'source_objitems__geo_obj'
|
||||||
@@ -164,8 +166,6 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
'objitem_count': objitem_count,
|
'objitem_count': objitem_count,
|
||||||
'created_at': source.created_at,
|
'created_at': source.created_at,
|
||||||
'updated_at': source.updated_at,
|
'updated_at': source.updated_at,
|
||||||
'created_by': source.created_by,
|
|
||||||
'updated_by': source.updated_by,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Prepare context for template
|
# Prepare context for template
|
||||||
@@ -188,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "mainapp/source_list.html", context)
|
return render(request, "mainapp/source_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AdminModeratorMixin(UserPassesTestMixin):
|
||||||
|
"""Mixin to restrict access to admin and moderator roles only."""
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return (
|
||||||
|
self.request.user.is_authenticated and
|
||||||
|
hasattr(self.request.user, 'customuser') and
|
||||||
|
self.request.user.customuser.role in ['admin', 'moderator']
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_no_permission(self):
|
||||||
|
messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
|
||||||
|
return redirect('mainapp:home')
|
||||||
|
|
||||||
|
|
||||||
|
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||||
|
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
form = SourceForm(instance=source)
|
||||||
|
|
||||||
|
# Get related ObjItems ordered by creation date
|
||||||
|
objitems = source.source_objitems.select_related(
|
||||||
|
'parameter_obj',
|
||||||
|
'parameter_obj__id_satellite',
|
||||||
|
'parameter_obj__polarization',
|
||||||
|
'parameter_obj__modulation',
|
||||||
|
'parameter_obj__standard',
|
||||||
|
'geo_obj',
|
||||||
|
'created_by__user',
|
||||||
|
'updated_by__user'
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': source,
|
||||||
|
'form': form,
|
||||||
|
'objitems': objitems,
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'mainapp/source_form.html', context)
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
form = SourceForm(request.POST, instance=source)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
source = form.save(commit=False)
|
||||||
|
# Set updated_by to current user
|
||||||
|
if hasattr(request.user, 'customuser'):
|
||||||
|
source.updated_by = request.user.customuser
|
||||||
|
source.save()
|
||||||
|
|
||||||
|
messages.success(request, f'Источник #{source.id} успешно обновлен.')
|
||||||
|
|
||||||
|
# Redirect back with query params if present
|
||||||
|
if request.GET.urlencode():
|
||||||
|
return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
|
||||||
|
return redirect('mainapp:source_update', pk=source.id)
|
||||||
|
|
||||||
|
# If form is invalid, re-render with errors
|
||||||
|
objitems = source.source_objitems.select_related(
|
||||||
|
'parameter_obj',
|
||||||
|
'parameter_obj__id_satellite',
|
||||||
|
'parameter_obj__polarization',
|
||||||
|
'parameter_obj__modulation',
|
||||||
|
'parameter_obj__standard',
|
||||||
|
'geo_obj',
|
||||||
|
'created_by__user',
|
||||||
|
'updated_by__user'
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': source,
|
||||||
|
'form': form,
|
||||||
|
'objitems': objitems,
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'mainapp/source_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||||
|
"""View for deleting Source."""
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': source,
|
||||||
|
'objitems_count': source.source_objitems.count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'mainapp/source_confirm_delete.html', context)
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
source_id = source.id
|
||||||
|
|
||||||
|
try:
|
||||||
|
source.delete()
|
||||||
|
messages.success(request, f'Источник #{source_id} успешно удален.')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f'Ошибка при удалении источника: {str(e)}')
|
||||||
|
return redirect('mainapp:source_update', pk=pk)
|
||||||
|
|
||||||
|
# Redirect to source list
|
||||||
|
if request.GET.urlencode():
|
||||||
|
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
|
||||||
|
return redirect('mainapp:home')
|
||||||
|
|||||||
20
dbapp/mainapp/widgets.py
Normal file
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',)
|
||||||
@@ -23,7 +23,7 @@ class Transponders(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Название транспондера",
|
verbose_name="Название транспондера",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Название транспондера"
|
help_text="Название транспондера",
|
||||||
)
|
)
|
||||||
downlink = models.FloatField(
|
downlink = models.FloatField(
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -52,14 +52,14 @@ class Transponders(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name="Название зоны",
|
verbose_name="Название зоны",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Название зоны покрытия транспондера"
|
help_text="Название зоны покрытия транспондера",
|
||||||
)
|
)
|
||||||
snr = models.FloatField(
|
snr = models.FloatField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Полоса",
|
verbose_name="Полоса",
|
||||||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||||
help_text="Полоса частот в МГц (0-1000)"
|
help_text="Полоса частот в МГц (0-1000)",
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(
|
created_at = models.DateTimeField(
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
@@ -99,7 +99,7 @@ class Transponders(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Поляризация",
|
verbose_name="Поляризация",
|
||||||
help_text="Поляризация сигнала"
|
help_text="Поляризация сигнала",
|
||||||
)
|
)
|
||||||
sat_id = models.ForeignKey(
|
sat_id = models.ForeignKey(
|
||||||
Satellite,
|
Satellite,
|
||||||
@@ -107,20 +107,19 @@ class Transponders(models.Model):
|
|||||||
related_name="tran_satellite",
|
related_name="tran_satellite",
|
||||||
verbose_name="Спутник",
|
verbose_name="Спутник",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Спутник, которому принадлежит транспондер"
|
help_text="Спутник, которому принадлежит транспондер",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Вычисляемые поля
|
# Вычисляемые поля
|
||||||
transfer = models.GeneratedField(
|
transfer = models.GeneratedField(
|
||||||
expression=ExpressionWrapper(
|
expression=ExpressionWrapper(
|
||||||
Abs(F('downlink') - F('uplink')),
|
Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
|
||||||
output_field=models.FloatField()
|
|
||||||
),
|
),
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Перенос"
|
verbose_name="Перенос",
|
||||||
)
|
)
|
||||||
|
|
||||||
# def clean(self):
|
# def clean(self):
|
||||||
@@ -143,10 +142,8 @@ class Transponders(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Транспондер"
|
verbose_name = "Транспондер"
|
||||||
verbose_name_plural = "Транспондеры"
|
verbose_name_plural = "Транспондеры"
|
||||||
ordering = ['sat_id', 'downlink']
|
ordering = ["sat_id", "downlink"]
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['sat_id', 'downlink']),
|
models.Index(fields=["sat_id", "downlink"]),
|
||||||
models.Index(fields=['sat_id', 'zone_name']),
|
models.Index(fields=["sat_id", "zone_name"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="db-objects-panel" style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
<div class="db-objects-panel"
|
||||||
|
style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||||
<div class="panel-title">Объекты из базы</div>
|
<div class="panel-title">Объекты из базы</div>
|
||||||
<select id="objectSelector" class="object-select">
|
<select id="objectSelector" class="object-select">
|
||||||
<option value="">— Выберите объект —</option>
|
<option value="">— Выберите объект —</option>
|
||||||
@@ -10,14 +11,18 @@
|
|||||||
<option value="{{ sat.id }}">{{ sat.name }}</option>
|
<option value="{{ sat.id }}">{{ sat.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все точки</button>
|
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все
|
||||||
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки транспондеров</button>
|
точки</button>
|
||||||
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()" style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки
|
||||||
|
транспондеров</button>
|
||||||
|
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()"
|
||||||
|
style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footprint-control" style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
<div class="footprint-control"
|
||||||
|
style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||||
<div class="panel-title">Области покрытия</div>
|
<div class="panel-title">Области покрытия</div>
|
||||||
<div class="footprint-actions">
|
<div class="footprint-actions">
|
||||||
<button id="showAllFootprints">Показать все</button>
|
<button id="showAllFootprints">Показать все</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user