diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index c78fca1..5071fe2 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -32,13 +32,13 @@ from .models import ( ObjItem, CustomUser, Band, - Source + Source, ) from .filters import ( GeoKupDistanceFilter, GeoValidDistanceFilter, UniqueToggleFilter, - HasSigmaParameterFilter + HasSigmaParameterFilter, ) @@ -55,22 +55,24 @@ admin.site.unregister(Group) # Base Admin Classes # ============================================================================ + class BaseAdmin(admin.ModelAdmin): """ Базовый класс для всех admin моделей. - + Предоставляет общую функциональность: - Кнопки сохранения сверху и снизу - Настройка количества элементов на странице - Автоматическое заполнение полей created_by и updated_by """ + save_on_top = True list_per_page = 50 - + def save_model(self, request, obj, form, change): """ Автоматически заполняет поля created_by и updated_by при сохранении. - + Args: request: HTTP запрос obj: Сохраняемый объект модели @@ -79,20 +81,20 @@ class BaseAdmin(admin.ModelAdmin): """ if not change: # При создании нового объекта устанавливаем created_by - if hasattr(obj, 'created_by') and not obj.created_by_id: - obj.created_by = getattr(request.user, 'customuser', None) - + if hasattr(obj, "created_by") and not obj.created_by_id: + obj.created_by = getattr(request.user, "customuser", None) + # При любом сохранении обновляем updated_by - if hasattr(obj, 'updated_by'): - obj.updated_by = getattr(request.user, 'customuser', None) - + if hasattr(obj, "updated_by"): + obj.updated_by = getattr(request.user, "customuser", None) + super().save_model(request, obj, form, change) class CustomUserInline(admin.StackedInline): model = CustomUser can_delete = False - verbose_name_plural = 'Дополнительная информация пользователя' + verbose_name_plural = "Дополнительная информация пользователя" class LocationForm(forms.ModelForm): @@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm): class Meta: model = Geo - fields = '__all__' + fields = "__all__" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance and self.instance.coords: - self.fields['latitude_geo'].initial = self.instance.coords[1] - self.fields['longitude_geo'].initial = self.instance.coords[0] + self.fields["latitude_geo"].initial = self.instance.coords[1] + self.fields["longitude_geo"].initial = self.instance.coords[0] # if self.instance and self.instance.coords_kupsat: # self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1] # self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0] @@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm): def save(self, commit=True): instance = super().save(commit=False) from django.contrib.gis.geos import Point - lat = self.cleaned_data.get('latitude_geo') - lon = self.cleaned_data.get('longitude_geo') + + lat = self.cleaned_data.get("latitude_geo") + lon = self.cleaned_data.get("longitude_geo") if lat is not None and lon is not None: instance.coords = Point(lon, lat, srid=4326) @@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline): form = LocationForm # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") prefetch_related = ("mirrors",) - autocomplete_fields = ('mirrors',) + autocomplete_fields = ("mirrors",) fieldsets = ( - ("Основная информация", { - "fields": ("mirrors", "location", - # "distance_coords_kup", - # "distance_coords_valid", - # "distance_kup_valid", - "timestamp", "comment",) - }), - ("Координаты: геолокация", { - "fields": ("longitude_geo", "latitude_geo", "coords"), - }), + ( + "Основная информация", + { + "fields": ( + "mirrors", + "location", + # "distance_coords_kup", + # "distance_coords_valid", + # "distance_kup_valid", + "timestamp", + "comment", + ) + }, + ), + ( + "Координаты: геолокация", + { + "fields": ("longitude_geo", "latitude_geo", "coords"), + }, + ), # ("Координаты: Кубсат", { # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"), # }), @@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline): class UserAdmin(BaseUserAdmin): inlines = [CustomUserInline] + admin.site.register(User, UserAdmin) @@ -181,80 +195,83 @@ admin.site.register(User, UserAdmin) # Custom Admin Actions # ============================================================================ + @admin.action(description="Показать выбранные на карте") def show_on_map(modeladmin, request, queryset): """ Action для отображения выбранных Geo объектов на карте. - + Оптимизирован для работы с большим количеством объектов: использует values_list для получения только ID. """ - selected_ids = queryset.values_list('id', flat=True) - ids_str = ','.join(str(pk) for pk in selected_ids) - return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}') + selected_ids = queryset.values_list("id", flat=True) + ids_str = ",".join(str(pk) for pk in selected_ids) + return redirect(reverse("mainapp:admin_show_map") + f"?ids={ids_str}") @admin.action(description="Показать выбранные объекты на карте") def show_selected_on_map(modeladmin, request, queryset): """ Action для отображения выбранных ObjItem объектов на карте. - + Оптимизирован для работы с большим количеством объектов: использует values_list для получения только ID. """ - selected_ids = queryset.values_list('id', flat=True) - ids_str = ','.join(str(pk) for pk in selected_ids) - return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}') + selected_ids = queryset.values_list("id", flat=True) + ids_str = ",".join(str(pk) for pk in selected_ids) + return redirect(reverse("mainapp:show_selected_objects_map") + f"?ids={ids_str}") @admin.action(description="Экспортировать выбранные объекты в CSV") def export_objects_to_csv(modeladmin, request, queryset): """ Action для экспорта выбранных ObjItem объектов в CSV формат. - + Оптимизирован с использованием select_related и prefetch_related для минимизации количества запросов к БД. """ import csv from django.http import HttpResponse - + # Оптимизируем queryset queryset = queryset.select_related( - 'geo_obj', - 'created_by__user', - 'updated_by__user', - 'parameter_obj', - 'parameter_obj__id_satellite', - 'parameter_obj__polarization', - 'parameter_obj__modulation' + "geo_obj", + "created_by__user", + "updated_by__user", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", + "parameter_obj__modulation", ) - - response = HttpResponse(content_type='text/csv; charset=utf-8') - response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"' - response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel - + + response = HttpResponse(content_type="text/csv; charset=utf-8") + response["Content-Disposition"] = 'attachment; filename="objitems_export.csv"' + response.write("\ufeff") # UTF-8 BOM для корректного отображения в Excel + writer = csv.writer(response) - writer.writerow([ - 'Название', - 'Спутник', - 'Частота (МГц)', - 'Полоса (МГц)', - 'Поляризация', - 'Модуляция', - 'ОСШ', - 'Координаты геолокации', - 'Координаты Кубсата', - 'Координаты оперативного отдела', - 'Расстояние Гео-Куб (км)', - 'Расстояние Гео-Опер (км)', - 'Дата создания', - 'Дата обновления' - ]) - + writer.writerow( + [ + "Название", + "Спутник", + "Частота (МГц)", + "Полоса (МГц)", + "Поляризация", + "Модуляция", + "ОСШ", + "Координаты геолокации", + "Координаты Кубсата", + "Координаты оперативного отдела", + "Расстояние Гео-Куб (км)", + "Расстояние Гео-Опер (км)", + "Дата создания", + "Дата обновления", + ] + ) + for obj in queryset: - param = getattr(obj, 'parameter_obj', None) + param = getattr(obj, "parameter_obj", None) geo = obj.geo_obj - + # Форматирование координат def format_coords(coords): if not coords: @@ -263,24 +280,30 @@ def export_objects_to_csv(modeladmin, request, queryset): lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W" lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S" return f"{lat_str} {lon_str}" - - writer.writerow([ - obj.name, - param.id_satellite.name if param and param.id_satellite else "-", - param.frequency if param else "-", - param.freq_range if param else "-", - param.polarization.name if param and param.polarization else "-", - param.modulation.name if param and param.modulation else "-", - param.snr if param else "-", - format_coords(geo) if geo and geo.coords else "-", - format_coords(geo) if geo and geo.coords_kupsat else "-", - format_coords(geo) if geo and geo.coords_valid else "-", - round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-", - round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-", - obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-", - obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-" - ]) - + + writer.writerow( + [ + obj.name, + param.id_satellite.name if param and param.id_satellite else "-", + param.frequency if param else "-", + param.freq_range if param else "-", + param.polarization.name if param and param.polarization else "-", + param.modulation.name if param and param.modulation else "-", + param.snr if param else "-", + format_coords(geo) if geo and geo.coords else "-", + format_coords(geo) if geo and geo.coords_kupsat else "-", + format_coords(geo) if geo and geo.coords_valid else "-", + round(geo.distance_coords_kup, 3) + if geo and geo.distance_coords_kup + else "-", + round(geo.distance_coords_valid, 3) + if geo and geo.distance_coords_valid + else "-", + obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-", + obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-", + ] + ) + return response @@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset): # Inline Admin Classes # ============================================================================ + class ParameterInline(admin.StackedInline): """Inline для редактирования параметра объекта.""" + model = Parameter extra = 0 max_num = 1 @@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline): verbose_name = "ВЧ загрузка" verbose_name_plural = "ВЧ загрузка" fields = ( - 'id_satellite', - 'frequency', - 'freq_range', - 'polarization', - 'modulation', - 'bod_velocity', - 'snr', - 'standard' + "id_satellite", + "frequency", + "freq_range", + "polarization", + "modulation", + "bod_velocity", + "snr", + "standard", ) - autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard') + autocomplete_fields = ("id_satellite", "polarization", "modulation", "standard") # ============================================================================ # Admin Classes # ============================================================================ + @admin.register(SigmaParMark) class SigmaParMarkAdmin(BaseAdmin): """Админ-панель для модели SigmaParMark.""" + list_display = ("mark", "timestamp") search_fields = ("mark",) ordering = ("-timestamp",) - list_filter = ( - ("timestamp", DateRangeQuickSelectListFilterBuilder()), - ) + list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),) @admin.register(Polarization) class PolarizationAdmin(BaseAdmin): """Админ-панель для модели Polarization.""" + list_display = ("name",) search_fields = ("name",) ordering = ("name",) @@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin): @admin.register(Modulation) class ModulationAdmin(BaseAdmin): """Админ-панель для модели Modulation.""" + list_display = ("name",) search_fields = ("name",) ordering = ("name",) @@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin): @admin.register(Standard) class StandardAdmin(BaseAdmin): """Админ-панель для модели Standard.""" + list_display = ("name",) search_fields = ("name",) ordering = ("name",) @@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin): class SigmaParameterInline(admin.StackedInline): model = SigmaParameter extra = 0 - autocomplete_fields = ['mark'] + autocomplete_fields = ["mark"] readonly_fields = ( - "datetime_begin", - "datetime_end", + "datetime_begin", + "datetime_end", ) + def has_add_permission(self, request, obj=None): return False @@ -364,12 +393,13 @@ class SigmaParameterInline(admin.StackedInline): class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): """ Админ-панель для модели Parameter. - + Оптимизирована для работы с большим количеством параметров: - Использует select_related для оптимизации запросов - Предоставляет фильтры по основным характеристикам - Поддерживает импорт/экспорт данных """ + list_display = ( "id_satellite", "frequency", @@ -380,11 +410,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "snr", "standard", "related_objitem", - "sigma_parameter" + "sigma_parameter", ) list_display_links = ("frequency", "id_satellite") - list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem") - + list_select_related = ( + "polarization", + "modulation", + "standard", + "id_satellite", + "objitem", + ) + list_filter = ( HasSigmaParameterFilter, ("objitem", MultiSelectRelatedDropdownFilter), @@ -396,7 +432,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): ("freq_range", NumericRangeFilterBuilder()), ("snr", NumericRangeFilterBuilder()), ) - + search_fields = ( "id_satellite__name", "frequency", @@ -408,16 +444,17 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "standard__name", "objitem__name", ) - + ordering = ("-frequency",) autocomplete_fields = ("objitem",) inlines = [SigmaParameterInline] def related_objitem(self, obj): """Отображает связанный ObjItem.""" - if hasattr(obj, 'objitem') and obj.objitem: + if hasattr(obj, "objitem") and obj.objitem: return obj.objitem.name return "-" + related_objitem.short_description = "Объект" related_objitem.admin_order_field = "objitem__name" @@ -427,19 +464,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): if sigma_obj: return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}" return "-" + sigma_parameter.short_description = "ВЧ sigma" - + @admin.register(SigmaParameter) class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): """ Админ-панель для модели SigmaParameter. - + Оптимизирована для работы с параметрами Sigma: - Использует select_related и prefetch_related для оптимизации - Предоставляет фильтры по основным характеристикам - Поддерживает импорт/экспорт данных """ + list_display = ( "id_satellite", "frequency", @@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "datetime_end", ) list_display_links = ("id_satellite",) - list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization") - - readonly_fields = ( - "datetime_begin", - "datetime_end", - "transfer_frequency" + list_select_related = ( + "modulation", + "standard", + "id_satellite", + "parameter", + "polarization", ) - + + readonly_fields = ("datetime_begin", "datetime_end", "transfer_frequency") + list_filter = ( ("id_satellite__name", MultiSelectDropdownFilter), ("modulation__name", MultiSelectDropdownFilter), @@ -472,7 +513,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): ("datetime_begin", DateRangeQuickSelectListFilterBuilder()), ("datetime_end", DateRangeQuickSelectListFilterBuilder()), ) - + search_fields = ( "id_satellite__name", "frequency", @@ -484,17 +525,25 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): ) autocomplete_fields = ("mark",) ordering = ("-frequency",) - + def get_queryset(self, request): """Оптимизированный queryset с prefetch_related для mark.""" qs = super().get_queryset(request) return qs.prefetch_related("mark") - + @admin.register(Satellite) class SatelliteAdmin(BaseAdmin): """Админ-панель для модели Satellite.""" - list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at") + + list_display = ( + "name", + "norad", + "undersat_point", + "launch_date", + "created_at", + "updated_at", + ) search_fields = ("name", "norad") ordering = ("name",) filter_horizontal = ("band",) @@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin): @admin.register(Mirror) class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): """Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" + list_display = ("name",) search_fields = ("name",) ordering = ("name",) @@ -514,28 +564,37 @@ class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): """ Админ-панель для модели Geo с поддержкой карты Leaflet. - + Оптимизирована для работы с геоданными: - Использует prefetch_related для оптимизации запросов к mirrors - Предоставляет фильтры по зеркалам, локации и дате - Поддерживает импорт/экспорт данных - Интегрирована с Leaflet для отображения на карте """ + form = LocationForm - + # readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") - + fieldsets = ( - ("Основная информация", { - "fields": ("mirrors", "location", + ( + "Основная информация", + { + "fields": ( + "mirrors", + "location", # "distance_coords_kup", - # "distance_coords_valid", - # "distance_kup_valid", - "timestamp", "comment", "transponder") - }), - ("Координаты: геолокация", { - "fields": ("longitude_geo", "latitude_geo", "coords") - }), + # "distance_coords_valid", + # "distance_kup_valid", + "timestamp", + "comment", + ) + }, + ), + ( + "Координаты: геолокация", + {"fields": ("longitude_geo", "latitude_geo", "coords")}, + ), # ("Координаты: Кубсат", { # "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat") # }), @@ -543,7 +602,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): # "fields": ("longitude_valid", "latitude_valid", "coords_valid") # }), ) - + list_display = ( "formatted_timestamp", "location", @@ -554,38 +613,39 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): "is_average", ) list_display_links = ("formatted_timestamp",) - + list_filter = ( ("mirrors", MultiSelectRelatedDropdownFilter), - ("transponder", MultiSelectRelatedDropdownFilter), "is_average", ("location", MultiSelectDropdownFilter), ("timestamp", DateRangeQuickSelectListFilterBuilder()), ) - + search_fields = ( "mirrors__name", "location", - "transponder__name", ) - - autocomplete_fields = ("mirrors", ) + + autocomplete_fields = ("mirrors",) ordering = ("-timestamp",) actions = [show_on_map] - + settings_overrides = { - 'DEFAULT_CENTER': (55.7558, 37.6173), - 'DEFAULT_ZOOM': 12, + "DEFAULT_CENTER": (55.7558, 37.6173), + "DEFAULT_ZOOM": 12, } - + def get_queryset(self, request): """Оптимизированный queryset с prefetch_related для mirrors.""" qs = super().get_queryset(request) - return qs.prefetch_related("mirrors", "transponder") + return qs.prefetch_related( + "mirrors", + ) def mirrors_names(self, obj): """Отображает список зеркал через запятую.""" return ", ".join(m.name for m in obj.mirrors.all()) + mirrors_names.short_description = "Зеркала" def formatted_timestamp(self, obj): @@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): return "" local_time = timezone.localtime(obj.timestamp) return local_time.strftime("%d.%m.%Y %H:%M:%S") + formatted_timestamp.short_description = "Дата и время" formatted_timestamp.admin_order_field = "timestamp" @@ -606,6 +667,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" + geo_coords.short_description = "Координаты геолокации" # def kupsat_coords(self, obj): @@ -631,19 +693,18 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): # valid_coords.short_description = "Координаты оперативного отдела" - - @admin.register(ObjItem) class ObjItemAdmin(BaseAdmin): """ Админ-панель для модели ObjItem. - + Оптимизирована для работы с большим количеством объектов: - Использует select_related и prefetch_related для оптимизации запросов - Предоставляет фильтры по основным параметрам - Поддерживает поиск по имени, координатам и частоте - Включает кастомные actions для отображения на карте """ + list_display = ( "name", "sat_name", @@ -664,16 +725,16 @@ class ObjItemAdmin(BaseAdmin): ) list_display_links = ("name",) list_select_related = ( - "geo_obj", - "created_by__user", + "geo_obj", + "created_by__user", "updated_by__user", "parameter_obj", "parameter_obj__id_satellite", "parameter_obj__polarization", "parameter_obj__modulation", - "parameter_obj__standard" + "parameter_obj__standard", ) - + list_filter = ( UniqueToggleFilter, ("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter), @@ -687,33 +748,34 @@ class ObjItemAdmin(BaseAdmin): ("created_at", DateRangeQuickSelectListFilterBuilder()), ("updated_at", DateRangeQuickSelectListFilterBuilder()), ) - + search_fields = ( "name", "geo_obj__location", "parameter_obj__frequency", "parameter_obj__id_satellite__name", ) - + ordering = ("-updated_at",) inlines = [GeoInline, ParameterInline] actions = [show_selected_on_map, export_objects_to_csv] readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") - + fieldsets = ( - ("Основная информация", { - "fields": ("name",) - }), - ("Метаданные", { - "fields": ("created_at", "created_by", "updated_at", "updated_by"), - "classes": ("collapse",) - }), + ("Основная информация", {"fields": ("name",)}), + ( + "Метаданные", + { + "fields": ("created_at", "created_by", "updated_at", "updated_by"), + "classes": ("collapse",), + }, + ), ) def get_queryset(self, request): """ Оптимизированный queryset с использованием select_related. - + Загружает связанные объекты одним запросом для улучшения производительности. """ qs = super().get_queryset(request) @@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin): "parameter_obj__id_satellite", "parameter_obj__polarization", "parameter_obj__modulation", - "parameter_obj__standard" + "parameter_obj__standard", ) def sat_name(self, obj): """Отображает название спутника из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: if obj.parameter_obj.id_satellite: return obj.parameter_obj.id_satellite.name return "-" + sat_name.short_description = "Спутник" sat_name.admin_order_field = "parameter_obj__id_satellite__name" def freq(self, obj): """Отображает частоту из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.frequency return "-" + freq.short_description = "Частота, МГц" freq.admin_order_field = "parameter_obj__frequency" @@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin): def pol(self, obj): """Отображает поляризацию из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: if obj.parameter_obj.polarization: return obj.parameter_obj.polarization.name return "-" + pol.short_description = "Поляризация" def freq_range(self, obj): """Отображает полосу частот из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.freq_range return "-" + freq_range.short_description = "Полоса, МГц" freq_range.admin_order_field = "parameter_obj__freq_range" def bod_velocity(self, obj): """Отображает символьную скорость из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.bod_velocity return "-" + bod_velocity.short_description = "Сим. v, БОД" def modulation(self, obj): """Отображает модуляцию из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: if obj.parameter_obj.modulation: return obj.parameter_obj.modulation.name return "-" + modulation.short_description = "Модуляция" def snr(self, obj): """Отображает отношение сигнал/шум из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.snr return "-" + snr.short_description = "ОСШ" def geo_coords(self, obj): @@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin): lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" + geo_coords.short_description = "Координаты геолокации" geo_coords.admin_order_field = "geo_obj__coords" @@ -830,6 +900,7 @@ class ObjItemAdmin(BaseAdmin): lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" + kupsat_coords.short_description = "Координаты Кубсата" def valid_coords(self, obj): @@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin): lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" + valid_coords.short_description = "Координаты оперативного отдела" @admin.register(Band) class BandAdmin(ImportExportActionModelAdmin, BaseAdmin): """Админ-панель для модели Band.""" + list_display = ("name", "border_start", "border_end") search_fields = ("name",) ordering = ("name",) @@ -855,29 +928,44 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin): class ObjItemInline(admin.TabularInline): """Inline для отображения объектов ObjItem в Source.""" + model = ObjItem fk_name = "source" extra = 0 can_delete = False verbose_name = "Объект" verbose_name_plural = "Объекты" - - fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at") - readonly_fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at") - + + fields = ( + "name", + "get_geo_coords", + "get_satellite", + "get_frequency", + "get_polarization", + "updated_at", + ) + readonly_fields = ( + "name", + "get_geo_coords", + "get_satellite", + "get_frequency", + "get_polarization", + "updated_at", + ) + def get_queryset(self, request): """Оптимизированный queryset с предзагрузкой связанных объектов.""" qs = super().get_queryset(request) return qs.select_related( - 'geo_obj', - 'parameter_obj', - 'parameter_obj__id_satellite', - 'parameter_obj__polarization' + "geo_obj", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", ) - + def get_geo_coords(self, obj): """Отображает координаты из связанной модели Geo.""" - if not obj or not hasattr(obj, 'geo_obj'): + if not obj or not hasattr(obj, "geo_obj"): return "-" geo = obj.geo_obj if not geo or not geo.coords: @@ -887,29 +975,41 @@ class ObjItemInline(admin.TabularInline): lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" + get_geo_coords.short_description = "Координаты" - + def get_satellite(self, obj): """Отображает спутник из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.id_satellite: + if ( + hasattr(obj, "parameter_obj") + and obj.parameter_obj + and obj.parameter_obj.id_satellite + ): return obj.parameter_obj.id_satellite.name return "-" + get_satellite.short_description = "Спутник" - + def get_frequency(self, obj): """Отображает частоту из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj: + if hasattr(obj, "parameter_obj") and obj.parameter_obj: return obj.parameter_obj.frequency return "-" + get_frequency.short_description = "Частота, МГц" - + def get_polarization(self, obj): """Отображает поляризацию из связанного параметра.""" - if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.polarization: + if ( + hasattr(obj, "parameter_obj") + and obj.parameter_obj + and obj.parameter_obj.polarization + ): return obj.parameter_obj.polarization.name return "-" + get_polarization.short_description = "Поляризация" - + def has_add_permission(self, request, obj=None): return False @@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline): @admin.register(Source) class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): """Админ-панель для модели Source.""" + list_display = ("id", "created_at", "updated_at") list_filter = ( ("created_at", DateRangeQuickSelectListFilterBuilder()), @@ -925,13 +1026,17 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): ordering = ("-created_at",) readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") inlines = [ObjItemInline] - + fieldsets = ( - ("Координаты: геолокация", { - "fields": ("coords_kupsat", "coords_valid", "coords_reference") - }), - ("Метаданные", { - "fields": ("created_at", "created_by", "updated_at", "updated_by"), - "classes": ("collapse",) - }), + ( + "Координаты: геолокация", + {"fields": ("coords_kupsat", "coords_valid", "coords_reference")}, + ), + ( + "Метаданные", + { + "fields": ("created_at", "created_by", "updated_at", "updated_by"), + "classes": ("collapse",), + }, + ), ) diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 27850c9..a2ccd03 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -9,6 +9,7 @@ from .models import ( Parameter, Polarization, Satellite, + Source, Standard, ) from .widgets import CheckboxSelectMultipleWidget @@ -305,8 +306,8 @@ class GeoForm(forms.ModelForm): "is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}), "mirrors": CheckboxSelectMultipleWidget( attrs={ - 'id': 'id_geo-mirrors', - 'placeholder': 'Выберите спутники...', + "id": "id_geo-mirrors", + "placeholder": "Выберите спутники...", } ), } @@ -372,3 +373,160 @@ class ObjItemForm(forms.ModelForm): ) return name + + +class SourceForm(forms.ModelForm): + """Form for editing Source model with 4 coordinate fields.""" + + # Координаты ГЛ (coords_average) + average_latitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} + ), + label="Широта ГЛ", + ) + average_longitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "step": "0.000001", + "placeholder": "Долгота", + } + ), + label="Долгота ГЛ", + ) + + # Координаты Кубсата (coords_kupsat) + kupsat_latitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} + ), + label="Широта Кубсата", + ) + kupsat_longitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "step": "0.000001", + "placeholder": "Долгота", + } + ), + label="Долгота Кубсата", + ) + + # Координаты оперативников (coords_valid) + valid_latitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} + ), + label="Широта оперативников", + ) + valid_longitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "step": "0.000001", + "placeholder": "Долгота", + } + ), + label="Долгота оперативников", + ) + + # Координаты справочные (coords_reference) + reference_latitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"} + ), + label="Широта справочные", + ) + reference_longitude = forms.FloatField( + required=False, + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "step": "0.000001", + "placeholder": "Долгота", + } + ), + label="Долгота справочные", + ) + + class Meta: + model = Source + fields = [] # Все поля обрабатываются вручную + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Заполняем поля координат из instance + if self.instance and self.instance.pk: + if self.instance.coords_average: + self.fields[ + "average_longitude" + ].initial = self.instance.coords_average.x + self.fields["average_latitude"].initial = self.instance.coords_average.y + + if self.instance.coords_kupsat: + self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x + self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y + + if self.instance.coords_valid: + self.fields["valid_longitude"].initial = self.instance.coords_valid.x + self.fields["valid_latitude"].initial = self.instance.coords_valid.y + + if self.instance.coords_reference: + self.fields[ + "reference_longitude" + ].initial = self.instance.coords_reference.x + self.fields[ + "reference_latitude" + ].initial = self.instance.coords_reference.y + + def save(self, commit=True): + from django.contrib.gis.geos import Point + + instance = super().save(commit=False) + + # Обработка coords_average + avg_lat = self.cleaned_data.get("average_latitude") + avg_lng = self.cleaned_data.get("average_longitude") + if avg_lat is not None and avg_lng is not None: + instance.coords_average = Point(avg_lng, avg_lat, srid=4326) + else: + instance.coords_average = None + + # Обработка coords_kupsat + kup_lat = self.cleaned_data.get("kupsat_latitude") + kup_lng = self.cleaned_data.get("kupsat_longitude") + if kup_lat is not None and kup_lng is not None: + instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326) + else: + instance.coords_kupsat = None + + # Обработка coords_valid + val_lat = self.cleaned_data.get("valid_latitude") + val_lng = self.cleaned_data.get("valid_longitude") + if val_lat is not None and val_lng is not None: + instance.coords_valid = Point(val_lng, val_lat, srid=4326) + else: + instance.coords_valid = None + + # Обработка coords_reference + ref_lat = self.cleaned_data.get("reference_latitude") + ref_lng = self.cleaned_data.get("reference_longitude") + if ref_lat is not None and ref_lng is not None: + instance.coords_reference = Point(ref_lng, ref_lat, srid=4326) + else: + instance.coords_reference = None + + if commit: + instance.save() + + return instance diff --git a/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html b/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html index adfefa7..bde10ab 100644 --- a/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html +++ b/dbapp/mainapp/templates/mainapp/components/_column_visibility_dropdown.html @@ -22,24 +22,25 @@ {% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %} {% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %} {% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %} - {% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %} + {% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %} \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html b/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html index 2f580cb..84b4d43 100644 --- a/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html +++ b/dbapp/mainapp/templates/mainapp/components/_selected_items_offcanvas.html @@ -32,6 +32,7 @@ Имя Спутник + Транспондер Част, МГц Полоса, МГц Поляризация @@ -41,8 +42,6 @@ Время ГЛ Местоположение Геолокация - Кубсат - Опер. отд Обновлено Кем(обн) Создано diff --git a/dbapp/mainapp/templates/mainapp/objitem_list.html b/dbapp/mainapp/templates/mainapp/objitem_list.html index ebb67f5..6b5a639 100644 --- a/dbapp/mainapp/templates/mainapp/objitem_list.html +++ b/dbapp/mainapp/templates/mainapp/objitem_list.html @@ -70,7 +70,8 @@
- @@ -82,10 +83,11 @@ Добавить к
- +
- @@ -112,295 +114,277 @@
- -
- -
- - -
- -
- - -
- - - -
- - -
- - - -
- - -
- - - -
- - -
- - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
-
- - -
-
- - -
- - -
- - Сбросить -
-
+ +
+ +
+ + +
+
-
- - -
-
-
-
- - - - - {% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %} - {% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %} - {% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %} - - - - {% for item in processed_objects %} - - - - - - - - - - - - - - - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
- -
- - {{ item.name }}{{ item.satellite_name }}{{ item.frequency }}{{ item.freq_range }}{{ item.polarization }}{{ item.bod_velocity }}{{ item.modulation }}{{ item.snr }}{{ item.geo_timestamp|date:"d.m.Y H:i" }}{{ item.geo_location}}{{ item.geo_coords }}{{ item.obj.updated_at|date:"d.m.Y H:i" }}{{ item.updated_by }}{{ item.obj.created_at|date:"d.m.Y H:i" }}{{ item.obj.created_by }}{{ item.comment }}{{ item.is_average }}{{ item.standard }} - {% if item.obj.lyngsat_source %} - - ТВ - - {% else %} - - - {% endif %} - - {% if item.has_sigma %} - - {{ item.sigma_info }} - - {% else %} - - - {% endif %} - {{ item.mirrors }}
- {% if selected_satellite_id %} - Нет данных для выбранных фильтров - {% else %} - Пожалуйста, выберите спутник для отображения данных - {% endif %} -
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+
+ + +
+
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + Сбросить +
+ +
+
+
+ + +
+
+
+
+ + + + + {% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Транспондер" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %} + {% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %} + {% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %} + + + + {% for item in processed_objects %} + + + + + + + + + + + + + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
+ +
+ + + {{ item.name }}{{ item.satellite_name }} + {% if item.obj.transponder %} + + {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }} + + {% else %} + - + {% endif %} + {{ item.frequency }}{{ item.freq_range }}{{ item.polarization }}{{ item.bod_velocity }}{{ item.modulation }}{{ item.snr }}{{ item.geo_timestamp|date:"d.m.Y H:i" }}{{ item.geo_location}}{{ item.geo_coords }}{{ item.obj.updated_at|date:"d.m.Y H:i" }}{{ item.updated_by }}{{ item.obj.created_at|date:"d.m.Y H:i" }}{{ item.obj.created_by }}{{ item.comment }}{{ item.is_average }}{{ item.standard }} + {% if item.obj.lyngsat_source %} + + ТВ + + {% else %} + - + {% endif %} + + {% if item.has_sigma %} + + {{ item.sigma_info }} + + {% else %} + - + {% endif %} + {{ item.mirrors }}
+ {% if selected_satellite_id %} + Нет данных для выбранных фильтров + {% else %} + Пожалуйста, выберите спутник для отображения данных + {% endif %} +
+
+ + + + + {% endblock %} \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/objitem_map.html b/dbapp/mainapp/templates/mainapp/objitem_map.html index b8262d7..f0427eb 100644 --- a/dbapp/mainapp/templates/mainapp/objitem_map.html +++ b/dbapp/mainapp/templates/mainapp/objitem_map.html @@ -1,10 +1,74 @@ -{% extends "mapsapp/map2d_base.html" %} +{% extends "mainapp/base.html" %} {% load static %} {% block title %}Карта выбранных объектов{% endblock title %} +{% block extra_css %} + + + + + +{% endblock %} + +{% block content %} +
+{% endblock content %} + {% block extra_js %} + + + + + {% endblock extra_js %} \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/source_confirm_delete.html b/dbapp/mainapp/templates/mainapp/source_confirm_delete.html new file mode 100644 index 0000000..e4b0485 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_confirm_delete.html @@ -0,0 +1,63 @@ +{% extends 'mainapp/base.html' %} +{% load static %} + +{% block title %}Удалить источник #{{ object.id }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Подтверждение удаления

