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

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>
</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 %}

View File

@@ -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')

View File

@@ -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",)

View File

@@ -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)

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 = "Пользователи"
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,

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">
<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>

View File

@@ -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 %}

View File

@@ -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>
<!-- Блок с картой -->

View File

@@ -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,22 +1169,28 @@ 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
function toggleAllModalColumns(selectAllCheckbox) {
@@ -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 %}

View File

@@ -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 %}

View File

@@ -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'),

View File

@@ -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

View File

@@ -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',

View File

@@ -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)

View File

@@ -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):

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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 "-",