diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py
index f954a39..25f311a 100644
--- a/dbapp/mainapp/admin.py
+++ b/dbapp/mainapp/admin.py
@@ -35,6 +35,8 @@ from .models import (
Band,
Source,
TechAnalyze,
+ SourceRequest,
+ SourceRequestStatusHistory,
)
from .filters import (
GeoKupDistanceFilter,
@@ -1162,3 +1164,121 @@ class TechAnalyzeAdmin(ImportExportActionModelAdmin, BaseAdmin):
},
),
)
+
+
+class SourceRequestStatusHistoryInline(admin.TabularInline):
+ """Inline для отображения истории статусов заявки."""
+ model = SourceRequestStatusHistory
+ extra = 0
+ readonly_fields = ('old_status', 'new_status', 'changed_at', 'changed_by')
+ can_delete = False
+
+ def has_add_permission(self, request, obj=None):
+ return False
+
+
+@admin.register(SourceRequest)
+class SourceRequestAdmin(BaseAdmin):
+ """Админ-панель для модели SourceRequest."""
+
+ list_display = (
+ 'id',
+ 'source',
+ 'status',
+ 'priority',
+ 'planned_at',
+ 'request_date',
+ 'gso_success',
+ 'kubsat_success',
+ 'points_count',
+ 'status_updated_at',
+ 'created_at',
+ 'created_by',
+ )
+ list_display_links = ('id', 'source')
+ list_select_related = ('source', 'created_by__user', 'updated_by__user')
+
+ list_filter = (
+ 'status',
+ 'priority',
+ 'gso_success',
+ 'kubsat_success',
+ ('planned_at', DateRangeQuickSelectListFilterBuilder()),
+ ('request_date', DateRangeQuickSelectListFilterBuilder()),
+ ('created_at', DateRangeQuickSelectListFilterBuilder()),
+ )
+
+ search_fields = (
+ 'source__id',
+ 'comment',
+ )
+
+ ordering = ('-created_at',)
+ readonly_fields = ('status_updated_at', 'created_at', 'created_by', 'updated_by', 'coords', 'points_count')
+ autocomplete_fields = ('source',)
+ inlines = [SourceRequestStatusHistoryInline]
+
+ fieldsets = (
+ (
+ 'Основная информация',
+ {'fields': ('source', 'status', 'priority')},
+ ),
+ (
+ 'Даты',
+ {'fields': ('planned_at', 'request_date', 'status_updated_at')},
+ ),
+ (
+ 'Результаты',
+ {'fields': ('gso_success', 'kubsat_success')},
+ ),
+ (
+ 'Координаты',
+ {'fields': ('coords', 'points_count')},
+ ),
+ (
+ 'Комментарий',
+ {'fields': ('comment',)},
+ ),
+ (
+ 'Метаданные',
+ {
+ 'fields': ('created_at', 'created_by', 'updated_by'),
+ 'classes': ('collapse',),
+ },
+ ),
+ )
+
+
+@admin.register(SourceRequestStatusHistory)
+class SourceRequestStatusHistoryAdmin(BaseAdmin):
+ """Админ-панель для модели SourceRequestStatusHistory."""
+
+ list_display = (
+ 'id',
+ 'source_request',
+ 'old_status',
+ 'new_status',
+ 'changed_at',
+ 'changed_by',
+ )
+ list_display_links = ('id',)
+ list_select_related = ('source_request', 'changed_by__user')
+
+ list_filter = (
+ 'old_status',
+ 'new_status',
+ ('changed_at', DateRangeQuickSelectListFilterBuilder()),
+ )
+
+ search_fields = (
+ 'source_request__id',
+ )
+
+ ordering = ('-changed_at',)
+ readonly_fields = ('source_request', 'old_status', 'new_status', 'changed_at', 'changed_by')
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ return False
diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py
index 7862fd3..06e8219 100644
--- a/dbapp/mainapp/forms.py
+++ b/dbapp/mainapp/forms.py
@@ -913,3 +913,134 @@ class SatelliteForm(forms.ModelForm):
raise forms.ValidationError('Спутник с таким названием уже существует')
return name
+
+
+class SourceRequestForm(forms.ModelForm):
+ """
+ Форма для создания и редактирования заявок на источники.
+ """
+
+ # Дополнительные поля для координат
+ coords_lat = forms.FloatField(
+ required=False,
+ label='Широта',
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'step': '0.000001',
+ 'placeholder': 'Например: 55.751244'
+ })
+ )
+ coords_lon = forms.FloatField(
+ required=False,
+ label='Долгота',
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'step': '0.000001',
+ 'placeholder': 'Например: 37.618423'
+ })
+ )
+
+ class Meta:
+ from .models import SourceRequest
+ model = SourceRequest
+ fields = [
+ 'source',
+ 'status',
+ 'priority',
+ 'planned_at',
+ 'request_date',
+ 'gso_success',
+ 'kubsat_success',
+ 'comment',
+ ]
+ widgets = {
+ 'source': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'status': forms.Select(attrs={
+ 'class': 'form-select'
+ }),
+ 'priority': forms.Select(attrs={
+ 'class': 'form-select'
+ }),
+ 'planned_at': forms.DateTimeInput(attrs={
+ 'class': 'form-control',
+ 'type': 'datetime-local'
+ }),
+ 'request_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ 'gso_success': forms.Select(
+ choices=[(None, '-'), (True, 'Да'), (False, 'Нет')],
+ attrs={'class': 'form-select'}
+ ),
+ 'kubsat_success': forms.Select(
+ choices=[(None, '-'), (True, 'Да'), (False, 'Нет')],
+ attrs={'class': 'form-select'}
+ ),
+ 'comment': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Введите комментарий'
+ }),
+ }
+ labels = {
+ 'source': 'Источник',
+ 'status': 'Статус',
+ 'priority': 'Приоритет',
+ 'planned_at': 'Дата и время планирования',
+ 'request_date': 'Дата заявки',
+ 'gso_success': 'ГСО успешно?',
+ 'kubsat_success': 'Кубсат успешно?',
+ 'comment': 'Комментарий',
+ }
+
+ def __init__(self, *args, **kwargs):
+ # Извлекаем source_id если передан
+ source_id = kwargs.pop('source_id', None)
+ super().__init__(*args, **kwargs)
+
+ # Загружаем queryset для источников
+ self.fields['source'].queryset = Source.objects.all().order_by('-id')
+
+ # Если передан source_id, устанавливаем его как начальное значение
+ if source_id:
+ self.fields['source'].initial = source_id
+ # Можно сделать поле только для чтения
+ self.fields['source'].widget.attrs['readonly'] = True
+
+ # Настраиваем виджеты для булевых полей
+ self.fields['gso_success'].widget = forms.Select(
+ choices=[(None, '-'), (True, 'Да'), (False, 'Нет')],
+ attrs={'class': 'form-select'}
+ )
+ self.fields['kubsat_success'].widget = forms.Select(
+ choices=[(None, '-'), (True, 'Да'), (False, 'Нет')],
+ attrs={'class': 'form-select'}
+ )
+
+ # Заполняем координаты из существующего объекта
+ if self.instance and self.instance.pk and self.instance.coords:
+ self.fields['coords_lat'].initial = self.instance.coords.y
+ self.fields['coords_lon'].initial = self.instance.coords.x
+
+ def save(self, commit=True):
+ from django.contrib.gis.geos import Point
+
+ instance = super().save(commit=False)
+
+ # Обрабатываем координаты
+ coords_lat = self.cleaned_data.get('coords_lat')
+ coords_lon = self.cleaned_data.get('coords_lon')
+
+ if coords_lat is not None and coords_lon is not None:
+ instance.coords = Point(coords_lon, coords_lat, srid=4326)
+ elif coords_lat is None and coords_lon is None:
+ instance.coords = None
+
+ if commit:
+ instance.save()
+
+ return instance
diff --git a/dbapp/mainapp/migrations/0018_add_source_request_models.py b/dbapp/mainapp/migrations/0018_add_source_request_models.py
new file mode 100644
index 0000000..58c989a
--- /dev/null
+++ b/dbapp/mainapp/migrations/0018_add_source_request_models.py
@@ -0,0 +1,87 @@
+# Generated by Django 5.2.7 on 2025-12-08 08:45
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mainapp', '0017_add_satellite_alternative_name'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='objectownership',
+ name='name',
+ field=models.CharField(help_text='Принадлежность объекта', max_length=255, unique=True, verbose_name='Принадлежность'),
+ ),
+ migrations.AlterField(
+ model_name='satellite',
+ name='alternative_name',
+ field=models.CharField(blank=True, db_index=True, help_text='Альтернативное название спутника', max_length=100, null=True, verbose_name='Альтернативное имя'),
+ ),
+ migrations.CreateModel(
+ name='SourceRequest',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('status', models.CharField(choices=[('planned', 'Запланировано'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], db_index=True, default='planned', help_text='Текущий статус заявки', max_length=20, verbose_name='Статус')),
+ ('priority', models.CharField(choices=[('low', 'Низкий'), ('medium', 'Средний'), ('high', 'Высокий')], db_index=True, default='medium', help_text='Приоритет заявки', max_length=10, verbose_name='Приоритет')),
+ ('planned_at', models.DateTimeField(blank=True, help_text='Запланированная дата и время', null=True, verbose_name='Дата и время планирования')),
+ ('request_date', models.DateField(blank=True, help_text='Дата подачи заявки', null=True, verbose_name='Дата заявки')),
+ ('status_updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего обновления статуса', verbose_name='Дата обновления статуса')),
+ ('gso_success', models.BooleanField(blank=True, help_text='Успешность ГСО', null=True, verbose_name='ГСО успешно?')),
+ ('kubsat_success', models.BooleanField(blank=True, help_text='Успешность Кубсат', null=True, verbose_name='Кубсат успешно?')),
+ ('comment', models.TextField(blank=True, help_text='Дополнительные комментарии к заявке', null=True, verbose_name='Комментарий')),
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
+ ('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_requests_created', to='mainapp.customuser', verbose_name='Создан пользователем')),
+ ('source', models.ForeignKey(help_text='Связанный источник', on_delete=django.db.models.deletion.CASCADE, related_name='source_requests', to='mainapp.source', verbose_name='Источник')),
+ ('updated_by', models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_requests_updated', to='mainapp.customuser', verbose_name='Изменен пользователем')),
+ ],
+ options={
+ 'verbose_name': 'Заявка на источник',
+ 'verbose_name_plural': 'Заявки на источники',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ migrations.CreateModel(
+ name='SourceRequestStatusHistory',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('old_status', models.CharField(choices=[('planned', 'Запланировано'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], help_text='Статус до изменения', max_length=20, verbose_name='Старый статус')),
+ ('new_status', models.CharField(choices=[('planned', 'Запланировано'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], help_text='Статус после изменения', max_length=20, verbose_name='Новый статус')),
+ ('changed_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Дата и время изменения статуса', verbose_name='Дата изменения')),
+ ('changed_by', models.ForeignKey(blank=True, help_text='Пользователь, изменивший статус', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='status_changes', to='mainapp.customuser', verbose_name='Изменен пользователем')),
+ ('source_request', models.ForeignKey(help_text='Связанная заявка', on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='mainapp.sourcerequest', verbose_name='Заявка')),
+ ],
+ options={
+ 'verbose_name': 'История статуса заявки',
+ 'verbose_name_plural': 'История статусов заявок',
+ 'ordering': ['-changed_at'],
+ },
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequest',
+ index=models.Index(fields=['-created_at'], name='mainapp_sou_created_61d8ae_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequest',
+ index=models.Index(fields=['status'], name='mainapp_sou_status_31dc99_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequest',
+ index=models.Index(fields=['priority'], name='mainapp_sou_priorit_5b5044_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequest',
+ index=models.Index(fields=['source', '-created_at'], name='mainapp_sou_source__6bb459_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequeststatushistory',
+ index=models.Index(fields=['-changed_at'], name='mainapp_sou_changed_9b876e_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='sourcerequeststatushistory',
+ index=models.Index(fields=['source_request', '-changed_at'], name='mainapp_sou_source__957c28_idx'),
+ ),
+ ]
diff --git a/dbapp/mainapp/migrations/0019_add_coords_to_source_request.py b/dbapp/mainapp/migrations/0019_add_coords_to_source_request.py
new file mode 100644
index 0000000..e65a324
--- /dev/null
+++ b/dbapp/mainapp/migrations/0019_add_coords_to_source_request.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.2.7 on 2025-12-08 09:24
+
+import django.contrib.gis.db.models.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mainapp', '0018_add_source_request_models'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='sourcerequest',
+ name='coords',
+ field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Усреднённые координаты по выбранным точкам (WGS84)', null=True, srid=4326, verbose_name='Координаты'),
+ ),
+ migrations.AddField(
+ model_name='sourcerequest',
+ name='points_count',
+ field=models.PositiveIntegerField(default=0, help_text='Количество точек ГЛ, использованных для расчёта координат', verbose_name='Количество точек'),
+ ),
+ ]
diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py
index 83c2a40..fb2deea 100644
--- a/dbapp/mainapp/models.py
+++ b/dbapp/mainapp/models.py
@@ -1191,6 +1191,231 @@ class SigmaParameter(models.Model):
verbose_name_plural = "ВЧ sigma"
+class SourceRequest(models.Model):
+ """
+ Модель заявки на источник.
+
+ Хранит информацию о заявках на обработку источников с различными статусами.
+ """
+
+ STATUS_CHOICES = [
+ ('planned', 'Запланировано'),
+ ('conducted', 'Проведён'),
+ ('successful', 'Успешно'),
+ ('no_correlation', 'Нет корреляции'),
+ ('no_signal', 'Нет сигнала в спектре'),
+ ('unsuccessful', 'Неуспешно'),
+ ('downloading', 'Скачивание'),
+ ('processing', 'Обработка'),
+ ('result_received', 'Результат получен'),
+ ]
+
+ PRIORITY_CHOICES = [
+ ('low', 'Низкий'),
+ ('medium', 'Средний'),
+ ('high', 'Высокий'),
+ ]
+
+ # Связь с источником
+ source = models.ForeignKey(
+ Source,
+ on_delete=models.CASCADE,
+ related_name='source_requests',
+ verbose_name='Источник',
+ help_text='Связанный источник',
+ )
+
+ # Основные поля
+ status = models.CharField(
+ max_length=20,
+ choices=STATUS_CHOICES,
+ default='planned',
+ verbose_name='Статус',
+ db_index=True,
+ help_text='Текущий статус заявки',
+ )
+ priority = models.CharField(
+ max_length=10,
+ choices=PRIORITY_CHOICES,
+ default='medium',
+ verbose_name='Приоритет',
+ db_index=True,
+ help_text='Приоритет заявки',
+ )
+
+ # Даты
+ planned_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ verbose_name='Дата и время планирования',
+ help_text='Запланированная дата и время',
+ )
+ request_date = models.DateField(
+ null=True,
+ blank=True,
+ verbose_name='Дата заявки',
+ help_text='Дата подачи заявки',
+ )
+ status_updated_at = models.DateTimeField(
+ auto_now=True,
+ verbose_name='Дата обновления статуса',
+ help_text='Дата и время последнего обновления статуса',
+ )
+
+ # Результаты
+ gso_success = models.BooleanField(
+ null=True,
+ blank=True,
+ verbose_name='ГСО успешно?',
+ help_text='Успешность ГСО',
+ )
+ kubsat_success = models.BooleanField(
+ null=True,
+ blank=True,
+ verbose_name='Кубсат успешно?',
+ help_text='Успешность Кубсат',
+ )
+
+ # Комментарий
+ comment = models.TextField(
+ null=True,
+ blank=True,
+ verbose_name='Комментарий',
+ help_text='Дополнительные комментарии к заявке',
+ )
+
+ # Координаты (усреднённые по выбранным точкам)
+ coords = gis.PointField(
+ srid=4326,
+ null=True,
+ blank=True,
+ verbose_name='Координаты',
+ help_text='Усреднённые координаты по выбранным точкам (WGS84)',
+ )
+
+ # Количество точек, использованных для расчёта координат
+ points_count = models.PositiveIntegerField(
+ default=0,
+ verbose_name='Количество точек',
+ help_text='Количество точек ГЛ, использованных для расчёта координат',
+ )
+
+ # Метаданные
+ created_at = models.DateTimeField(
+ auto_now_add=True,
+ verbose_name='Дата создания',
+ help_text='Дата и время создания записи',
+ )
+ created_by = models.ForeignKey(
+ CustomUser,
+ on_delete=models.SET_NULL,
+ related_name='source_requests_created',
+ null=True,
+ blank=True,
+ verbose_name='Создан пользователем',
+ help_text='Пользователь, создавший запись',
+ )
+ updated_by = models.ForeignKey(
+ CustomUser,
+ on_delete=models.SET_NULL,
+ related_name='source_requests_updated',
+ null=True,
+ blank=True,
+ verbose_name='Изменен пользователем',
+ help_text='Пользователь, последним изменивший запись',
+ )
+
+ def __str__(self):
+ return f"Заявка #{self.pk} - {self.source_id} ({self.get_status_display()})"
+
+ def save(self, *args, **kwargs):
+ # Определяем, изменился ли статус
+ old_status = None
+ if self.pk:
+ try:
+ old_instance = SourceRequest.objects.get(pk=self.pk)
+ old_status = old_instance.status
+ except SourceRequest.DoesNotExist:
+ pass
+
+ super().save(*args, **kwargs)
+
+ # Если статус изменился, создаем запись в истории
+ if old_status is not None and old_status != self.status:
+ SourceRequestStatusHistory.objects.create(
+ source_request=self,
+ old_status=old_status,
+ new_status=self.status,
+ changed_by=self.updated_by,
+ )
+
+ class Meta:
+ verbose_name = 'Заявка на источник'
+ verbose_name_plural = 'Заявки на источники'
+ ordering = ['-created_at']
+ indexes = [
+ models.Index(fields=['-created_at']),
+ models.Index(fields=['status']),
+ models.Index(fields=['priority']),
+ models.Index(fields=['source', '-created_at']),
+ ]
+
+
+class SourceRequestStatusHistory(models.Model):
+ """
+ Модель истории изменений статусов заявок.
+
+ Хранит полную хронологию изменений статусов заявок.
+ """
+
+ source_request = models.ForeignKey(
+ SourceRequest,
+ on_delete=models.CASCADE,
+ related_name='status_history',
+ verbose_name='Заявка',
+ help_text='Связанная заявка',
+ )
+ old_status = models.CharField(
+ max_length=20,
+ choices=SourceRequest.STATUS_CHOICES,
+ verbose_name='Старый статус',
+ help_text='Статус до изменения',
+ )
+ new_status = models.CharField(
+ max_length=20,
+ choices=SourceRequest.STATUS_CHOICES,
+ verbose_name='Новый статус',
+ help_text='Статус после изменения',
+ )
+ changed_at = models.DateTimeField(
+ auto_now_add=True,
+ verbose_name='Дата изменения',
+ db_index=True,
+ help_text='Дата и время изменения статуса',
+ )
+ changed_by = models.ForeignKey(
+ CustomUser,
+ on_delete=models.SET_NULL,
+ related_name='status_changes',
+ null=True,
+ blank=True,
+ verbose_name='Изменен пользователем',
+ help_text='Пользователь, изменивший статус',
+ )
+
+ def __str__(self):
+ return f"{self.source_request_id}: {self.get_old_status_display()} → {self.get_new_status_display()}"
+
+ class Meta:
+ verbose_name = 'История статуса заявки'
+ verbose_name_plural = 'История статусов заявок'
+ ordering = ['-changed_at']
+ indexes = [
+ models.Index(fields=['-changed_at']),
+ models.Index(fields=['source_request', '-changed_at']),
+ ]
+
+
class Geo(models.Model):
"""
Модель геолокационных данных.
diff --git a/dbapp/mainapp/templates/mainapp/components/_kubsat_filters_tab.html b/dbapp/mainapp/templates/mainapp/components/_kubsat_filters_tab.html
new file mode 100644
index 0000000..72f0dba
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/components/_kubsat_filters_tab.html
@@ -0,0 +1,838 @@
+{% load l10n %}
+
+
+
+
+{% if sources_with_date_info %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Найдено объектов: {{ sources_with_date_info|length }},
+ точек: {% for source_data in sources_with_date_info %}{{ source_data.objitems_data|length }}{% if not forloop.last %}+{% endif %}{% endfor %}
+
+
+
+
+
+
+{% endif %}
+
+
+{% if sources_with_date_info %}
+
+
+
+
+
+
+
+
+ | ID объекта |
+ Тип объекта |
+ Принадлежность объекта |
+ Заявки |
+ ГСО |
+ Кубсат |
+ Статус заявки |
+ Кол-во точек |
+ Усреднённая координата |
+ Имя точки |
+ Спутник |
+ Частота (МГц) |
+ Полоса (МГц) |
+ Поляризация |
+ Модуляция |
+ Координаты ГЛ |
+ Дата ГЛ |
+ Действия |
+
+
+
+ {% for source_data in sources_with_date_info %}
+ {% for objitem_data in source_data.objitems_data %}
+
+
+ {% if forloop.first %}
+ | {{ source_data.source.id }} |
+ {% endif %}
+
+ {% if forloop.first %}
+ {{ source_data.source.info.name|default:"-" }} |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.source.ownership %}
+ {% if source_data.source.ownership.name == "ТВ" and source_data.has_lyngsat %}
+
+ {{ source_data.source.ownership.name }}
+
+ {% else %}
+ {{ source_data.source.ownership.name }}
+ {% endif %}
+ {% else %}
+ -
+ {% endif %}
+ |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.requests_count > 0 %}
+ {{ source_data.requests_count }}
+ {% else %}
+ 0
+ {% endif %}
+ |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.gso_success == True %}
+
+ {% elif source_data.gso_success == False %}
+
+ {% else %}
+ -
+ {% endif %}
+ |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.kubsat_success == True %}
+
+ {% elif source_data.kubsat_success == False %}
+
+ {% else %}
+ -
+ {% endif %}
+ |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.request_status %}
+ {% if source_data.request_status_raw == 'successful' or source_data.request_status_raw == 'result_received' %}
+ {{ source_data.request_status }}
+ {% elif source_data.request_status_raw == 'unsuccessful' or source_data.request_status_raw == 'no_correlation' or source_data.request_status_raw == 'no_signal' %}
+ {{ source_data.request_status }}
+ {% elif source_data.request_status_raw == 'planned' %}
+ {{ source_data.request_status }}
+ {% elif source_data.request_status_raw == 'downloading' or source_data.request_status_raw == 'processing' %}
+ {{ source_data.request_status }}
+ {% else %}
+ {{ source_data.request_status }}
+ {% endif %}
+ {% else %}
+ -
+ {% endif %}
+ |
+ {% endif %}
+
+ {% if forloop.first %}
+ {{ source_data.objitems_data|length }} |
+ {% endif %}
+
+ {% if forloop.first %}
+
+ {% if source_data.avg_lat and source_data.avg_lon %}
+ {{ source_data.avg_lat|floatformat:6|unlocalize }}, {{ source_data.avg_lon|floatformat:6|unlocalize }}
+ {% else %}
+ -
+ {% endif %}
+ |
+ {% endif %}
+
+ {{ objitem_data.objitem.name|default:"-" }} |
+
+
+ {% if objitem_data.objitem.parameter_obj and objitem_data.objitem.parameter_obj.id_satellite %}
+ {{ objitem_data.objitem.parameter_obj.id_satellite.name }}
+ {% if objitem_data.objitem.parameter_obj.id_satellite.norad %}
+ ({{ objitem_data.objitem.parameter_obj.id_satellite.norad }})
+ {% endif %}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.objitem.parameter_obj %}
+ {{ objitem_data.objitem.parameter_obj.frequency|default:"-"|floatformat:3 }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.objitem.parameter_obj %}
+ {{ objitem_data.objitem.parameter_obj.freq_range|default:"-"|floatformat:3 }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.objitem.parameter_obj and objitem_data.objitem.parameter_obj.polarization %}
+ {{ objitem_data.objitem.parameter_obj.polarization.name }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.objitem.parameter_obj and objitem_data.objitem.parameter_obj.modulation %}
+ {{ objitem_data.objitem.parameter_obj.modulation.name }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.objitem.geo_obj and objitem_data.objitem.geo_obj.coords %}
+ {{ objitem_data.objitem.geo_obj.coords.y|floatformat:6|unlocalize }}, {{ objitem_data.objitem.geo_obj.coords.x|floatformat:6|unlocalize }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+ {% if objitem_data.geo_date %}
+ {{ objitem_data.geo_date|date:"d.m.Y" }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+
+
+ {% if forloop.first %}
+
+ {% endif %}
+
+ |
+
+ {% endfor %}
+ {% endfor %}
+
+
+
+
+
+
+
+{% elif request.GET %}
+
+ По заданным критериям ничего не найдено.
+
+{% endif %}
+
+
diff --git a/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html b/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html
new file mode 100644
index 0000000..5f607f1
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Показано заявок: {{ requests|length }}
+
+
+
+
+
+
+
+
+
+ | ID |
+ Источник |
+ Статус |
+ Приоритет |
+ Координаты |
+ Имя точки |
+ Модуляция |
+ Симв. скор. |
+ Дата планирования |
+ ГСО |
+ Кубсат |
+ Обновлено |
+ Действия |
+
+
+
+ {% for req in requests %}
+
+ | {{ req.id }} |
+
+
+ #{{ req.source_id }}
+
+ |
+
+
+ {{ req.get_status_display }}
+
+ |
+
+
+ {{ req.get_priority_display }}
+
+ |
+
+ {% if req.coords %}
+ {{ req.coords.y|floatformat:6 }}, {{ req.coords.x|floatformat:6 }}
+ {% else %}-{% endif %}
+ |
+ {{ req.objitem_name|default:"-" }} |
+ {{ req.modulation|default:"-" }} |
+ {{ req.symbol_rate|default:"-" }} |
+ {{ req.planned_at|date:"d.m.Y H:i"|default:"-" }} |
+
+ {% if req.gso_success is True %}
+ Да
+ {% elif req.gso_success is False %}
+ Нет
+ {% else %}-{% endif %}
+ |
+
+ {% if req.kubsat_success is True %}
+ Да
+ {% elif req.kubsat_success is False %}
+ Нет
+ {% else %}-{% endif %}
+ |
+ {{ req.status_updated_at|date:"d.m.Y H:i"|default:"-" }} |
+
+
+
+
+
+
+ |
+
+ {% empty %}
+
+ | Нет заявок |
+
+ {% endfor %}
+
+
+
+
+
+ {% if page_obj %}
+
+ {% endif %}
+
+
+
+
diff --git a/dbapp/mainapp/templates/mainapp/kubsat.html b/dbapp/mainapp/templates/mainapp/kubsat.html
index 39f422b..8058a01 100644
--- a/dbapp/mainapp/templates/mainapp/kubsat.html
+++ b/dbapp/mainapp/templates/mainapp/kubsat.html
@@ -212,6 +212,16 @@
+
+
+
+
+
+
@@ -256,6 +266,7 @@
{% for objitem_data in source_data.objitems_data %}
@@ -500,12 +511,16 @@ function updateCounter() {
const rows = document.querySelectorAll('#resultsTable tbody tr');
const counter = document.getElementById('statsCounter');
if (counter) {
- // Подсчитываем уникальные источники
+ // Подсчитываем уникальные источники и точки (только видимые)
const uniqueSources = new Set();
+ let visibleRowsCount = 0;
rows.forEach(row => {
- uniqueSources.add(row.dataset.sourceId);
+ if (row.style.display !== 'none') {
+ uniqueSources.add(row.dataset.sourceId);
+ visibleRowsCount++;
+ }
});
- counter.textContent = `Найдено объектов: ${uniqueSources.size}, точек: ${rows.length}`;
+ counter.textContent = `Найдено объектов: ${uniqueSources.size}, точек: ${visibleRowsCount}`;
}
}
@@ -561,6 +576,108 @@ function selectAllOptions(selectName, selectAll) {
}
}
+// Фильтрация таблицы по имени точки
+function filterTableByName() {
+ const searchValue = document.getElementById('searchObjitemName').value.toLowerCase().trim();
+ const rows = document.querySelectorAll('#resultsTable tbody tr');
+
+ if (!searchValue) {
+ // Показываем все строки
+ rows.forEach(row => {
+ row.style.display = '';
+ });
+ // Восстанавливаем rowspan
+ recalculateRowspans();
+ updateCounter();
+ return;
+ }
+
+ // Группируем строки по source_id
+ const sourceGroups = {};
+ rows.forEach(row => {
+ const sourceId = row.dataset.sourceId;
+ if (!sourceGroups[sourceId]) {
+ sourceGroups[sourceId] = [];
+ }
+ sourceGroups[sourceId].push(row);
+ });
+
+ // Фильтруем по имени точки используя data-атрибут
+ Object.keys(sourceGroups).forEach(sourceId => {
+ const sourceRows = sourceGroups[sourceId];
+ let hasVisibleRows = false;
+
+ sourceRows.forEach(row => {
+ // Используем data-атрибут для получения имени точки
+ const name = (row.dataset.objitemName || '').toLowerCase();
+
+ if (name.includes(searchValue)) {
+ row.style.display = '';
+ hasVisibleRows = true;
+ } else {
+ row.style.display = 'none';
+ }
+ });
+
+ // Если нет видимых строк в группе, скрываем все (включая ячейки с rowspan)
+ if (!hasVisibleRows) {
+ sourceRows.forEach(row => {
+ row.style.display = 'none';
+ });
+ }
+ });
+
+ // Пересчитываем rowspan для видимых строк
+ recalculateRowspans();
+ updateCounter();
+}
+
+// Пересчет rowspan для видимых строк
+function recalculateRowspans() {
+ const rows = document.querySelectorAll('#resultsTable tbody tr');
+
+ // Группируем видимые строки по source_id
+ const sourceGroups = {};
+ rows.forEach(row => {
+ if (row.style.display !== 'none') {
+ const sourceId = row.dataset.sourceId;
+ if (!sourceGroups[sourceId]) {
+ sourceGroups[sourceId] = [];
+ }
+ sourceGroups[sourceId].push(row);
+ }
+ });
+
+ // Обновляем rowspan для каждой группы
+ Object.keys(sourceGroups).forEach(sourceId => {
+ const visibleRows = sourceGroups[sourceId];
+ const newRowspan = visibleRows.length;
+
+ if (visibleRows.length > 0) {
+ const firstRow = visibleRows[0];
+ const sourceIdCell = firstRow.querySelector('.source-id-cell');
+ const sourceTypeCell = firstRow.querySelector('.source-type-cell');
+ const sourceOwnershipCell = firstRow.querySelector('.source-ownership-cell');
+ const sourceCountCell = firstRow.querySelector('.source-count-cell');
+
+ if (sourceIdCell) sourceIdCell.setAttribute('rowspan', newRowspan);
+ if (sourceTypeCell) sourceTypeCell.setAttribute('rowspan', newRowspan);
+ if (sourceOwnershipCell) sourceOwnershipCell.setAttribute('rowspan', newRowspan);
+ if (sourceCountCell) {
+ sourceCountCell.setAttribute('rowspan', newRowspan);
+ // Обновляем отображаемое количество точек
+ sourceCountCell.textContent = newRowspan;
+ }
+ }
+ });
+}
+
+// Очистка поиска
+function clearSearch() {
+ document.getElementById('searchObjitemName').value = '';
+ filterTableByName();
+}
+
// Обновляем счетчик при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
updateCounter();
diff --git a/dbapp/mainapp/templates/mainapp/kubsat_tabs.html b/dbapp/mainapp/templates/mainapp/kubsat_tabs.html
new file mode 100644
index 0000000..5b7f4b5
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/kubsat_tabs.html
@@ -0,0 +1,529 @@
+{% extends 'mainapp/base.html' %}
+{% load static %}
+
+{% block title %}Кубсат{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+ {% include 'mainapp/components/_source_requests_tab.html' %}
+
+
+
+
+ {% include 'mainapp/components/_kubsat_filters_tab.html' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html
index fdfb707..fb93db0 100644
--- a/dbapp/mainapp/templates/mainapp/source_list.html
+++ b/dbapp/mainapp/templates/mainapp/source_list.html
@@ -339,6 +339,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -581,6 +687,12 @@
+
+
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Статус |
+ Приоритет |
+ Дата планирования |
+ Дата заявки |
+ ГСО |
+ Кубсат |
+ Комментарий |
+ Обновлено |
+ Действия |
+
+
+
+
+
+
+
+
+ Нет заявок для этого источника
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index e0783f2..4df0fd8 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -19,6 +19,8 @@ from .views import (
HomeView,
KubsatView,
KubsatExportView,
+ KubsatCreateRequestsView,
+ KubsatRecalculateCoordsView,
LinkLyngsatSourcesView,
LinkVchSigmaView,
LoadCsvDataView,
@@ -61,6 +63,15 @@ from .views import (
custom_logout,
)
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
+from .views.source_requests import (
+ SourceRequestListView,
+ SourceRequestCreateView,
+ SourceRequestUpdateView,
+ SourceRequestDeleteView,
+ SourceRequestAPIView,
+ SourceRequestDetailAPIView,
+ SourceDataAPIView,
+)
from .views.tech_analyze import (
TechAnalyzeEntryView,
TechAnalyzeSaveView,
@@ -137,6 +148,16 @@ urlpatterns = [
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
path('kubsat/', KubsatView.as_view(), name='kubsat'),
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
+ path('kubsat/create-requests/', KubsatCreateRequestsView.as_view(), name='kubsat_create_requests'),
+ path('kubsat/recalculate-coords/', KubsatRecalculateCoordsView.as_view(), name='kubsat_recalculate_coords'),
+ # Source Requests
+ path('source-requests/', SourceRequestListView.as_view(), name='source_request_list'),
+ path('source-requests/create/', SourceRequestCreateView.as_view(), name='source_request_create'),
+ path('source-requests//edit/', SourceRequestUpdateView.as_view(), name='source_request_update'),
+ path('source-requests//delete/', SourceRequestDeleteView.as_view(), name='source_request_delete'),
+ path('api/source//requests/', SourceRequestAPIView.as_view(), name='source_requests_api'),
+ path('api/source-request//', SourceRequestDetailAPIView.as_view(), name='source_request_detail_api'),
+ path('api/source//data/', SourceDataAPIView.as_view(), name='source_data_api'),
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
path('tech-analyze/', TechAnalyzeEntryView.as_view(), name='tech_analyze_entry'),
diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py
index 4a512f0..5bdc5ae 100644
--- a/dbapp/mainapp/views/__init__.py
+++ b/dbapp/mainapp/views/__init__.py
@@ -61,6 +61,8 @@ from .map import (
from .kubsat import (
KubsatView,
KubsatExportView,
+ KubsatCreateRequestsView,
+ KubsatRecalculateCoordsView,
)
from .data_entry import (
DataEntryView,
@@ -75,6 +77,14 @@ from .statistics import (
StatisticsView,
StatisticsAPIView,
)
+from .source_requests import (
+ SourceRequestListView,
+ SourceRequestCreateView,
+ SourceRequestUpdateView,
+ SourceRequestDeleteView,
+ SourceRequestAPIView,
+ SourceRequestDetailAPIView,
+)
__all__ = [
# Base
@@ -141,6 +151,8 @@ __all__ = [
# Kubsat
'KubsatView',
'KubsatExportView',
+ 'KubsatCreateRequestsView',
+ 'KubsatRecalculateCoordsView',
# Data Entry
'DataEntryView',
'SearchObjItemAPIView',
@@ -151,4 +163,11 @@ __all__ = [
# Statistics
'StatisticsView',
'StatisticsAPIView',
+ # Source Requests
+ 'SourceRequestListView',
+ 'SourceRequestCreateView',
+ 'SourceRequestUpdateView',
+ 'SourceRequestDeleteView',
+ 'SourceRequestAPIView',
+ 'SourceRequestDetailAPIView',
]
diff --git a/dbapp/mainapp/views/kubsat.py b/dbapp/mainapp/views/kubsat.py
index 1475985..9a3d8b6 100644
--- a/dbapp/mainapp/views/kubsat.py
+++ b/dbapp/mainapp/views/kubsat.py
@@ -19,13 +19,65 @@ from mainapp.utils import calculate_mean_coords
class KubsatView(LoginRequiredMixin, FormView):
"""Страница Кубсат с фильтрами и таблицей источников"""
- template_name = 'mainapp/kubsat.html'
+ template_name = 'mainapp/kubsat_tabs.html'
form_class = KubsatFilterForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['full_width_page'] = True
+ # Добавляем данные для вкладки заявок
+ from mainapp.models import SourceRequest
+ requests_qs = SourceRequest.objects.select_related(
+ 'source', 'source__info', 'source__ownership',
+ 'created_by__user', 'updated_by__user'
+ ).prefetch_related(
+ 'source__source_objitems__parameter_obj__modulation'
+ ).order_by('-created_at')
+
+ # Фильтры для заявок
+ status = self.request.GET.get('status')
+ if status:
+ requests_qs = requests_qs.filter(status=status)
+
+ priority = self.request.GET.get('priority')
+ if priority:
+ requests_qs = requests_qs.filter(priority=priority)
+
+ # Добавляем данные источника к каждой заявке
+ requests_list = []
+ for req in requests_qs[:100]:
+ # Получаем данные из первой точки источника
+ objitem_name = '-'
+ modulation = '-'
+ symbol_rate = '-'
+
+ if req.source:
+ first_objitem = req.source.source_objitems.select_related(
+ 'parameter_obj__modulation'
+ ).order_by('geo_obj__timestamp').first()
+
+ if first_objitem:
+ objitem_name = first_objitem.name or '-'
+ if first_objitem.parameter_obj:
+ if first_objitem.parameter_obj.modulation:
+ modulation = first_objitem.parameter_obj.modulation.name
+ if first_objitem.parameter_obj.bod_velocity and first_objitem.parameter_obj.bod_velocity > 0:
+ symbol_rate = str(int(first_objitem.parameter_obj.bod_velocity))
+
+ # Добавляем атрибуты к объекту заявки
+ req.objitem_name = objitem_name
+ req.modulation = modulation
+ req.symbol_rate = symbol_rate
+ requests_list.append(req)
+
+ context['requests'] = requests_list
+ context['status_choices'] = SourceRequest.STATUS_CHOICES
+ context['priority_choices'] = SourceRequest.PRIORITY_CHOICES
+ context['current_status'] = status or ''
+ context['current_priority'] = priority or ''
+ context['search_query'] = self.request.GET.get('search', '')
+
# Если форма была отправлена, применяем фильтры
if self.request.GET:
form = self.form_class(self.request.GET)
@@ -38,11 +90,23 @@ class KubsatView(LoginRequiredMixin, FormView):
objitem_count = form.cleaned_data.get('objitem_count')
sources_with_date_info = []
for source in sources:
+ # Get latest request info for this source
+ latest_request = source.source_requests.order_by('-created_at').first()
+ requests_count = source.source_requests.count()
+
source_data = {
'source': source,
'objitems_data': [],
'has_lyngsat': False,
- 'lyngsat_id': None
+ 'lyngsat_id': None,
+ 'has_request': latest_request is not None,
+ 'request_status': latest_request.get_status_display() if latest_request else None,
+ 'request_status_raw': latest_request.status if latest_request else None,
+ 'gso_success': latest_request.gso_success if latest_request else None,
+ 'kubsat_success': latest_request.kubsat_success if latest_request else None,
+ 'planned_at': latest_request.planned_at if latest_request else None,
+ 'requests_count': requests_count,
+ 'average_coords': None, # Будет рассчитано после сбора точек
}
for objitem in source.source_objitems.all():
@@ -89,6 +153,27 @@ class KubsatView(LoginRequiredMixin, FormView):
elif objitem_count == '2+':
include_source = (filtered_count >= 2)
+ # Сортируем точки по дате ГЛ перед расчётом усреднённых координат
+ source_data['objitems_data'].sort(
+ key=lambda x: x['geo_date'] if x['geo_date'] else datetime.min.date()
+ )
+
+ # Рассчитываем усреднённые координаты из отфильтрованных точек
+ if source_data['objitems_data']:
+ avg_coords = None
+ for objitem_info in source_data['objitems_data']:
+ objitem = objitem_info['objitem']
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
+ coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y))
+ if avg_coords is None:
+ avg_coords = coord
+ else:
+ avg_coords, _ = calculate_mean_coords(avg_coords, coord)
+ if avg_coords:
+ source_data['average_coords'] = avg_coords
+ source_data['avg_lat'] = avg_coords[1]
+ source_data['avg_lon'] = avg_coords[0]
+
if source_data['objitems_data'] and include_source:
sources_with_date_info.append(source_data)
@@ -99,12 +184,17 @@ class KubsatView(LoginRequiredMixin, FormView):
def apply_filters(self, filters):
"""Применяет фильтры к queryset Source"""
+ from mainapp.models import SourceRequest
+ from django.db.models import Subquery, OuterRef, Exists
+
queryset = Source.objects.select_related('info', 'ownership').prefetch_related(
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__transponder__sat_id',
- 'source_objitems__lyngsat_source'
+ 'source_objitems__lyngsat_source',
+ 'source_objitems__geo_obj',
+ 'source_requests'
).annotate(objitem_count=Count('source_objitems'))
# Фильтр по спутникам
@@ -166,8 +256,38 @@ class KubsatView(LoginRequiredMixin, FormView):
elif objitem_count == '2+':
queryset = queryset.filter(objitem_count__gte=2)
- # Фиктивные фильтры (пока не применяются)
- # has_plans, success_1, success_2, date_from, date_to
+ # Фильтр по наличию планов (заявок со статусом 'planned')
+ has_plans = filters.get('has_plans')
+ if has_plans == 'yes':
+ queryset = queryset.filter(
+ source_requests__status='planned'
+ ).distinct()
+ elif has_plans == 'no':
+ queryset = queryset.exclude(
+ source_requests__status='planned'
+ ).distinct()
+
+ # Фильтр по ГСО успешно
+ success_1 = filters.get('success_1')
+ if success_1 == 'yes':
+ queryset = queryset.filter(
+ source_requests__gso_success=True
+ ).distinct()
+ elif success_1 == 'no':
+ queryset = queryset.filter(
+ source_requests__gso_success=False
+ ).distinct()
+
+ # Фильтр по Кубсат успешно
+ success_2 = filters.get('success_2')
+ if success_2 == 'yes':
+ queryset = queryset.filter(
+ source_requests__kubsat_success=True
+ ).distinct()
+ elif success_2 == 'no':
+ queryset = queryset.filter(
+ source_requests__kubsat_success=False
+ ).distinct()
return queryset.distinct()
@@ -268,6 +388,11 @@ class KubsatExportView(LoginRequiredMixin, FormView):
source = data['source']
objitems_list = data['objitems']
+ # Сортируем точки по дате ГЛ перед расчётом
+ objitems_list.sort(
+ key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min
+ )
+
# Рассчитываем инкрементальное среднее координат из оставшихся точек
average_coords = None
for objitem in objitems_list:
@@ -411,3 +536,162 @@ class KubsatExportView(LoginRequiredMixin, FormView):
response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d")}.xlsx"'
return response
+
+
+class KubsatCreateRequestsView(LoginRequiredMixin, FormView):
+ """Массовое создание заявок из отфильтрованных данных"""
+ form_class = KubsatFilterForm
+
+ def post(self, request, *args, **kwargs):
+ import json
+ from django.http import JsonResponse
+ from mainapp.models import SourceRequest, CustomUser
+
+ # Получаем список ID точек (ObjItem) из POST
+ objitem_ids = request.POST.getlist('objitem_ids')
+
+ if not objitem_ids:
+ return JsonResponse({'success': False, 'error': 'Нет данных для создания заявок'}, status=400)
+
+ # Получаем ObjItem с их источниками
+ objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
+ 'source',
+ 'geo_obj'
+ )
+
+ # Группируем ObjItem по Source
+ sources_objitems = {}
+ for objitem in objitems:
+ if objitem.source:
+ if objitem.source.id not in sources_objitems:
+ sources_objitems[objitem.source.id] = {
+ 'source': objitem.source,
+ 'objitems': []
+ }
+ sources_objitems[objitem.source.id]['objitems'].append(objitem)
+
+ # Получаем CustomUser для текущего пользователя
+ try:
+ custom_user = CustomUser.objects.get(user=request.user)
+ except CustomUser.DoesNotExist:
+ custom_user = None
+
+ created_count = 0
+ errors = []
+
+ for source_id, data in sources_objitems.items():
+ source = data['source']
+ objitems_list = data['objitems']
+
+ # Сортируем точки по дате ГЛ перед расчётом
+ objitems_list.sort(
+ key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min
+ )
+
+ # Рассчитываем усреднённые координаты из выбранных точек
+ average_coords = None
+ points_with_coords = 0
+
+ for objitem in objitems_list:
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
+ coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
+ points_with_coords += 1
+
+ if average_coords is None:
+ average_coords = coord
+ else:
+ average_coords, _ = calculate_mean_coords(average_coords, coord)
+
+ # Создаём Point объект если есть координаты
+ coords_point = None
+ if average_coords:
+ coords_point = Point(average_coords[0], average_coords[1], srid=4326)
+
+ try:
+ # Создаём новую заявку со статусом "planned"
+ source_request = SourceRequest.objects.create(
+ source=source,
+ status='planned',
+ priority='medium',
+ coords=coords_point,
+ points_count=points_with_coords,
+ created_by=custom_user,
+ updated_by=custom_user,
+ comment=f'Создано из Кубсат. Точек: {len(objitems_list)}'
+ )
+ created_count += 1
+ except Exception as e:
+ errors.append(f'Источник #{source_id}: {str(e)}')
+
+ return JsonResponse({
+ 'success': True,
+ 'created_count': created_count,
+ 'total_sources': len(sources_objitems),
+ 'errors': errors
+ })
+
+
+class KubsatRecalculateCoordsView(LoginRequiredMixin, FormView):
+ """API для пересчёта усреднённых координат по списку ObjItem ID"""
+ form_class = KubsatFilterForm
+
+ def post(self, request, *args, **kwargs):
+ import json
+ from django.http import JsonResponse
+
+ # Получаем список ID точек (ObjItem) из POST
+ objitem_ids = request.POST.getlist('objitem_ids')
+
+ if not objitem_ids:
+ return JsonResponse({'success': False, 'error': 'Нет данных для расчёта'}, status=400)
+
+ # Получаем ObjItem с их источниками, сортируем по дате ГЛ
+ objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
+ 'source',
+ 'geo_obj'
+ ).order_by('geo_obj__timestamp') # Сортировка по дате ГЛ
+
+ # Группируем ObjItem по Source
+ sources_objitems = {}
+ for objitem in objitems:
+ if objitem.source:
+ if objitem.source.id not in sources_objitems:
+ sources_objitems[objitem.source.id] = []
+ sources_objitems[objitem.source.id].append(objitem)
+
+ # Рассчитываем усреднённые координаты для каждого источника
+ results = {}
+ for source_id, objitems_list in sources_objitems.items():
+ # Сортируем по дате ГЛ (на случай если порядок сбился)
+ objitems_list.sort(key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min)
+
+ average_coords = None
+ points_count = 0
+
+ for objitem in objitems_list:
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
+ coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y))
+ points_count += 1
+
+ if average_coords is None:
+ average_coords = coord
+ else:
+ average_coords, _ = calculate_mean_coords(average_coords, coord)
+
+ if average_coords:
+ results[str(source_id)] = {
+ 'avg_lon': average_coords[0],
+ 'avg_lat': average_coords[1],
+ 'points_count': points_count
+ }
+ else:
+ results[str(source_id)] = {
+ 'avg_lon': None,
+ 'avg_lat': None,
+ 'points_count': 0
+ }
+
+ return JsonResponse({
+ 'success': True,
+ 'results': results
+ })
diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py
index 29c05fa..49cd7cc 100644
--- a/dbapp/mainapp/views/source.py
+++ b/dbapp/mainapp/views/source.py
@@ -48,6 +48,17 @@ class SourceListView(LoginRequiredMixin, View):
mark_date_from = request.GET.get("mark_date_from", "").strip()
mark_date_to = request.GET.get("mark_date_to", "").strip()
+ # Source request filters
+ has_requests = request.GET.get("has_requests")
+ selected_request_statuses = request.GET.getlist("request_status")
+ selected_request_priorities = request.GET.getlist("request_priority")
+ request_gso_success = request.GET.get("request_gso_success")
+ request_kubsat_success = request.GET.get("request_kubsat_success")
+ request_planned_from = request.GET.get("request_planned_from", "").strip()
+ request_planned_to = request.GET.get("request_planned_to", "").strip()
+ request_date_from = request.GET.get("request_date_from", "").strip()
+ request_date_to = request.GET.get("request_date_to", "").strip()
+
# Get filter parameters - ObjItem level (параметры точек)
geo_date_from = request.GET.get("geo_date_from", "").strip()
geo_date_to = request.GET.get("geo_date_to", "").strip()
@@ -423,6 +434,73 @@ class SourceListView(LoginRequiredMixin, View):
if mark_filter_q:
sources = sources.filter(mark_filter_q).distinct()
+ # Filter by source requests
+ if has_requests == "1":
+ # Has requests - apply subfilters
+ from ..models import SourceRequest
+ from django.db.models import Exists, OuterRef
+
+ # Build subquery for filtering requests
+ request_subquery = SourceRequest.objects.filter(source=OuterRef('pk'))
+
+ # Filter by request status
+ if selected_request_statuses:
+ request_subquery = request_subquery.filter(status__in=selected_request_statuses)
+
+ # Filter by request priority
+ if selected_request_priorities:
+ request_subquery = request_subquery.filter(priority__in=selected_request_priorities)
+
+ # Filter by GSO success
+ if request_gso_success == "true":
+ request_subquery = request_subquery.filter(gso_success=True)
+ elif request_gso_success == "false":
+ request_subquery = request_subquery.filter(gso_success=False)
+
+ # Filter by Kubsat success
+ if request_kubsat_success == "true":
+ request_subquery = request_subquery.filter(kubsat_success=True)
+ elif request_kubsat_success == "false":
+ request_subquery = request_subquery.filter(kubsat_success=False)
+
+ # Filter by planned date range
+ if request_planned_from:
+ try:
+ planned_from_obj = datetime.strptime(request_planned_from, "%Y-%m-%d")
+ request_subquery = request_subquery.filter(planned_at__gte=planned_from_obj)
+ except (ValueError, TypeError):
+ pass
+
+ if request_planned_to:
+ try:
+ from datetime import timedelta
+ planned_to_obj = datetime.strptime(request_planned_to, "%Y-%m-%d")
+ planned_to_obj = planned_to_obj + timedelta(days=1)
+ request_subquery = request_subquery.filter(planned_at__lt=planned_to_obj)
+ except (ValueError, TypeError):
+ pass
+
+ # Filter by request date range
+ if request_date_from:
+ try:
+ req_date_from_obj = datetime.strptime(request_date_from, "%Y-%m-%d")
+ request_subquery = request_subquery.filter(request_date__gte=req_date_from_obj)
+ except (ValueError, TypeError):
+ pass
+
+ if request_date_to:
+ try:
+ req_date_to_obj = datetime.strptime(request_date_to, "%Y-%m-%d")
+ request_subquery = request_subquery.filter(request_date__lte=req_date_to_obj)
+ except (ValueError, TypeError):
+ pass
+
+ # Apply the subquery filter using Exists
+ sources = sources.filter(Exists(request_subquery))
+ elif has_requests == "0":
+ # No requests
+ sources = sources.filter(source_requests__isnull=True)
+
# Filter by ObjItem count
if objitem_count_min:
try:
@@ -700,6 +778,16 @@ class SourceListView(LoginRequiredMixin, View):
'has_signal_mark': has_signal_mark,
'mark_date_from': mark_date_from,
'mark_date_to': mark_date_to,
+ # Source request filters
+ 'has_requests': has_requests,
+ 'selected_request_statuses': selected_request_statuses,
+ 'selected_request_priorities': selected_request_priorities,
+ 'request_gso_success': request_gso_success,
+ 'request_kubsat_success': request_kubsat_success,
+ 'request_planned_from': request_planned_from,
+ 'request_planned_to': request_planned_to,
+ 'request_date_from': request_date_from,
+ 'request_date_to': request_date_to,
# ObjItem-level filters
'geo_date_from': geo_date_from,
'geo_date_to': geo_date_to,
diff --git a/dbapp/mainapp/views/source_requests.py b/dbapp/mainapp/views/source_requests.py
new file mode 100644
index 0000000..b891dd0
--- /dev/null
+++ b/dbapp/mainapp/views/source_requests.py
@@ -0,0 +1,378 @@
+"""
+Представления для управления заявками на источники.
+"""
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import JsonResponse
+from django.views import View
+from django.views.generic import ListView, CreateView, UpdateView
+from django.urls import reverse_lazy
+from django.db.models import Q
+
+from mainapp.models import SourceRequest, SourceRequestStatusHistory, Source
+from mainapp.forms import SourceRequestForm
+
+
+class SourceRequestListView(LoginRequiredMixin, ListView):
+ """Список заявок на источники."""
+ model = SourceRequest
+ template_name = 'mainapp/source_request_list.html'
+ context_object_name = 'requests'
+ paginate_by = 50
+
+ def get_queryset(self):
+ queryset = SourceRequest.objects.select_related(
+ 'source', 'source__info', 'source__ownership',
+ 'created_by__user', 'updated_by__user'
+ ).order_by('-created_at')
+
+ # Фильтр по статусу
+ status = self.request.GET.get('status')
+ if status:
+ queryset = queryset.filter(status=status)
+
+ # Фильтр по приоритету
+ priority = self.request.GET.get('priority')
+ if priority:
+ queryset = queryset.filter(priority=priority)
+
+ # Фильтр по источнику
+ source_id = self.request.GET.get('source_id')
+ if source_id:
+ queryset = queryset.filter(source_id=source_id)
+
+ # Фильтр по ГСО успешно
+ gso_success = self.request.GET.get('gso_success')
+ if gso_success == 'true':
+ queryset = queryset.filter(gso_success=True)
+ elif gso_success == 'false':
+ queryset = queryset.filter(gso_success=False)
+
+ # Фильтр по Кубсат успешно
+ kubsat_success = self.request.GET.get('kubsat_success')
+ if kubsat_success == 'true':
+ queryset = queryset.filter(kubsat_success=True)
+ elif kubsat_success == 'false':
+ queryset = queryset.filter(kubsat_success=False)
+
+ # Поиск
+ search = self.request.GET.get('search')
+ if search:
+ queryset = queryset.filter(
+ Q(source__id__icontains=search) |
+ Q(comment__icontains=search)
+ )
+
+ return queryset
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['status_choices'] = SourceRequest.STATUS_CHOICES
+ context['priority_choices'] = SourceRequest.PRIORITY_CHOICES
+ context['current_status'] = self.request.GET.get('status', '')
+ context['current_priority'] = self.request.GET.get('priority', '')
+ context['search_query'] = self.request.GET.get('search', '')
+ context['form'] = SourceRequestForm()
+ return context
+
+
+class SourceRequestCreateView(LoginRequiredMixin, CreateView):
+ """Создание заявки на источник."""
+ model = SourceRequest
+ form_class = SourceRequestForm
+ template_name = 'mainapp/source_request_form.html'
+ success_url = reverse_lazy('mainapp:source_request_list')
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ # Передаем source_id если он есть в GET параметрах
+ source_id = self.request.GET.get('source_id')
+ if source_id:
+ kwargs['source_id'] = source_id
+ return kwargs
+
+ def form_valid(self, form):
+ # Устанавливаем created_by
+ form.instance.created_by = getattr(self.request.user, 'customuser', None)
+ form.instance.updated_by = getattr(self.request.user, 'customuser', None)
+
+ response = super().form_valid(form)
+
+ # Создаем начальную запись в истории
+ SourceRequestStatusHistory.objects.create(
+ source_request=self.object,
+ old_status='',
+ new_status=self.object.status,
+ changed_by=form.instance.created_by,
+ )
+
+ # Если это AJAX запрос, возвращаем JSON
+ if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Заявка успешно создана',
+ 'request_id': self.object.id
+ })
+
+ return response
+
+ def form_invalid(self, form):
+ if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return JsonResponse({
+ 'success': False,
+ 'errors': form.errors
+ }, status=400)
+ return super().form_invalid(form)
+
+
+class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
+ """Редактирование заявки на источник."""
+ model = SourceRequest
+ form_class = SourceRequestForm
+ template_name = 'mainapp/source_request_form.html'
+ success_url = reverse_lazy('mainapp:source_request_list')
+
+ def form_valid(self, form):
+ # Устанавливаем updated_by
+ form.instance.updated_by = getattr(self.request.user, 'customuser', None)
+
+ response = super().form_valid(form)
+
+ # Если это AJAX запрос, возвращаем JSON
+ if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Заявка успешно обновлена',
+ 'request_id': self.object.id
+ })
+
+ return response
+
+ def form_invalid(self, form):
+ if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return JsonResponse({
+ 'success': False,
+ 'errors': form.errors
+ }, status=400)
+ return super().form_invalid(form)
+
+
+class SourceRequestDeleteView(LoginRequiredMixin, View):
+ """Удаление заявки на источник."""
+
+ def post(self, request, pk):
+ try:
+ source_request = SourceRequest.objects.get(pk=pk)
+ source_request.delete()
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Заявка успешно удалена'
+ })
+ except SourceRequest.DoesNotExist:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Заявка не найдена'
+ }, status=404)
+
+
+class SourceRequestAPIView(LoginRequiredMixin, View):
+ """API для получения данных о заявках источника."""
+
+ def get(self, request, source_id):
+ try:
+ source = Source.objects.get(pk=source_id)
+ except Source.DoesNotExist:
+ return JsonResponse({'error': 'Источник не найден'}, status=404)
+
+ requests = SourceRequest.objects.filter(source=source).select_related(
+ 'created_by__user', 'updated_by__user'
+ ).prefetch_related('status_history__changed_by__user').order_by('-created_at')
+
+ data = []
+ for req in requests:
+ # Получаем историю статусов
+ history = []
+ for h in req.status_history.all().order_by('-changed_at'):
+ history.append({
+ 'old_status': h.get_old_status_display() if h.old_status else '-',
+ 'new_status': h.get_new_status_display(),
+ 'changed_at': h.changed_at.strftime('%d.%m.%Y %H:%M') if h.changed_at else '-',
+ 'changed_by': str(h.changed_by) if h.changed_by else '-',
+ })
+
+ data.append({
+ 'id': req.id,
+ 'status': req.status,
+ 'status_display': req.get_status_display(),
+ 'priority': req.priority,
+ 'priority_display': req.get_priority_display(),
+ 'planned_at': req.planned_at.strftime('%d.%m.%Y %H:%M') if req.planned_at else '-',
+ 'request_date': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-',
+ 'status_updated_at': req.status_updated_at.strftime('%d.%m.%Y %H:%M') if req.status_updated_at else '-',
+ 'gso_success': req.gso_success,
+ 'kubsat_success': req.kubsat_success,
+ 'comment': req.comment or '-',
+ 'created_at': req.created_at.strftime('%d.%m.%Y %H:%M') if req.created_at else '-',
+ 'created_by': str(req.created_by) if req.created_by else '-',
+ 'history': history,
+ })
+
+ return JsonResponse({
+ 'source_id': source_id,
+ 'requests': data,
+ 'count': len(data)
+ })
+
+
+class SourceRequestDetailAPIView(LoginRequiredMixin, View):
+ """API для получения детальной информации о заявке."""
+
+ def get(self, request, pk):
+ try:
+ req = SourceRequest.objects.select_related(
+ 'source', 'source__info', 'source__ownership',
+ 'created_by__user', 'updated_by__user'
+ ).prefetch_related(
+ 'status_history__changed_by__user',
+ 'source__source_objitems__parameter_obj__modulation',
+ 'source__source_objitems__geo_obj'
+ ).get(pk=pk)
+ except SourceRequest.DoesNotExist:
+ return JsonResponse({'error': 'Заявка не найдена'}, status=404)
+
+ # Получаем историю статусов
+ history = []
+ for h in req.status_history.all().order_by('-changed_at'):
+ history.append({
+ 'old_status': h.get_old_status_display() if h.old_status else '-',
+ 'new_status': h.get_new_status_display(),
+ 'changed_at': h.changed_at.strftime('%d.%m.%Y %H:%M') if h.changed_at else '-',
+ 'changed_by': str(h.changed_by) if h.changed_by else '-',
+ })
+
+ # Получаем данные из первой точки источника (имя, модуляция, символьная скорость)
+ source_data = _get_source_extra_data(req.source)
+
+ # Координаты из заявки или из источника
+ coords_lat = None
+ coords_lon = None
+ if req.coords:
+ coords_lat = req.coords.y
+ coords_lon = req.coords.x
+ elif req.source.coords_average:
+ coords_lat = req.source.coords_average.y
+ coords_lon = req.source.coords_average.x
+
+ data = {
+ 'id': req.id,
+ 'source_id': req.source_id,
+ 'status': req.status,
+ 'status_display': req.get_status_display(),
+ 'priority': req.priority,
+ 'priority_display': req.get_priority_display(),
+ 'planned_at': req.planned_at.isoformat() if req.planned_at else None,
+ 'planned_at_display': req.planned_at.strftime('%d.%m.%Y %H:%M') if req.planned_at else '-',
+ 'request_date': req.request_date.isoformat() if req.request_date else None,
+ 'request_date_display': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-',
+ 'status_updated_at': req.status_updated_at.strftime('%d.%m.%Y %H:%M') if req.status_updated_at else '-',
+ 'gso_success': req.gso_success,
+ 'kubsat_success': req.kubsat_success,
+ 'comment': req.comment or '',
+ 'created_at': req.created_at.strftime('%d.%m.%Y %H:%M') if req.created_at else '-',
+ 'created_by': str(req.created_by) if req.created_by else '-',
+ 'history': history,
+ # Дополнительные данные
+ 'coords_lat': coords_lat,
+ 'coords_lon': coords_lon,
+ 'points_count': req.points_count,
+ 'objitem_name': source_data['objitem_name'],
+ 'modulation': source_data['modulation'],
+ 'symbol_rate': source_data['symbol_rate'],
+ }
+
+ return JsonResponse(data)
+
+
+def _get_source_extra_data(source):
+ """Получает дополнительные данные из первой точки источника."""
+ objitem_name = '-'
+ modulation = '-'
+ symbol_rate = '-'
+
+ if source:
+ # Получаем первую точку источника (сортируем по дате ГЛ)
+ objitems = source.source_objitems.select_related(
+ 'parameter_obj__modulation', 'geo_obj'
+ ).order_by('geo_obj__timestamp')
+
+ first_objitem = objitems.first()
+ if first_objitem:
+ objitem_name = first_objitem.name or '-'
+ if first_objitem.parameter_obj:
+ if first_objitem.parameter_obj.modulation:
+ modulation = first_objitem.parameter_obj.modulation.name
+ if first_objitem.parameter_obj.bod_velocity and first_objitem.parameter_obj.bod_velocity > 0:
+ symbol_rate = str(int(first_objitem.parameter_obj.bod_velocity))
+
+ return {
+ 'objitem_name': objitem_name,
+ 'modulation': modulation,
+ 'symbol_rate': symbol_rate,
+ }
+
+
+class SourceDataAPIView(LoginRequiredMixin, View):
+ """API для получения данных источника (координаты, имя точки, модуляция, символьная скорость)."""
+
+ def get(self, request, source_id):
+ from mainapp.utils import calculate_mean_coords
+ from datetime import datetime
+
+ try:
+ source = Source.objects.select_related('info', 'ownership').prefetch_related(
+ 'source_objitems__parameter_obj__modulation',
+ 'source_objitems__geo_obj'
+ ).get(pk=source_id)
+ except Source.DoesNotExist:
+ return JsonResponse({'error': 'Источник не найден', 'found': False}, status=404)
+
+ # Получаем данные из точек источника
+ source_data = _get_source_extra_data(source)
+
+ # Рассчитываем усреднённые координаты из всех точек (сортируем по дате ГЛ)
+ objitems = source.source_objitems.select_related('geo_obj').order_by('geo_obj__timestamp')
+
+ avg_coords = None
+ points_count = 0
+ for objitem in objitems:
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
+ coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y))
+ points_count += 1
+ if avg_coords is None:
+ avg_coords = coord
+ else:
+ avg_coords, _ = calculate_mean_coords(avg_coords, coord)
+
+ # Если нет координат из точек, берём из источника
+ coords_lat = None
+ coords_lon = None
+ if avg_coords:
+ coords_lon = avg_coords[0]
+ coords_lat = avg_coords[1]
+ elif source.coords_average:
+ coords_lat = source.coords_average.y
+ coords_lon = source.coords_average.x
+
+ data = {
+ 'found': True,
+ 'source_id': source_id,
+ 'coords_lat': coords_lat,
+ 'coords_lon': coords_lon,
+ 'points_count': points_count,
+ 'objitem_name': source_data['objitem_name'],
+ 'modulation': source_data['modulation'],
+ 'symbol_rate': source_data['symbol_rate'],
+ 'info': source.info.name if source.info else '-',
+ 'ownership': source.ownership.name if source.ownership else '-',
+ }
+
+ return JsonResponse(data)