+
+
+ + +
Информация об источнике:
+
    +
  • + ID: {{ object.id }} +
  • +
  • + Дата создания: + {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
  • +
  • + Создан пользователем: + {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} +
  • +
  • + Привязанных объектов: + {{ objitems_count }} +
  • +
+ + {% if objitems_count > 0 %} + + {% endif %} + +

Это действие нельзя отменить. Вы уверены, что хотите продолжить?

+ +
+ {% csrf_token %} + + + Отмена + +
+
+
+
+
+
+{% endblock %} diff --git a/dbapp/mainapp/templates/mainapp/source_form.html b/dbapp/mainapp/templates/mainapp/source_form.html new file mode 100644 index 0000000..8143a1b --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_form.html @@ -0,0 +1,652 @@ +{% extends 'mainapp/base.html' %} +{% load static %} +{% load static leaflet_tags %} +{% load l10n %} + +{% block title %}Редактировать источник #{{ object.id }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+
+

Редактировать источник #{{ object.id }}

+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} + + Удалить + {% endif %} + Назад +
+
+
+ +
+ {% csrf_token %} + + +
+
+

Основная информация

+
+
+
+
+ +
{{ object.id }}
+
+
+
+
+ +
+ {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} +
+
+
+
+
+ +
+ {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} +
+
+
+
+
+ + +
+
+

