Добавил информацию о типе объекта. Просто фиксы
This commit is contained in:
@@ -55,6 +55,19 @@
|
||||
</select>
|
||||
</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 -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
@@ -252,10 +265,19 @@
|
||||
{% for item in lyngsat_items %}
|
||||
<tr>
|
||||
<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.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.standard.name|default:"-" }}</td>
|
||||
<td>{{ item.fec|default:"-" }}</td>
|
||||
@@ -425,4 +447,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -124,8 +124,10 @@ class LyngSatListView(LoginRequiredMixin, ListView):
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
context['sort'] = self.request.GET.get('sort', '-id')
|
||||
|
||||
# Данные для фильтров
|
||||
context['satellites'] = Satellite.objects.all().order_by('name')
|
||||
# Данные для фильтров - только спутники с существующими записями LyngSat
|
||||
context['satellites'] = Satellite.objects.filter(
|
||||
lyngsat__isnull=False
|
||||
).distinct().order_by('name')
|
||||
context['polarizations'] = Polarization.objects.all().order_by('name')
|
||||
context['modulations'] = Modulation.objects.all().order_by('name')
|
||||
context['standards'] = Standard.objects.all().order_by('name')
|
||||
|
||||
@@ -25,6 +25,7 @@ from .models import (
|
||||
Standard,
|
||||
SigmaParMark,
|
||||
ObjectMark,
|
||||
ObjectInfo,
|
||||
SigmaParameter,
|
||||
Parameter,
|
||||
Satellite,
|
||||
@@ -394,6 +395,15 @@ class StandardAdmin(BaseAdmin):
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(ObjectInfo)
|
||||
class ObjectInfoAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели ObjectInfo (Тип объекта)."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
class SigmaParameterInline(admin.StackedInline):
|
||||
model = SigmaParameter
|
||||
extra = 0
|
||||
@@ -1036,20 +1046,26 @@ class ObjItemInline(admin.TabularInline):
|
||||
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Source."""
|
||||
|
||||
list_display = ("id", "created_at", "updated_at")
|
||||
list_display = ("id", "info", "created_at", "updated_at")
|
||||
list_select_related = ("info",)
|
||||
list_filter = (
|
||||
("info", MultiSelectRelatedDropdownFilter),
|
||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
search_fields = ("id",)
|
||||
search_fields = ("id", "info__name")
|
||||
ordering = ("-created_at",)
|
||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||
inlines = [ObjItemInline]
|
||||
|
||||
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",)
|
||||
|
||||
@@ -463,7 +463,16 @@ class SourceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Source
|
||||
fields = [] # Все поля обрабатываются вручную
|
||||
fields = ['info'] # Добавляем поле info
|
||||
widgets = {
|
||||
'info': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
'id': 'id_info',
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'info': 'Тип объекта',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
31
dbapp/mainapp/migrations/0008_objectinfo_source_info.py
Normal file
31
dbapp/mainapp/migrations/0008_objectinfo_source_info.py
Normal 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='Тип объекта'),
|
||||
),
|
||||
]
|
||||
@@ -67,6 +67,22 @@ class CustomUser(models.Model):
|
||||
verbose_name_plural = "Пользователи"
|
||||
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):
|
||||
"""
|
||||
@@ -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(
|
||||
srid=4326,
|
||||
null=True,
|
||||
|
||||
@@ -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>
|
||||
@@ -113,7 +113,13 @@
|
||||
<!-- Фильтры -->
|
||||
<div class="filter-section">
|
||||
<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>
|
||||
<select class="form-select" id="satellite" name="satellite">
|
||||
<option value="">Все спутники</option>
|
||||
@@ -124,7 +130,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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>
|
||||
<a href="{% url 'mainapp:object_marks' %}" class="btn btn-secondary">Сбросить</a>
|
||||
</div>
|
||||
|
||||
@@ -315,13 +315,22 @@
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>
|
||||
{% 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>
|
||||
{% 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;"
|
||||
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>
|
||||
{% else %}
|
||||
-
|
||||
@@ -1337,4 +1346,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -194,6 +194,19 @@
|
||||
</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>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<!-- Search bar -->
|
||||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||
<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:'' }}">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick="performSearch()">Найти</button>
|
||||
@@ -67,6 +67,12 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<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' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedSources()">
|
||||
@@ -194,7 +200,7 @@
|
||||
|
||||
<!-- LyngSat Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Тип объекта (ТВ):</label>
|
||||
<label class="form-label">ТВ или нет:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
|
||||
@@ -209,6 +215,24 @@
|
||||
</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 -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество точек:</label>
|
||||
@@ -227,6 +251,35 @@
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
</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 -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата ГЛ:</label>
|
||||
@@ -236,6 +289,96 @@
|
||||
placeholder="До" value="{{ geo_date_to|default:'' }}">
|
||||
</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 -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
@@ -267,15 +410,15 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</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: 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>
|
||||
{% if has_any_lyngsat %}
|
||||
<th scope="col" class="text-center" style="min-width: 80px;">Тип объекта</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="text-center" style="min-width: 80px;">ТВ или нет</th>
|
||||
<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">
|
||||
Кол-во точек
|
||||
@@ -317,7 +460,18 @@
|
||||
value="{{ 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_kupsat }}</td>
|
||||
<td>{{ source.coords_valid }}</td>
|
||||
@@ -345,7 +499,6 @@
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if has_any_lyngsat %}
|
||||
<td class="text-center">
|
||||
{% if source.has_lyngsat %}
|
||||
<a href="#" class="text-primary text-decoration-none"
|
||||
@@ -356,7 +509,6 @@
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-center">{{ source.objitem_count }}</td>
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
@@ -411,7 +563,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="12" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -775,6 +927,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setupRadioLikeCheckboxes('has_coords_valid');
|
||||
setupRadioLikeCheckboxes('has_coords_reference');
|
||||
setupRadioLikeCheckboxes('has_lyngsat');
|
||||
setupRadioLikeCheckboxes('has_signal_mark');
|
||||
|
||||
// Update filter counter on page load
|
||||
updateFilterCounter();
|
||||
@@ -896,10 +1049,10 @@ function showSourceDetails(sourceId) {
|
||||
// Build transponder cell
|
||||
let transponderCell = '-';
|
||||
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;" ' +
|
||||
'title="Показать данные транспондера">' +
|
||||
'<i class="bi bi-broadcast"></i> ' + objitem.transponder_info +
|
||||
objitem.transponder_info +
|
||||
'</a>';
|
||||
}
|
||||
|
||||
@@ -922,12 +1075,21 @@ function showSourceDetails(sourceId) {
|
||||
'</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">' +
|
||||
'<input type="checkbox" class="form-check-input modal-item-checkbox" value="' + objitem.id + '">' +
|
||||
'</td>' +
|
||||
'<td class="text-center">' + objitem.id + '</td>' +
|
||||
'<td>' + objitem.name + '</td>' +
|
||||
'<td>' + objitem.satellite_name + '</td>' +
|
||||
'<td>' + satelliteCell + '</td>' +
|
||||
'<td>' + transponderCell + '</td>' +
|
||||
'<td>' + objitem.frequency + '</td>' +
|
||||
'<td>' + objitem.freq_range + '</td>' +
|
||||
@@ -954,8 +1116,13 @@ function showSourceDetails(sourceId) {
|
||||
// Setup modal select-all checkbox
|
||||
setupModalSelectAll();
|
||||
|
||||
// Initialize column visibility
|
||||
// Initialize column visibility after DOM update
|
||||
// Use requestAnimationFrame to ensure DOM is rendered
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
initModalColumnVisibility();
|
||||
}, 50);
|
||||
});
|
||||
} else {
|
||||
// Show no data message
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
@@ -1002,21 +1169,27 @@ function setupModalSelectAll() {
|
||||
// Function to toggle modal column visibility
|
||||
function toggleModalColumn(checkbox) {
|
||||
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;
|
||||
|
||||
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');
|
||||
rows.forEach(row => {
|
||||
const cells = row.children;
|
||||
if (cells[columnIndex]) {
|
||||
if (checkbox.checked) {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = '';
|
||||
});
|
||||
cells[columnIndex].style.removeProperty('display');
|
||||
} else {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = 'none';
|
||||
});
|
||||
cells[columnIndex].style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to toggle all modal columns
|
||||
@@ -1032,11 +1205,31 @@ function toggleAllModalColumns(selectAllCheckbox) {
|
||||
function initModalColumnVisibility() {
|
||||
// Hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22)
|
||||
const columnsToHide = [16, 17, 18, 19, 20, 22];
|
||||
columnsToHide.forEach(columnIndex => {
|
||||
const checkbox = document.querySelector('.modal-column-toggle[data-column="' + columnIndex + '"]');
|
||||
if (checkbox && !checkbox.checked) {
|
||||
toggleModalColumn(checkbox);
|
||||
|
||||
// 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 => {
|
||||
// Get all rows in the table (including thead and tbody)
|
||||
const rows = table.querySelectorAll('tr');
|
||||
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 'mainapp/components/_sigma_parameter_modal.html' %}
|
||||
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -330,7 +330,16 @@
|
||||
</td>
|
||||
<td class="text-center">{{ transponder.id }}</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.uplink }}</td>
|
||||
<td>{{ transponder.frequency_range }}</td>
|
||||
@@ -574,4 +583,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,6 +26,7 @@ from .views import (
|
||||
ObjItemListView,
|
||||
ObjItemUpdateView,
|
||||
ProcessKubsatView,
|
||||
SatelliteDataAPIView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
@@ -79,6 +80,7 @@ urlpatterns = [
|
||||
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/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('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||
|
||||
@@ -1205,3 +1205,106 @@ def get_first_param_subquery(field_name: str):
|
||||
... print(obj.first_freq)
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -20,6 +20,7 @@ from .data_import import (
|
||||
from .api import (
|
||||
GetLocationsView,
|
||||
LyngsatDataAPIView,
|
||||
SatelliteDataAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
SourceObjItemsAPIView,
|
||||
LyngsatTaskStatusAPIView,
|
||||
@@ -71,6 +72,7 @@ __all__ = [
|
||||
# API
|
||||
'GetLocationsView',
|
||||
'LyngsatDataAPIView',
|
||||
'SatelliteDataAPIView',
|
||||
'SigmaParameterDataAPIView',
|
||||
'SourceObjItemsAPIView',
|
||||
'LyngsatTaskStatusAPIView',
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.utils import timezone
|
||||
from django.views import View
|
||||
|
||||
from ..models import ObjItem
|
||||
from ..utils import format_coordinate, format_coords_display, format_frequency, format_symbol_rate
|
||||
|
||||
|
||||
class GetLocationsView(LoginRequiredMixin, View):
|
||||
@@ -76,11 +77,11 @@ class LyngsatDataAPIView(LoginRequiredMixin, View):
|
||||
data = {
|
||||
'id': lyngsat.id,
|
||||
'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 '-',
|
||||
'modulation': lyngsat.modulation.name if lyngsat.modulation 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 '-',
|
||||
'channel_info': lyngsat.channel_info or '-',
|
||||
'last_update': last_update_str,
|
||||
@@ -146,13 +147,13 @@ class SigmaParameterDataAPIView(LoginRequiredMixin, View):
|
||||
sigma_data.append({
|
||||
'id': sigma.id,
|
||||
'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
|
||||
'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
|
||||
'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
|
||||
'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
|
||||
'frequency': format_frequency(sigma.frequency),
|
||||
'transfer_frequency': format_frequency(sigma.transfer_frequency),
|
||||
'freq_range': format_frequency(sigma.freq_range),
|
||||
'polarization': sigma.polarization.name if sigma.polarization else '-',
|
||||
'modulation': sigma.modulation.name if sigma.modulation 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 '-',
|
||||
'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
|
||||
'status': sigma.status or '-',
|
||||
@@ -235,6 +236,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||
# Get parameter data
|
||||
param = getattr(objitem, 'parameter_obj', None)
|
||||
satellite_name = '-'
|
||||
satellite_id = None
|
||||
frequency = '-'
|
||||
freq_range = '-'
|
||||
polarization = '-'
|
||||
@@ -248,11 +250,12 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||
parameter_id = param.id
|
||||
if hasattr(param, 'id_satellite') and param.id_satellite:
|
||||
satellite_name = param.id_satellite.name
|
||||
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
|
||||
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-'
|
||||
satellite_id = param.id_satellite.id
|
||||
frequency = format_frequency(param.frequency)
|
||||
freq_range = format_frequency(param.freq_range)
|
||||
if hasattr(param, 'polarization') and param.polarization:
|
||||
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:
|
||||
modulation = param.modulation.name
|
||||
if hasattr(param, 'standard') and param.standard:
|
||||
@@ -272,11 +275,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||
geo_location = objitem.geo_obj.location or '-'
|
||||
|
||||
if objitem.geo_obj.coords:
|
||||
longitude = objitem.geo_obj.coords.coords[0]
|
||||
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}"
|
||||
geo_coords = format_coords_display(objitem.geo_obj.coords)
|
||||
|
||||
# Get created/updated info
|
||||
created_at = '-'
|
||||
@@ -332,6 +331,7 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||
'id': objitem.id,
|
||||
'name': objitem.name or '-',
|
||||
'satellite_name': satellite_name,
|
||||
'satellite_id': satellite_id,
|
||||
'frequency': frequency,
|
||||
'freq_range': freq_range,
|
||||
'polarization': polarization,
|
||||
@@ -454,12 +454,12 @@ class TransponderDataAPIView(LoginRequiredMixin, View):
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or '-',
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
|
||||
'downlink': format_frequency(transponder.downlink),
|
||||
'uplink': format_frequency(transponder.uplink) if transponder.uplink else None,
|
||||
'frequency_range': format_frequency(transponder.frequency_range),
|
||||
'polarization': transponder.polarization.name if transponder.polarization else '-',
|
||||
'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,
|
||||
'created_at': created_at_str,
|
||||
'created_by': created_by_str,
|
||||
@@ -470,3 +470,57 @@ class TransponderDataAPIView(LoginRequiredMixin, View):
|
||||
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
|
||||
except Exception as e:
|
||||
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)
|
||||
|
||||
@@ -37,6 +37,11 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
if satellite_id:
|
||||
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
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -15,7 +15,13 @@ from django.views.generic import CreateView, DeleteView, UpdateView
|
||||
from ..forms import GeoForm, ObjItemForm, ParameterForm
|
||||
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
||||
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):
|
||||
@@ -323,13 +329,10 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
|
||||
|
||||
if obj.geo_obj.coords:
|
||||
longitude = obj.geo_obj.coords.coords[0]
|
||||
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}"
|
||||
geo_coords = format_coords_display(obj.geo_obj.coords)
|
||||
|
||||
satellite_name = "-"
|
||||
satellite_id = None
|
||||
frequency = "-"
|
||||
freq_range = "-"
|
||||
polarization_name = "-"
|
||||
@@ -347,18 +350,11 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
if hasattr(param.id_satellite, "name")
|
||||
else "-"
|
||||
)
|
||||
satellite_id = param.id_satellite.id
|
||||
|
||||
frequency = (
|
||||
f"{param.frequency:.3f}" if param.frequency is not None else "-"
|
||||
)
|
||||
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 "-"
|
||||
)
|
||||
frequency = format_frequency(param.frequency)
|
||||
freq_range = format_frequency(param.freq_range)
|
||||
bod_velocity = format_symbol_rate(param.bod_velocity)
|
||||
snr = f"{param.snr:.0f}" if param.snr is not None else "-"
|
||||
|
||||
if hasattr(param, "polarization") and param.polarization:
|
||||
@@ -396,8 +392,8 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
has_sigma = True
|
||||
first_sigma = param.sigma_parameter.first()
|
||||
if first_sigma:
|
||||
sigma_freq = f"{first_sigma.transfer_frequency:.3f}" if first_sigma.transfer_frequency else "-"
|
||||
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
|
||||
sigma_freq = format_frequency(first_sigma.transfer_frequency)
|
||||
sigma_range = format_frequency(first_sigma.freq_range)
|
||||
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_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
|
||||
@@ -407,6 +403,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"id": obj.id,
|
||||
"name": obj.name or "-",
|
||||
"satellite_name": satellite_name,
|
||||
"satellite_id": satellite_id,
|
||||
"frequency": frequency,
|
||||
"freq_range": freq_range,
|
||||
"polarization": polarization_name,
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.views import View
|
||||
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source, Satellite
|
||||
from ..utils import parse_pagination_params
|
||||
from ..utils import format_coords_display, parse_pagination_params
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, View):
|
||||
@@ -29,20 +29,38 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# Get sorting parameters (default to ID ascending)
|
||||
sort_param = request.GET.get("sort", "id")
|
||||
|
||||
# Get filter parameters
|
||||
# Get filter parameters - Source level
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
has_coords_average = request.GET.get("has_coords_average")
|
||||
has_coords_kupsat = request.GET.get("has_coords_kupsat")
|
||||
has_coords_valid = request.GET.get("has_coords_valid")
|
||||
has_coords_reference = request.GET.get("has_coords_reference")
|
||||
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_max = request.GET.get("objitem_count_max", "").strip()
|
||||
date_from = request.GET.get("date_from", "").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_to = request.GET.get("geo_date_to", "").strip()
|
||||
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
|
||||
satellites = (
|
||||
@@ -52,14 +70,44 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
# Build Q object for geo date filtering
|
||||
geo_date_q = Q()
|
||||
has_geo_date_filter = False
|
||||
# Get all polarizations, modulations for filters
|
||||
from ..models import Polarization, Modulation, ObjectInfo
|
||||
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:
|
||||
try:
|
||||
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)
|
||||
has_geo_date_filter = True
|
||||
objitem_filter_q &= Q(source_objitems__geo_obj__timestamp__gte=geo_date_from_obj)
|
||||
has_objitem_filter = True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
@@ -69,15 +117,105 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
|
||||
# Add one day to include entire end date
|
||||
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)
|
||||
has_geo_date_filter = True
|
||||
objitem_filter_q &= Q(source_objitems__geo_obj__timestamp__lt=geo_date_to_obj)
|
||||
has_objitem_filter = True
|
||||
except (ValueError, TypeError):
|
||||
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
|
||||
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||
# 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__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
@@ -85,7 +223,7 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'marks',
|
||||
'marks__created_by__user'
|
||||
).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
|
||||
@@ -121,6 +259,41 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
~Q(source_objitems__lyngsat_source__isnull=False)
|
||||
).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
|
||||
if objitem_count_min:
|
||||
try:
|
||||
@@ -155,17 +328,36 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
pass
|
||||
|
||||
# Filter by Geo timestamp range (only filter sources that have matching objitems)
|
||||
if has_geo_date_filter:
|
||||
sources = sources.filter(geo_date_q).distinct()
|
||||
if geo_date_from or geo_date_to:
|
||||
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:
|
||||
try:
|
||||
# Try to search by ID first
|
||||
search_id = int(search_query)
|
||||
sources = sources.filter(id=search_id)
|
||||
except ValueError:
|
||||
# If not a number, ignore
|
||||
pass
|
||||
# If not a number, search by name in related objitems
|
||||
sources = sources.filter(
|
||||
source_objitems__name__icontains=search_query
|
||||
).distinct()
|
||||
|
||||
# Filter by satellites
|
||||
if selected_satellites:
|
||||
@@ -173,6 +365,84 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
|
||||
).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
|
||||
valid_sort_fields = {
|
||||
"id": "id",
|
||||
@@ -194,27 +464,18 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
|
||||
# Prepare data for display
|
||||
processed_sources = []
|
||||
has_any_lyngsat = False # Track if any source has LyngSat data
|
||||
|
||||
for source in page_obj:
|
||||
# Format coordinates
|
||||
def format_coords(point):
|
||||
if point:
|
||||
longitude = point.coords[0]
|
||||
latitude = point.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"
|
||||
return f"{lat} {lon}"
|
||||
return "-"
|
||||
coords_average_str = format_coords_display(source.coords_average)
|
||||
coords_kupsat_str = format_coords_display(source.coords_kupsat)
|
||||
coords_valid_str = format_coords_display(source.coords_valid)
|
||||
coords_reference_str = format_coords_display(source.coords_reference)
|
||||
|
||||
coords_average_str = format_coords(source.coords_average)
|
||||
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
|
||||
# Filter objitems for display (to get satellites and lyngsat info)
|
||||
objitems_to_display = source.source_objitems.all()
|
||||
if geo_date_from or geo_date_to:
|
||||
|
||||
# Apply the same filters as in the count annotation
|
||||
if geo_date_from:
|
||||
try:
|
||||
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
|
||||
@@ -229,27 +490,85 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
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 count of related ObjItems (filtered)
|
||||
objitem_count = objitems_to_display.count()
|
||||
# Use annotated count (consistent with filtering)
|
||||
objitem_count = source.objitem_count
|
||||
|
||||
# Get satellites for this source and check for LyngSat
|
||||
# Get satellites, name and check for LyngSat
|
||||
satellite_names = set()
|
||||
satellite_ids = set()
|
||||
has_lyngsat = False
|
||||
lyngsat_id = None
|
||||
source_name = None
|
||||
|
||||
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, 'id_satellite') and objitem.parameter_obj.id_satellite:
|
||||
satellite_names.add(objitem.parameter_obj.id_satellite.name)
|
||||
satellite_ids.add(objitem.parameter_obj.id_satellite.id)
|
||||
|
||||
# Check if any objitem has LyngSat
|
||||
if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source:
|
||||
has_lyngsat = True
|
||||
lyngsat_id = objitem.lyngsat_source.id
|
||||
has_any_lyngsat = True
|
||||
|
||||
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)
|
||||
marks_data = []
|
||||
@@ -260,14 +579,20 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'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({
|
||||
'id': source.id,
|
||||
'name': source_name if source_name else '-',
|
||||
'info': info_name,
|
||||
'coords_average': coords_average_str,
|
||||
'coords_kupsat': coords_kupsat_str,
|
||||
'coords_valid': coords_valid_str,
|
||||
'coords_reference': coords_reference_str,
|
||||
'objitem_count': objitem_count,
|
||||
'satellite': satellite_str,
|
||||
'satellite_id': first_satellite_id,
|
||||
'created_at': source.created_at,
|
||||
'updated_at': source.updated_at,
|
||||
'has_lyngsat': has_lyngsat,
|
||||
@@ -283,22 +608,50 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'available_items_per_page': [50, 100, 500, 1000],
|
||||
'sort': sort_param,
|
||||
'search_query': search_query,
|
||||
# Source-level filters
|
||||
'has_coords_average': has_coords_average,
|
||||
'has_coords_kupsat': has_coords_kupsat,
|
||||
'has_coords_valid': has_coords_valid,
|
||||
'has_coords_reference': has_coords_reference,
|
||||
'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_max': objitem_count_max,
|
||||
'date_from': date_from,
|
||||
'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_to': geo_date_to,
|
||||
'satellites': 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()))
|
||||
],
|
||||
'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,
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ class TransponderListView(LoginRequiredMixin, View):
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or "-",
|
||||
'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 "-",
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else "-",
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",
|
||||
|
||||
Reference in New Issue
Block a user