diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 8e4e4ca..40a55d2 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -478,6 +478,9 @@ class SourceForm(forms.ModelForm): 'info': 'Тип объекта', 'ownership': 'Принадлежность объекта', } + help_texts = { + 'info': 'Стационарные: координата усредняется. Подвижные: координата = последняя точка. При изменении типа координата пересчитывается автоматически.', + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dbapp/mainapp/migrations/0010_set_default_source_type.py b/dbapp/mainapp/migrations/0010_set_default_source_type.py new file mode 100644 index 0000000..6adc847 --- /dev/null +++ b/dbapp/mainapp/migrations/0010_set_default_source_type.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2025-11-21 07:35 + +from django.db import migrations + + +def set_default_source_type(apps, schema_editor): + """ + Устанавливает тип "Стационарные" для всех Source, у которых не указан тип. + """ + Source = apps.get_model('mainapp', 'Source') + ObjectInfo = apps.get_model('mainapp', 'ObjectInfo') + + # Создаем или получаем тип "Стационарные" + stationary_info, _ = ObjectInfo.objects.get_or_create(name="Стационарные") + + # Обновляем все Source без типа + sources_without_type = Source.objects.filter(info__isnull=True) + count = sources_without_type.update(info=stationary_info) + + print(f"Обновлено {count} источников с типом 'Стационарные'") + + +def reverse_set_default_source_type(apps, schema_editor): + """ + Обратная миграция - ничего не делаем, так как это безопасная операция. + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0009_objectownership_alter_source_info_source_ownership'), + ] + + operations = [ + migrations.RunPython(set_default_source_type, reverse_set_default_source_type), + ] diff --git a/dbapp/mainapp/migrations/0011_fix_source_type_capitalization.py b/dbapp/mainapp/migrations/0011_fix_source_type_capitalization.py new file mode 100644 index 0000000..1dea769 --- /dev/null +++ b/dbapp/mainapp/migrations/0011_fix_source_type_capitalization.py @@ -0,0 +1,74 @@ +# Generated by Django 5.2.7 on 2025-11-21 07:42 + +from django.db import migrations + + +def fix_capitalization(apps, schema_editor): + """ + Исправляет регистр типов объектов: "стационарные" -> "Стационарные", "подвижные" -> "Подвижные" + """ + ObjectInfo = apps.get_model('mainapp', 'ObjectInfo') + Source = apps.get_model('mainapp', 'Source') + + # Создаем правильные типы с большой буквы + stationary_new, _ = ObjectInfo.objects.get_or_create(name="Стационарные") + mobile_new, _ = ObjectInfo.objects.get_or_create(name="Подвижные") + + # Находим старые типы с маленькой буквы + try: + stationary_old = ObjectInfo.objects.get(name="стационарные") + # Обновляем все Source, которые используют старый тип + count = Source.objects.filter(info=stationary_old).update(info=stationary_new) + print(f"Обновлено {count} источников: 'стационарные' -> 'Стационарные'") + # Удаляем старый тип + stationary_old.delete() + except ObjectInfo.DoesNotExist: + pass + + try: + mobile_old = ObjectInfo.objects.get(name="подвижные") + # Обновляем все Source, которые используют старый тип + count = Source.objects.filter(info=mobile_old).update(info=mobile_new) + print(f"Обновлено {count} источников: 'подвижные' -> 'Подвижные'") + # Удаляем старый тип + mobile_old.delete() + except ObjectInfo.DoesNotExist: + pass + + +def reverse_fix_capitalization(apps, schema_editor): + """ + Обратная миграция - возвращаем маленькие буквы + """ + ObjectInfo = apps.get_model('mainapp', 'ObjectInfo') + Source = apps.get_model('mainapp', 'Source') + + # Создаем типы с маленькой буквы + stationary_old, _ = ObjectInfo.objects.get_or_create(name="стационарные") + mobile_old, _ = ObjectInfo.objects.get_or_create(name="подвижные") + + # Находим типы с большой буквы + try: + stationary_new = ObjectInfo.objects.get(name="Стационарные") + Source.objects.filter(info=stationary_new).update(info=stationary_old) + stationary_new.delete() + except ObjectInfo.DoesNotExist: + pass + + try: + mobile_new = ObjectInfo.objects.get(name="Подвижные") + Source.objects.filter(info=mobile_new).update(info=mobile_old) + mobile_new.delete() + except ObjectInfo.DoesNotExist: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0010_set_default_source_type'), + ] + + operations = [ + migrations.RunPython(fix_capitalization, reverse_fix_capitalization), + ] diff --git a/dbapp/mainapp/migrations/0012_source_confirm_at_source_last_signal_at.py b/dbapp/mainapp/migrations/0012_source_confirm_at_source_last_signal_at.py new file mode 100644 index 0000000..482ccc0 --- /dev/null +++ b/dbapp/mainapp/migrations/0012_source_confirm_at_source_last_signal_at.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-11-21 12:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0011_fix_source_type_capitalization'), + ] + + operations = [ + migrations.AddField( + model_name='source', + name='confirm_at', + field=models.DateTimeField(blank=True, help_text='Дата и время добавления последней полученной точки ГЛ', null=True, verbose_name='Дата подтверждения'), + ), + migrations.AddField( + model_name='source', + name='last_signal_at', + field=models.DateTimeField(blank=True, help_text='Дата и время последней отметки о наличии сигнала', null=True, verbose_name='Последний сигнал'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index 80cf387..bd4fae4 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -491,6 +491,18 @@ class Source(models.Model): verbose_name="Принадлежность объекта", help_text="Принадлежность объекта (страна, организация и т.д.)", ) + confirm_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Дата подтверждения", + help_text="Дата и время добавления последней полученной точки ГЛ", + ) + last_signal_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Последний сигнал", + help_text="Дата и время последней отметки о наличии сигнала", + ) coords_average = gis.PointField( srid=4326, @@ -550,6 +562,135 @@ class Source(models.Model): help_text="Пользователь, последним изменивший запись", ) + def update_coords_average(self, new_coord_tuple): + """ + Обновляет coords_average в зависимости от типа объекта (info). + + Логика: + - Если info == "Подвижные": coords_average = последняя добавленная координата + - Иначе (Стационарные и др.): coords_average = инкрементальное среднее + + Args: + new_coord_tuple: кортеж (longitude, latitude) новой координаты + """ + from django.contrib.gis.geos import Point + from .utils import calculate_mean_coords + + # Если тип объекта "Подвижные" - просто устанавливаем последнюю координату + if self.info and self.info.name == "Подвижные": + self.coords_average = Point(new_coord_tuple, srid=4326) + else: + # Для стационарных объектов - вычисляем среднее + if self.coords_average: + # Есть предыдущее среднее - вычисляем новое среднее + current_coord = (self.coords_average.x, self.coords_average.y) + new_avg, _ = calculate_mean_coords(current_coord, new_coord_tuple) + self.coords_average = Point(new_avg, srid=4326) + else: + # Первая координата - просто устанавливаем её + self.coords_average = Point(new_coord_tuple, srid=4326) + + def get_last_geo_coords(self): + """ + Получает координаты последней добавленной точки ГЛ для этого источника. + Сортировка по ID (последняя добавленная в базу). + + Returns: + tuple: (longitude, latitude) или None если точек нет + """ + # Получаем последний ObjItem для этого Source (по ID) + last_objitem = self.source_objitems.filter( + geo_obj__coords__isnull=False + ).select_related('geo_obj').order_by('-id').first() + + if last_objitem and last_objitem.geo_obj and last_objitem.geo_obj.coords: + return (last_objitem.geo_obj.coords.x, last_objitem.geo_obj.coords.y) + + return None + + def update_confirm_at(self): + """ + Обновляет дату confirm_at на дату создания последней добавленной точки ГЛ. + """ + last_objitem = self.source_objitems.order_by('-created_at').first() + if last_objitem: + self.confirm_at = last_objitem.created_at + + def update_last_signal_at(self): + """ + Обновляет дату last_signal_at на дату последней отметки о наличии сигнала (mark=True). + """ + last_signal_mark = self.marks.filter(mark=True).order_by('-timestamp').first() + if last_signal_mark: + self.last_signal_at = last_signal_mark.timestamp + else: + self.last_signal_at = None + + def save(self, *args, **kwargs): + """ + Переопределенный метод save для автоматического обновления coords_average + при изменении типа объекта. + """ + from django.contrib.gis.geos import Point + + # Проверяем, изменился ли тип объекта + if self.pk: # Объект уже существует + try: + old_instance = Source.objects.get(pk=self.pk) + old_info = old_instance.info + new_info = self.info + + # Если тип изменился на "Подвижные" + if new_info and new_info.name == "Подвижные" and (not old_info or old_info.name != "Подвижные"): + # Устанавливаем координату последней точки + last_coords = self.get_last_geo_coords() + if last_coords: + self.coords_average = Point(last_coords, srid=4326) + + # Если тип изменился с "Подвижные" на что-то другое + elif old_info and old_info.name == "Подвижные" and (not new_info or new_info.name != "Подвижные"): + # Пересчитываем среднюю координату по всем точкам + self._recalculate_average_coords() + + except Source.DoesNotExist: + pass + + super().save(*args, **kwargs) + + def _recalculate_average_coords(self): + """ + Пересчитывает среднюю координату по всем точкам источника. + Используется при переключении с "Подвижные" на "Стационарные". + + Сортировка по ID (порядок добавления в базу), инкрементальное усреднение + как в функциях импорта. + """ + from django.contrib.gis.geos import Point + from .utils import calculate_mean_coords + + # Получаем все точки для этого источника, сортируем по ID (порядок добавления) + objitems = self.source_objitems.filter( + geo_obj__coords__isnull=False + ).select_related('geo_obj').order_by('id') + + if not objitems.exists(): + return + + # Вычисляем среднюю координату инкрементально (как в функциях импорта) + coords_average = None + for objitem in objitems: + if objitem.geo_obj and objitem.geo_obj.coords: + coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y) + if coords_average is None: + # Первая точка - просто устанавливаем её + coords_average = coord + else: + # Последующие точки - вычисляем среднее между текущим средним и новой точкой + coords_average, _ = calculate_mean_coords(coords_average, coord) + + if coords_average: + self.coords_average = Point(coords_average, srid=4326) + class Meta: verbose_name = "Источник" verbose_name_plural = "Источники" diff --git a/dbapp/mainapp/templates/mainapp/object_marks.html b/dbapp/mainapp/templates/mainapp/object_marks.html index 035f0e1..a96350a 100644 --- a/dbapp/mainapp/templates/mainapp/object_marks.html +++ b/dbapp/mainapp/templates/mainapp/object_marks.html @@ -12,10 +12,15 @@ } .source-info-cell { - min-width: 250px; + min-width: 200px; background-color: #f8f9fa; } + .param-cell { + min-width: 120px; + text-align: center; + } + .marks-cell { min-width: 150px; text-align: center; @@ -82,11 +87,24 @@ background-color: #6c757d; color: white; } + + .satellite-selector { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .satellite-selector h5 { + margin-bottom: 1rem; + color: #495057; + } {% endblock %} {% block content %} -
+
@@ -94,6 +112,28 @@
+ +
+
+
+
Выберите спутник:
+
+
+ +
+
+
+
+
+ + {% if selected_satellite_id %}
@@ -111,7 +151,18 @@ - {% include 'mainapp/components/_sort_header.html' with field='id' label='Информация об объекте' current_sort=sort %} + {% include 'mainapp/components/_sort_header.html' with field='id' label='ID / Имя' current_sort=sort %} + + + {% include 'mainapp/components/_sort_header.html' with field='frequency' label='Частота, МГц' current_sort=sort %} + + + {% include 'mainapp/components/_sort_header.html' with field='freq_range' label='Полоса, МГц' current_sort=sort %} + + Поляризация + Модуляция + + {% include 'mainapp/components/_sort_header.html' with field='bod_velocity' label='Бодовая скорость' current_sort=sort %} Наличие @@ -128,19 +179,13 @@
ID: {{ source.id }}
-
Имя объекта: {{ source.objitem_name }}
-
Дата создания: {{ source.created_at|date:"d.m.Y H:i" }}
-
Кол-во объектов: {{ source.source_objitems.count }}
- {% if source.coords_average %} -
Усреднённые координаты: Есть
- {% endif %} - {% if source.coords_kupsat %} -
Координаты Кубсата: Есть
- {% endif %} - {% if source.coords_valid %} -
Координаты оперативников: Есть
- {% endif %} +
Имя: {{ source.objitem_name }}
+ {{ source.frequency }} + {{ source.freq_range }} + {{ source.polarization }} + {{ source.modulation }} + {{ source.bod_velocity }} {% with first_mark=marks.0 %} @@ -197,19 +242,13 @@
ID: {{ source.id }}
-
Имя объекта: {{ source.objitem_name }}
-
Дата создания: {{ source.created_at|date:"d.m.Y H:i" }}
-
Кол-во объектов: {{ source.source_objitems.count }}
- {% if source.coords_average %} -
Усреднённые координаты: Есть
- {% endif %} - {% if source.coords_kupsat %} -
Координаты Кубсата: Есть
- {% endif %} - {% if source.coords_valid %} -
Координаты оперативников: Есть
- {% endif %} +
Имя: {{ source.objitem_name }}
+ {{ source.frequency }} + {{ source.freq_range }} + {{ source.polarization }} + {{ source.modulation }} + {{ source.bod_velocity }} Отметок нет
@@ -228,8 +267,8 @@ {% endwith %} {% empty %} - -

Объекты не найдены

+ +

Объекты не найдены для выбранного спутника

{% endfor %} @@ -240,6 +279,16 @@
+ {% else %} + +
+
+
+
Пожалуйста, выберите спутник для просмотра объектов
+
+
+
+ {% endif %}
@@ -250,24 +299,6 @@
- -
- -
- - -
- -
-
@@ -307,7 +338,10 @@
- {# Сохраняем параметры сортировки и поиска при применении фильтров #} + {# Сохраняем параметры сортировки, поиска и спутника при применении фильтров #} + {% if selected_satellite_id %} + + {% endif %} {% if request.GET.sort %} {% endif %} @@ -322,7 +356,7 @@ - + Сбросить
@@ -331,6 +365,25 @@