Карта

+
+
+
+
+
+ + +
+
+

Координаты

+
+ + +
+
Координаты ГЛ (усреднённые)
+
+
+
+ + {{ form.average_latitude }} +
+
+
+
+ + {{ form.average_longitude }} +
+
+
+
+ + +
+
Координаты Кубсата
+
+
+
+ + {{ form.kupsat_latitude }} +
+
+
+
+ + {{ form.kupsat_longitude }} +
+
+
+
+ + +
+
Координаты оперативников
+
+
+
+ + {{ form.valid_latitude }} +
+
+
+
+ + {{ form.valid_longitude }} +
+
+
+
+ + +
+
Координаты справочные
+
+
+
+ + {{ form.reference_latitude }} +
+
+
+
+ + {{ form.reference_longitude }} +
+
+
+
+
+ + +
+
+

Привязанные объекты ({{ objitems.count }})

+
+ + {% if objitems %} +
+ + + + + + + + + + + + + + + + + {% for item in objitems %} + + + + + + + + + + + + + {% endfor %} + +
IDНазваниеСпутникЧастота, МГцПолоса, МГцПоляризацияМодуляцияКоординатыСозданОбновлен
+ + {{ item.id }} + + {{ item.name|default:"-" }} + {% if item.parameter_obj and item.parameter_obj.id_satellite %} + {{ item.parameter_obj.id_satellite.name }} + {% else %} + - + {% endif %} + + {% if item.parameter_obj %} + {{ item.parameter_obj.frequency|default:"-" }} + {% else %} + - + {% endif %} + + {% if item.parameter_obj %} + {{ item.parameter_obj.freq_range|default:"-" }} + {% else %} + - + {% endif %} + + {% if item.parameter_obj and item.parameter_obj.polarization %} + {{ item.parameter_obj.polarization.name }} + {% else %} + - + {% endif %} + + {% if item.parameter_obj and item.parameter_obj.modulation %} + {{ item.parameter_obj.modulation.name }} + {% else %} + - + {% endif %} + + {% 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 %} + {{ item.created_at|date:"d.m.Y H:i" }}{{ item.updated_at|date:"d.m.Y H:i" }}
+
+ {% else %} +

