Добавил информацию о типе объекта. Просто фиксы

This commit is contained in:
2025-11-17 15:54:27 +03:00
parent f438e74946
commit b889fb29a3
20 changed files with 1086 additions and 134 deletions

View File

@@ -55,6 +55,19 @@
</select> </select>
</div> </div>
<!-- Action buttons -->
<div class="d-flex gap-2">
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary btn-sm" title="Заполнить данные Lyngsat">
<i class="bi bi-cloud-download"></i> Добавить данные
</a>
<a href="{% url 'mainapp:link_lyngsat' %}" class="btn btn-primary btn-sm" title="Привязать источники LyngSat">
<i class="bi bi-link-45deg"></i> Привязать
</a>
<a href="{% url 'mainapp:unlink_all_lyngsat' %}" class="btn btn-warning btn-sm" title="Отвязать все источники LyngSat">
<i class="bi bi-x-circle"></i> Отвязать
</a>
</div>
<!-- Filter Toggle Button --> <!-- Filter Toggle Button -->
<div> <div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
@@ -252,10 +265,19 @@
{% for item in lyngsat_items %} {% for item in lyngsat_items %}
<tr> <tr>
<td class="text-center">{{ item.id }}</td> <td class="text-center">{{ item.id }}</td>
<td>{{ item.id_satellite.name|default:"-" }}</td> <td>
{% if item.id_satellite %}
<a href="#" class="text-decoration-underline"
onclick="showSatelliteModal({{ item.id_satellite.id }}); return false;">
{{ item.id_satellite.name }}
</a>
{% else %}
-
{% endif %}
</td>
<td>{{ item.frequency|floatformat:3|default:"-" }}</td> <td>{{ item.frequency|floatformat:3|default:"-" }}</td>
<td>{{ item.polarization.name|default:"-" }}</td> <td>{{ item.polarization.name|default:"-" }}</td>
<td>{{ item.sym_velocity|floatformat:3|default:"-" }}</td> <td>{{ item.sym_velocity|floatformat:0|default:"-" }}</td>
<td>{{ item.modulation.name|default:"-" }}</td> <td>{{ item.modulation.name|default:"-" }}</td>
<td>{{ item.standard.name|default:"-" }}</td> <td>{{ item.standard.name|default:"-" }}</td>
<td>{{ item.fec|default:"-" }}</td> <td>{{ item.fec|default:"-" }}</td>
@@ -425,4 +447,8 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -124,8 +124,10 @@ class LyngSatListView(LoginRequiredMixin, ListView):
context['search_query'] = self.request.GET.get('search', '') context['search_query'] = self.request.GET.get('search', '')
context['sort'] = self.request.GET.get('sort', '-id') context['sort'] = self.request.GET.get('sort', '-id')
# Данные для фильтров # Данные для фильтров - только спутники с существующими записями LyngSat
context['satellites'] = Satellite.objects.all().order_by('name') context['satellites'] = Satellite.objects.filter(
lyngsat__isnull=False
).distinct().order_by('name')
context['polarizations'] = Polarization.objects.all().order_by('name') context['polarizations'] = Polarization.objects.all().order_by('name')
context['modulations'] = Modulation.objects.all().order_by('name') context['modulations'] = Modulation.objects.all().order_by('name')
context['standards'] = Standard.objects.all().order_by('name') context['standards'] = Standard.objects.all().order_by('name')

View File

@@ -25,6 +25,7 @@ from .models import (
Standard, Standard,
SigmaParMark, SigmaParMark,
ObjectMark, ObjectMark,
ObjectInfo,
SigmaParameter, SigmaParameter,
Parameter, Parameter,
Satellite, Satellite,
@@ -394,6 +395,15 @@ class StandardAdmin(BaseAdmin):
ordering = ("name",) ordering = ("name",)
@admin.register(ObjectInfo)
class ObjectInfoAdmin(BaseAdmin):
"""Админ-панель для модели ObjectInfo (Тип объекта)."""
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
class SigmaParameterInline(admin.StackedInline): class SigmaParameterInline(admin.StackedInline):
model = SigmaParameter model = SigmaParameter
extra = 0 extra = 0
@@ -1036,20 +1046,26 @@ class ObjItemInline(admin.TabularInline):
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin): class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
"""Админ-панель для модели Source.""" """Админ-панель для модели Source."""
list_display = ("id", "created_at", "updated_at") list_display = ("id", "info", "created_at", "updated_at")
list_select_related = ("info",)
list_filter = ( list_filter = (
("info", MultiSelectRelatedDropdownFilter),
("created_at", DateRangeQuickSelectListFilterBuilder()), ("created_at", DateRangeQuickSelectListFilterBuilder()),
("updated_at", DateRangeQuickSelectListFilterBuilder()), ("updated_at", DateRangeQuickSelectListFilterBuilder()),
) )
search_fields = ("id",) search_fields = ("id", "info__name")
ordering = ("-created_at",) ordering = ("-created_at",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
inlines = [ObjItemInline] inlines = [ObjItemInline]
fieldsets = ( fieldsets = (
( (
"Координаты: геолокация", "Основная информация",
{"fields": ("coords_kupsat", "coords_valid", "coords_reference")}, {"fields": ("info",)},
),
(
"Координаты",
{"fields": ("coords_average", "coords_kupsat", "coords_valid", "coords_reference")},
), ),
( (
"Метаданные", "Метаданные",
@@ -1059,3 +1075,5 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
}, },
), ),
) )
autocomplete_fields = ("info",)

View File

@@ -463,7 +463,16 @@ class SourceForm(forms.ModelForm):
class Meta: class Meta:
model = Source model = Source
fields = [] # Все поля обрабатываются вручную fields = ['info'] # Добавляем поле info
widgets = {
'info': forms.Select(attrs={
'class': 'form-select',
'id': 'id_info',
}),
}
labels = {
'info': 'Тип объекта',
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.7 on 2025-11-17 12:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0007_make_source_required'),
]
operations = [
migrations.CreateModel(
name='ObjectInfo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Информация о типе объекта', max_length=255, unique=True, verbose_name='Тип объекта')),
],
options={
'verbose_name': 'Тип объекта',
'verbose_name_plural': 'Типы объектов',
'ordering': ['name'],
},
),
migrations.AddField(
model_name='source',
name='info',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_info', to='mainapp.objectinfo', verbose_name='Тип объекта'),
),
]

View File

@@ -67,6 +67,22 @@ class CustomUser(models.Model):
verbose_name_plural = "Пользователи" verbose_name_plural = "Пользователи"
ordering = ["user__username"] ordering = ["user__username"]
class ObjectInfo(models.Model):
name = models.CharField(
max_length=255,
unique=True,
verbose_name="Тип объекта",
help_text="Информация о типе объекта",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Тип объекта"
verbose_name_plural = "Типы объектов"
ordering = ["name"]
class ObjectMark(models.Model): class ObjectMark(models.Model):
""" """
@@ -435,6 +451,15 @@ class Source(models.Model):
Модель источника сигнала. Модель источника сигнала.
""" """
info = models.ForeignKey(
ObjectInfo,
on_delete=models.SET_NULL,
related_name="source_info",
null=True,
blank=True,
verbose_name="Тип объекта"
)
coords_average = gis.PointField( coords_average = gis.PointField(
srid=4326, srid=4326,
null=True, null=True,

View File

@@ -0,0 +1,84 @@
<!-- Satellite Data Modal -->
<div class="modal fade" id="satelliteModal" tabindex="-1" aria-labelledby="satelliteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="satelliteModalLabel">
<i class="bi bi-satellite"></i> Информация о спутнике
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="satelliteModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
// Function to show satellite modal
function showSatelliteModal(satelliteId) {
const modal = new bootstrap.Modal(document.getElementById('satelliteModal'));
modal.show();
const modalBody = document.getElementById('satelliteModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-warning" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/satellite/' + satelliteId + '/')
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных спутника');
}
return response.json();
})
.then(data => {
let html = '<div class="container-fluid"><div class="row g-3">' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></td></tr>' +
'<tr><td class="text-muted">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
'<tr><td class="text-muted">Подспутниковая точка:</td><td><strong>' + data.undersat_point + '</strong></td></tr>' +
'<tr><td class="text-muted">Диапазоны:</td><td>' + data.bands + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-calendar"></i> Дополнительная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Дата запуска:</td><td><strong>' + data.launch_date + '</strong></td></tr>' +
'<tr><td class="text-muted">Создан:</td><td>' + data.created_at + '</td></tr>' +
'<tr><td class="text-muted">Кем создан:</td><td>' + data.created_by + '</td></tr>' +
'<tr><td class="text-muted">Обновлён:</td><td>' + data.updated_at + '</td></tr>' +
'<tr><td class="text-muted">Кем обновлён:</td><td>' + data.updated_by + '</td></tr>' +
'</tbody></table></div></div></div>';
if (data.comment && data.comment !== '-') {
html += '<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-chat-left-text"></i> Комментарий</strong></div>' +
'<div class="card-body"><p class="mb-0">' + data.comment + '</p></div></div></div>';
}
if (data.url) {
html += '<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-link-45deg"></i> Ссылка</strong></div>' +
'<div class="card-body">' +
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-box-arrow-up-right"></i> Открыть ссылку</a>' +
'</div></div></div>';
}
html += '</div></div>';
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
});
}
</script>