Нет привязанных объектов

+ {% endif %} +
+
+
+{% endblock %} + +{% block extra_js %} +{{ block.super }} + +{% leaflet_js %} +{% leaflet_css %} + + + +{% endblock %} diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html index bf04666..753061f 100644 --- a/dbapp/mainapp/templates/mainapp/source_list.html +++ b/dbapp/mainapp/templates/mainapp/source_list.html @@ -12,6 +12,16 @@ top: 0; z-index: 10; } + .btn-group .badge { + position: absolute; + top: -5px; + right: -5px; + font-size: 0.65rem; + padding: 0.2em 0.4em; + } + .btn-group .btn { + position: relative; + } {% endblock %} @@ -55,11 +65,20 @@ + +
+ +
+
@@ -185,6 +204,9 @@ + - + {% for source in processed_sources %} + @@ -244,15 +270,44 @@ {% empty %} - + {% endfor %} @@ -285,6 +340,10 @@
+ + ID @@ -229,12 +251,16 @@ {% endif %} ДеталиДействия
+ + {{ source.id }} {{ source.coords_average }} {{ source.coords_kupsat }}{{ source.created_at|date:"d.m.Y H:i" }} {{ source.updated_at|date:"d.m.Y H:i" }} - +
+ {% if source.objitem_count > 0 %} + + + {{ source.objitem_count }} + + {% else %} + + {% endif %} + + + + {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} + + + + {% else %} + + {% endif %} +
Нет данных для отображенияНет данных для отображения
+ + @@ -319,6 +378,55 @@ {% block extra_js %} {% endblock %} diff --git a/dbapp/mainapp/templates/mainapp/source_map.html b/dbapp/mainapp/templates/mainapp/source_map.html new file mode 100644 index 0000000..6fca671 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_map.html @@ -0,0 +1,189 @@ +{% extends "mainapp/base.html" %} +{% load static %} +{% block title %}Карта источников{% endblock title %} + +{% block extra_css %} + + + + + +{% endblock %} + +{% block content %} +
+{% endblock content %} + +{% block extra_js %} + + + + + + +{% endblock extra_js %} diff --git a/dbapp/mainapp/templates/mainapp/source_with_points_map.html b/dbapp/mainapp/templates/mainapp/source_with_points_map.html new file mode 100644 index 0000000..5c58082 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_with_points_map.html @@ -0,0 +1,244 @@ +{% extends "mainapp/base.html" %} +{% load static %} +{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %} + +{% block extra_css %} + + + + + +{% endblock %} + +{% block content %} +
+{% endblock content %} + +{% block extra_js %} + + + + + + +{% endblock extra_js %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 902155d..f847111 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -25,9 +25,14 @@ from .views import ( ProcessKubsatView, ShowMapView, ShowSelectedObjectsMapView, + ShowSourcesMapView, + ShowSourceWithPointsMapView, SourceListView, + SourceUpdateView, + SourceDeleteView, SourceObjItemsAPIView, SigmaParameterDataAPIView, + TransponderDataAPIView, UploadVchLoadView, custom_logout, ) @@ -36,6 +41,8 @@ app_name = 'mainapp' urlpatterns = [ path('', SourceListView.as_view(), name='home'), + path('source//edit/', SourceUpdateView.as_view(), name='source_update'), + path('source//delete/', SourceDeleteView.as_view(), name='source_delete'), path('objitems/', ObjItemListView.as_view(), name='objitem_list'), path('actions/', ActionsPageView.as_view(), name='actions'), path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'), @@ -45,6 +52,8 @@ urlpatterns = [ path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'), path('map-points/', ShowMapView.as_view(), name='admin_show_map'), path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'), + path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'), + path('show-source-with-points-map//', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'), path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'), path('cluster/', ClusterTestView.as_view(), name='cluster'), path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'), @@ -53,6 +62,7 @@ urlpatterns = [ path('api/lyngsat//', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'), path('api/sigma-parameter//', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'), path('api/source//objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'), + path('api/transponder//', TransponderDataAPIView.as_view(), name='transponder_data_api'), path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'), path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'), path('object//edit/', ObjItemUpdateView.as_view(), name='objitem_update'), diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py index 6b829e9..f1b3300 100644 --- a/dbapp/mainapp/views/__init__.py +++ b/dbapp/mainapp/views/__init__.py @@ -23,6 +23,7 @@ from .api import ( SigmaParameterDataAPIView, SourceObjItemsAPIView, LyngsatTaskStatusAPIView, + TransponderDataAPIView, ) from .lyngsat import ( LinkLyngsatSourcesView, @@ -30,8 +31,14 @@ from .lyngsat import ( LyngsatTaskStatusView, ClearLyngsatCacheView, ) -from .source import SourceListView -from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView +from .source import SourceListView, SourceUpdateView, SourceDeleteView +from .map import ( + ShowMapView, + ShowSelectedObjectsMapView, + ShowSourcesMapView, + ShowSourceWithPointsMapView, + ClusterTestView, +) __all__ = [ # Base @@ -58,6 +65,7 @@ __all__ = [ 'SigmaParameterDataAPIView', 'SourceObjItemsAPIView', 'LyngsatTaskStatusAPIView', + 'TransponderDataAPIView', # LyngSat 'LinkLyngsatSourcesView', 'FillLyngsatDataView', @@ -65,8 +73,12 @@ __all__ = [ 'ClearLyngsatCacheView', # Source 'SourceListView', + 'SourceUpdateView', + 'SourceDeleteView', # Map 'ShowMapView', 'ShowSelectedObjectsMapView', + 'ShowSourcesMapView', + 'ShowSourceWithPointsMapView', 'ClusterTestView', ] diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py index dd54e87..489aa70 100644 --- a/dbapp/mainapp/views/api.py +++ b/dbapp/mainapp/views/api.py @@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View): response_data['status'] = task.state return JsonResponse(response_data) + + +class TransponderDataAPIView(LoginRequiredMixin, View): + """API endpoint for getting Transponder data.""" + + def get(self, request, transponder_id): + from mapsapp.models import Transponders + + try: + transponder = Transponders.objects.select_related( + 'sat_id', + 'polarization' + ).get(id=transponder_id) + + data = { + 'id': transponder.id, + 'name': transponder.name or '-', + 'satellite': transponder.sat_id.name if transponder.sat_id else '-', + 'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-', + 'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None, + 'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-', + 'polarization': transponder.polarization.name if transponder.polarization else '-', + 'zone_name': transponder.zone_name or '-', + 'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None, + } + + return JsonResponse(data) + except Transponders.DoesNotExist: + return JsonResponse({'error': 'Транспондер не найден'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/dbapp/mainapp/views/map.py b/dbapp/mainapp/views/map.py index 59f2577..c1a2089 100644 --- a/dbapp/mainapp/views/map.py +++ b/dbapp/mainapp/views/map.py @@ -1,6 +1,7 @@ """ Map related views for displaying objects on maps. """ + from collections import defaultdict from django.contrib.admin.views.decorators import staff_member_required @@ -18,7 +19,7 @@ from ..models import ObjItem @method_decorator(staff_member_required, name="dispatch") class ShowMapView(RoleRequiredMixin, View): """View for displaying objects on map (admin interface).""" - + required_roles = ["admin", "moderator"] def get(self, request): @@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View): or not obj.geo_obj.coords ): continue - param = getattr(obj, 'parameter_obj', None) + param = getattr(obj, "parameter_obj", None) if not param: continue points.append( @@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View): ) else: return redirect("admin") - + grouped = defaultdict(list) for p in points: grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]}) @@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View): class ShowSelectedObjectsMapView(LoginRequiredMixin, View): """View for displaying selected objects on map.""" - + def get(self, request): ids = request.GET.get("ids", "") points = [] @@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View): or not obj.geo_obj.coords ): continue - param = getattr(obj, 'parameter_obj', None) + param = getattr(obj, "parameter_obj", None) if not param: continue points.append( @@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View): return render(request, "mainapp/objitem_map.html", context) +class ShowSourcesMapView(LoginRequiredMixin, View): + """View for displaying selected sources on map.""" + + def get(self, request): + from ..models import Source + + ids = request.GET.get("ids", "") + groups = [] + + if ids: + id_list = [int(x) for x in ids.split(",") if x.isdigit()] + sources = Source.objects.filter(id__in=id_list) + + # Define coordinate types with their labels and colors + coord_types = [ + ("coords_average", "Усредненные координаты", "blue"), + ("coords_kupsat", "Координаты Кубсата", "orange"), + ("coords_valid", "Координаты оперативников", "green"), + ("coords_reference", "Координаты справочные", "violet"), + ] + + # Group points by coordinate type + for coord_field, label, color in coord_types: + points = [] + for source in sources: + coords = getattr(source, coord_field) + if coords: + # coords is a Point object with x (longitude) and y (latitude) + points.append( + { + "point": (coords.x, coords.y), # (lon, lat) + "source_id": f"Источник #{source.id}", + } + ) + + if points: + groups.append( + { + "name": label, + "points": points, + "color": color, + } + ) + else: + return redirect("mainapp:home") + + context = { + "groups": groups, + } + return render(request, "mainapp/source_map.html", context) + + +class ShowSourceWithPointsMapView(LoginRequiredMixin, View): + """View for displaying a single source with all its related ObjItem points.""" + + def get(self, request, source_id): + from ..models import Source + + try: + source = Source.objects.prefetch_related( + "source_objitems", + "source_objitems__parameter_obj", + "source_objitems__geo_obj", + ).get(id=source_id) + except Source.DoesNotExist: + return redirect("mainapp:home") + + groups = [] + + # Цвета для разных типов координат источника + source_coord_types = [ + ("coords_average", "Усредненные координаты", "blue"), + ("coords_kupsat", "Координаты Кубсата", "orange"), + ("coords_valid", "Координаты оперативников", "green"), + ("coords_reference", "Координаты справочные", "violet"), + ] + + # Добавляем координаты источника + for coord_field, label, color in source_coord_types: + coords = getattr(source, coord_field) + if coords: + groups.append( + { + "name": label, + "points": [ + { + "point": (coords.x, coords.y), + "source_id": f"Источник #{source.id}", + } + ], + "color": color, + } + ) + + # Добавляем все точки ГЛ одной группой + gl_points = source.source_objitems.select_related( + "parameter_obj", "geo_obj" + ).all() + + # Собираем все точки ГЛ в одну группу + all_gl_points = [] + for obj in gl_points: + if ( + not hasattr(obj, "geo_obj") + or not obj.geo_obj + or not obj.geo_obj.coords + ): + continue + param = getattr(obj, "parameter_obj", None) + if not param: + continue + + all_gl_points.append( + { + "point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y), + "name": obj.name, + "frequency": f"{param.frequency} [{param.freq_range}] МГц", + } + ) + + # Добавляем все точки ГЛ одним цветом (красный) + if all_gl_points: + groups.append( + {"name": "Точки ГЛ", "points": all_gl_points, "color": "red"} + ) + + context = { + "groups": groups, + "source_id": source_id, + } + return render(request, "mainapp/source_with_points_map.html", context) + + class ClusterTestView(LoginRequiredMixin, View): """Test view for clustering functionality.""" - + def get(self, request): objs = ObjItem.objects.filter( name__icontains="! Astra 4A 12654,040 [1,962] МГц H" diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py index f3737fa..31d0e86 100644 --- a/dbapp/mainapp/views/source.py +++ b/dbapp/mainapp/views/source.py @@ -3,12 +3,15 @@ Source related views. """ from datetime import datetime -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.core.paginator import Paginator from django.db.models import Count -from django.shortcuts import render +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse from django.views import View +from ..forms import SourceForm from ..models import Source from ..utils import parse_pagination_params @@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View): # Get pagination parameters page_number, items_per_page = parse_pagination_params(request) - # Get sorting parameters - sort_param = request.GET.get("sort", "-created_at") + # Get sorting parameters (default to ID ascending) + sort_param = request.GET.get("sort", "id") # Get filter parameters search_query = request.GET.get("search", "").strip() @@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View): } return render(request, "mainapp/source_list.html", context) + + + +class AdminModeratorMixin(UserPassesTestMixin): + """Mixin to restrict access to admin and moderator roles only.""" + + def test_func(self): + return ( + self.request.user.is_authenticated and + hasattr(self.request.user, 'customuser') and + self.request.user.customuser.role in ['admin', 'moderator'] + ) + + def handle_no_permission(self): + messages.error(self.request, 'У вас нет прав для выполнения этого действия.') + return redirect('mainapp:home') + + +class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View): + """View for editing Source with 4 coordinate fields and related ObjItems.""" + + def get(self, request, pk): + source = get_object_or_404(Source, pk=pk) + form = SourceForm(instance=source) + + # Get related ObjItems ordered by creation date + objitems = source.source_objitems.select_related( + 'parameter_obj', + 'parameter_obj__id_satellite', + 'parameter_obj__polarization', + 'parameter_obj__modulation', + 'parameter_obj__standard', + 'geo_obj', + 'created_by__user', + 'updated_by__user' + ).order_by('created_at') + + context = { + 'object': source, + 'form': form, + 'objitems': objitems, + 'full_width_page': True, + } + + return render(request, 'mainapp/source_form.html', context) + + def post(self, request, pk): + source = get_object_or_404(Source, pk=pk) + form = SourceForm(request.POST, instance=source) + + if form.is_valid(): + source = form.save(commit=False) + # Set updated_by to current user + if hasattr(request.user, 'customuser'): + source.updated_by = request.user.customuser + source.save() + + messages.success(request, f'Источник #{source.id} успешно обновлен.') + + # Redirect back with query params if present + if request.GET.urlencode(): + return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}") + return redirect('mainapp:source_update', pk=source.id) + + # If form is invalid, re-render with errors + objitems = source.source_objitems.select_related( + 'parameter_obj', + 'parameter_obj__id_satellite', + 'parameter_obj__polarization', + 'parameter_obj__modulation', + 'parameter_obj__standard', + 'geo_obj', + 'created_by__user', + 'updated_by__user' + ).order_by('created_at') + + context = { + 'object': source, + 'form': form, + 'objitems': objitems, + 'full_width_page': True, + } + + return render(request, 'mainapp/source_form.html', context) + + +class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View): + """View for deleting Source.""" + + def get(self, request, pk): + source = get_object_or_404(Source, pk=pk) + + context = { + 'object': source, + 'objitems_count': source.source_objitems.count(), + } + + return render(request, 'mainapp/source_confirm_delete.html', context) + + def post(self, request, pk): + source = get_object_or_404(Source, pk=pk) + source_id = source.id + + try: + source.delete() + messages.success(request, f'Источник #{source_id} успешно удален.') + except Exception as e: + messages.error(request, f'Ошибка при удалении источника: {str(e)}') + return redirect('mainapp:source_update', pk=pk) + + # Redirect to source list + if request.GET.urlencode(): + return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}") + return redirect('mainapp:home') diff --git a/dbapp/mapsapp/models.py b/dbapp/mapsapp/models.py index d0c44f9..aa241c9 100644 --- a/dbapp/mapsapp/models.py +++ b/dbapp/mapsapp/models.py @@ -12,54 +12,54 @@ from mainapp.models import Polarization, Satellite, get_default_polarization, Cu class Transponders(models.Model): """ Модель транспондера спутника. - + Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации. """ - + # Основные поля name = models.CharField( - max_length=30, - null=True, - blank=True, + max_length=30, + null=True, + blank=True, verbose_name="Название транспондера", db_index=True, - help_text="Название транспондера" + help_text="Название транспондера", ) downlink = models.FloatField( - blank=True, - null=True, + blank=True, + null=True, verbose_name="Downlink", # validators=[MinValueValidator(0), MaxValueValidator(50000)], # help_text="Частота downlink в МГц (0-50000)" ) frequency_range = models.FloatField( - blank=True, - null=True, + blank=True, + null=True, verbose_name="Полоса", # validators=[MinValueValidator(0), MaxValueValidator(1000)], # help_text="Полоса частот в МГц (0-1000)" ) uplink = models.FloatField( - blank=True, - null=True, + blank=True, + null=True, verbose_name="Uplink", # validators=[MinValueValidator(0), MaxValueValidator(50000)], # help_text="Частота uplink в МГц (0-50000)" ) zone_name = models.CharField( - max_length=255, - blank=True, - null=True, + max_length=255, + blank=True, + null=True, verbose_name="Название зоны", db_index=True, - help_text="Название зоны покрытия транспондера" + help_text="Название зоны покрытия транспондера", ) snr = models.FloatField( - blank=True, - null=True, + blank=True, + null=True, verbose_name="Полоса", # validators=[MinValueValidator(0), MaxValueValidator(1000)], - help_text="Полоса частот в МГц (0-1000)" + help_text="Полоса частот в МГц (0-1000)", ) created_at = models.DateTimeField( auto_now_add=True, @@ -89,44 +89,43 @@ class Transponders(models.Model): verbose_name="Изменен пользователем", help_text="Пользователь, последним изменивший запись", ) - + # Связи polarization = models.ForeignKey( - Polarization, - default=get_default_polarization, - on_delete=models.SET_DEFAULT, - related_name="tran_polarizations", - null=True, - blank=True, + Polarization, + default=get_default_polarization, + on_delete=models.SET_DEFAULT, + related_name="tran_polarizations", + null=True, + blank=True, verbose_name="Поляризация", - help_text="Поляризация сигнала" + help_text="Поляризация сигнала", ) sat_id = models.ForeignKey( - Satellite, - on_delete=models.PROTECT, - related_name="tran_satellite", + Satellite, + on_delete=models.PROTECT, + related_name="tran_satellite", verbose_name="Спутник", db_index=True, - help_text="Спутник, которому принадлежит транспондер" + help_text="Спутник, которому принадлежит транспондер", ) - + # Вычисляемые поля transfer = models.GeneratedField( expression=ExpressionWrapper( - Abs(F('downlink') - F('uplink')), - output_field=models.FloatField() + Abs(F("downlink") - F("uplink")), output_field=models.FloatField() ), output_field=models.FloatField(), db_persist=True, - null=True, - blank=True, - verbose_name="Перенос" + null=True, + blank=True, + verbose_name="Перенос", ) # def clean(self): # """Валидация на уровне модели""" # super().clean() - + # # Проверка что downlink и uplink заданы # if self.downlink and self.uplink: # # Обычно uplink выше downlink для спутниковой связи @@ -139,14 +138,12 @@ class Transponders(models.Model): if self.name: return self.name return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}" - + class Meta: verbose_name = "Транспондер" verbose_name_plural = "Транспондеры" - ordering = ['sat_id', 'downlink'] + ordering = ["sat_id", "downlink"] indexes = [ - models.Index(fields=['sat_id', 'downlink']), - models.Index(fields=['sat_id', 'zone_name']), + models.Index(fields=["sat_id", "downlink"]), + models.Index(fields=["sat_id", "zone_name"]), ] - - diff --git a/dbapp/mapsapp/templates/mapsapp/map2d.html b/dbapp/mapsapp/templates/mapsapp/map2d.html index 76054de..84c56ca 100644 --- a/dbapp/mapsapp/templates/mapsapp/map2d.html +++ b/dbapp/mapsapp/templates/mapsapp/map2d.html @@ -2,560 +2,565 @@ {% load static %} {% block content %} -
-
Объекты из базы
- - - - +
+
Объекты из базы
+ + + + -
+
-
+
Области покрытия
-
+
{% endblock content %} {% block extra_js %} {% endblock extra_js %} \ No newline at end of file
+ + ID Имя Спутник Частота, МГц