View File

@@ -113,7 +113,13 @@
<!-- Фильтры --> <!-- Фильтры -->
<div class="filter-section"> <div class="filter-section">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-6"> <div class="col-md-4">
<label for="search" class="form-label">Поиск по имени объекта</label>
<input type="text" class="form-control" id="search" name="search"
placeholder="Введите имя объекта..."
value="{{ request.GET.search|default:'' }}">
</div>
<div class="col-md-4">
<label for="satellite" class="form-label">Спутник</label> <label for="satellite" class="form-label">Спутник</label>
<select class="form-select" id="satellite" name="satellite"> <select class="form-select" id="satellite" name="satellite">
<option value="">Все спутники</option> <option value="">Все спутники</option>
@@ -124,7 +130,7 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-6 d-flex align-items-end"> <div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">Применить</button> <button type="submit" class="btn btn-primary me-2">Применить</button>
<a href="{% url 'mainapp:object_marks' %}" class="btn btn-secondary">Сбросить</a> <a href="{% url 'mainapp:object_marks' %}" class="btn btn-secondary">Сбросить</a>
</div> </div>

View File

@@ -315,13 +315,22 @@
</td> </td>
<td> <td>
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td> <a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
<td>{{ item.satellite_name }}</td> <td>
{% if item.satellite_id %}
<a href="#" class="text-decoration-underline"
onclick="showSatelliteModal({{ item.satellite_id }}); return false;">
{{ item.satellite_name }}
</a>
{% else %}
{{ item.satellite_name }}
{% endif %}
</td>
<td> <td>
{% if item.obj.transponder %} {% if item.obj.transponder %}
<a href="#" class="text-success text-decoration-none" <a href="#" class="text-decoration-underline"
onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;" onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;"
title="Показать данные транспондера"> title="Показать данные транспондера">
<i class="bi bi-broadcast"></i> {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }} {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }}
</a> </a>
{% else %} {% else %}
- -
@@ -1337,4 +1346,7 @@
</div> </div>
</div> </div>
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -194,6 +194,19 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_info" class="form-label">{{ form.info.label }}:</label>
{{ form.info }}
{% if form.info.errors %}
<div class="invalid-feedback d-block">
{{ form.info.errors }}
</div>
{% endif %}
</div>
</div>
</div>
</div> </div>
<!-- Блок с картой --> <!-- Блок с картой -->

View File

@@ -42,7 +42,7 @@
<!-- Search bar --> <!-- Search bar -->
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;"> <div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
<div class="input-group"> <div class="input-group">
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по ID..." <input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по ID или имени..."
value="{{ search_query|default:'' }}"> value="{{ search_query|default:'' }}">
<button type="button" class="btn btn-outline-primary" <button type="button" class="btn btn-outline-primary"
onclick="performSearch()">Найти</button> onclick="performSearch()">Найти</button>
@@ -67,6 +67,12 @@
<!-- Action buttons --> <!-- Action buttons -->
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
<i class="bi bi-file-earmark-excel"></i> Excel
</a>
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success btn-sm" title="Загрузка данных из CSV">
<i class="bi bi-file-earmark-text"></i> CSV
</a>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="button" class="btn btn-danger btn-sm" title="Удалить" <button type="button" class="btn btn-danger btn-sm" title="Удалить"
onclick="deleteSelectedSources()"> onclick="deleteSelectedSources()">
@@ -194,7 +200,7 @@
<!-- LyngSat Filter --> <!-- LyngSat Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Тип объекта (ТВ):</label> <label class="form-label">ТВ или нет:</label>
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1" <input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
@@ -209,6 +215,24 @@
</div> </div>
</div> </div>
<!-- ObjectInfo Filter -->
<div class="mb-2">
<label class="form-label">Тип объекта:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('info_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('info_id', false)">Снять</button>
</div>
<select name="info_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for info in object_infos %}
<option value="{{ info.id }}" {% if info.id in selected_info %}selected{% endif %}>
{{ info.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Point Count Filter --> <!-- Point Count Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Количество точек:</label> <label class="form-label">Количество точек:</label>
@@ -227,6 +251,35 @@
placeholder="До" value="{{ date_to|default:'' }}"> placeholder="До" value="{{ date_to|default:'' }}">
</div> </div>
<!-- Signal Mark Filter -->
<div class="mb-2">
<label class="form-label">Наличие сигнала:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_signal_mark" id="has_signal_mark_1"
value="1" {% if has_signal_mark == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_signal_mark_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_signal_mark" id="has_signal_mark_0"
value="0" {% if has_signal_mark == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_signal_mark_0">Нет</label>
</div>
</div>
</div>
<!-- Mark Date Filter -->
<div class="mb-2">
<label class="form-label">Дата отметки сигнала:</label>
<input type="date" name="mark_date_from" id="mark_date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ mark_date_from|default:'' }}">
<input type="date" name="mark_date_to" id="mark_date_to" class="form-control form-control-sm"
placeholder="До" value="{{ mark_date_to|default:'' }}">
</div>
<hr class="my-3">
<h6 class="text-muted mb-2"><i class="bi bi-sliders"></i> Фильтры по параметрам точек</h6>
<!-- Geo Timestamp Filter --> <!-- Geo Timestamp Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Дата ГЛ:</label> <label class="form-label">Дата ГЛ:</label>
@@ -236,6 +289,96 @@
placeholder="До" value="{{ geo_date_to|default:'' }}"> placeholder="До" value="{{ geo_date_to|default:'' }}">
</div> </div>
<!-- Polarization Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization_id', false)">Снять</button>
</div>
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for polarization in polarizations %}
<option value="{{ polarization.id }}" {% if polarization.id in selected_polarizations %}selected{% endif %}>
{{ polarization.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Modulation Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Модуляция:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation_id', false)">Снять</button>
</div>
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for modulation in modulations %}
<option value="{{ modulation.id }}" {% if modulation.id in selected_modulations %}selected{% endif %}>
{{ modulation.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Frequency Range (Bandwidth) Filter -->
<div class="mb-2">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="freq_range_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ freq_range_min|default:'' }}">
<input type="number" step="0.001" name="freq_range_max" class="form-control form-control-sm"
placeholder="До" value="{{ freq_range_max|default:'' }}">
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Символьная скорость, БОД:</label>
<input type="number" step="0.001" name="bod_velocity_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ bod_velocity_min|default:'' }}">
<input type="number" step="0.001" name="bod_velocity_max" class="form-control form-control-sm"
placeholder="До" value="{{ bod_velocity_max|default:'' }}">
</div>
<!-- SNR Filter -->
<div class="mb-2">
<label class="form-label">ОСШ, дБ:</label>
<input type="number" step="0.1" name="snr_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ snr_min|default:'' }}">
<input type="number" step="0.1" name="snr_max" class="form-control form-control-sm"
placeholder="До" value="{{ snr_max|default:'' }}">
</div>
<!-- Mirrors Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Зеркала:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('mirror_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('mirror_id', false)">Снять</button>
</div>
<select name="mirror_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for mirror in mirrors %}
<option value="{{ mirror.id }}" {% if mirror.id in selected_mirrors %}selected{% endif %}>
{{ mirror.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Apply Filters and Reset Buttons --> <!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2"> <div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button> <button type="submit" class="btn btn-primary btn-sm">Применить</button>
@@ -267,15 +410,15 @@
{% endif %} {% endif %}
</a> </a>
</th> </th>
<th scope="col" style="min-width: 150px;">Имя</th>
<th scope="col" style="min-width: 120px;">Спутник</th> <th scope="col" style="min-width: 120px;">Спутник</th>
<th scope="col" style="min-width: 120px;">Тип объекта</th>
<th scope="col" style="min-width: 150px;">Усредненные координаты</th> <th scope="col" style="min-width: 150px;">Усредненные координаты</th>
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th> <th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
<th scope="col" style="min-width: 150px;">Координаты оперативников</th> <th scope="col" style="min-width: 150px;">Координаты оперативников</th>
<th scope="col" style="min-width: 150px;">Координаты справочные</th> <th scope="col" style="min-width: 150px;">Координаты справочные</th>
<th scope="col" style="min-width: 180px;">Наличие сигнала</th> <th scope="col" style="min-width: 180px;">Наличие сигнала</th>
{% if has_any_lyngsat %} <th scope="col" class="text-center" style="min-width: 80px;">ТВ или нет</th>
<th scope="col" class="text-center" style="min-width: 80px;">Тип объекта</th>
{% endif %}
<th scope="col" class="text-center" style="min-width: 100px;"> <th scope="col" class="text-center" style="min-width: 100px;">
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none"> <a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
Кол-во точек Кол-во точек
@@ -317,7 +460,18 @@
value="{{ source.id }}"> value="{{ source.id }}">
</td> </td>
<td class="text-center">{{ source.id }}</td> <td class="text-center">{{ source.id }}</td>
<td>{{ source.satellite }}</td> <td>{{ source.name }}</td>
<td>
{% if source.satellite_id %}
<a href="#" class="text-decoration-underline"
onclick="showSatelliteModal({{ source.satellite_id }}); return false;">
{{ source.satellite }}
</a>
{% else %}
{{ source.satellite }}
{% endif %}
</td>
<td>{{ source.info }}</td>
<td>{{ source.coords_average }}</td> <td>{{ source.coords_average }}</td>
<td>{{ source.coords_kupsat }}</td> <td>{{ source.coords_kupsat }}</td>
<td>{{ source.coords_valid }}</td> <td>{{ source.coords_valid }}</td>
@@ -345,7 +499,6 @@
<span class="text-muted">-</span> <span class="text-muted">-</span>
{% endif %} {% endif %}
</td> </td>
{% if has_any_lyngsat %}
<td class="text-center"> <td class="text-center">
{% if source.has_lyngsat %} {% if source.has_lyngsat %}
<a href="#" class="text-primary text-decoration-none" <a href="#" class="text-primary text-decoration-none"
@@ -356,7 +509,6 @@
- -
{% endif %} {% endif %}
</td> </td>
{% endif %}
<td class="text-center">{{ source.objitem_count }}</td> <td class="text-center">{{ source.objitem_count }}</td>
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td> <td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td> <td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
@@ -411,7 +563,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="12" class="text-center text-muted">Нет данных для отображения</td> <td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -775,6 +927,7 @@ document.addEventListener('DOMContentLoaded', function() {
setupRadioLikeCheckboxes('has_coords_valid'); setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference'); setupRadioLikeCheckboxes('has_coords_reference');
setupRadioLikeCheckboxes('has_lyngsat'); setupRadioLikeCheckboxes('has_lyngsat');
setupRadioLikeCheckboxes('has_signal_mark');
// Update filter counter on page load // Update filter counter on page load
updateFilterCounter(); updateFilterCounter();
@@ -896,10 +1049,10 @@ function showSourceDetails(sourceId) {
// Build transponder cell // Build transponder cell
let transponderCell = '-'; let transponderCell = '-';
if (objitem.has_transponder) { if (objitem.has_transponder) {
transponderCell = '<a href="#" class="text-success text-decoration-none" ' + transponderCell = '<a href="#" class="text-decoration-underline" ' +
'onclick="showTransponderModal(' + objitem.transponder_id + '); return false;" ' + 'onclick="showTransponderModal(' + objitem.transponder_id + '); return false;" ' +
'title="Показать данные транспондера">' + 'title="Показать данные транспондера">' +
'<i class="bi bi-broadcast"></i> ' + objitem.transponder_info + objitem.transponder_info +
'</a>'; '</a>';
} }
@@ -922,12 +1075,21 @@ function showSourceDetails(sourceId) {
'</a>'; '</a>';
} }
// Build satellite cell with link
let satelliteCell = objitem.satellite_name;
if (objitem.satellite_id) {
satelliteCell = '<a href="#" class="text-decoration-underline" ' +
'onclick="showSatelliteModal(' + objitem.satellite_id + '); return false;">' +
objitem.satellite_name +
'</a>';
}
row.innerHTML = '<td class="text-center">' + row.innerHTML = '<td class="text-center">' +
'<input type="checkbox" class="form-check-input modal-item-checkbox" value="' + objitem.id + '">' + '<input type="checkbox" class="form-check-input modal-item-checkbox" value="' + objitem.id + '">' +
'</td>' + '</td>' +
'<td class="text-center">' + objitem.id + '</td>' + '<td class="text-center">' + objitem.id + '</td>' +
'<td>' + objitem.name + '</td>' + '<td>' + objitem.name + '</td>' +
'<td>' + objitem.satellite_name + '</td>' + '<td>' + satelliteCell + '</td>' +
'<td>' + transponderCell + '</td>' + '<td>' + transponderCell + '</td>' +
'<td>' + objitem.frequency + '</td>' + '<td>' + objitem.frequency + '</td>' +
'<td>' + objitem.freq_range + '</td>' + '<td>' + objitem.freq_range + '</td>' +
@@ -954,8 +1116,13 @@ function showSourceDetails(sourceId) {
// Setup modal select-all checkbox // Setup modal select-all checkbox
setupModalSelectAll(); setupModalSelectAll();
// Initialize column visibility // Initialize column visibility after DOM update
initModalColumnVisibility(); // Use requestAnimationFrame to ensure DOM is rendered
requestAnimationFrame(() => {
setTimeout(() => {
initModalColumnVisibility();
}, 50);
});
} else { } else {
// Show no data message // Show no data message
document.getElementById('modalNoData').style.display = 'block'; document.getElementById('modalNoData').style.display = 'block';
@@ -1002,21 +1169,27 @@ function setupModalSelectAll() {
// Function to toggle modal column visibility // Function to toggle modal column visibility
function toggleModalColumn(checkbox) { function toggleModalColumn(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column')); const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const modal = document.getElementById('sourceDetailsModal');
const table = modal.querySelector('.table'); // Get the specific tbody for objitems
const tbody = document.getElementById('objitemTableBody');
if (!tbody) return;
// Get the parent table
const table = tbody.closest('table');
if (!table) return; if (!table) return;
const cells = table.querySelectorAll('td:nth-child(' + (columnIndex + 1) + '), th:nth-child(' + (columnIndex + 1) + ')'); // Get all rows and toggle specific cell in each
const rows = table.querySelectorAll('tr');
if (checkbox.checked) { rows.forEach(row => {
cells.forEach(cell => { const cells = row.children;
cell.style.display = ''; if (cells[columnIndex]) {
}); if (checkbox.checked) {
} else { cells[columnIndex].style.removeProperty('display');
cells.forEach(cell => { } else {
cell.style.display = 'none'; cells[columnIndex].style.setProperty('display', 'none', 'important');
}); }
} }
});
} }
// Function to toggle all modal columns // Function to toggle all modal columns
@@ -1032,11 +1205,31 @@ function toggleAllModalColumns(selectAllCheckbox) {
function initModalColumnVisibility() { function initModalColumnVisibility() {
// Hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22) // Hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22)
const columnsToHide = [16, 17, 18, 19, 20, 22]; const columnsToHide = [16, 17, 18, 19, 20, 22];
// Get the specific tbody for objitems
const tbody = document.getElementById('objitemTableBody');
if (!tbody) {
console.log('objitemTableBody not found');
return;
}
// Get the parent table
const table = tbody.closest('table');
if (!table) {
console.log('Table not found');
return;
}
// Hide columns that should be hidden by default
columnsToHide.forEach(columnIndex => { columnsToHide.forEach(columnIndex => {
const checkbox = document.querySelector('.modal-column-toggle[data-column="' + columnIndex + '"]'); // Get all rows in the table (including thead and tbody)
if (checkbox && !checkbox.checked) { const rows = table.querySelectorAll('tr');
toggleModalColumn(checkbox); rows.forEach(row => {
} const cells = row.children;
if (cells[columnIndex]) {
cells[columnIndex].style.setProperty('display', 'none', 'important');
}
});
}); });
} }
@@ -1190,4 +1383,7 @@ function showTransponderModal(transponderId) {
<!-- Include the sigma parameter modal component --> <!-- Include the sigma parameter modal component -->
{% include 'mainapp/components/_sigma_parameter_modal.html' %} {% include 'mainapp/components/_sigma_parameter_modal.html' %}
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -330,7 +330,16 @@
</td> </td>
<td class="text-center">{{ transponder.id }}</td> <td class="text-center">{{ transponder.id }}</td>
<td>{{ transponder.name }}</td> <td>{{ transponder.name }}</td>
<td>{{ transponder.satellite }}</td> <td>
{% if transponder.satellite_id %}
<a href="#" class="text-decoration-underline"
onclick="showSatelliteModal({{ transponder.satellite_id }}); return false;">
{{ transponder.satellite }}
</a>
{% else %}
{{ transponder.satellite }}
{% endif %}
</td>
<td>{{ transponder.downlink }}</td> <td>{{ transponder.downlink }}</td>
<td>{{ transponder.uplink }}</td> <td>{{ transponder.uplink }}</td>
<td>{{ transponder.frequency_range }}</td> <td>{{ transponder.frequency_range }}</td>
@@ -574,4 +583,8 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -26,6 +26,7 @@ from .views import (
ObjItemListView, ObjItemListView,
ObjItemUpdateView, ObjItemUpdateView,
ProcessKubsatView, ProcessKubsatView,
SatelliteDataAPIView,
ShowMapView, ShowMapView,
ShowSelectedObjectsMapView, ShowSelectedObjectsMapView,
ShowSourcesMapView, ShowSourcesMapView,
@@ -79,6 +80,7 @@ urlpatterns = [
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'), path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'), path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'), path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'), path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'), path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),

View File

@@ -1205,3 +1205,106 @@ def get_first_param_subquery(field_name: str):
... print(obj.first_freq) ... print(obj.first_freq)
""" """
return F(f"parameter_obj__{field_name}") return F(f"parameter_obj__{field_name}")
# ============================================================================
# Number Formatting Functions
# ============================================================================
def format_coordinate(value):
"""
Format coordinate value to 4 decimal places.
Args:
value: Numeric coordinate value
Returns:
str: Formatted coordinate or '-' if None
"""
if value is None:
return '-'
try:
return f"{float(value):.4f}"
except (ValueError, TypeError):
return '-'
def format_frequency(value):
"""
Format frequency value to 3 decimal places.
Args:
value: Numeric frequency value in MHz
Returns:
str: Formatted frequency or '-' if None
"""
if value is None:
return '-'
try:
return f"{float(value):.3f}"
except (ValueError, TypeError):
return '-'
def format_symbol_rate(value):
"""
Format symbol rate (bod_velocity) to integer.
Args:
value: Numeric symbol rate value
Returns:
str: Formatted symbol rate or '-' if None
"""
if value is None:
return '-'
try:
return f"{float(value):.0f}"
except (ValueError, TypeError):
return '-'
def format_coords_display(point):
"""
Format geographic point coordinates for display.
Args:
point: GeoDjango Point object
Returns:
str: Formatted coordinates as "LAT LON" or '-' if None
"""
if not point:
return '-'
try:
longitude = point.coords[0]
latitude = point.coords[1]
lon = f"{abs(longitude):.4f}E" if longitude > 0 else f"{abs(longitude):.4f}W"
lat = f"{abs(latitude):.4f}N" if latitude > 0 else f"{abs(latitude):.4f}S"
return f"{lat} {lon}"
except (AttributeError, IndexError, TypeError):
return '-'
def parse_pagination_params(request):
"""
Parse pagination parameters from request.
Args:
request: Django request object
Returns:
tuple: (page_number, items_per_page)
"""
page_number = request.GET.get("page", 1)
items_per_page = request.GET.get("items_per_page", 50)
try:
items_per_page = int(items_per_page)
if items_per_page not in [50, 100, 500, 1000]:
items_per_page = 50
except (ValueError, TypeError):
items_per_page = 50
return page_number, items_per_page

View File

@@ -20,6 +20,7 @@ from .data_import import (
from .api import ( from .api import (
GetLocationsView, GetLocationsView,
LyngsatDataAPIView, LyngsatDataAPIView,
SatelliteDataAPIView,
SigmaParameterDataAPIView, SigmaParameterDataAPIView,
SourceObjItemsAPIView, SourceObjItemsAPIView,
LyngsatTaskStatusAPIView, LyngsatTaskStatusAPIView,
@@ -71,6 +72,7 @@ __all__ = [
# API # API
'GetLocationsView', 'GetLocationsView',
'LyngsatDataAPIView', 'LyngsatDataAPIView',
'SatelliteDataAPIView',
'SigmaParameterDataAPIView', 'SigmaParameterDataAPIView',
'SourceObjItemsAPIView', 'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView', 'LyngsatTaskStatusAPIView',

View File

@@ -7,6 +7,7 @@ from django.utils import timezone
from django.views import View from django.views import View
from ..models import ObjItem from ..models import ObjItem
from ..utils import format_coordinate, format_coords_display, format_frequency, format_symbol_rate
class GetLocationsView(LoginRequiredMixin, View): class GetLocationsView(LoginRequiredMixin, View):
@@ -76,11 +77,11 @@ class LyngsatDataAPIView(LoginRequiredMixin, View):
data = { data = {
'id': lyngsat.id, 'id': lyngsat.id,
'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-', 'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-', 'frequency': format_frequency(lyngsat.frequency),
'polarization': lyngsat.polarization.name if lyngsat.polarization else '-', 'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
'modulation': lyngsat.modulation.name if lyngsat.modulation else '-', 'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
'standard': lyngsat.standard.name if lyngsat.standard else '-', 'standard': lyngsat.standard.name if lyngsat.standard else '-',
'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-', 'sym_velocity': format_symbol_rate(lyngsat.sym_velocity),
'fec': lyngsat.fec or '-', 'fec': lyngsat.fec or '-',
'channel_info': lyngsat.channel_info or '-', 'channel_info': lyngsat.channel_info or '-',
'last_update': last_update_str, 'last_update': last_update_str,
@@ -146,13 +147,13 @@ class SigmaParameterDataAPIView(LoginRequiredMixin, View):
sigma_data.append({ sigma_data.append({
'id': sigma.id, 'id': sigma.id,
'satellite': sigma.id_satellite.name if sigma.id_satellite else '-', 'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-', 'frequency': format_frequency(sigma.frequency),
'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-', 'transfer_frequency': format_frequency(sigma.transfer_frequency),
'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-', 'freq_range': format_frequency(sigma.freq_range),
'polarization': sigma.polarization.name if sigma.polarization else '-', 'polarization': sigma.polarization.name if sigma.polarization else '-',
'modulation': sigma.modulation.name if sigma.modulation else '-', 'modulation': sigma.modulation.name if sigma.modulation else '-',
'standard': sigma.standard.name if sigma.standard else '-', 'standard': sigma.standard.name if sigma.standard else '-',
'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-', 'bod_velocity': format_symbol_rate(sigma.bod_velocity),
'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-', 'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
'power': f"{sigma.power:.1f}" if sigma.power is not None else '-', 'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
'status': sigma.status or '-', 'status': sigma.status or '-',
@@ -235,6 +236,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
# Get parameter data # Get parameter data
param = getattr(objitem, 'parameter_obj', None) param = getattr(objitem, 'parameter_obj', None)
satellite_name = '-' satellite_name = '-'
satellite_id = None
frequency = '-' frequency = '-'
freq_range = '-' freq_range = '-'
polarization = '-' polarization = '-'
@@ -248,11 +250,12 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
parameter_id = param.id parameter_id = param.id
if hasattr(param, 'id_satellite') and param.id_satellite: if hasattr(param, 'id_satellite') and param.id_satellite:
satellite_name = param.id_satellite.name satellite_name = param.id_satellite.name
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-' satellite_id = param.id_satellite.id
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-' frequency = format_frequency(param.frequency)
freq_range = format_frequency(param.freq_range)
if hasattr(param, 'polarization') and param.polarization: if hasattr(param, 'polarization') and param.polarization:
polarization = param.polarization.name polarization = param.polarization.name
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-' bod_velocity = format_symbol_rate(param.bod_velocity)
if hasattr(param, 'modulation') and param.modulation: if hasattr(param, 'modulation') and param.modulation:
modulation = param.modulation.name modulation = param.modulation.name
if hasattr(param, 'standard') and param.standard: if hasattr(param, 'standard') and param.standard:
@@ -272,11 +275,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
geo_location = objitem.geo_obj.location or '-' geo_location = objitem.geo_obj.location or '-'
if objitem.geo_obj.coords: if objitem.geo_obj.coords:
longitude = objitem.geo_obj.coords.coords[0] geo_coords = format_coords_display(objitem.geo_obj.coords)
latitude = objitem.geo_obj.coords.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}"
# Get created/updated info # Get created/updated info
created_at = '-' created_at = '-'
@@ -332,6 +331,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'id': objitem.id, 'id': objitem.id,
'name': objitem.name or '-', 'name': objitem.name or '-',
'satellite_name': satellite_name, 'satellite_name': satellite_name,
'satellite_id': satellite_id,
'frequency': frequency, 'frequency': frequency,
'freq_range': freq_range, 'freq_range': freq_range,
'polarization': polarization, 'polarization': polarization,
@@ -454,12 +454,12 @@ class TransponderDataAPIView(LoginRequiredMixin, View):
'id': transponder.id, 'id': transponder.id,
'name': transponder.name or '-', 'name': transponder.name or '-',
'satellite': transponder.sat_id.name if transponder.sat_id else '-', 'satellite': transponder.sat_id.name if transponder.sat_id else '-',
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-', 'downlink': format_frequency(transponder.downlink),
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None, 'uplink': format_frequency(transponder.uplink) if transponder.uplink else None,
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-', 'frequency_range': format_frequency(transponder.frequency_range),
'polarization': transponder.polarization.name if transponder.polarization else '-', 'polarization': transponder.polarization.name if transponder.polarization else '-',
'zone_name': transponder.zone_name or '-', 'zone_name': transponder.zone_name or '-',
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None, 'transfer': format_frequency(transponder.transfer) if transponder.transfer else None,
'snr': f"{transponder.snr:.1f}" if transponder.snr is not None else None, 'snr': f"{transponder.snr:.1f}" if transponder.snr is not None else None,
'created_at': created_at_str, 'created_at': created_at_str,
'created_by': created_by_str, 'created_by': created_by_str,
@@ -470,3 +470,57 @@ class TransponderDataAPIView(LoginRequiredMixin, View):
return JsonResponse({'error': 'Транспондер не найден'}, status=404) return JsonResponse({'error': 'Транспондер не найден'}, status=404)
except Exception as e: except Exception as e:
return JsonResponse({'error': str(e)}, status=500) return JsonResponse({'error': str(e)}, status=500)
class SatelliteDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting Satellite data."""
def get(self, request, satellite_id):
from ..models import Satellite
try:
satellite = Satellite.objects.prefetch_related(
'band',
'created_by__user',
'updated_by__user'
).get(id=satellite_id)
# Format dates
created_at_str = '-'
if satellite.created_at:
local_time = timezone.localtime(satellite.created_at)
created_at_str = local_time.strftime("%d.%m.%Y %H:%M")
updated_at_str = '-'
if satellite.updated_at:
local_time = timezone.localtime(satellite.updated_at)
updated_at_str = local_time.strftime("%d.%m.%Y %H:%M")
launch_date_str = '-'
if satellite.launch_date:
launch_date_str = satellite.launch_date.strftime("%d.%m.%Y")
# Get bands
bands_list = list(satellite.band.values_list('name', flat=True))
bands_str = ', '.join(bands_list) if bands_list else '-'
data = {
'id': satellite.id,
'name': satellite.name,
'norad': satellite.norad if satellite.norad else '-',
'undersat_point': f"{satellite.undersat_point}°" if satellite.undersat_point is not None else '-',
'bands': bands_str,
'launch_date': launch_date_str,
'url': satellite.url or None,
'comment': satellite.comment or '-',
'created_at': created_at_str,
'created_by': str(satellite.created_by) if satellite.created_by else '-',
'updated_at': updated_at_str,
'updated_by': str(satellite.updated_by) if satellite.updated_by else '-',
}
return JsonResponse(data)
except Satellite.DoesNotExist:
return JsonResponse({'error': 'Спутник не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

View File

@@ -37,6 +37,11 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
if satellite_id: if satellite_id:
queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id=satellite_id).distinct() queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id=satellite_id).distinct()
# Поиск по имени объекта
search_query = self.request.GET.get('search', '').strip()
if search_query:
queryset = queryset.filter(source_objitems__name__icontains=search_query).distinct()
return queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@@ -15,7 +15,13 @@ from django.views.generic import CreateView, DeleteView, UpdateView
from ..forms import GeoForm, ObjItemForm, ParameterForm from ..forms import GeoForm, ObjItemForm, ParameterForm
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
from ..models import Geo, Modulation, ObjItem, Polarization, Satellite from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
from ..utils import parse_pagination_params from ..utils import (
format_coordinate,
format_coords_display,
format_frequency,
format_symbol_rate,
parse_pagination_params,
)
class DeleteSelectedObjectsView(RoleRequiredMixin, View): class DeleteSelectedObjectsView(RoleRequiredMixin, View):
@@ -323,13 +329,10 @@ class ObjItemListView(LoginRequiredMixin, View):
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True)) mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
if obj.geo_obj.coords: if obj.geo_obj.coords:
longitude = obj.geo_obj.coords.coords[0] geo_coords = format_coords_display(obj.geo_obj.coords)
latitude = obj.geo_obj.coords.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}"
satellite_name = "-" satellite_name = "-"
satellite_id = None
frequency = "-" frequency = "-"
freq_range = "-" freq_range = "-"
polarization_name = "-" polarization_name = "-"
@@ -347,18 +350,11 @@ class ObjItemListView(LoginRequiredMixin, View):
if hasattr(param.id_satellite, "name") if hasattr(param.id_satellite, "name")
else "-" else "-"
) )
satellite_id = param.id_satellite.id
frequency = ( frequency = format_frequency(param.frequency)
f"{param.frequency:.3f}" if param.frequency is not None else "-" freq_range = format_frequency(param.freq_range)
) bod_velocity = format_symbol_rate(param.bod_velocity)
freq_range = (
f"{param.freq_range:.3f}" if param.freq_range is not None else "-"
)
bod_velocity = (
f"{param.bod_velocity:.0f}"
if param.bod_velocity is not None
else "-"
)
snr = f"{param.snr:.0f}" if param.snr is not None else "-" snr = f"{param.snr:.0f}" if param.snr is not None else "-"
if hasattr(param, "polarization") and param.polarization: if hasattr(param, "polarization") and param.polarization:
@@ -396,8 +392,8 @@ class ObjItemListView(LoginRequiredMixin, View):
has_sigma = True has_sigma = True
first_sigma = param.sigma_parameter.first() first_sigma = param.sigma_parameter.first()
if first_sigma: if first_sigma:
sigma_freq = f"{first_sigma.transfer_frequency:.3f}" if first_sigma.transfer_frequency else "-" sigma_freq = format_frequency(first_sigma.transfer_frequency)
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-" sigma_range = format_frequency(first_sigma.freq_range)
sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-" sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-" sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}" sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
@@ -407,6 +403,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"id": obj.id, "id": obj.id,
"name": obj.name or "-", "name": obj.name or "-",
"satellite_name": satellite_name, "satellite_name": satellite_name,
"satellite_id": satellite_id,
"frequency": frequency, "frequency": frequency,
"freq_range": freq_range, "freq_range": freq_range,
"polarization": polarization_name, "polarization": polarization_name,

View File

@@ -14,7 +14,7 @@ from django.views import View
from ..forms import SourceForm from ..forms import SourceForm
from ..models import Source, Satellite from ..models import Source, Satellite
from ..utils import parse_pagination_params from ..utils import format_coords_display, parse_pagination_params
class SourceListView(LoginRequiredMixin, View): class SourceListView(LoginRequiredMixin, View):
@@ -29,20 +29,38 @@ class SourceListView(LoginRequiredMixin, View):
# Get sorting parameters (default to ID ascending) # Get sorting parameters (default to ID ascending)
sort_param = request.GET.get("sort", "id") sort_param = request.GET.get("sort", "id")
# Get filter parameters # Get filter parameters - Source level
search_query = request.GET.get("search", "").strip() search_query = request.GET.get("search", "").strip()
has_coords_average = request.GET.get("has_coords_average") has_coords_average = request.GET.get("has_coords_average")
has_coords_kupsat = request.GET.get("has_coords_kupsat") has_coords_kupsat = request.GET.get("has_coords_kupsat")
has_coords_valid = request.GET.get("has_coords_valid") has_coords_valid = request.GET.get("has_coords_valid")
has_coords_reference = request.GET.get("has_coords_reference") has_coords_reference = request.GET.get("has_coords_reference")
has_lyngsat = request.GET.get("has_lyngsat") has_lyngsat = request.GET.get("has_lyngsat")
selected_info = request.GET.getlist("info_id")
objitem_count_min = request.GET.get("objitem_count_min", "").strip() objitem_count_min = request.GET.get("objitem_count_min", "").strip()
objitem_count_max = request.GET.get("objitem_count_max", "").strip() objitem_count_max = request.GET.get("objitem_count_max", "").strip()
date_from = request.GET.get("date_from", "").strip() date_from = request.GET.get("date_from", "").strip()
date_to = request.GET.get("date_to", "").strip() date_to = request.GET.get("date_to", "").strip()
# Signal mark filters
has_signal_mark = request.GET.get("has_signal_mark")
mark_date_from = request.GET.get("mark_date_from", "").strip()
mark_date_to = request.GET.get("mark_date_to", "").strip()
# Get filter parameters - ObjItem level (параметры точек)
geo_date_from = request.GET.get("geo_date_from", "").strip() geo_date_from = request.GET.get("geo_date_from", "").strip()
geo_date_to = request.GET.get("geo_date_to", "").strip() geo_date_to = request.GET.get("geo_date_to", "").strip()
selected_satellites = request.GET.getlist("satellite_id") selected_satellites = request.GET.getlist("satellite_id")
selected_polarizations = request.GET.getlist("polarization_id")
selected_modulations = request.GET.getlist("modulation_id")
selected_mirrors = request.GET.getlist("mirror_id")
freq_min = request.GET.get("freq_min", "").strip()
freq_max = request.GET.get("freq_max", "").strip()
freq_range_min = request.GET.get("freq_range_min", "").strip()
freq_range_max = request.GET.get("freq_range_max", "").strip()
bod_velocity_min = request.GET.get("bod_velocity_min", "").strip()
bod_velocity_max = request.GET.get("bod_velocity_max", "").strip()
snr_min = request.GET.get("snr_min", "").strip()
snr_max = request.GET.get("snr_max", "").strip()
# Get all satellites for filter # Get all satellites for filter
satellites = ( satellites = (
@@ -52,14 +70,44 @@ class SourceListView(LoginRequiredMixin, View):
.order_by("name") .order_by("name")
) )
# Build Q object for geo date filtering # Get all polarizations, modulations for filters
geo_date_q = Q() from ..models import Polarization, Modulation, ObjectInfo
has_geo_date_filter = False polarizations = Polarization.objects.all().order_by("name")
modulations = Modulation.objects.all().order_by("name")
# Get all ObjectInfo for filter
object_infos = ObjectInfo.objects.all().order_by("name")
# Get all satellites that are used as mirrors
mirrors = (
Satellite.objects.filter(geo_mirrors__isnull=False)
.distinct()
.only("id", "name")
.order_by("name")
)
# Build Q object for filtering objitems in count
# This will be used in the annotate to count only objitems that match filters
objitem_filter_q = Q()
has_objitem_filter = False
# Check if search is by name (not by ID)
search_by_name = False
if search_query:
try:
int(search_query) # Try to parse as ID
except ValueError:
# Not a number, so it's a name search
search_by_name = True
objitem_filter_q &= Q(source_objitems__name__icontains=search_query)
has_objitem_filter = True
# Add geo date filter
if geo_date_from: if geo_date_from:
try: try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d") geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
geo_date_q &= Q(source_objitems__geo_obj__timestamp__gte=geo_date_from_obj) objitem_filter_q &= Q(source_objitems__geo_obj__timestamp__gte=geo_date_from_obj)
has_geo_date_filter = True has_objitem_filter = True
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
@@ -69,15 +117,105 @@ class SourceListView(LoginRequiredMixin, View):
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d") geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
# Add one day to include entire end date # Add one day to include entire end date
geo_date_to_obj = geo_date_to_obj + timedelta(days=1) geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
geo_date_q &= Q(source_objitems__geo_obj__timestamp__lt=geo_date_to_obj) objitem_filter_q &= Q(source_objitems__geo_obj__timestamp__lt=geo_date_to_obj)
has_geo_date_filter = True has_objitem_filter = True
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
# Add satellite filter to count
if selected_satellites:
objitem_filter_q &= Q(source_objitems__parameter_obj__id_satellite_id__in=selected_satellites)
has_objitem_filter = True
# Add polarization filter
if selected_polarizations:
objitem_filter_q &= Q(source_objitems__parameter_obj__polarization_id__in=selected_polarizations)
has_objitem_filter = True
# Add modulation filter
if selected_modulations:
objitem_filter_q &= Q(source_objitems__parameter_obj__modulation_id__in=selected_modulations)
has_objitem_filter = True
# Add frequency filter
if freq_min:
try:
freq_min_val = float(freq_min)
objitem_filter_q &= Q(source_objitems__parameter_obj__frequency__gte=freq_min_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
if freq_max:
try:
freq_max_val = float(freq_max)
objitem_filter_q &= Q(source_objitems__parameter_obj__frequency__lte=freq_max_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
# Add frequency range (bandwidth) filter
if freq_range_min:
try:
freq_range_min_val = float(freq_range_min)
objitem_filter_q &= Q(source_objitems__parameter_obj__freq_range__gte=freq_range_min_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
if freq_range_max:
try:
freq_range_max_val = float(freq_range_max)
objitem_filter_q &= Q(source_objitems__parameter_obj__freq_range__lte=freq_range_max_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
# Add symbol rate (bod_velocity) filter
if bod_velocity_min:
try:
bod_velocity_min_val = float(bod_velocity_min)
objitem_filter_q &= Q(source_objitems__parameter_obj__bod_velocity__gte=bod_velocity_min_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
if bod_velocity_max:
try:
bod_velocity_max_val = float(bod_velocity_max)
objitem_filter_q &= Q(source_objitems__parameter_obj__bod_velocity__lte=bod_velocity_max_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
# Add SNR filter
if snr_min:
try:
snr_min_val = float(snr_min)
objitem_filter_q &= Q(source_objitems__parameter_obj__snr__gte=snr_min_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
if snr_max:
try:
snr_max_val = float(snr_max)
objitem_filter_q &= Q(source_objitems__parameter_obj__snr__lte=snr_max_val)
has_objitem_filter = True
except (ValueError, TypeError):
pass
# Add mirrors filter
if selected_mirrors:
objitem_filter_q &= Q(source_objitems__geo_obj__mirrors__id__in=selected_mirrors)
has_objitem_filter = True
# Get all Source objects with query optimization # Get all Source objects with query optimization
# Using annotate to count ObjItems efficiently (single query with GROUP BY) # Using annotate to count ObjItems efficiently (single query with GROUP BY)
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries # Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
sources = Source.objects.prefetch_related( sources = Source.objects.select_related(
'info'
).prefetch_related(
'source_objitems', 'source_objitems',
'source_objitems__parameter_obj', 'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite', 'source_objitems__parameter_obj__id_satellite',
@@ -85,7 +223,7 @@ class SourceListView(LoginRequiredMixin, View):
'marks', 'marks',
'marks__created_by__user' 'marks__created_by__user'
).annotate( ).annotate(
objitem_count=Count('source_objitems', filter=geo_date_q) if has_geo_date_filter else Count('source_objitems') objitem_count=Count('source_objitems', filter=objitem_filter_q, distinct=True) if has_objitem_filter else Count('source_objitems')
) )
# Apply filters # Apply filters
@@ -121,6 +259,41 @@ class SourceListView(LoginRequiredMixin, View):
~Q(source_objitems__lyngsat_source__isnull=False) ~Q(source_objitems__lyngsat_source__isnull=False)
).distinct() ).distinct()
# Filter by ObjectInfo (info field)
if selected_info:
sources = sources.filter(info_id__in=selected_info)
# Filter by signal marks
if has_signal_mark or mark_date_from or mark_date_to:
mark_filter_q = Q()
# Filter by mark value (signal presence)
if has_signal_mark == "1":
mark_filter_q &= Q(marks__mark=True)
elif has_signal_mark == "0":
mark_filter_q &= Q(marks__mark=False)
# Filter by mark date range
if mark_date_from:
try:
mark_date_from_obj = datetime.strptime(mark_date_from, "%Y-%m-%d")
mark_filter_q &= Q(marks__timestamp__gte=mark_date_from_obj)
except (ValueError, TypeError):
pass
if mark_date_to:
try:
from datetime import timedelta
mark_date_to_obj = datetime.strptime(mark_date_to, "%Y-%m-%d")
# Add one day to include entire end date
mark_date_to_obj = mark_date_to_obj + timedelta(days=1)
mark_filter_q &= Q(marks__timestamp__lt=mark_date_to_obj)
except (ValueError, TypeError):
pass
if mark_filter_q:
sources = sources.filter(mark_filter_q).distinct()
# Filter by ObjItem count # Filter by ObjItem count
if objitem_count_min: if objitem_count_min:
try: try:
@@ -155,17 +328,36 @@ class SourceListView(LoginRequiredMixin, View):
pass pass
# Filter by Geo timestamp range (only filter sources that have matching objitems) # Filter by Geo timestamp range (only filter sources that have matching objitems)
if has_geo_date_filter: if geo_date_from or geo_date_to:
sources = sources.filter(geo_date_q).distinct() geo_filter_q = Q()
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
geo_filter_q &= Q(source_objitems__geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
geo_filter_q &= Q(source_objitems__geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
if geo_filter_q:
sources = sources.filter(geo_filter_q).distinct()
# Search by ID # Search by ID or name
if search_query: if search_query:
try: try:
# Try to search by ID first
search_id = int(search_query) search_id = int(search_query)
sources = sources.filter(id=search_id) sources = sources.filter(id=search_id)
except ValueError: except ValueError:
# If not a number, ignore # If not a number, search by name in related objitems
pass sources = sources.filter(
source_objitems__name__icontains=search_query
).distinct()
# Filter by satellites # Filter by satellites
if selected_satellites: if selected_satellites:
@@ -173,6 +365,84 @@ class SourceListView(LoginRequiredMixin, View):
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
).distinct() ).distinct()
# Filter by polarizations
if selected_polarizations:
sources = sources.filter(
source_objitems__parameter_obj__polarization_id__in=selected_polarizations
).distinct()
# Filter by modulations
if selected_modulations:
sources = sources.filter(
source_objitems__parameter_obj__modulation_id__in=selected_modulations
).distinct()
# Filter by frequency range
if freq_min:
try:
freq_min_val = float(freq_min)
sources = sources.filter(source_objitems__parameter_obj__frequency__gte=freq_min_val).distinct()
except (ValueError, TypeError):
pass
if freq_max:
try:
freq_max_val = float(freq_max)
sources = sources.filter(source_objitems__parameter_obj__frequency__lte=freq_max_val).distinct()
except (ValueError, TypeError):
pass
# Filter by frequency range (bandwidth)
if freq_range_min:
try:
freq_range_min_val = float(freq_range_min)
sources = sources.filter(source_objitems__parameter_obj__freq_range__gte=freq_range_min_val).distinct()
except (ValueError, TypeError):
pass
if freq_range_max:
try:
freq_range_max_val = float(freq_range_max)
sources = sources.filter(source_objitems__parameter_obj__freq_range__lte=freq_range_max_val).distinct()
except (ValueError, TypeError):
pass
# Filter by symbol rate
if bod_velocity_min:
try:
bod_velocity_min_val = float(bod_velocity_min)
sources = sources.filter(source_objitems__parameter_obj__bod_velocity__gte=bod_velocity_min_val).distinct()
except (ValueError, TypeError):
pass
if bod_velocity_max:
try:
bod_velocity_max_val = float(bod_velocity_max)
sources = sources.filter(source_objitems__parameter_obj__bod_velocity__lte=bod_velocity_max_val).distinct()
except (ValueError, TypeError):
pass
# Filter by SNR
if snr_min:
try:
snr_min_val = float(snr_min)
sources = sources.filter(source_objitems__parameter_obj__snr__gte=snr_min_val).distinct()
except (ValueError, TypeError):
pass
if snr_max:
try:
snr_max_val = float(snr_max)
sources = sources.filter(source_objitems__parameter_obj__snr__lte=snr_max_val).distinct()
except (ValueError, TypeError):
pass
# Filter by mirrors
if selected_mirrors:
sources = sources.filter(
source_objitems__geo_obj__mirrors__id__in=selected_mirrors
).distinct()
# Apply sorting # Apply sorting
valid_sort_fields = { valid_sort_fields = {
"id": "id", "id": "id",
@@ -194,62 +464,111 @@ class SourceListView(LoginRequiredMixin, View):
# Prepare data for display # Prepare data for display
processed_sources = [] processed_sources = []
has_any_lyngsat = False # Track if any source has LyngSat data
for source in page_obj: for source in page_obj:
# Format coordinates # Format coordinates
def format_coords(point): coords_average_str = format_coords_display(source.coords_average)
if point: coords_kupsat_str = format_coords_display(source.coords_kupsat)
longitude = point.coords[0] coords_valid_str = format_coords_display(source.coords_valid)
latitude = point.coords[1] coords_reference_str = format_coords_display(source.coords_reference)
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}"
return "-"
coords_average_str = format_coords(source.coords_average) # Filter objitems for display (to get satellites and lyngsat info)
coords_kupsat_str = format_coords(source.coords_kupsat)
coords_valid_str = format_coords(source.coords_valid)
coords_reference_str = format_coords(source.coords_reference)
# Filter objitems by geo date if filter is applied
objitems_to_display = source.source_objitems.all() objitems_to_display = source.source_objitems.all()
if geo_date_from or geo_date_to:
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
# Get count of related ObjItems (filtered) # Apply the same filters as in the count annotation
objitem_count = objitems_to_display.count() if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
if selected_satellites:
objitems_to_display = objitems_to_display.filter(parameter_obj__id_satellite_id__in=selected_satellites)
if selected_polarizations:
objitems_to_display = objitems_to_display.filter(parameter_obj__polarization_id__in=selected_polarizations)
if selected_modulations:
objitems_to_display = objitems_to_display.filter(parameter_obj__modulation_id__in=selected_modulations)
if freq_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__frequency__gte=float(freq_min))
except (ValueError, TypeError):
pass
if freq_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__frequency__lte=float(freq_max))
except (ValueError, TypeError):
pass
if freq_range_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__freq_range__gte=float(freq_range_min))
except (ValueError, TypeError):
pass
if freq_range_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__freq_range__lte=float(freq_range_max))
except (ValueError, TypeError):
pass
if bod_velocity_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__bod_velocity__gte=float(bod_velocity_min))
except (ValueError, TypeError):
pass
if bod_velocity_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__bod_velocity__lte=float(bod_velocity_max))
except (ValueError, TypeError):
pass
if snr_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__snr__gte=float(snr_min))
except (ValueError, TypeError):
pass
if snr_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__snr__lte=float(snr_max))
except (ValueError, TypeError):
pass
if selected_mirrors:
objitems_to_display = objitems_to_display.filter(geo_obj__mirrors__id__in=selected_mirrors)
if search_by_name:
objitems_to_display = objitems_to_display.filter(name__icontains=search_query)
# Get satellites for this source and check for LyngSat # Use annotated count (consistent with filtering)
objitem_count = source.objitem_count
# Get satellites, name and check for LyngSat
satellite_names = set() satellite_names = set()
satellite_ids = set()
has_lyngsat = False has_lyngsat = False
lyngsat_id = None lyngsat_id = None
source_name = None
for objitem in objitems_to_display: for objitem in objitems_to_display:
# Get name from first objitem
if source_name is None and objitem.name:
source_name = objitem.name
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj: if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite: if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite:
satellite_names.add(objitem.parameter_obj.id_satellite.name) satellite_names.add(objitem.parameter_obj.id_satellite.name)
satellite_ids.add(objitem.parameter_obj.id_satellite.id)
# Check if any objitem has LyngSat # Check if any objitem has LyngSat
if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source: if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source:
has_lyngsat = True has_lyngsat = True
lyngsat_id = objitem.lyngsat_source.id lyngsat_id = objitem.lyngsat_source.id
has_any_lyngsat = True
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-" satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
# Get first satellite ID for modal link (if multiple satellites, use first one)
first_satellite_id = min(satellite_ids) if satellite_ids else None
# Get all marks (presence/absence) # Get all marks (presence/absence)
marks_data = [] marks_data = []
@@ -260,14 +579,20 @@ class SourceListView(LoginRequiredMixin, View):
'created_by': str(mark.created_by) if mark.created_by else '-', 'created_by': str(mark.created_by) if mark.created_by else '-',
}) })
# Get info name
info_name = source.info.name if source.info else '-'
processed_sources.append({ processed_sources.append({
'id': source.id, 'id': source.id,
'name': source_name if source_name else '-',
'info': info_name,
'coords_average': coords_average_str, 'coords_average': coords_average_str,
'coords_kupsat': coords_kupsat_str, 'coords_kupsat': coords_kupsat_str,
'coords_valid': coords_valid_str, 'coords_valid': coords_valid_str,
'coords_reference': coords_reference_str, 'coords_reference': coords_reference_str,
'objitem_count': objitem_count, 'objitem_count': objitem_count,
'satellite': satellite_str, 'satellite': satellite_str,
'satellite_id': first_satellite_id,
'created_at': source.created_at, 'created_at': source.created_at,
'updated_at': source.updated_at, 'updated_at': source.updated_at,
'has_lyngsat': has_lyngsat, 'has_lyngsat': has_lyngsat,
@@ -283,22 +608,50 @@ class SourceListView(LoginRequiredMixin, View):
'available_items_per_page': [50, 100, 500, 1000], 'available_items_per_page': [50, 100, 500, 1000],
'sort': sort_param, 'sort': sort_param,
'search_query': search_query, 'search_query': search_query,
# Source-level filters
'has_coords_average': has_coords_average, 'has_coords_average': has_coords_average,
'has_coords_kupsat': has_coords_kupsat, 'has_coords_kupsat': has_coords_kupsat,
'has_coords_valid': has_coords_valid, 'has_coords_valid': has_coords_valid,
'has_coords_reference': has_coords_reference, 'has_coords_reference': has_coords_reference,
'has_lyngsat': has_lyngsat, 'has_lyngsat': has_lyngsat,
'has_any_lyngsat': has_any_lyngsat, 'selected_info': [
int(x) if isinstance(x, str) else x for x in selected_info if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'objitem_count_min': objitem_count_min, 'objitem_count_min': objitem_count_min,
'objitem_count_max': objitem_count_max, 'objitem_count_max': objitem_count_max,
'date_from': date_from, 'date_from': date_from,
'date_to': date_to, 'date_to': date_to,
'has_signal_mark': has_signal_mark,
'mark_date_from': mark_date_from,
'mark_date_to': mark_date_to,
# ObjItem-level filters
'geo_date_from': geo_date_from, 'geo_date_from': geo_date_from,
'geo_date_to': geo_date_to, 'geo_date_to': geo_date_to,
'satellites': satellites, 'satellites': satellites,
'selected_satellites': [ 'selected_satellites': [
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit())) int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
], ],
'polarizations': polarizations,
'selected_polarizations': [
int(x) if isinstance(x, str) else x for x in selected_polarizations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'modulations': modulations,
'selected_modulations': [
int(x) if isinstance(x, str) else x for x in selected_modulations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'freq_min': freq_min,
'freq_max': freq_max,
'freq_range_min': freq_range_min,
'freq_range_max': freq_range_max,
'bod_velocity_min': bod_velocity_min,
'bod_velocity_max': bod_velocity_max,
'snr_min': snr_min,
'snr_max': snr_max,
'mirrors': mirrors,
'selected_mirrors': [
int(x) if isinstance(x, str) else x for x in selected_mirrors if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'object_infos': object_infos,
'full_width_page': True, 'full_width_page': True,
} }

View File

@@ -197,6 +197,7 @@ class TransponderListView(LoginRequiredMixin, View):
'id': transponder.id, 'id': transponder.id,
'name': transponder.name or "-", 'name': transponder.name or "-",
'satellite': transponder.sat_id.name if transponder.sat_id else "-", 'satellite': transponder.sat_id.name if transponder.sat_id else "-",
'satellite_id': transponder.sat_id.id if transponder.sat_id else None,
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-", 'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-",
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else "-", 'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else "-",
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-", 'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",