Compare commits

...

4 Commits

38 changed files with 4010 additions and 757 deletions

View File

@@ -24,6 +24,7 @@ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
path('', include('mainapp.urls', namespace='mainapp')), path('', include('mainapp.urls', namespace='mainapp')),
path('', include('mapsapp.urls', namespace='mapsapp')), path('', include('mapsapp.urls', namespace='mapsapp')),
path('lyngsat/', include('lyngsatapp.urls', namespace='lyngsatapp')),
# Authentication URLs # Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'), path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', custom_logout, name='logout'), path('logout/', custom_logout, name='logout'),

View File

@@ -0,0 +1,428 @@
{% extends 'mainapp/base.html' %}
{% block title %}Источники LyngSat{% endblock %}
{% block extra_css %}
<style>
.table-responsive tr.selected {
background-color: #d4edff;
}
.sticky-top {
position: sticky;
top: 0;
z-index: 10;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Источники LyngSat</h2>
</div>
</div>
<!-- Toolbar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3">
<!-- 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..."
value="{{ search_query|default:'' }}">
<button type="button" class="btn btn-outline-primary"
onclick="performSearch()">Найти</button>
<button type="button" class="btn btn-outline-secondary"
onclick="clearSearch()">Очистить</button>
</div>
</div>
<!-- Items per page select -->
<div>
<label for="items-per-page" class="form-label mb-0">Показать:</label>
<select name="items_per_page" id="items-per-page"
class="form-select form-select-sm d-inline-block" style="width: auto;"
onchange="updateItemsPerPage()">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<!-- Filter Toggle Button -->
<div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
<!-- Pagination -->
<div class="ms-auto">
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offcanvas Filter Panel -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
</div>
<div class="offcanvas-body">
<form method="get" id="filter-form">
<!-- Satellite 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('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</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>
<!-- Standard 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('standard_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('standard_id', false)">Снять</button>
</div>
<select name="standard_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for standard in standards %}
<option value="{{ standard.id }}" {% if standard.id in selected_standards %}selected{% endif %}>
{{ standard.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>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Символьная скорость, БОД:</label>
<input type="number" step="0.001" name="sym_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ sym_min|default:'' }}">
<input type="number" step="0.001" name="sym_max" class="form-control form-control-sm"
placeholder="До" value="{{ sym_max|default:'' }}">
</div>
<!-- Date Filter -->
<div class="mb-2">
<label class="form-label">Дата обновления:</label>
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ date_from|default:'' }}">
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
placeholder="До" value="{{ date_to|default:'' }}">
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
<!-- Main Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="min-width: 60px;">
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
ID
{% if sort == 'id' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-id' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 120px;">Спутник</th>
<th scope="col" style="min-width: 100px;">
<a href="javascript:void(0)" onclick="updateSort('frequency')" class="text-white text-decoration-none">
Частота, МГц
{% if sort == 'frequency' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-frequency' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Поляризация</th>
<th scope="col" style="min-width: 120px;">
<a href="javascript:void(0)" onclick="updateSort('sym_velocity')" class="text-white text-decoration-none">
Сим. скорость, БОД
{% if sort == 'sym_velocity' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-sym_velocity' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Модуляция</th>
<th scope="col" style="min-width: 100px;">Стандарт</th>
<th scope="col" style="min-width: 80px;">FEC</th>
<th scope="col" style="min-width: 150px;">Описание</th>
<th scope="col" style="min-width: 120px;">
<a href="javascript:void(0)" onclick="updateSort('last_update')" class="text-white text-decoration-none">
Обновлено
{% if sort == 'last_update' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-last_update' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Ссылка</th>
</tr>
</thead>
<tbody>
{% for item in lyngsat_items %}
<tr>
<td class="text-center">{{ item.id }}</td>
<td>{{ item.id_satellite.name|default:"-" }}</td>
<td>{{ item.frequency|floatformat:3|default:"-" }}</td>
<td>{{ item.polarization.name|default:"-" }}</td>
<td>{{ item.sym_velocity|floatformat:3|default:"-" }}</td>
<td>{{ item.modulation.name|default:"-" }}</td>
<td>{{ item.standard.name|default:"-" }}</td>
<td>{{ item.fec|default:"-" }}</td>
<td>{{ item.channel_info|default:"-" }}</td>
<td>{{ item.last_update|date:"d.m.Y"|default:"-" }}</td>
<td>
{% if item.url %}
<a href="{{ item.url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="Открыть ссылку">
<i class="bi bi-link-45deg"></i>
</a>
{% else %}
-
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Search functionality
function performSearch() {
const searchValue = document.getElementById('toolbar-search').value.trim();
const urlParams = new URLSearchParams(window.location.search);
if (searchValue) {
urlParams.set('search', searchValue);
} else {
urlParams.delete('search');
}
urlParams.delete('page');
window.location.search = urlParams.toString();
}
function clearSearch() {
document.getElementById('toolbar-search').value = '';
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete('search');
urlParams.delete('page');
window.location.search = urlParams.toString();
}
// Handle Enter key in search input
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
// Items per page functionality
function updateItemsPerPage() {
const itemsPerPage = document.getElementById('items-per-page').value;
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('items_per_page', itemsPerPage);
urlParams.delete('page');
window.location.search = urlParams.toString();
}
// Sorting functionality
function updateSort(field) {
const urlParams = new URLSearchParams(window.location.search);
const currentSort = urlParams.get('sort');
let newSort;
if (currentSort === field) {
newSort = '-' + field;
} else if (currentSort === '-' + field) {
newSort = field;
} else {
newSort = field;
}
urlParams.set('sort', newSort);
urlParams.delete('page');
window.location.search = urlParams.toString();
}
// Function to select/deselect all options in a select element
function selectAllOptions(selectName, selectAll) {
const selectElement = document.querySelector(`select[name="${selectName}"]`);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
}
}
// Filter counter functionality
function updateFilterCounter() {
const form = document.getElementById('filter-form');
const formData = new FormData(form);
let filterCount = 0;
// Count non-empty form fields
for (const [key, value] of formData.entries()) {
if (value && value.trim() !== '') {
// For multi-select fields, skip counting individual selections
if (key === 'satellite_id' || key === 'polarization_id' || key === 'modulation_id' || key === 'standard_id') {
continue;
}
filterCount++;
}
}
// Count selected options in multi-select fields
const multiSelectFields = ['satellite_id', 'polarization_id', 'modulation_id', 'standard_id'];
multiSelectFields.forEach(fieldName => {
const selectElement = document.querySelector(`select[name="${fieldName}"]`);
if (selectElement) {
const selectedOptions = Array.from(selectElement.selectedOptions).filter(opt => opt.selected);
if (selectedOptions.length > 0) {
filterCount++;
}
}
});
// Display the filter counter
const counterElement = document.getElementById('filterCounter');
if (counterElement) {
if (filterCount > 0) {
counterElement.textContent = filterCount;
counterElement.style.display = 'inline';
} else {
counterElement.style.display = 'none';
}
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Update filter counter on page load
updateFilterCounter();
// Add event listeners to form elements to update counter when filters change
const form = document.getElementById('filter-form');
if (form) {
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
inputFields.forEach(input => {
input.addEventListener('input', updateFilterCounter);
input.addEventListener('change', updateFilterCounter);
});
const selectFields = form.querySelectorAll('select');
selectFields.forEach(select => {
select.addEventListener('change', updateFilterCounter);
});
}
// Update counter when offcanvas is shown
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
});
</script>
{% endblock %}

8
dbapp/lyngsatapp/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name = 'lyngsatapp'
urlpatterns = [
path('', views.LyngSatListView.as_view(), name='lyngsat_list'),
]

View File

@@ -1,3 +1,147 @@
from django.shortcuts import render from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q
from django.views.generic import ListView
# Create your views here. from .models import LyngSat
from mainapp.models import Satellite, Polarization, Modulation, Standard
from mainapp.utils import parse_pagination_params
class LyngSatListView(LoginRequiredMixin, ListView):
"""
Представление для отображения списка источников LyngSat с фильтрацией и пагинацией.
"""
model = LyngSat
template_name = 'lyngsatapp/lyngsat_list.html'
context_object_name = 'lyngsat_items'
paginate_by = 50
def get_queryset(self):
"""
Возвращает отфильтрованный и отсортированный queryset.
"""
queryset = LyngSat.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).all()
# Поиск по ID
search_query = self.request.GET.get('search', '').strip()
if search_query:
try:
search_id = int(search_query)
queryset = queryset.filter(id=search_id)
except ValueError:
queryset = queryset.none()
# Фильтр по спутнику
satellite_ids = self.request.GET.getlist('satellite_id')
if satellite_ids:
queryset = queryset.filter(id_satellite_id__in=satellite_ids)
# Фильтр по поляризации
polarization_ids = self.request.GET.getlist('polarization_id')
if polarization_ids:
queryset = queryset.filter(polarization_id__in=polarization_ids)
# Фильтр по модуляции
modulation_ids = self.request.GET.getlist('modulation_id')
if modulation_ids:
queryset = queryset.filter(modulation_id__in=modulation_ids)
# Фильтр по стандарту
standard_ids = self.request.GET.getlist('standard_id')
if standard_ids:
queryset = queryset.filter(standard_id__in=standard_ids)
# Фильтр по частоте
freq_min = self.request.GET.get('freq_min', '').strip()
freq_max = self.request.GET.get('freq_max', '').strip()
if freq_min:
try:
queryset = queryset.filter(frequency__gte=float(freq_min))
except ValueError:
pass
if freq_max:
try:
queryset = queryset.filter(frequency__lte=float(freq_max))
except ValueError:
pass
# Фильтр по символьной скорости
sym_min = self.request.GET.get('sym_min', '').strip()
sym_max = self.request.GET.get('sym_max', '').strip()
if sym_min:
try:
queryset = queryset.filter(sym_velocity__gte=float(sym_min))
except ValueError:
pass
if sym_max:
try:
queryset = queryset.filter(sym_velocity__lte=float(sym_max))
except ValueError:
pass
# Фильтр по дате обновления
date_from = self.request.GET.get('date_from', '').strip()
date_to = self.request.GET.get('date_to', '').strip()
if date_from:
queryset = queryset.filter(last_update__gte=date_from)
if date_to:
queryset = queryset.filter(last_update__lte=date_to)
# Сортировка
sort = self.request.GET.get('sort', '-id')
valid_sort_fields = ['id', '-id', 'frequency', '-frequency', 'sym_velocity', '-sym_velocity', 'last_update', '-last_update']
if sort in valid_sort_fields:
queryset = queryset.order_by(sort)
else:
queryset = queryset.order_by('-id')
return queryset
def get_context_data(self, **kwargs):
"""
Добавляет дополнительный контекст для шаблона.
"""
context = super().get_context_data(**kwargs)
# Параметры пагинации
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
context['items_per_page'] = items_per_page
context['available_items_per_page'] = [25, 50, 100, 200, 500]
# Пагинация
paginator = Paginator(self.get_queryset(), items_per_page)
page_obj = paginator.get_page(page_number)
context['page_obj'] = page_obj
context['lyngsat_items'] = page_obj.object_list
# Параметры поиска и фильтрации
context['search_query'] = self.request.GET.get('search', '')
context['sort'] = self.request.GET.get('sort', '-id')
# Данные для фильтров
context['satellites'] = Satellite.objects.all().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')
# Выбранные фильтры
context['selected_satellites'] = [int(x) for x in self.request.GET.getlist('satellite_id') if x.isdigit()]
context['selected_polarizations'] = [int(x) for x in self.request.GET.getlist('polarization_id') if x.isdigit()]
context['selected_modulations'] = [int(x) for x in self.request.GET.getlist('modulation_id') if x.isdigit()]
context['selected_standards'] = [int(x) for x in self.request.GET.getlist('standard_id') if x.isdigit()]
# Параметры фильтров
context['freq_min'] = self.request.GET.get('freq_min', '')
context['freq_max'] = self.request.GET.get('freq_max', '')
context['sym_min'] = self.request.GET.get('sym_min', '')
context['sym_max'] = self.request.GET.get('sym_max', '')
context['date_from'] = self.request.GET.get('date_from', '')
context['date_to'] = self.request.GET.get('date_to', '')
return context

View File

@@ -24,6 +24,7 @@ from .models import (
Modulation, Modulation,
Standard, Standard,
SigmaParMark, SigmaParMark,
ObjectMark,
SigmaParameter, SigmaParameter,
Parameter, Parameter,
Satellite, Satellite,
@@ -339,6 +340,23 @@ class ParameterInline(admin.StackedInline):
# ============================================================================ # ============================================================================
@admin.register(ObjectMark)
class ObjectMarkAdmin(BaseAdmin):
"""Админ-панель для модели ObjectMark."""
list_display = ("source", "mark", "timestamp", "created_by")
list_select_related = ("source", "created_by__user")
search_fields = ("source__id",)
ordering = ("-timestamp",)
list_filter = (
"mark",
("timestamp", DateRangeQuickSelectListFilterBuilder()),
("source", MultiSelectRelatedDropdownFilter),
)
readonly_fields = ("timestamp", "created_by")
autocomplete_fields = ("source",)
@admin.register(SigmaParMark) @admin.register(SigmaParMark)
class SigmaParMarkAdmin(BaseAdmin): class SigmaParMarkAdmin(BaseAdmin):
"""Админ-панель для модели SigmaParMark.""" """Админ-панель для модели SigmaParMark."""
@@ -1023,6 +1041,7 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
("created_at", DateRangeQuickSelectListFilterBuilder()), ("created_at", DateRangeQuickSelectListFilterBuilder()),
("updated_at", DateRangeQuickSelectListFilterBuilder()), ("updated_at", DateRangeQuickSelectListFilterBuilder()),
) )
search_fields = ("id",)
ordering = ("-created_at",) ordering = ("-created_at",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
inlines = [ObjItemInline] inlines = [ObjItemInline]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-11-16 10:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0004_change_geo_mirrors_to_satellites'),
]
operations = [
migrations.AlterModelOptions(
name='sigmaparmark',
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка сигнала', 'verbose_name_plural': 'Отметки сигналов'},
),
migrations.CreateModel(
name='ObjectMark',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mark', models.BooleanField(blank=True, help_text='True - объект обнаружен, False - объект отсутствует', null=True, verbose_name='Наличие объекта')),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Время фиксации отметки', verbose_name='Время')),
('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший отметку', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marks_created', to='mainapp.customuser', verbose_name='Создан пользователем')),
('objitem', models.ForeignKey(help_text='Связанный объект', on_delete=django.db.models.deletion.CASCADE, related_name='marks', to='mainapp.objitem', verbose_name='Объект')),
],
options={
'verbose_name': 'Отметка объекта',
'verbose_name_plural': 'Отметки объектов',
'ordering': ['-timestamp'],
},
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2025-11-16 15:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0005_alter_sigmaparmark_options_objectmark'),
]
operations = [
migrations.AlterModelOptions(
name='objectmark',
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка источника', 'verbose_name_plural': 'Отметки источников'},
),
migrations.RemoveField(
model_name='objectmark',
name='objitem',
),
migrations.AddField(
model_name='objectmark',
name='source',
field=models.ForeignKey(help_text='Связанный источник', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='marks', to='mainapp.source', verbose_name='Источник'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-16 15:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_change_objectmark_to_source'),
]
operations = [
migrations.AlterField(
model_name='objectmark',
name='source',
field=models.ForeignKey(help_text='Связанный источник', on_delete=django.db.models.deletion.CASCADE, related_name='marks', to='mainapp.source', verbose_name='Источник'),
),
]

View File

@@ -68,9 +68,75 @@ class CustomUser(models.Model):
ordering = ["user__username"] ordering = ["user__username"]
class ObjectMark(models.Model):
"""
Модель отметки о наличии объекта.
Используется для фиксации моментов времени когда объект был обнаружен или отсутствовал.
"""
# Основные поля
mark = models.BooleanField(
null=True,
blank=True,
verbose_name="Наличие объекта",
help_text="True - объект обнаружен, False - объект отсутствует",
)
timestamp = models.DateTimeField(
auto_now_add=True,
verbose_name="Время",
db_index=True,
help_text="Время фиксации отметки",
)
source = models.ForeignKey(
'Source',
on_delete=models.CASCADE,
related_name="marks",
verbose_name="Источник",
help_text="Связанный источник",
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="marks_created",
null=True,
blank=True,
verbose_name="Создан пользователем",
help_text="Пользователь, создавший отметку",
)
def can_edit(self):
"""Проверка возможности редактирования отметки (в течение 5 минут)"""
from datetime import timedelta
if not self.timestamp:
return False
time_diff = timezone.now() - self.timestamp
return time_diff < timedelta(minutes=5)
def can_add_new_mark_for_object(self):
"""Проверка возможности добавления новой отметки для объекта (прошло 5 минут с последней)"""
from datetime import timedelta
if not self.timestamp:
return True
time_diff = timezone.now() - self.timestamp
return time_diff >= timedelta(minutes=5)
def __str__(self):
if self.timestamp:
timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
return f"+ {timestamp}" if self.mark else f"- {timestamp}"
return "Отметка без времени"
class Meta:
verbose_name = "Отметка источника"
verbose_name_plural = "Отметки источников"
ordering = ["-timestamp"]
# Для обратной совместимости с SigmaParameter
class SigmaParMark(models.Model): class SigmaParMark(models.Model):
""" """
Модель отметки о наличии сигнала. Модель отметки о наличии сигнала (для Sigma).
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян. Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
""" """
@@ -97,8 +163,8 @@ class SigmaParMark(models.Model):
return "Отметка без времени" return "Отметка без времени"
class Meta: class Meta:
verbose_name = "Отметка" verbose_name = "Отметка сигнала"
verbose_name_plural = "Отметки" verbose_name_plural = "Отметки сигналов"
ordering = ["-timestamp"] ordering = ["-timestamp"]

View File

@@ -205,6 +205,28 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Unlink All LyngSat Sources Card -->
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="bg-warning bg-opacity-10 rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-unlink text-warning" viewBox="0 0 16 16">
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9q-.13 0-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4 4 0 0 1-.82 1H12a3 3 0 1 0 0-6z"/>
<path d="M1 1l14 14"/>
</svg>
</div>
<h3 class="card-title mb-0">Отвязка всех источников LyngSat</h3>
</div>
<p class="card-text">Отвязать все источники LyngSat от объектов. Все объекты перестанут отображаться как "ТВ" источники. Операция обратима через повторную привязку.</p>
<a href="{% url 'mainapp:unlink_all_lyngsat' %}" class="btn btn-warning">
Отвязать все источники
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -13,15 +13,27 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Главная</a>
</li> -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a> <a class="nav-link" href="{% url 'mainapp:source_list' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Точки</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a> <a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{% url 'lyngsatapp:lyngsat_list' %}">LyngSat</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a> <a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:object_marks' %}">Отметки</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a> <a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li> </li>

View File

@@ -0,0 +1,126 @@
<!-- ObjItems Table Component -->
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Объекты (ObjItems)</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered mb-0" style="font-size: 0.9rem;">
<thead class="table-dark sticky-top">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Спутник</th>
<th>Частота, МГц</th>
<th>Полоса, МГц</th>
<th>Поляризация</th>
<th>Модуляция</th>
<th>Сим. v</th>
<th>ОСШ</th>
<th>Геолокация</th>
<th>Дата гео</th>
<th>Объект</th>
<th>LyngSat</th>
{% if show_marks == '1' %}
<th>Отметки</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for item in processed_objitems %}
<tr>
<td><a href="{% url 'mainapp:objitem_detail' item.id %}">{{ item.id }}</a></td>
<td>{{ item.name }}</td>
<td>{{ item.satellite }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.geo_date }}</td>
<td>
{% if item.source_id %}
<a href="{% url 'mainapp:source_update' item.source_id %}">{{ item.source_id }}</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if item.lyngsat_id %}
<a href="{% url 'admin:lyngsatapp_lyngsat_change' item.lyngsat_id %}" target="_blank">
<i class="bi bi-link-45deg"></i>
</a>
{% else %}
-
{% endif %}
</td>
{% if show_marks == '1' %}
<td>
{% if item.marks %}
<div style="max-height: 150px; overflow-y: auto;">
{% for mark in item.marks %}
<div class="mb-1">
<span class="mark-badge {% if mark.mark %}mark-present{% else %}mark-absent{% endif %}">
{% if mark.mark %}✓ Есть{% else %}✗ Нет{% endif %}
</span>
<br>
<small class="text-muted">{{ mark.timestamp|date:"d.m.Y H:i" }}</small>
<br>
<small class="text-muted">{{ mark.created_by }}</small>
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="{% if show_marks == '1' %}14{% else %}13{% endif %}" class="text-center py-4 text-muted">
Нет данных для выбранных фильтров
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer">
<nav>
<ul class="pagination justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
<div class="text-center mt-2">
<small class="text-muted">Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</small>
</div>
</div>
{% endif %}
</div>

View File

@@ -24,7 +24,7 @@
<!-- Table container --> <!-- Table container -->
<div class="flex-grow-1 overflow-auto"> <div class="flex-grow-1 overflow-auto">
<div class="table-responsive" style="height: 100%;"> <div class="table-responsive" style="height: 100%;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th scope="col" class="text-center" style="width: 3%;"> <th scope="col" class="text-center" style="width: 3%;">

View File

@@ -180,10 +180,10 @@ function showSigmaParameterModal(parameterId) {
if (sigma.marks.length > 0) { if (sigma.marks.length > 0) {
html += ` html += `
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;"> <div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
<table class="table table-sm table-striped mb-0"> <table class="table table-sm table-striped table-bordered mb-0">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th style="width: 20%;">Отметка</th> <th style="width: 20%;">Наличие сигнала</th>
<th>Дата</th> <th>Дата</th>
</tr> </tr>
</thead> </thead>

View File

@@ -0,0 +1,122 @@
<!-- Sources Table Component -->
<style>
.mark-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8rem;
margin: 2px 0;
}
.mark-present {
background: #28a745;
color: white;
}
.mark-absent {
background: #dc3545;
color: white;
}
</style>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Объекты (Sources)</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered mb-0">
<thead class="table-dark sticky-top">
<tr>
<th>ID</th>
<th>Спутники</th>
<th>Кол-во объектов</th>
<th>Усреднённые координаты</th>
<th>Координаты Кубсата</th>
<th>Координаты оперативников</th>
<th>Справочные координаты</th>
<th>Дата создания</th>
{% if show_marks == '1' %}
<th>Отметки</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for source in processed_sources %}
<tr>
<td><a href="{% url 'mainapp:source_update' source.id %}">{{ source.id }}</a></td>
<td>{{ source.satellites }}</td>
<td>{{ source.objitem_count }}</td>
<td>{{ source.coords_average }}</td>
<td>{{ source.coords_kupsat }}</td>
<td>{{ source.coords_valid }}</td>
<td>{{ source.coords_reference }}</td>
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
{% if show_marks == '1' %}
<td>
{% if source.marks %}
<div style="max-height: 150px; overflow-y: auto;">
{% for mark in source.marks %}
<div class="mb-1">
<span class="mark-badge {% if mark.mark %}mark-present{% else %}mark-absent{% endif %}">
{% if mark.mark %}✓ Есть{% else %}✗ Нет{% endif %}
</span>
<br>
<small class="text-muted">{{ mark.timestamp|date:"d.m.Y H:i" }}</small>
<br>
<small class="text-muted">{{ mark.created_by }}</small>
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="{% if show_marks == '1' %}9{% else %}8{% endif %}" class="text-center py-4 text-muted">
Нет данных для выбранных фильтров
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer">
<nav>
<ul class="pagination justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
<div class="text-center mt-2">
<small class="text-muted">Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</small>
</div>
</div>
{% endif %}
</div>

View File

@@ -1,422 +1,483 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% load static %}
{% block title %}Список объектов{% endblock %} {% block title %}Главная страница{% endblock %}
{% block extra_css %}
<style>
.filter-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.filter-group {
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.filter-group-title {
font-weight: 600;
color: #495057;
margin-bottom: 10px;
font-size: 0.95rem;
}
.conditional-filters {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e9ecef;
}
.table-container {
display: none;
margin-top: 20px;
}
.table-container.show {
display: block;
}
.btn-generate {
font-size: 1.1rem;
padding: 12px 40px;
font-weight: 600;
}
.filter-badge {
display: inline-block;
padding: 4px 10px;
background: #007bff;
color: white;
border-radius: 12px;
font-size: 0.85rem;
margin: 2px;
}
.active-filters {
margin-bottom: 15px;
}
.mark-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8rem;
margin: 2px 0;
}
.mark-present {
background: #28a745;
color: white;
}
.mark-absent {
background: #dc3545;
color: white;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-3"> <div class="container-fluid px-4 py-3">
<div class="row mb-3"> <h2 class="mb-4">Главная страница - Динамический отчёт</h2>
<div class="col-12">
<h2>Список объектов</h2>
</div>
</div>
<!-- Toolbar --> <!-- Фильтры -->
<div class="row mb-3"> <div class="filter-section">
<div class="col-12"> <form method="get" id="filter-form">
<div class="card"> <div class="row">
<div class="card-body"> <!-- Основной выбор: Объекти или Объекты -->
<div class="d-flex flex-wrap align-items-center gap-3"> <div class="col-12">
<div style="min-width: 300px; flex-grow: 1;"> <div class="filter-group">
<label for="toolbar-search" class="form-label mb-0">Поиск:</label> <div class="filter-group-title">1. Тип отображения</div>
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по имени, местоположению..." value="{{ search_query|default:'' }}"> <div class="btn-group w-100" role="group">
</div> <input type="radio" class="btn-check" name="display_mode" id="mode_sources" value="sources"
<div class="ms-auto"> {% if display_mode == 'sources' %}checked{% endif %} onchange="updateConditionalFilters()">
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button> <label class="btn btn-outline-primary" for="mode_sources">Объекти (Source)</label>
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
<input type="radio" class="btn-check" name="display_mode" id="mode_objitems" value="objitems"
{% if display_mode == 'objitems' %}checked{% endif %} onchange="updateConditionalFilters()">
<label class="btn btn-outline-primary" for="mode_objitems">Объекты (ObjItem)</label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div class="row g-3"> <div class="row">
<!-- Filters Sidebar - Made narrower --> <!-- Общие фильтры -->
<div class="col-md-2"> <div class="col-md-6">
<div class="card h-100"> <div class="filter-group">
<div class="card-body"> <div class="filter-group-title">2. Общие фильтры</div>
<h5 class="card-title">Фильтры</h5>
<form method="get" id="filter-form"> <!-- Спутники -->
<!-- Satellite Selection - Multi-select --> <div class="mb-3">
<div class="mb-2"> <label class="form-label">Спутники:</label>
<label class="form-label">Спутник:</label> <div class="d-flex gap-2 mb-2">
<div class="d-flex justify-content-between mb-1"> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('satellite_id', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('satellite_id', false)">Снять</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div> </div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4"> <select name="satellite_id" class="form-select form-select-sm" multiple size="5">
{% for satellite in satellites %} {% for sat in satellites %}
<option value="{{ satellite.id }}" <option value="{{ sat.id }}" {% if sat.id in selected_satellites %}selected{% endif %}>
{% if satellite.id in selected_satellites %}selected{% endif %}> {{ sat.name }}
{{ satellite.name }} </option>
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<!-- Frequency Filter --> </div>
<div class="mb-2"> </div>
<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>
<!-- Range Filter --> <!-- Условные фильтры (зависят от типа отображения) -->
<div class="mb-2"> <div class="col-md-6">
<label class="form-label">Полоса, МГц:</label> <!-- Фильтры для Объектов -->
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}"> <div class="filter-group" id="sources-filters" style="display: none;">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}"> <div class="filter-group-title">3. Фильтры для Объектов</div>
</div>
<!-- SNR Filter --> <div class="mb-3">
<div class="mb-2"> <label class="form-label">Усреднённые координаты:</label>
<label class="form-label">ОСШ:</label> <div class="form-check">
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}"> <input class="form-check-input" type="radio" name="has_coords_average" id="avg_all" value="" {% if not has_coords_average %}checked{% endif %}>
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}"> <label class="form-check-label" for="avg_all">Все</label>
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Сим. v, БОД:</label>
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
</div>
<!-- Removed old search input as it's now in the toolbar -->
<!-- Modulation 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('modulation', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
</div> </div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4"> <div class="form-check">
{% for mod in modulations %} <input class="form-check-input" type="radio" name="has_coords_average" id="avg_yes" value="1" {% if has_coords_average == '1' %}checked{% endif %}>
<option value="{{ mod.id }}" <label class="form-check-label" for="avg_yes">Есть</label>
{% if mod.id in selected_modulations %}selected{% endif %}> </div>
{{ mod.name }} <div class="form-check">
</option> <input class="form-check-input" type="radio" name="has_coords_average" id="avg_no" value="0" {% if has_coords_average == '0' %}checked{% endif %}>
{% endfor %} <label class="form-check-label" for="avg_no">Нет</label>
</select>
</div>
<!-- Polarization 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('polarization', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
</div> </div>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}"
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div> </div>
<!-- Kubsat Coordinates Filter --> <div class="mb-3">
<div class="mb-2">
<label class="form-label">Координаты Кубсата:</label> <label class="form-label">Координаты Кубсата:</label>
<div> <div class="form-check">
<div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="has_kupsat" id="kup_all" value="" {% if not has_kupsat %}checked{% endif %}>
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1" <label class="form-check-label" for="kup_all">Все</label>
{% if has_kupsat == '1' %}checked{% endif %}> </div>
<label class="form-check-label" for="has_kupsat_1">Есть</label> <div class="form-check">
</div> <input class="form-check-input" type="radio" name="has_kupsat" id="kup_yes" value="1" {% if has_kupsat == '1' %}checked{% endif %}>
<div class="form-check form-check-inline"> <label class="form-check-label" for="kup_yes">Есть</label>
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0" </div>
{% if has_kupsat == '0' %}checked{% endif %}> <div class="form-check">
<label class="form-check-label" for="has_kupsat_0">Нет</label> <input class="form-check-input" type="radio" name="has_kupsat" id="kup_no" value="0" {% if has_kupsat == '0' %}checked{% endif %}>
</div> <label class="form-check-label" for="kup_no">Нет</label>
</div> </div>
</div> </div>
<!-- Valid Coordinates Filter --> <div class="mb-3">
<div class="mb-2"> <label class="form-label">Координаты оперативников:</label>
<label class="form-label">Координаты опер. отдела:</label> <div class="form-check">
<div> <input class="form-check-input" type="radio" name="has_valid" id="val_all" value="" {% if not has_valid %}checked{% endif %}>
<div class="form-check form-check-inline"> <label class="form-check-label" for="val_all">Все</label>
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1" </div>
{% if has_valid == '1' %}checked{% endif %}> <div class="form-check">
<label class="form-check-label" for="has_valid_1">Есть</label> <input class="form-check-input" type="radio" name="has_valid" id="val_yes" value="1" {% if has_valid == '1' %}checked{% endif %}>
</div> <label class="form-check-label" for="val_yes">Есть</label>
<div class="form-check form-check-inline"> </div>
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0" <div class="form-check">
{% if has_valid == '0' %}checked{% endif %}> <input class="form-check-input" type="radio" name="has_valid" id="val_no" value="0" {% if has_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_0">Нет</label> <label class="form-check-label" for="val_no">Нет</label>
</div>
</div> </div>
</div> </div>
<!-- Items Per Page --> <div class="mb-3">
<div class="mb-2"> <label class="form-label">Справочные координаты:</label>
<label for="items-per-page" class="form-label">Элементов:</label> <div class="form-check">
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();"> <input class="form-check-input" type="radio" name="has_reference" id="ref_all" value="" {% if not has_reference %}checked{% endif %}>
{% for option in available_items_per_page %} <label class="form-check-label" for="ref_all">Все</label>
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}> </div>
{{ option }} <div class="form-check">
</option> <input class="form-check-input" type="radio" name="has_reference" id="ref_yes" value="1" {% if has_reference == '1' %}checked{% endif %}>
<label class="form-check-label" for="ref_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_reference" id="ref_no" value="0" {% if has_reference == '0' %}checked{% endif %}>
<label class="form-check-label" for="ref_no">Нет</label>
</div>
</div>
<!-- Наличие сигнала для объектов -->
<div class="conditional-filters">
<div class="mb-3">
<label class="form-label fw-bold">Отображать наличие сигнала:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="show_marks" id="marks_src" value="1"
{% if show_marks == '1' %}checked{% endif %} onchange="toggleMarksFilters()">
<label class="form-check-label" for="marks_src">Да</label>
</div>
</div>
<div id="marks-additional-filters-src" style="{% if show_marks != '1' %}display:none;{% endif %}">
<div class="mb-3">
<label class="form-label">Период отметок:</label>
<input type="datetime-local" name="marks_date_from" class="form-control form-control-sm mb-1" value="{{ marks_date_from }}">
<input type="datetime-local" name="marks_date_to" class="form-control form-control-sm" value="{{ marks_date_to }}">
</div>
<div class="mb-3">
<label class="form-label">Статус отметок:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_all_src" value="" {% if not marks_status %}checked{% endif %}>
<label class="form-check-label" for="marks_all_src">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_present_src" value="present" {% if marks_status == 'present' %}checked{% endif %}>
<label class="form-check-label" for="marks_present_src">✓ Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_absent_src" value="absent" {% if marks_status == 'absent' %}checked{% endif %}>
<label class="form-check-label" for="marks_absent_src">✗ Нет</label>
</div>
</div>
</div>
</div>
</div>
<!-- Фильтры для Объектов -->
<div class="filter-group" id="objitems-filters" style="display: none;">
<div class="filter-group-title">3. Фильтры для Объектов</div>
<!-- Дата геолокации -->
<div class="mb-3">
<label class="form-label">Дата геолокации:</label>
<input type="date" name="date_from" class="form-control form-control-sm mb-1" value="{{ date_from }}">
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to }}">
</div>
<!-- Частота -->
<div class="mb-3">
<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 }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max }}">
</div>
<!-- Полоса -->
<div class="mb-3">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max }}">
</div>
<!-- Модуляция -->
<div class="mb-3">
<label class="form-label">Модуляция:</label>
<div class="d-flex gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm" multiple size="4">
{% for mod in modulations %}
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<!-- Apply Filters and Reset Buttons --> <!-- Поляризация -->
<div class="d-grid gap-2 mt-2"> <div class="mb-3">
<button type="submit" class="btn btn-primary btn-sm">Применить</button> <label class="form-label">Поляризация:</label>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a> <div class="d-flex gap-2 mb-2">
</div> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('polarization', true)">Все</button>
</form> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('polarization', false)">Снять</button>
</div> </div>
</div> <select name="polarization" class="form-select form-select-sm" multiple size="4">
</div> {% for pol in polarizations %}
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
<!-- Main Table --> {{ pol.name }}
<div class="col-md-10"> </option>
<div class="card h-100">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input">
</th>
<th scope="col">Имя</th>
<th scope="col">Спутник</th>
<th scope="col">Част, МГц</th>
<th scope="col">Полоса, МГц</th>
<th scope="col">Поляр</th>
<th scope="col">Сим. v</th>
<th scope="col">Модул</th>
<th scope="col">ОСШ</th>
<th scope="col">Геолокация</th>
<th scope="col">Кубсат</th>
<th scope="col">Опер. отд</th>
<th scope="col">Гео-куб, км</th>
<th scope="col">Гео-опер, км</th>
<th scope="col">Куб-опер, км</th>
</tr>
</thead>
<tbody>
{% for item in processed_objects %}
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
</td>
<td>{{ item.name }}</td>
<td>{{ item.satellite_name }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td>
<td>{{ item.distance_geo_kup }}</td>
<td>{{ item.distance_geo_valid }}</td>
<td>{{ item.distance_kup_valid }}</td>
</tr>
{% empty %}
<tr>
<td colspan="15" class="text-center py-4">
{% if selected_satellite_id %}
Нет данных для выбранных фильтров
{% else %}
Пожалуйста, выберите спутник для отображения данных
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </select>
</table> </div>
<div class="mb-3">
<label class="form-label">Координаты геолокации:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_all" value="" {% if not has_geo %}checked{% endif %}>
<label class="form-check-label" for="geo_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_yes" value="1" {% if has_geo == '1' %}checked{% endif %}>
<label class="form-check-label" for="geo_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_no" value="0" {% if has_geo == '0' %}checked{% endif %}>
<label class="form-check-label" for="geo_no">Нет</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Связь с LyngSat:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_all" value="" {% if not has_lyngsat %}checked{% endif %}>
<label class="form-check-label" for="lyng_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_yes" value="1" {% if has_lyngsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="lyng_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_no" value="0" {% if has_lyngsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="lyng_no">Нет</label>
</div>
</div>
<!-- Наличие сигнала -->
<div class="conditional-filters">
<div class="mb-3">
<label class="form-label fw-bold">Отображать наличие сигнала:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="show_marks" id="marks_obj" value="1"
{% if show_marks == '1' %}checked{% endif %} onchange="toggleMarksFilters()">
<label class="form-check-label" for="marks_obj">Да</label>
</div>
</div>
<div id="marks-additional-filters" style="{% if show_marks != '1' %}display:none;{% endif %}">
<div class="mb-3">
<label class="form-label">Период отметок:</label>
<input type="datetime-local" name="marks_date_from" class="form-control form-control-sm mb-1" value="{{ marks_date_from }}">
<input type="datetime-local" name="marks_date_to" class="form-control form-control-sm" value="{{ marks_date_to }}">
</div>
<div class="mb-3">
<label class="form-label">Статус отметок:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_all" value="" {% if not marks_status %}checked{% endif %}>
<label class="form-check-label" for="marks_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_present" value="present" {% if marks_status == 'present' %}checked{% endif %}>
<label class="form-check-label" for="marks_present">✓ Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_absent" value="absent" {% if marks_status == 'absent' %}checked{% endif %}>
<label class="form-check-label" for="marks_absent">✗ Нет</label>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="px-3 pb-3">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- Pagination Info -->
{% if page_obj %}
<div class="px-3 pb-3 d-flex justify-content-between align-items-center">
<div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div>
<!-- Настройки отображения -->
<div class="row">
<div class="col-12">
<div class="filter-group">
<div class="filter-group-title">4. Настройки отображения</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">Элементов на странице:</label>
<select name="items_per_page" class="form-select form-select-sm">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Кнопки -->
<div class="row">
<div class="col-12 text-center">
<button type="submit" class="btn btn-primary btn-generate">
<i class="bi bi-table"></i> Сформировать таблицу
</button>
<a href="?" class="btn btn-secondary ms-2">Сбросить фильтры</a>
</div>
</div>
</form>
</div>
<!-- Активные фильтры -->
{% if page_obj %}
<div class="active-filters">
<strong>Активные фильтры:</strong>
{% if display_mode == 'sources' %}
<span class="filter-badge">Объекти</span>
{% else %}
<span class="filter-badge">Объекты</span>
{% endif %}
{% if selected_satellites %}
<span class="filter-badge">Спутники: {{ selected_satellites|length }}</span>
{% endif %}
{% if date_from or date_to %}
<span class="filter-badge">Дата геолокации</span>
{% endif %}
{% if show_marks == '1' %}
<span class="filter-badge">С наличие сигналами</span>
{% endif %}
</div>
{% endif %}
<!-- Таблица (показывается только после генерации) -->
<div class="table-container {% if page_obj %}show{% endif %}">
{% if display_mode == 'sources' %}
{% include 'mainapp/components/_sources_table.html' %}
{% else %}
{% include 'mainapp/components/_objitems_table.html' %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %}
<!-- JavaScript for checkbox functionality and filters --> {% block extra_js %}
<script> <script>
function selectAll(name, select) {
const element = document.querySelector(`select[name="${name}"]`);
if (element) {
for (let option of element.options) {
option.selected = select;
}
}
}
function updateConditionalFilters() {
const mode = document.querySelector('input[name="display_mode"]:checked').value;
const sourcesFilters = document.getElementById('sources-filters');
const objitemsFilters = document.getElementById('objitems-filters');
if (mode === 'sources') {
sourcesFilters.style.display = 'block';
objitemsFilters.style.display = 'none';
} else {
sourcesFilters.style.display = 'none';
objitemsFilters.style.display = 'block';
}
}
function toggleMarksFilters() {
const mode = document.querySelector('input[name="display_mode"]:checked')?.value;
if (mode === 'sources') {
const checkbox = document.getElementById('marks_src');
const filters = document.getElementById('marks-additional-filters-src');
if (checkbox && filters) {
filters.style.display = checkbox.checked ? 'block' : 'none';
}
} else if (mode === 'objitems') {
const checkbox = document.getElementById('marks_obj');
const filters = document.getElementById('marks-additional-filters');
if (checkbox && filters) {
filters.style.display = checkbox.checked ? 'block' : 'none';
}
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Select/Deselect all checkboxes updateConditionalFilters();
const selectAllCheckbox = document.getElementById('select-all'); toggleMarksFilters();
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
if (selectAllCheckbox && itemCheckboxes.length > 0) {
selectAllCheckbox.addEventListener('change', function() {
itemCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
});
// Update select all checkbox state based on individual selections
itemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
});
});
}
// Handle multiple selection for modulations and polarizations
const modulationSelect = document.querySelector('select[name="modulation"]');
const polarizationSelect = document.querySelector('select[name="polarization"]');
// Prevent deselecting all options when Ctrl+click is used
if (modulationSelect) {
modulationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
});
}
if (polarizationSelect) {
polarizationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
});
}
// Handle kubsat and valid coords checkboxes (mutually exclusive)
// Add a function to handle radio-like behavior for these checkboxes
function setupRadioLikeCheckboxes(name) {
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
// If this checkbox is checked, uncheck the other
if (this.checked) {
checkboxes.forEach(other => {
if (other !== this) {
other.checked = false;
}
});
} else {
// If both are unchecked, no action needed
}
document.getElementById('filter-form').submit();
});
});
}
setupRadioLikeCheckboxes('has_kupsat');
setupRadioLikeCheckboxes('has_valid');
// Function to select/deselect all options in a select element
window.selectAllOptions = function(selectName, selectAll) {
const selectElement = document.querySelector(`select[name="${selectName}"]`);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
document.getElementById('filter-form').submit();
}
};
// Function to update the page when satellite selection changes
function updateSatelliteSelection() {
document.getElementById('filter-form').submit();
}
// Get all current filter values and return as URL parameters
function getAllFilterParams() {
const form = document.getElementById('filter-form');
const searchValue = document.getElementById('toolbar-search').value;
// Create URLSearchParams object from the form
const params = new URLSearchParams(new FormData(form));
// Add search value from toolbar if present
if (searchValue.trim() !== '') {
params.set('search', searchValue);
} else {
// Remove search parameter if empty
params.delete('search');
}
return params.toString();
}
// Function to perform search
window.performSearch = function() {
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Function to clear search
window.clearSearch = function() {
// Clear only the search input in the toolbar
document.getElementById('toolbar-search').value = '';
// Submit the form to update the results
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Handle Enter key in toolbar search
const toolbarSearch = document.getElementById('toolbar-search');
if (toolbarSearch) {
toolbarSearch.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
}
// Add event listener to satellite select for immediate update
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
if (satelliteSelect) {
satelliteSelect.addEventListener('change', function() {
updateSatelliteSelection();
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,450 @@
{% extends 'mainapp/base.html' %}
{% block title %}Главная страница{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Главная страница</h2>
</div>
</div>
<div class="row g-3">
<!-- Filters Sidebar -->
<div class="col-md-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Фильтры</h5>
<form method="get" id="filter-form">
<!-- Display Mode -->
<div class="mb-3">
<label class="form-label">Отображение:</label>
<select name="display_mode" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();">
<option value="sources" {% if display_mode == 'sources' %}selected{% endif %}>Список источников</option>
<option value="objitems" {% if display_mode == 'objitems' %}selected{% endif %}>Список объектов</option>
</select>
</div>
<!-- Satellite Selection -->
<div class="mb-3">
<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('satellite_id', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm" multiple size="5">
{% for satellite in satellites %}
<option value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Date Range Filter -->
<div class="mb-3">
<label class="form-label">Дата геолокации:</label>
<input type="date" name="date_from" class="form-control form-control-sm mb-1" placeholder="От" value="{{ date_from }}">
<input type="date" name="date_to" class="form-control form-control-sm" placeholder="До" value="{{ date_to }}">
</div>
<!-- Frequency Filter -->
<div class="mb-3">
<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 }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max }}">
</div>
<!-- Range Filter -->
<div class="mb-3">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max }}">
</div>
<!-- Modulation Filter -->
<div class="mb-3">
<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', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm" multiple size="4">
{% for mod in modulations %}
<option value="{{ mod.id }}"
{% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Polarization Filter -->
<div class="mb-3">
<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', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
</div>
<select name="polarization" class="form-select form-select-sm" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}"
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Marks Filters -->
<div class="mb-3">
<label class="form-label">Отображать отметки:</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="show_marks" id="show_marks_0" value="0"
{% if show_marks == '0' %}checked{% endif %}>
<label class="form-check-label" for="show_marks_0">Нет</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="show_marks" id="show_marks_1" value="1"
{% if show_marks == '1' %}checked{% endif %}>
<label class="form-check-label" for="show_marks_1">Да</label>
</div>
</div>
</div>
<!-- Marks Date Range (shown only if show_marks is enabled) -->
<div class="mb-3" id="marks-date-filter" style="{% if show_marks != '1' %}display:none;{% endif %}">
<label class="form-label">Период отметок:</label>
<input type="date" name="marks_date_from" class="form-control form-control-sm mb-1" placeholder="От" value="{{ marks_date_from }}">
<input type="date" name="marks_date_to" class="form-control form-control-sm" placeholder="До" value="{{ marks_date_to }}">
</div>
<!-- Marks Status Filter -->
<div class="mb-3" id="marks-status-filter" style="{% if show_marks != '1' %}display:none;{% endif %}">
<label class="form-label">Статус отметок:</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_status_all" value=""
{% if not marks_status %}checked{% endif %}>
<label class="form-check-label" for="marks_status_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_status_present" value="present"
{% if marks_status == 'present' %}checked{% endif %}>
<label class="form-check-label" for="marks_status_present">✓ Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_status_absent" value="absent"
{% if marks_status == 'absent' %}checked{% endif %}>
<label class="form-check-label" for="marks_status_absent">✗ Нет</label>
</div>
</div>
</div>
<!-- Coordinates Filters -->
<div class="mb-3">
<label class="form-label">Координаты геолокации:</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="has_geo_all" value=""
{% if not has_geo %}checked{% endif %}>
<label class="form-check-label" for="has_geo_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="has_geo_1" value="1"
{% if has_geo == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_geo_1">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="has_geo_0" value="0"
{% if has_geo == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_geo_0">Нет</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="has_kupsat_all" value=""
{% if not has_kupsat %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="has_kupsat_1" value="1"
{% if has_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_1">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="has_kupsat_0" value="0"
{% if has_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_0">Нет</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Координаты опер. отдела:</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_valid" id="has_valid_all" value=""
{% if not has_valid %}checked{% endif %}>
<label class="form-check-label" for="has_valid_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_valid" id="has_valid_1" value="1"
{% if has_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_1">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_valid" id="has_valid_0" value="0"
{% if has_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_0">Нет</label>
</div>
</div>
</div>
<!-- Items Per Page -->
<div class="mb-3">
<label for="items-per-page" class="form-label">Элементов на странице:</label>
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main Content -->
<div class="col-md-9">
<div class="card h-100">
<div class="card-body p-0">
{% if display_mode == 'sources' %}
<!-- Sources Table -->
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col">ID</th>
<th scope="col">Спутники</th>
<th scope="col">Кол-во объектов</th>
<th scope="col">Средние координаты</th>
<th scope="col">Координаты Кубсата</th>
<th scope="col">Координаты опер. отдела</th>
<th scope="col">Дата создания</th>
</tr>
</thead>
<tbody>
{% for source in processed_sources %}
<tr>
<td><a href="{% url 'mainapp:source_update' source.id %}">{{ source.id }}</a></td>
<td>{{ source.satellites }}</td>
<td>{{ source.objitem_count }}</td>
<td>{{ source.coords_average }}</td>
<td>{{ source.coords_kupsat }}</td>
<td>{{ source.coords_valid }}</td>
<td>{{ source.created_at|date:"Y-m-d H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4">
Нет данных для выбранных фильтров
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<!-- ObjItems Table -->
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col">ID</th>
<th scope="col">Имя</th>
<th scope="col">Спутник</th>
<th scope="col">Частота, МГц</th>
<th scope="col">Полоса, МГц</th>
<th scope="col">Поляризация</th>
<th scope="col">Модуляция</th>
<th scope="col">Сим. v</th>
<th scope="col">ОСШ</th>
<th scope="col">Геолокация</th>
<th scope="col">Дата гео</th>
<th scope="col">Кубсат</th>
<th scope="col">Опер. отд</th>
<th scope="col">Источник</th>
{% if show_marks == '1' %}
<th scope="col">Отметки</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for item in processed_objitems %}
<tr>
<td><a href="{% url 'mainapp:objitem_detail' item.id %}">{{ item.id }}</a></td>
<td>{{ item.name }}</td>
<td>{{ item.satellite }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.geo_date }}</td>
<td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td>
<td>
{% if item.source_id %}
<a href="{% url 'mainapp:source_update' item.source_id %}">{{ item.source_id }}</a>
{% else %}
-
{% endif %}
</td>
{% if show_marks == '1' %}
<td>
{% if item.marks %}
<div class="marks-list">
{% for mark in item.marks %}
<div class="mark-item mb-1">
<span class="badge {% if mark.mark %}bg-success{% else %}bg-danger{% endif %}">
{% if mark.mark %}✓ Есть{% else %}✗ Нет{% endif %}
</span>
<small class="text-muted d-block">
{{ mark.timestamp|date:"d.m.Y H:i" }}
</small>
<small class="text-muted d-block">
{{ mark.created_by }}
</small>
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="{% if show_marks == '1' %}15{% else %}14{% endif %}" class="text-center py-4">
Нет данных для выбранных фильтров
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="px-3 pb-3">
<ul class="pagination justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- Pagination Info -->
{% if page_obj %}
<div class="px-3 pb-3 text-center">
<small class="text-muted">Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<style>
.marks-list {
max-height: 200px;
overflow-y: auto;
}
.mark-item {
padding: 4px;
border-bottom: 1px solid #e9ecef;
}
.mark-item:last-child {
border-bottom: none;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to select/deselect all options in a select element
window.selectAllOptions = function(selectName, selectAll) {
const selectElement = document.querySelector(`select[name="${selectName}"]`);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
}
};
// Toggle marks filters visibility
const showMarksRadios = document.querySelectorAll('input[name="show_marks"]');
const marksDateFilter = document.getElementById('marks-date-filter');
const marksStatusFilter = document.getElementById('marks-status-filter');
showMarksRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.value === '1') {
marksDateFilter.style.display = 'block';
marksStatusFilter.style.display = 'block';
} else {
marksDateFilter.style.display = 'none';
marksStatusFilter.style.display = 'none';
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,383 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Наличие сигнала объектов{% endblock %}
{% block extra_css %}
<style>
.marks-table {
width: 100%;
border-collapse: collapse;
}
.marks-table th,
.marks-table td {
border: 1px solid #dee2e6;
padding: 8px;
vertical-align: middle;
}
.marks-table th {
background-color: #f8f9fa;
font-weight: 600;
text-align: center;
}
.source-info-cell {
min-width: 250px;
background-color: #f8f9fa;
}
.marks-cell {
min-width: 150px;
text-align: center;
}
.actions-cell {
min-width: 180px;
text-align: center;
}
.mark-status {
font-size: 1.1rem;
}
.mark-present {
color: #28a745;
font-weight: 600;
}
.mark-absent {
color: #dc3545;
font-weight: 600;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.btn-mark {
padding: 6px 16px;
font-size: 0.875rem;
min-width: 100px;
}
.btn-edit-mark {
padding: 2px 8px;
font-size: 0.75rem;
}
.filter-section {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.no-marks {
color: #6c757d;
font-style: italic;
text-align: center;
}
.btn-mark:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-edit-mark:disabled {
opacity: 0.5;
cursor: wait;
}
.mark-status {
transition: color 0.3s ease;
}
.btn-edit-mark:hover:not(:disabled) {
background-color: #6c757d;
color: white;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Наличие сигнала объектов</h2>
</div>
<!-- Фильтры -->
<div class="filter-section">
<form method="get" class="row g-3">
<div class="col-md-6">
<label for="satellite" class="form-label">Спутник</label>
<select class="form-select" id="satellite" name="satellite">
<option value="">Все спутники</option>
{% for sat in satellites %}
<option value="{{ sat.id }}" {% if request.GET.satellite == sat.id|stringformat:"s" %}selected{% endif %}>
{{ sat.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 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>
</form>
</div>
<!-- Таблица с наличие сигналами -->
<div class="table-responsive">
<table class="marks-table table table-bordered">
<thead>
<tr>
<th class="source-info-cell">Информация об объекте</th>
<th class="marks-cell">Наличие</th>
<th class="marks-cell">Дата и время</th>
<th class="actions-cell">Действия</th>
</tr>
</thead>
<tbody>
{% for source in sources %}
{% with marks=source.marks.all %}
{% if marks %}
<!-- Первая строка с информацией об объекте и первой отметкой -->
<tr data-source-id="{{ source.id }}">
<td class="source-info-cell" rowspan="{{ marks.count }}">
<div><strong>ID:</strong> {{ source.id }}</div>
<div><strong>Дата создания:</strong> {{ source.created_at|date:"d.m.Y H:i" }}</div>
<div><strong>Кол-во объектов:</strong> {{ source.source_objitems.count }}</div>
{% if source.coords_average %}
<div><strong>Усреднённые координаты:</strong> Есть</div>
{% endif %}
{% if source.coords_kupsat %}
<div><strong>Координаты Кубсата:</strong> Есть</div>
{% endif %}
{% if source.coords_valid %}
<div><strong>Координаты оперативников:</strong> Есть</div>
{% endif %}
</td>
{% with first_mark=marks.0 %}
<td class="marks-cell" data-mark-id="{{ first_mark.id }}">
<span class="mark-status {% if first_mark.mark %}mark-present{% else %}mark-absent{% endif %}">
{% if first_mark.mark %}✓ Есть{% else %}✗ Нет{% endif %}
</span>
{% if first_mark.can_edit %}
<button class="btn btn-sm btn-outline-secondary btn-edit-mark ms-2"
onclick="toggleMark({{ first_mark.id }}, {{ first_mark.mark|yesno:'true,false' }})">
</button>
{% endif %}
</td>
<td class="marks-cell">
<div>{{ first_mark.timestamp|date:"d.m.Y H:i" }}</div>
<small class="text-muted">{{ first_mark.created_by|default:"—" }}</small>
</td>
<td class="actions-cell" rowspan="{{ marks.count }}">
<div class="action-buttons" id="actions-{{ source.id }}">
<button class="btn btn-success btn-mark btn-sm"
onclick="addMark({{ source.id }}, true)">
✓ Есть
</button>
<button class="btn btn-danger btn-mark btn-sm"
onclick="addMark({{ source.id }}, false)">
✗ Нет
</button>
</div>
</td>
{% endwith %}
</tr>
<!-- Остальные наличие сигнала -->
{% for mark in marks|slice:"1:" %}
<tr data-source-id="{{ source.id }}">
<td class="marks-cell" data-mark-id="{{ mark.id }}">
<span class="mark-status {% if mark.mark %}mark-present{% else %}mark-absent{% endif %}">
{% if mark.mark %}✓ Есть{% else %}✗ Нет{% endif %}
</span>
{% if mark.can_edit %}
<button class="btn btn-sm btn-outline-secondary btn-edit-mark ms-2"
onclick="toggleMark({{ mark.id }}, {{ mark.mark|yesno:'true,false' }})">
</button>
{% endif %}
</td>
<td class="marks-cell">
<div>{{ mark.timestamp|date:"d.m.Y H:i" }}</div>
<small class="text-muted">{{ mark.created_by|default:"—" }}</small>
</td>
</tr>
{% endfor %}
{% else %}
<!-- Объект без отметок -->
<tr data-source-id="{{ source.id }}">
<td class="source-info-cell">
<div><strong>ID:</strong> {{ source.id }}</div>
<div><strong>Дата создания:</strong> {{ source.created_at|date:"d.m.Y H:i" }}</div>
<div><strong>Кол-во объектов:</strong> {{ source.source_objitems.count }}</div>
{% if source.coords_average %}
<div><strong>Усреднённые координаты:</strong> Есть</div>
{% endif %}
{% if source.coords_kupsat %}
<div><strong>Координаты Кубсата:</strong> Есть</div>
{% endif %}
{% if source.coords_valid %}
<div><strong>Координаты оперативников:</strong> Есть</div>
{% endif %}
</td>
<td colspan="2" class="no-marks">Отметок нет</td>
<td class="actions-cell">
<div class="action-buttons" id="actions-{{ source.id }}">
<button class="btn btn-success btn-mark btn-sm"
onclick="addMark({{ source.id }}, true)">
✓ Есть
</button>
<button class="btn btn-danger btn-mark btn-sm"
onclick="addMark({{ source.id }}, false)">
✗ Нет
</button>
</div>
</td>
</tr>
{% endif %}
{% endwith %}
{% empty %}
<tr>
<td colspan="4" class="text-center py-4">
<p class="text-muted mb-0">Объекти не найдены</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if is_paginated %}
<nav aria-label="Навигация по страницам" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if request.GET.satellite %}&satellite={{ request.GET.satellite }}{% endif %}">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.satellite %}&satellite={{ request.GET.satellite }}{% endif %}">Предыдущая</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.satellite %}&satellite={{ request.GET.satellite }}{% endif %}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.satellite %}&satellite={{ request.GET.satellite }}{% endif %}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
function addMark(sourceId, mark) {
// Отключить кнопки для этого объекта
const buttons = document.querySelectorAll(`#actions-${sourceId} button`);
buttons.forEach(btn => btn.disabled = true);
fetch("{% url 'mainapp:add_object_mark' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: `source_id=${sourceId}&mark=${mark}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Перезагрузить страницу для обновления таблицы
location.reload();
} else {
// Включить кнопки обратно
buttons.forEach(btn => btn.disabled = false);
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
buttons.forEach(btn => btn.disabled = false);
alert('Ошибка при добавлении наличие сигнала');
});
}
function toggleMark(markId, currentValue) {
const newValue = !currentValue;
const cell = document.querySelector(`td[data-mark-id="${markId}"]`);
const editBtn = cell.querySelector('.btn-edit-mark');
// Отключить кнопку редактирования на время запроса
if (editBtn) {
editBtn.disabled = true;
}
fetch("{% url 'mainapp:update_object_mark' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: `mark_id=${markId}&mark=${newValue}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Обновить отображение наличие сигнала без перезагрузки страницы
const statusSpan = cell.querySelector('.mark-status');
if (data.mark.mark) {
statusSpan.textContent = '✓ Есть';
statusSpan.className = 'mark-status mark-present';
} else {
statusSpan.textContent = '✗ Нет';
statusSpan.className = 'mark-status mark-absent';
}
// Обновить значение в onclick для следующего переключения
if (editBtn) {
editBtn.setAttribute('onclick', `toggleMark(${markId}, ${data.mark.mark})`);
editBtn.disabled = false;
}
// Если больше нельзя редактировать, убрать кнопку
if (!data.mark.can_edit && editBtn) {
editBtn.remove();
}
} else {
// Включить кнопку обратно при ошибке
if (editBtn) {
editBtn.disabled = false;
}
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
if (editBtn) {
editBtn.disabled = false;
}
alert('Ошибка при изменении наличие сигнала');
});
}
</script>
{% endblock %}

View File

@@ -12,7 +12,7 @@
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<h2>Список объектов</h2> <h2>Список точек</h2>
</div> </div>
</div> </div>
@@ -207,7 +207,7 @@
<!-- Source Type Filter --> <!-- Source Type Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Тип источника:</label> <label class="form-label">Тип точки:</label>
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" <input class="form-check-input" type="checkbox" name="has_source_type"
@@ -277,7 +277,7 @@
<div class="card h-100"> <div class="card h-100">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th scope="col" class="text-center" style="width: 3%;"> <th scope="col" class="text-center" style="width: 3%;">
@@ -302,7 +302,7 @@
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Тип точки" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
</tr> </tr>
@@ -1036,7 +1036,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-primary text-white"> <div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel"> <h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat <i class="bi bi-tv"></i> Данные объекта LyngSat
</h5> </h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button> aria-label="Закрыть"></button>
@@ -1160,7 +1160,7 @@
<div class="col-md-6"> <div class="col-md-6">
${data.url ? ` ${data.url ? `
<p class="mb-2"> <p class="mb-2">
<span class="text-muted">Ссылка на источник:</span><br> <span class="text-muted">Ссылка на объект:</span><br>
<a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary"> <a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-link-45deg"></i> Открыть на LyngSat <i class="bi bi-link-45deg"></i> Открыть на LyngSat
</a> </a>

View File

@@ -30,7 +30,7 @@
<h5 class="mt-4 mb-3">Детали удаления:</h5> <h5 class="mt-4 mb-3">Детали удаления:</h5>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;"> <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-striped table-hover table-sm"> <table class="table table-striped table-hover table-sm table-bordered">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th class="text-center" style="width: 15%;">ID источника</th> <th class="text-center" style="width: 15%;">ID источника</th>

View File

@@ -1,7 +1,7 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% load static %} {% load static %}
{% block title %}Удалить источник #{{ object.id }}{% endblock %} {% block title %}Удалить объект #{{ object.id }}{% endblock %}
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container mt-5">
@@ -14,10 +14,10 @@
<div class="card-body"> <div class="card-body">
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i> <i class="bi bi-exclamation-triangle-fill"></i>
<strong>Внимание!</strong> Вы собираетесь удалить источник. <strong>Внимание!</strong> Вы собираетесь удалить объект.
</div> </div>
<h5>Информация об источнике:</h5> <h5>Информация об объекте:</h5>
<ul class="list-group mb-3"> <ul class="list-group mb-3">
<li class="list-group-item"> <li class="list-group-item">
<strong>ID:</strong> {{ object.id }} <strong>ID:</strong> {{ object.id }}
@@ -39,7 +39,7 @@
{% if objitems_count > 0 %} {% if objitems_count > 0 %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-circle-fill"></i> <i class="bi bi-exclamation-circle-fill"></i>
<strong>Важно!</strong> При удалении источника будут также удалены все {{ objitems_count }} привязанных объектов! <strong>Важно!</strong> При удалении объекта будут также удалены все {{ objitems_count }} привязанных объектов!
</div> </div>
{% endif %} {% endif %}

View File

@@ -3,7 +3,7 @@
{% load static leaflet_tags %} {% load static leaflet_tags %}
{% load l10n %} {% load l10n %}
{% block title %}Редактировать источник #{{ object.id }}{% endblock %} {% block title %}Редактировать объект #{{ object.id }}{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
@@ -129,7 +129,7 @@
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center"> <div class="col-12 d-flex justify-content-between align-items-center">
<h2>Редактировать источник #{{ object.id }}</h2> <h2>Редактировать объект #{{ object.id }}</h2>
<div> <div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button> <button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
@@ -153,7 +153,7 @@
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">ID источника:</label> <label class="form-label">ID объекта:</label>
<div class="readonly-field">{{ object.id }}</div> <div class="readonly-field">{{ object.id }}</div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Список источников{% endblock %} {% block title %}Список объектов{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
@@ -29,7 +29,7 @@
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<h2>Список источников</h2> <h2>Список объектов</h2>
</div> </div>
</div> </div>
@@ -192,6 +192,23 @@
</div> </div>
</div> </div>
<!-- LyngSat 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_lyngsat" id="has_lyngsat_1"
value="1" {% if has_lyngsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_lyngsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_0"
value="0" {% if has_lyngsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_lyngsat_0">Нет</label>
</div>
</div>
</div>
<!-- Point Count Filter --> <!-- Point Count Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Количество точек:</label> <label class="form-label">Количество точек:</label>
@@ -210,6 +227,15 @@
placeholder="До" value="{{ date_to|default:'' }}"> placeholder="До" value="{{ date_to|default:'' }}">
</div> </div>
<!-- Geo Timestamp Filter -->
<div class="mb-2">
<label class="form-label">Дата ГЛ:</label>
<input type="date" name="geo_date_from" id="geo_date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ geo_date_from|default:'' }}">
<input type="date" name="geo_date_to" id="geo_date_to" class="form-control form-control-sm"
placeholder="До" value="{{ geo_date_to|default:'' }}">
</div>
<!-- Apply Filters and Reset Buttons --> <!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2"> <div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button> <button type="submit" class="btn btn-primary btn-sm">Применить</button>
@@ -225,7 +251,7 @@
<div class="card"> <div class="card">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th scope="col" class="text-center" style="width: 3%;"> <th scope="col" class="text-center" style="width: 3%;">
@@ -246,6 +272,10 @@
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th> <th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
<th scope="col" style="min-width: 150px;">Координаты оперативников</th> <th scope="col" style="min-width: 150px;">Координаты оперативников</th>
<th scope="col" style="min-width: 150px;">Координаты справочные</th> <th scope="col" style="min-width: 150px;">Координаты справочные</th>
<th scope="col" style="min-width: 180px;">Наличие сигнала</th>
{% 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: 100px;"> <th scope="col" class="text-center" style="min-width: 100px;">
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none"> <a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
Кол-во точек Кол-во точек
@@ -292,6 +322,41 @@
<td>{{ source.coords_kupsat }}</td> <td>{{ source.coords_kupsat }}</td>
<td>{{ source.coords_valid }}</td> <td>{{ source.coords_valid }}</td>
<td>{{ source.coords_reference }}</td> <td>{{ source.coords_reference }}</td>
<td style="padding: 0.3rem; vertical-align: top;">
{% if source.marks %}
<div style="font-size: 0.75rem; line-height: 1.3;">
{% for mark in source.marks %}
<div style="{% if not forloop.last %}border-bottom: 1px solid #dee2e6; padding-bottom: 3px; margin-bottom: 3px;{% endif %}">
<div style="margin-bottom: 1px;">
{% if mark.mark %}
<span class="badge bg-success" style="font-size: 0.7rem;">Есть</span>
{% elif mark.mark == False %}
<span class="badge bg-danger" style="font-size: 0.7rem;">Нет</span>
{% else %}
<span class="badge bg-secondary" style="font-size: 0.7rem;">-</span>
{% endif %}
<span class="text-muted" style="font-size: 0.7rem;">{{ mark.timestamp|date:"d.m.y H:i" }}</span>
</div>
<div class="text-muted" style="font-size: 0.65rem;">{{ mark.created_by }}</div>
</div>
{% endfor %}
</div>
{% else %}
<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"
onclick="showLyngsatModal({{ source.lyngsat_id }}); return false;">
<i class="bi bi-tv"></i> ТВ
</a>
{% else %}
-
{% endif %}
</td>
{% endif %}
<td class="text-center">{{ source.objitem_count }}</td> <td class="text-center">{{ source.objitem_count }}</td>
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td> <td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td> <td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
@@ -301,7 +366,7 @@
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}" <a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
target="_blank" target="_blank"
class="btn btn-sm btn-outline-success" class="btn btn-sm btn-outline-success"
title="Показать источник с точками на карте"> title="Показать объект с точками на карте">
<i class="bi bi-geo-alt"></i> <i class="bi bi-geo-alt"></i>
<span class="badge bg-success">{{ source.objitem_count }}</span> <span class="badge bg-success">{{ source.objitem_count }}</span>
</a> </a>
@@ -333,7 +398,7 @@
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:source_update' source.id %}" <a href="{% url 'mainapp:source_update' source.id %}"
class="btn btn-sm btn-outline-warning" class="btn btn-sm btn-outline-warning"
title="Редактировать источник"> title="Редактировать объект">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </a>
{% else %} {% else %}
@@ -346,7 +411,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td> <td colspan="12" class="text-center text-muted">Нет данных для отображения</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -360,10 +425,10 @@
<!-- Modal for Source Details --> <!-- Modal for Source Details -->
<div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true"> <div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-fullscreen">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника #<span id="modalSourceId"></span></h5> <h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта #<span id="modalSourceId"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -374,26 +439,96 @@
</div> </div>
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div> <div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
<div id="modalContent" style="display: none;"> <div id="modalContent" style="display: none;">
<h6>Связанные точки (<span id="objitemCount">0</span>):</h6> <!-- Marks Section -->
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;"> <div id="marksSection" class="mb-3" style="display: none;">
<table class="table table-striped table-hover table-sm"> <h6 class="mb-2">Наличие сигнала объекта (<span id="marksCount">0</span>):</h6>
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th style="width: 20%;">Наличие сигнала</th>
<th style="width: 40%;">Дата и время</th>
<th style="width: 40%;">Пользователь</th>
</tr>
</thead>
<tbody id="marksTableBody">
<!-- Marks will be loaded here -->
</tbody>
</table>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
id="modalColumnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="modalColumnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="modal-select-all-columns" onchange="toggleAllModalColumns(this)"> Выбрать всё
</label>
</li>
<li><hr class="dropdown-divider"></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="0" checked onchange="toggleModalColumn(this)"> Выбрать</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="1" checked onchange="toggleModalColumn(this)"> ID</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="2" checked onchange="toggleModalColumn(this)"> Имя</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="3" checked onchange="toggleModalColumn(this)"> Спутник</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="4" checked onchange="toggleModalColumn(this)"> Транспондер</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="5" checked onchange="toggleModalColumn(this)"> Частота, МГц</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="6" checked onchange="toggleModalColumn(this)"> Полоса, МГц</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="7" checked onchange="toggleModalColumn(this)"> Поляризация</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="8" checked onchange="toggleModalColumn(this)"> Сим. V</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="9" checked onchange="toggleModalColumn(this)"> Модул</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="10" checked onchange="toggleModalColumn(this)"> ОСШ</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="11" checked onchange="toggleModalColumn(this)"> Время ГЛ</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="12" checked onchange="toggleModalColumn(this)"> Местоположение</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="13" checked onchange="toggleModalColumn(this)"> Геолокация</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="14" checked onchange="toggleModalColumn(this)"> Обновлено</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="15" checked onchange="toggleModalColumn(this)"> Кем(обн)</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="16" onchange="toggleModalColumn(this)"> Создано</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="17" onchange="toggleModalColumn(this)"> Кем(созд)</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="18" onchange="toggleModalColumn(this)"> Комментарий</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип объекта</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
</ul>
</div>
</div>
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th class="text-center" style="width: 3%;"> <th class="text-center" style="width: 3%;">
<input type="checkbox" id="modal-select-all" class="form-check-input"> <input type="checkbox" id="modal-select-all" class="form-check-input">
</th> </th>
<th class="text-center" style="min-width: 60px;">ID</th> <th class="text-center" style="min-width: 60px;">ID</th>
<th>Имя</th> <th style="min-width: 120px;">Имя</th>
<th>Спутник</th> <th style="min-width: 120px;">Спутник</th>
<th>Частота, МГц</th> <th style="min-width: 100px;">Транспондер</th>
<th>Полоса, МГц</th> <th style="min-width: 100px;">Частота, МГц</th>
<th>Поляризация</th> <th style="min-width: 100px;">Полоса, МГц</th>
<th>Сим. скорость, БОД</th> <th style="min-width: 100px;">Поляризация</th>
<th>Модуляция</th> <th style="min-width: 100px;">Сим. V</th>
<th>ОСШ</th> <th style="min-width: 100px;">Модул</th>
<th>Время ГЛ</th> <th style="min-width: 80px;">ОСШ</th>
<th>Местоположение</th> <th style="min-width: 120px;">Время ГЛ</th>
<th>Координаты ГЛ</th> <th style="min-width: 120px;">Местоположение</th>
<th style="min-width: 120px;">Геолокация</th>
<th style="min-width: 120px;">Обновлено</th>
<th style="min-width: 100px;">Кем(обн)</th>
<th style="min-width: 120px;">Создано</th>
<th style="min-width: 100px;">Кем(созд)</th>
<th style="min-width: 150px;">Комментарий</th>
<th style="min-width: 100px;">Усреднённое</th>
<th style="min-width: 100px;">Стандарт</th>
<th style="min-width: 100px;">Тип объекта</th>
<th style="min-width: 80px;">Sigma</th>
<th style="min-width: 80px;">Зеркала</th>
</tr> </tr>
</thead> </thead>
<tbody id="objitemTableBody"> <tbody id="objitemTableBody">
@@ -451,7 +586,7 @@ function showSelectedOnMap() {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked'); const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) { if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте'); alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
return; return;
} }
@@ -472,7 +607,7 @@ function deleteSelectedSources() {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked'); const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) { if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для удаления'); alert('Пожалуйста, выберите хотя бы один объект для удаления');
return; return;
} }
@@ -639,6 +774,7 @@ document.addEventListener('DOMContentLoaded', function() {
setupRadioLikeCheckboxes('has_coords_kupsat'); setupRadioLikeCheckboxes('has_coords_kupsat');
setupRadioLikeCheckboxes('has_coords_valid'); setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference'); setupRadioLikeCheckboxes('has_coords_reference');
setupRadioLikeCheckboxes('has_lyngsat');
// Update filter counter on page load // Update filter counter on page load
updateFilterCounter(); updateFilterCounter();
@@ -685,12 +821,29 @@ function showSourceDetails(sourceId) {
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal')); const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
modal.show(); modal.show();
// Build URL with filter parameters
const urlParams = new URLSearchParams(window.location.search);
const geoDateFrom = urlParams.get('geo_date_from');
const geoDateTo = urlParams.get('geo_date_to');
let apiUrl = '/api/source/' + sourceId + '/objitems/';
const params = new URLSearchParams();
if (geoDateFrom) {
params.append('geo_date_from', geoDateFrom);
}
if (geoDateTo) {
params.append('geo_date_to', geoDateTo);
}
if (params.toString()) {
apiUrl += '?' + params.toString();
}
// Fetch data from API // Fetch data from API
fetch(`/api/source/${sourceId}/objitems/`) fetch(apiUrl)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
if (response.status === 404) { if (response.status === 404) {
throw new Error('Источник не найден'); throw new Error('Объект не найден');
} else { } else {
throw new Error('Ошибка при загрузке данных'); throw new Error('Ошибка при загрузке данных');
} }
@@ -701,6 +854,33 @@ function showSourceDetails(sourceId) {
// Hide loading spinner // Hide loading spinner
document.getElementById('modalLoadingSpinner').style.display = 'none'; document.getElementById('modalLoadingSpinner').style.display = 'none';
// Show marks if available
if (data.marks && data.marks.length > 0) {
document.getElementById('marksSection').style.display = 'block';
document.getElementById('marksCount').textContent = data.marks.length;
const marksTableBody = document.getElementById('marksTableBody');
marksTableBody.innerHTML = '';
data.marks.forEach(mark => {
const row = document.createElement('tr');
let markBadge = '<span class="badge bg-secondary">-</span>';
if (mark.mark === true) {
markBadge = '<span class="badge bg-success">Есть</span>';
} else if (mark.mark === false) {
markBadge = '<span class="badge bg-danger">Нет</span>';
}
row.innerHTML = '<td class="text-center">' + markBadge + '</td>' +
'<td>' + mark.timestamp + '</td>' +
'<td>' + mark.created_by + '</td>';
marksTableBody.appendChild(row);
});
} else {
document.getElementById('marksSection').style.display = 'none';
}
if (data.objitems && data.objitems.length > 0) { if (data.objitems && data.objitems.length > 0) {
// Show content // Show content
document.getElementById('modalContent').style.display = 'block'; document.getElementById('modalContent').style.display = 'block';
@@ -712,28 +892,70 @@ function showSourceDetails(sourceId) {
data.objitems.forEach(objitem => { data.objitems.forEach(objitem => {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = `
<td class="text-center"> // Build transponder cell
<input type="checkbox" class="form-check-input modal-item-checkbox" value="${objitem.id}"> let transponderCell = '-';
</td> if (objitem.has_transponder) {
<td class="text-center">${objitem.id}</td> transponderCell = '<a href="#" class="text-success text-decoration-none" ' +
<td>${objitem.name}</td> 'onclick="showTransponderModal(' + objitem.transponder_id + '); return false;" ' +
<td>${objitem.satellite_name}</td> 'title="Показать данные транспондера">' +
<td>${objitem.frequency}</td> '<i class="bi bi-broadcast"></i> ' + objitem.transponder_info +
<td>${objitem.freq_range}</td> '</a>';
<td>${objitem.polarization}</td> }
<td>${objitem.bod_velocity}</td>
<td>${objitem.modulation}</td> // Build LyngSat cell
<td>${objitem.snr}</td> let lyngsatCell = '-';
<td>${objitem.geo_timestamp}</td> if (objitem.has_lyngsat) {
<td>${objitem.geo_location}</td> lyngsatCell = '<a href="#" class="text-primary text-decoration-none" ' +
<td>${objitem.geo_coords}</td> 'onclick="showLyngsatModal(' + objitem.lyngsat_id + '); return false;">' +
`; '<i class="bi bi-tv"></i> ТВ' +
'</a>';
}
// Build Sigma cell
let sigmaCell = '-';
if (objitem.has_sigma) {
sigmaCell = '<a href="#" class="text-info text-decoration-none" ' +
'onclick="showSigmaParameterModal(' + objitem.parameter_id + '); return false;" ' +
'title="' + objitem.sigma_info + '">' +
'<i class="bi bi-graph-up"></i> ' + objitem.sigma_info +
'</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>' + transponderCell + '</td>' +
'<td>' + objitem.frequency + '</td>' +
'<td>' + objitem.freq_range + '</td>' +
'<td>' + objitem.polarization + '</td>' +
'<td>' + objitem.bod_velocity + '</td>' +
'<td>' + objitem.modulation + '</td>' +
'<td>' + objitem.snr + '</td>' +
'<td>' + objitem.geo_timestamp + '</td>' +
'<td>' + objitem.geo_location + '</td>' +
'<td>' + objitem.geo_coords + '</td>' +
'<td>' + objitem.updated_at + '</td>' +
'<td>' + objitem.updated_by + '</td>' +
'<td>' + objitem.created_at + '</td>' +
'<td>' + objitem.created_by + '</td>' +
'<td>' + objitem.comment + '</td>' +
'<td>' + objitem.is_average + '</td>' +
'<td>' + objitem.standard + '</td>' +
'<td>' + lyngsatCell + '</td>' +
'<td>' + sigmaCell + '</td>' +
'<td>' + objitem.mirrors + '</td>';
tbody.appendChild(row); tbody.appendChild(row);
}); });
// Setup modal select-all checkbox // Setup modal select-all checkbox
setupModalSelectAll(); setupModalSelectAll();
// Initialize column visibility
initModalColumnVisibility();
} else { } else {
// Show no data message // Show no data message
document.getElementById('modalNoData').style.display = 'block'; document.getElementById('modalNoData').style.display = 'block';
@@ -776,5 +998,196 @@ 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');
if (!table) return;
const cells = table.querySelectorAll('td:nth-child(' + (columnIndex + 1) + '), th:nth-child(' + (columnIndex + 1) + ')');
if (checkbox.checked) {
cells.forEach(cell => {
cell.style.display = '';
});
} else {
cells.forEach(cell => {
cell.style.display = 'none';
});
}
}
// Function to toggle all modal columns
function toggleAllModalColumns(selectAllCheckbox) {
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
columnCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
toggleModalColumn(checkbox);
});
}
// Initialize modal column visibility
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);
}
});
}
// Function to show LyngSat modal
function showLyngsatModal(lyngsatId) {
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/lyngsat/' + lyngsatId + '/')
.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.satellite + '</strong></td></tr>' +
'<tr><td class="text-muted">Частота:</td><td><strong>' + data.frequency + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
'<tr><td class="text-muted">Канал:</td><td>' + data.channel_info + '</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-gear"></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><span class="badge bg-secondary">' + data.modulation + '</span></td></tr>' +
'<tr><td class="text-muted">Стандарт:</td><td><span class="badge bg-secondary">' + data.standard + '</span></td></tr>' +
'<tr><td class="text-muted">Сим. скорость:</td><td><strong>' + data.sym_velocity + ' БОД</strong></td></tr>' +
'<tr><td class="text-muted">FEC:</td><td>' + data.fec + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
'<div class="card-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
'</div></div></div></div></div></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>';
});
}
// Function to show transponder modal
function showTransponderModal(transponderId) {
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
modal.show();
const modalBody = document.getElementById('transponderModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-success" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/transponder/' + transponderId + '/')
.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">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
'<tr><td class="text-muted">Зона покрытия:</td><td>' + (data.zone_name || '-') + '</td></tr>' +
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></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-broadcast"></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%;">Downlink:</td><td><strong>' + data.downlink + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Uplink:</td><td><strong>' + (data.uplink || '-') + (data.uplink ? ' МГц' : '') + '</strong></td></tr>' +
'<tr><td class="text-muted">Полоса:</td><td><strong>' + data.frequency_range + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Перенос:</td><td>' + (data.transfer || '-') + (data.transfer ? ' МГц' : '') + '</td></tr>' +
'<tr><td class="text-muted">ОСШ:</td><td><strong>' + (data.snr || '-') + (data.snr ? ' дБ' : '') + '</strong></td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Метаданные</strong></div>' +
'<div class="card-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Дата создания:</span><br><strong>' + (data.created_at || '-') + '</strong></p></div>' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Создан пользователем:</span><br><strong>' + (data.created_by || '-') + '</strong></p></div>' +
'</div></div></div></div></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> </script>
<!-- LyngSat Data Modal -->
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные объекта LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="lyngsatModalBody">
<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>
<!-- Transponder Data Modal -->
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="transponderModalLabel">
<i class="bi bi-broadcast"></i> Данные транспондера
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="transponderModalBody">
<div class="text-center py-4">
<div class="spinner-border text-success" 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>
<!-- Include the sigma parameter modal component -->
{% include 'mainapp/components/_sigma_parameter_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "mainapp/base.html" %} {% extends "mainapp/base.html" %}
{% load static %} {% load static %}
{% block title %}Карта источников{% endblock title %} {% block title %}Карта объектов{% endblock title %}
{% block extra_css %} {% block extra_css %}
<!-- Leaflet CSS --> <!-- Leaflet CSS -->

View File

@@ -1,6 +1,6 @@
{% extends "mainapp/base.html" %} {% extends "mainapp/base.html" %}
{% load static %} {% load static %}
{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %} {% block title %}Карта объекта #{{ source_id }} с точками{% endblock title %}
{% block extra_css %} {% block extra_css %}
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
@@ -120,7 +120,7 @@
var sourceOverlays = []; var sourceOverlays = [];
var glPointLayers = []; var glPointLayers = [];
// Создаём слои для координат источника и точек ГЛ // Создаём слои для координат объекта и точек ГЛ
{% for group in groups %} {% for group in groups %}
var groupName = '{{ group.name|escapejs }}'; var groupName = '{{ group.name|escapejs }}';
var colorName = '{{ group.color }}'; var colorName = '{{ group.color }}';
@@ -129,7 +129,7 @@
{% for point_data in group.points %} {% for point_data in group.points %}
{% if point_data.source_id %} {% if point_data.source_id %}
// Это координата источника // Это координата объекта
var pointName = "{{ point_data.source_id|escapejs }}"; var pointName = "{{ point_data.source_id|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], { var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon icon: groupIcon
@@ -151,7 +151,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
// Для координат источника добавляем как отдельный слой без вложенности // Для координат объекта добавляем как отдельный слой без вложенности
{% if group.color in 'blue,orange,green,violet' %} {% if group.color in 'blue,orange,green,violet' %}
sourceOverlays.push({ sourceOverlays.push({
label: groupName, label: groupName,
@@ -165,7 +165,7 @@
if (sourceOverlays.length > 0) { if (sourceOverlays.length > 0) {
treeOverlays.push({ treeOverlays.push({
label: "Координаты источника #{{ source_id }}", label: "Координаты объекта #{{ source_id }}",
selectAllCheckbox: true, selectAllCheckbox: true,
children: sourceOverlays, children: sourceOverlays,
layer: L.layerGroup() layer: L.layerGroup()
@@ -205,13 +205,13 @@
var div = L.DomUtil.create('div', 'legend'); var div = L.DomUtil.create('div', 'legend');
div.innerHTML = '<h6><strong>Легенда</strong></h6>'; div.innerHTML = '<h6><strong>Легенда</strong></h6>';
// Координаты источника // Координаты объекта
var hasSourceCoords = false; var hasSourceCoords = false;
{% for group in groups %} {% for group in groups %}
{% if group.color in 'blue,orange,green,violet' %} {% if group.color in 'blue,orange,green,violet' %}
{% if not hasSourceCoords %} {% if not hasSourceCoords %}
if (!hasSourceCoords) { if (!hasSourceCoords) {
div.innerHTML += '<div class="legend-section-title">Координаты источника:</div>'; div.innerHTML += '<div class="legend-section-title">Координаты объекта:</div>';
hasSourceCoords = true; hasSourceCoords = true;
} }
{% endif %} {% endif %}

View File

@@ -30,7 +30,7 @@
<h5 class="mt-4 mb-3">Детали удаления:</h5> <h5 class="mt-4 mb-3">Детали удаления:</h5>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;"> <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-striped table-hover table-sm"> <table class="table table-striped table-hover table-sm table-bordered">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th class="text-center" style="width: 10%;">ID</th> <th class="text-center" style="width: 10%;">ID</th>

View File

@@ -191,7 +191,7 @@
<div class="card"> <div class="card">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;"> <table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th scope="col" class="text-center" style="width: 3%;"> <th scope="col" class="text-center" style="width: 3%;">

View File

@@ -0,0 +1,79 @@
{% extends 'mainapp/base.html' %}
{% block title %}Подтверждение отвязки LyngSat{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-warning text-dark">
<h4 class="mb-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Подтверждение отвязки источников LyngSat
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning" role="alert">
<h5 class="alert-heading">
<i class="bi bi-info-circle-fill me-2"></i>
Внимание!
</h5>
<p class="mb-0">
Вы собираетесь отвязать <strong>все</strong> источники LyngSat от объектов.
</p>
</div>
<div class="mb-4">
<h5>Информация:</h5>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">
Объектов с привязанными источниками LyngSat:
<span class="badge bg-primary rounded-pill">{{ linked_count }}</span>
</li>
</ul>
</div>
{% if linked_count > 0 %}
<div class="alert alert-info" role="alert">
<p class="mb-0">
<i class="bi bi-lightbulb-fill me-2"></i>
После отвязки все объекты перестанут отображаться как "ТВ" источники.
Вы сможете заново привязать источники через форму привязки LyngSat.
</p>
</div>
<form method="post">
{% csrf_token %}
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left me-1"></i>
Отмена
</a>
<button type="submit" class="btn btn-warning">
<i class="bi bi-unlink me-1"></i>
Отвязать все источники ({{ linked_count }})
</button>
</div>
</form>
{% else %}
<div class="alert alert-success" role="alert">
<p class="mb-0">
<i class="bi bi-check-circle-fill me-2"></i>
Нет объектов с привязанными источниками LyngSat. Отвязка не требуется.
</p>
</div>
<div class="d-grid">
<a href="{% url 'mainapp:actions' %}" class="btn btn-primary">
<i class="bi bi-arrow-left me-1"></i>
Вернуться к действиям
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -12,6 +12,7 @@ from .views import (
DeleteSelectedTranspondersView, DeleteSelectedTranspondersView,
FillLyngsatDataView, FillLyngsatDataView,
GetLocationsView, GetLocationsView,
HomeView,
LinkLyngsatSourcesView, LinkLyngsatSourcesView,
LinkVchSigmaView, LinkVchSigmaView,
LoadCsvDataView, LoadCsvDataView,
@@ -39,14 +40,17 @@ from .views import (
TransponderListView, TransponderListView,
TransponderCreateView, TransponderCreateView,
TransponderUpdateView, TransponderUpdateView,
UnlinkAllLyngsatSourcesView,
UploadVchLoadView, UploadVchLoadView,
custom_logout, custom_logout,
) )
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
app_name = 'mainapp' app_name = 'mainapp'
urlpatterns = [ urlpatterns = [
path('', SourceListView.as_view(), name='home'), path('', HomeView.as_view(), name='home'),
path('sources/', SourceListView.as_view(), name='source_list'),
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'), path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'), path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'), path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
@@ -85,5 +89,9 @@ urlpatterns = [
path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'), path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'), path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'),
path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'),
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
path('logout/', custom_logout, name='logout'), path('logout/', custom_logout, name='logout'),
] ]

View File

@@ -159,38 +159,98 @@ def remove_str(s: str):
return s return s
def _find_or_create_source_by_name_and_distance(
source_name: str, sat: Satellite, coord: tuple, user
) -> Source:
"""
Находит или создает Source на основе имени источника, спутника и расстояния.
Логика:
1. Ищет все существующие Source с ObjItem, у которых:
- Совпадает спутник
- Совпадает имя (source_name)
2. Для каждого найденного Source проверяет расстояние до новой координаты
3. Если найден Source в радиусе ≤56 км:
- Возвращает его и обновляет coords_average инкрементально
4. Если не найден подходящий Source:
- Создает новый Source
Важно: Может существовать несколько Source с одинаковым именем и спутником,
но они должны быть географически разделены (>56 км друг от друга).
Args:
source_name: имя источника (например, "Turksat 3A 10967,397 [9,348] МГц V")
sat: объект Satellite
coord: координата в формате (lon, lat)
user: пользователь для created_by
Returns:
Source: найденный или созданный объект Source
"""
# Ищем все существующие ObjItem с таким же именем и спутником
existing_objitems = ObjItem.objects.filter(
name=source_name,
parameter_obj__id_satellite=sat,
source__isnull=False,
source__coords_average__isnull=False
).select_related('source', 'parameter_obj')
# Собираем уникальные Source из найденных ObjItem
existing_sources = {}
for objitem in existing_objitems:
if objitem.source.id not in existing_sources:
existing_sources[objitem.source.id] = objitem.source
# Проверяем расстояние до каждого существующего Source
closest_source = None
min_distance = float('inf')
best_new_avg = None
for source in existing_sources.values():
if source.coords_average:
source_coord = (source.coords_average.x, source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, coord)
if distance <= RANGE_DISTANCE and distance < min_distance:
min_distance = distance
closest_source = source
best_new_avg = new_avg
# Если найден близкий Source (≤56 км)
if closest_source:
# Обновляем coords_average инкрементально
closest_source.coords_average = Point(best_new_avg, srid=4326)
closest_source.save()
return closest_source
# Если не найден подходящий Source - создаем новый
source = Source.objects.create(
coords_average=Point(coord, srid=4326),
created_by=user
)
return source
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None): def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
""" """
Импортирует данные из DataFrame с группировкой близких координат. Импортирует данные из DataFrame с группировкой по имени источника и расстоянию.
Улучшенный алгоритм с учетом существующих Source: Алгоритм:
1. Извлечь все координаты и данные строк из DataFrame 1. Для каждой строки DataFrame:
2. Создать список необработанных записей (координата + данные строки) a. Извлечь имя источника (из колонки "Объект наблюдения")
3. Получить все существующие Source из БД b. Найти подходящий Source:
4. Для каждой необработанной записи: - Ищет все Source с таким же именем и спутником
a. Найти ближайший существующий Source (расстояние <= 56 км) - Проверяет расстояние до каждого Source
b. Если найден: - Если найден Source в радиусе ≤56 км - использует его
- Обновить coords_average этого Source (инкрементально) - Иначе создает новый Source
- Создать ObjItem и связать с этим Source c. Обновить coords_average инкрементально
- Удалить запись из списка необработанных d. Создать ObjItem и связать с Source
5. Пока список необработанных записей не пуст:
a. Взять первую запись из списка
b. Создать новый Source с coords_average = эта координата
c. Создать ObjItem для этой записи и связать с Source
d. Удалить запись из списка
e. Для каждой оставшейся записи в списке:
- Вычислить расстояние от её координаты до coords_average
- Если расстояние <= 56 км:
* Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
new_avg = (coords_average + current_coord) / 2
* Обновить coords_average в Source
* Создать ObjItem для этой записи и связать с Source
* Удалить запись из списка
- Иначе: пропустить и проверить следующую запись
6. Сохранить все изменения в БД
Важно: Среднее вычисляется инкрементально - каждая новая точка Важные правила:
усредняется с текущим средним, а не со всеми точками кластера. - Источники разных спутников НЕ объединяются
- Может быть несколько Source с одинаковым именем, но разделенных географически
- Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника
Args: Args:
df: DataFrame с данными df: DataFrame с данными
@@ -208,112 +268,70 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
consts = get_all_constants() consts = get_all_constants()
df.fillna(-1, inplace=True) df.fillna(-1, inplace=True)
# Шаг 1: Извлечь все координаты и данные строк из DataFrame user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
unprocessed_records = [] new_sources_count = 0
added_count = 0
# Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, id Source), Значение: объект Source
# Используем id в ключе, т.к. может быть несколько Source с одним именем
sources_cache = {}
for idx, row in df.iterrows(): for idx, row in df.iterrows():
try: try:
# Извлекаем координату # Извлекаем координату
coord_tuple = coords_transform(row["Координаты"]) coord_tuple = coords_transform(row["Координаты"])
# Сохраняем запись с координатой и данными строки # Извлекаем имя источника
unprocessed_records.append({"coord": coord_tuple, "row": row, "index": idx}) source_name = row["Объект наблюдения"]
# Проверяем кэш: ищем подходящий Source среди закэшированных
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_id = cache_key
# Проверяем имя
if cached_name != source_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.coords_average = Point(new_avg, srid=4326)
cached_source.save()
source = cached_source
found_in_cache = True
break
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat, coord_tuple, user_to_use
)
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, source.id)] = source
# Создаем ObjItem и связываем с Source
_create_objitem_from_row(row, sat, source, user_to_use, consts)
added_count += 1
except Exception as e: except Exception as e:
print(f"Ошибка при обработке строки {idx}: {e}") print(f"Ошибка при обработке строки {idx}: {e}")
continue continue
user_to_use = current_user if current_user else CustomUser.objects.get(id=1) print(f"Импорт завершен: создано {new_sources_count} новых источников, "
source_count = 0 f"добавлено {added_count} точек")
added_to_existing_count = 0
# Шаг 3: Получить все существующие Source из БД return new_sources_count
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
# Шаг 4: Попытка добавить записи к существующим Source
records_to_remove = []
for i, record in enumerate(unprocessed_records):
current_coord = record["coord"]
# Найти ближайший существующий Source
closest_source = None
min_distance = float('inf')
best_new_avg = None
for source in existing_sources:
source_coord = (source.coords_average.x, source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
if distance < min_distance:
min_distance = distance
closest_source = source
best_new_avg = new_avg
# Если найден близкий Source (расстояние <= 56 км)
if closest_source and min_distance <= RANGE_DISTANCE:
# Обновить coords_average инкрементально
closest_source.coords_average = Point(best_new_avg, srid=4326)
closest_source.save()
# Создать ObjItem и связать с существующим Source
_create_objitem_from_row(
record["row"], sat, closest_source, user_to_use, consts
)
added_to_existing_count += 1
# Пометить запись для удаления
records_to_remove.append(i)
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
for i in reversed(records_to_remove):
unprocessed_records.pop(i)
# Шаг 5: Цикл обработки оставшихся записей - создание новых Source
while unprocessed_records:
# Шаг 5a: Взять первую запись из списка
first_record = unprocessed_records.pop(0)
first_coord = first_record["coord"]
# Шаг 5b: Создать новый Source с coords_average = эта координата
source = Source.objects.create(
coords_average=Point(first_coord, srid=4326), created_by=user_to_use
)
source_count += 1
# Шаг 5c: Создать ObjItem для этой записи и связать с Source
_create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
# Шаг 5e: Для каждой оставшейся записи в списке
records_to_remove = []
for i, record in enumerate(unprocessed_records):
current_coord = record["coord"]
# Вычислить расстояние от координаты до coords_average
current_avg = (source.coords_average.x, source.coords_average.y)
new_avg, distance = calculate_mean_coords(current_avg, current_coord)
if distance <= RANGE_DISTANCE:
# Обновить coords_average в Source
source.coords_average = Point(new_avg, srid=4326)
source.save()
# Создать ObjItem для этой записи и связать с Source
_create_objitem_from_row(
record["row"], sat, source, user_to_use, consts
)
# Пометить запись для удаления
records_to_remove.append(i)
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
for i in reversed(records_to_remove):
unprocessed_records.pop(i)
print(f"Импорт завершен: создано {source_count} новых источников, "
f"добавлено {added_to_existing_count} точек к существующим источникам")
return source_count
def _create_objitem_from_row(row, sat, source, user_to_use, consts): def _create_objitem_from_row(row, sat, source, user_to_use, consts):
@@ -509,24 +527,25 @@ def get_point_from_json(filepath: str):
def get_points_from_csv(file_content, current_user=None): def get_points_from_csv(file_content, current_user=None):
""" """
Импортирует данные из CSV с группировкой близких координат. Импортирует данные из CSV с группировкой по имени источника и расстоянию.
Улучшенный алгоритм с учетом существующих Source: Алгоритм:
1. Извлечь все координаты и данные строк из DataFrame 1. Для каждой строки CSV:
2. Создать список необработанных записей (координата + данные строки) a. Извлечь имя источника (из колонки "obj") и спутник
3. Получить все существующие Source из БД b. Проверить дубликаты (координаты + частота)
4. Для каждой записи: c. Найти подходящий Source:
a. Проверить, существует ли дубликат (координаты + частота) - Ищет все Source с таким же именем и спутником
b. Если дубликат найден, пропустить запись - Проверяет расстояние до каждого Source
c. Найти ближайший существующий Source (расстояние <= 56 км) - Если найден Source в радиусе ≤56 км - использует его
d. Если найден: - Иначе создает новый Source
- Обновить coords_average этого Source (инкрементально) d. Обновить coords_average инкрементально
- Создать ObjItem и связать с этим Source e. Создать ObjItem и связать с Source
e. Если не найден:
- Создать новый Source Важные правила:
- Создать ObjItem и связать с новым Source - Источники разных спутников НЕ объединяются
- Добавить новый Source в список существующих - Может быть несколько Source с одинаковым именем, но разделенных географически
5. Сохранить все изменения в БД - Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника
Args: Args:
file_content: содержимое CSV файла file_content: содержимое CSV файла
@@ -563,76 +582,77 @@ def get_points_from_csv(file_content, current_user=None):
) )
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S") df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
# Шаг 1: Извлечь все координаты и данные строк из DataFrame user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
records = [] new_sources_count = 0
added_count = 0
skipped_count = 0
# Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, имя спутника, id Source), Значение: объект Source
sources_cache = {}
for idx, row in df.iterrows(): for idx, row in df.iterrows():
try: try:
# Извлекаем координату из колонок lat и lon # Извлекаем координату из колонок lat и lon
coord_tuple = (row["lon"], row["lat"]) coord_tuple = (row["lon"], row["lat"])
# Сохраняем запись с координатой и данными строки # Извлекаем имя источника и спутника
records.append({"coord": coord_tuple, "row": row, "index": idx}) source_name = row["obj"]
sat_name = row["sat"]
# Проверяем дубликаты
if _is_duplicate_objitem(coord_tuple, row["freq"], row["f_range"]):
skipped_count += 1
continue
# Получаем или создаем объект спутника
sat_obj, _ = Satellite.objects.get_or_create(
name=sat_name, defaults={"norad": row["norad_id"]}
)
# Проверяем кэш: ищем подходящий Source среди закэшированных
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_sat, cached_id = cache_key
# Проверяем имя и спутник
if cached_name != source_name or cached_sat != sat_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.coords_average = Point(new_avg, srid=4326)
cached_source.save()
source = cached_source
found_in_cache = True
break
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat_obj, coord_tuple, user_to_use
)
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, sat_name, source.id)] = source
# Создаем ObjItem и связываем с Source
_create_objitem_from_csv_row(row, source, user_to_use)
added_count += 1
except Exception as e: except Exception as e:
print(f"Ошибка при обработке строки {idx}: {e}") print(f"Ошибка при обработке строки {idx}: {e}")
continue continue
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
# Шаг 3: Получить все существующие Source из БД
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
new_sources_count = 0
added_count = 0
skipped_count = 0
# Шаг 4: Обработка каждой записи
for record in records:
current_coord = record["coord"]
row = record["row"]
# Шаг 4a: Проверить, существует ли дубликат (координаты + частота)
if _is_duplicate_objitem(current_coord, row["freq"], row["f_range"]):
skipped_count += 1
continue
# Шаг 4c: Найти ближайший существующий Source
closest_source = None
min_distance = float('inf')
best_new_avg = None
for source in existing_sources:
source_coord = (source.coords_average.x, source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
if distance < min_distance:
min_distance = distance
closest_source = source
best_new_avg = new_avg
# Шаг 4d: Если найден близкий Source (расстояние <= 56 км)
if closest_source and min_distance <= RANGE_DISTANCE:
# Обновить coords_average инкрементально
closest_source.coords_average = Point(best_new_avg, srid=4326)
closest_source.save()
# Создать ObjItem и связать с существующим Source
_create_objitem_from_csv_row(row, closest_source, user_to_use)
added_count += 1
else:
# Шаг 4e: Создать новый Source
new_source = Source.objects.create(
coords_average=Point(current_coord, srid=4326),
created_by=user_to_use
)
new_sources_count += 1
# Создать ObjItem и связать с новым Source
_create_objitem_from_csv_row(row, new_source, user_to_use)
added_count += 1
# Добавить новый Source в список существующих
existing_sources.append(new_source)
print(f"Импорт завершен: создано {new_sources_count} новых источников, " print(f"Импорт завершен: создано {new_sources_count} новых источников, "
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов") f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")

View File

@@ -1,75 +0,0 @@
# Views Module Structure
This directory contains the refactored views from the original monolithic `views.py` file.
## File Organization
### `__init__.py`
Central import file that exports all views for easy access. This allows other modules to import views using:
```python
from mainapp.views import ObjItemListView, custom_logout
```
### `base.py`
Basic views and utilities:
- `ActionsPageView` - Displays the actions page
- `custom_logout()` - Custom logout function
### `objitem.py`
ObjItem CRUD operations and related views:
- `ObjItemListView` - List view with filtering and pagination
- `ObjItemFormView` - Base class for create/update operations
- `ObjItemCreateView` - Create new ObjItem
- `ObjItemUpdateView` - Update existing ObjItem
- `ObjItemDeleteView` - Delete ObjItem
- `ObjItemDetailView` - Read-only detail view
- `DeleteSelectedObjectsView` - Bulk delete operation
### `data_import.py`
Data import views for various formats:
- `AddSatellitesView` - Add satellites to database
- `AddTranspondersView` - Upload and parse transponder data from XML
- `LoadExcelDataView` - Load data from Excel files
- `LoadCsvDataView` - Load data from CSV files
- `UploadVchLoadView` - Upload VCH load data from HTML
- `LinkVchSigmaView` - Link VCH data with Sigma parameters
- `ProcessKubsatView` - Process Kubsat event data
### `api.py`
API endpoints for AJAX requests:
- `GetLocationsView` - Get locations by satellite ID in GeoJSON format
- `LyngsatDataAPIView` - Get LyngSat source data
- `SigmaParameterDataAPIView` - Get SigmaParameter data
- `SourceObjItemsAPIView` - Get ObjItems related to a Source
- `LyngsatTaskStatusAPIView` - Get Celery task status
### `lyngsat.py`
LyngSat related views:
- `LinkLyngsatSourcesView` - Link LyngSat sources to objects
- `FillLyngsatDataView` - Fill data from Lyngsat website
- `LyngsatTaskStatusView` - Track Lyngsat data filling task status
- `ClearLyngsatCacheView` - Clear LyngSat cache
### `source.py`
Source related views:
- `SourceListView` - List view for Source objects with filtering
### `map.py`
Map related views:
- `ShowMapView` - Display objects on map (admin interface)
- `ShowSelectedObjectsMapView` - Display selected objects on map
- `ClusterTestView` - Test view for clustering functionality
## Migration Notes
The original `views.py` has been renamed to `views_old.py` as a backup. All imports have been updated in:
- `dbapp/mainapp/urls.py`
- `dbapp/dbapp/urls.py`
## Benefits of This Structure
1. **Better Organization** - Related views are grouped together
2. **Easier Maintenance** - Smaller files are easier to navigate and modify
3. **Clear Responsibilities** - Each file has a specific purpose
4. **Improved Testability** - Easier to write focused unit tests
5. **Better Collaboration** - Multiple developers can work on different files without conflicts

View File

@@ -1,5 +1,5 @@
# Import all views for easy access # Import all views for easy access
from .base import ActionsPageView, custom_logout from .base import ActionsPageView, HomeView, custom_logout
from .objitem import ( from .objitem import (
ObjItemListView, ObjItemListView,
ObjItemCreateView, ObjItemCreateView,
@@ -30,6 +30,7 @@ from .lyngsat import (
FillLyngsatDataView, FillLyngsatDataView,
LyngsatTaskStatusView, LyngsatTaskStatusView,
ClearLyngsatCacheView, ClearLyngsatCacheView,
UnlinkAllLyngsatSourcesView,
) )
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
from .transponder import ( from .transponder import (
@@ -50,6 +51,7 @@ from .map import (
__all__ = [ __all__ = [
# Base # Base
'ActionsPageView', 'ActionsPageView',
'HomeView',
'custom_logout', 'custom_logout',
# ObjItem # ObjItem
'ObjItemListView', 'ObjItemListView',
@@ -78,6 +80,7 @@ __all__ = [
'FillLyngsatDataView', 'FillLyngsatDataView',
'LyngsatTaskStatusView', 'LyngsatTaskStatusView',
'ClearLyngsatCacheView', 'ClearLyngsatCacheView',
'UnlinkAllLyngsatSourcesView',
# Source # Source
'SourceListView', 'SourceListView',
'SourceUpdateView', 'SourceUpdateView',

View File

@@ -176,9 +176,14 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
"""API endpoint for getting ObjItems related to a Source.""" """API endpoint for getting ObjItems related to a Source."""
def get(self, request, source_id): def get(self, request, source_id):
from datetime import datetime, timedelta
from ..models import Source from ..models import Source
try: try:
# Get filter parameters from query string
geo_date_from = request.GET.get("geo_date_from", "").strip()
geo_date_to = request.GET.get("geo_date_to", "").strip()
# Load Source with prefetch_related for ObjItem # Load Source with prefetch_related for ObjItem
source = Source.objects.prefetch_related( source = Source.objects.prefetch_related(
'source_objitems', 'source_objitems',
@@ -186,11 +191,38 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'source_objitems__parameter_obj__id_satellite', 'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization', 'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation', 'source_objitems__parameter_obj__modulation',
'source_objitems__geo_obj' 'source_objitems__parameter_obj__standard',
'source_objitems__geo_obj',
'source_objitems__geo_obj__mirrors',
'source_objitems__lyngsat_source',
'source_objitems__transponder',
'source_objitems__created_by__user',
'source_objitems__updated_by__user',
'marks',
'marks__created_by__user'
).get(id=source_id) ).get(id=source_id)
# Get all related ObjItems, sorted by created_at # Get all related ObjItems, sorted by created_at
objitems = source.source_objitems.all().order_by('created_at') objitems = source.source_objitems.all()
# Apply Geo timestamp filter if provided
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
objitems = objitems.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
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)
objitems = objitems.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
objitems = objitems.order_by('created_at')
objitems_data = [] objitems_data = []
for objitem in objitems: for objitem in objitems:
@@ -202,9 +234,12 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
polarization = '-' polarization = '-'
bod_velocity = '-' bod_velocity = '-'
modulation = '-' modulation = '-'
standard = '-'
snr = '-' snr = '-'
parameter_id = None
if param: if param:
parameter_id = param.id
if hasattr(param, 'id_satellite') and param.id_satellite: if hasattr(param, 'id_satellite') and param.id_satellite:
satellite_name = param.id_satellite.name satellite_name = param.id_satellite.name
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-' frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
@@ -214,6 +249,8 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-' bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
if hasattr(param, 'modulation') and param.modulation: if hasattr(param, 'modulation') and param.modulation:
modulation = param.modulation.name modulation = param.modulation.name
if hasattr(param, 'standard') and param.standard:
standard = param.standard.name
snr = f"{param.snr:.0f}" if param.snr is not None else '-' snr = f"{param.snr:.0f}" if param.snr is not None else '-'
# Get geo data # Get geo data
@@ -235,6 +272,56 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}" geo_coords = f"{lat} {lon}"
# Get created/updated info
created_at = '-'
if objitem.created_at:
local_time = timezone.localtime(objitem.created_at)
created_at = local_time.strftime("%d.%m.%Y %H:%M")
updated_at = '-'
if objitem.updated_at:
local_time = timezone.localtime(objitem.updated_at)
updated_at = local_time.strftime("%d.%m.%Y %H:%M")
created_by = str(objitem.created_by) if objitem.created_by else '-'
updated_by = str(objitem.updated_by) if objitem.updated_by else '-'
# Check for LyngSat
has_lyngsat = hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source is not None
lyngsat_id = objitem.lyngsat_source.id if has_lyngsat else None
# Check for Transponder
has_transponder = hasattr(objitem, 'transponder') and objitem.transponder is not None
transponder_id = objitem.transponder.id if has_transponder else None
transponder_info = '-'
if has_transponder:
try:
downlink = objitem.transponder.downlink if objitem.transponder.downlink else '-'
freq_range_t = objitem.transponder.frequency_range if objitem.transponder.frequency_range else '-'
transponder_info = f"{downlink}:{freq_range_t}"
except Exception:
transponder_info = '-'
# Check for Sigma
has_sigma = False
sigma_info = '-'
if param and hasattr(param, 'sigma_parameter'):
sigma_count = param.sigma_parameter.count()
if sigma_count > 0:
has_sigma = True
sigma_info = f"{sigma_count}"
# Get comment, is_average, and mirrors from geo_obj
comment = '-'
is_average = '-'
mirrors = '-'
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
comment = objitem.geo_obj.comment or '-'
is_average = 'Да' if objitem.geo_obj.is_average else 'Нет'
# Get mirrors list
mirrors_list = list(objitem.geo_obj.mirrors.values_list('name', flat=True))
mirrors = ', '.join(mirrors_list) if mirrors_list else '-'
objitems_data.append({ objitems_data.append({
'id': objitem.id, 'id': objitem.id,
'name': objitem.name or '-', 'name': objitem.name or '-',
@@ -244,15 +331,47 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'polarization': polarization, 'polarization': polarization,
'bod_velocity': bod_velocity, 'bod_velocity': bod_velocity,
'modulation': modulation, 'modulation': modulation,
'standard': standard,
'snr': snr, 'snr': snr,
'geo_timestamp': geo_timestamp, 'geo_timestamp': geo_timestamp,
'geo_location': geo_location, 'geo_location': geo_location,
'geo_coords': geo_coords 'geo_coords': geo_coords,
'created_at': created_at,
'updated_at': updated_at,
'created_by': created_by,
'updated_by': updated_by,
'comment': comment,
'is_average': is_average,
'has_lyngsat': has_lyngsat,
'lyngsat_id': lyngsat_id,
'has_transponder': has_transponder,
'transponder_id': transponder_id,
'transponder_info': transponder_info,
'has_sigma': has_sigma,
'sigma_info': sigma_info,
'parameter_id': parameter_id,
'mirrors': mirrors,
})
# Get marks for the source
marks_data = []
for mark in source.marks.all().order_by('-timestamp'):
mark_timestamp = '-'
if mark.timestamp:
local_time = timezone.localtime(mark.timestamp)
mark_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
marks_data.append({
'id': mark.id,
'mark': mark.mark,
'timestamp': mark_timestamp,
'created_by': str(mark.created_by) if mark.created_by else '-',
}) })
return JsonResponse({ return JsonResponse({
'source_id': source_id, 'source_id': source_id,
'objitems': objitems_data 'objitems': objitems_data,
'marks': marks_data
}) })
except Source.DoesNotExist: except Source.DoesNotExist:
return JsonResponse({'error': 'Источник не найден'}, status=404) return JsonResponse({'error': 'Источник не найден'}, status=404)

View File

@@ -1,10 +1,491 @@
""" """
Base views and utilities. Base views and utilities.
""" """
from datetime import datetime, timedelta
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Count, Q
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.views import View from django.views import View
from ..models import Source, ObjItem, Satellite, Modulation, Polarization, ObjectMark
from ..utils import parse_pagination_params
class HomeView(LoginRequiredMixin, View):
"""
Main page with filters for displaying sources or objitems.
"""
def get(self, request):
# Get pagination parameters
page_number, items_per_page = parse_pagination_params(request)
# Get display mode: 'sources' or 'objitems'
display_mode = request.GET.get("display_mode", "sources")
# Get filter parameters
selected_satellites = request.GET.getlist("satellite_id")
date_from = request.GET.get("date_from", "").strip()
date_to = request.GET.get("date_to", "").strip()
freq_min = request.GET.get("freq_min", "").strip()
freq_max = request.GET.get("freq_max", "").strip()
range_min = request.GET.get("range_min", "").strip()
range_max = request.GET.get("range_max", "").strip()
selected_modulations = request.GET.getlist("modulation")
selected_polarizations = request.GET.getlist("polarization")
# Source-specific filters
has_coords_average = request.GET.get("has_coords_average")
has_kupsat = request.GET.get("has_kupsat")
has_valid = request.GET.get("has_valid")
has_reference = request.GET.get("has_reference")
# ObjItem-specific filters
has_geo = request.GET.get("has_geo")
has_lyngsat = request.GET.get("has_lyngsat")
# Marks filters
show_marks = request.GET.get("show_marks", "0")
marks_date_from = request.GET.get("marks_date_from", "").strip()
marks_date_to = request.GET.get("marks_date_to", "").strip()
marks_status = request.GET.get("marks_status", "") # all, present, absent
# Get all satellites, modulations, polarizations for filters
satellites = Satellite.objects.all().order_by("name")
modulations = Modulation.objects.all().order_by("name")
polarizations = Polarization.objects.all().order_by("name")
# Prepare context
context = {
'display_mode': display_mode,
'satellites': satellites,
'modulations': modulations,
'polarizations': polarizations,
'selected_satellites': [int(x) for x in selected_satellites if x.isdigit()],
'selected_modulations': [int(x) for x in selected_modulations if x.isdigit()],
'selected_polarizations': [int(x) for x in selected_polarizations if x.isdigit()],
'date_from': date_from,
'date_to': date_to,
'freq_min': freq_min,
'freq_max': freq_max,
'range_min': range_min,
'range_max': range_max,
'has_coords_average': has_coords_average,
'has_kupsat': has_kupsat,
'has_valid': has_valid,
'has_reference': has_reference,
'has_geo': has_geo,
'has_lyngsat': has_lyngsat,
'show_marks': show_marks,
'marks_date_from': marks_date_from,
'marks_date_to': marks_date_to,
'marks_status': marks_status,
'items_per_page': items_per_page,
'available_items_per_page': [50, 100, 500, 1000],
'full_width_page': True,
}
if display_mode == "objitems":
# Display ObjItems
queryset = self._get_objitems_queryset(
selected_satellites, date_from, date_to,
freq_min, freq_max, range_min, range_max,
selected_modulations, selected_polarizations,
has_geo, has_lyngsat
)
paginator = Paginator(queryset, items_per_page)
page_obj = paginator.get_page(page_number)
processed_objitems = self._process_objitems(
page_obj, show_marks, marks_date_from, marks_date_to, marks_status
)
context.update({
'page_obj': page_obj,
'processed_objitems': processed_objitems,
})
else:
# Display Sources
queryset = self._get_sources_queryset(
selected_satellites, date_from, date_to,
freq_min, freq_max, range_min, range_max,
selected_modulations, selected_polarizations,
has_coords_average, has_kupsat, has_valid, has_reference
)
paginator = Paginator(queryset, items_per_page)
page_obj = paginator.get_page(page_number)
processed_sources = self._process_sources(
page_obj, show_marks, marks_date_from, marks_date_to, marks_status
)
context.update({
'page_obj': page_obj,
'processed_sources': processed_sources,
})
return render(request, "mainapp/home.html", context)
def _get_sources_queryset(self, selected_satellites, date_from, date_to,
freq_min, freq_max, range_min, range_max,
selected_modulations, selected_polarizations,
has_coords_average, has_kupsat, has_valid, has_reference):
"""Build queryset for sources with filters."""
sources = Source.objects.prefetch_related(
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__geo_obj'
).annotate(objitem_count=Count('source_objitems'))
# Filter by satellites
if selected_satellites:
sources = sources.filter(
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
).distinct()
# Filter by date range (using Geo timestamps)
if date_from or date_to:
geo_filter = Q()
if date_from:
try:
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
geo_filter &= Q(source_objitems__geo_obj__timestamp__gte=date_from_obj)
except (ValueError, TypeError):
pass
if date_to:
try:
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
geo_filter &= Q(source_objitems__geo_obj__timestamp__lt=date_to_obj)
except (ValueError, TypeError):
pass
if geo_filter:
sources = sources.filter(geo_filter).distinct()
# Filter by frequency
if freq_min:
try:
sources = sources.filter(
source_objitems__parameter_obj__frequency__gte=float(freq_min)
).distinct()
except ValueError:
pass
if freq_max:
try:
sources = sources.filter(
source_objitems__parameter_obj__frequency__lte=float(freq_max)
).distinct()
except ValueError:
pass
# Filter by frequency range
if range_min:
try:
sources = sources.filter(
source_objitems__parameter_obj__freq_range__gte=float(range_min)
).distinct()
except ValueError:
pass
if range_max:
try:
sources = sources.filter(
source_objitems__parameter_obj__freq_range__lte=float(range_max)
).distinct()
except ValueError:
pass
# Filter by modulation
if selected_modulations:
sources = sources.filter(
source_objitems__parameter_obj__modulation_id__in=selected_modulations
).distinct()
# Filter by polarization
if selected_polarizations:
sources = sources.filter(
source_objitems__parameter_obj__polarization_id__in=selected_polarizations
).distinct()
# Filter by coordinates presence
if has_coords_average == "1":
sources = sources.filter(coords_average__isnull=False)
elif has_coords_average == "0":
sources = sources.filter(coords_average__isnull=True)
if has_kupsat == "1":
sources = sources.filter(coords_kupsat__isnull=False)
elif has_kupsat == "0":
sources = sources.filter(coords_kupsat__isnull=True)
if has_valid == "1":
sources = sources.filter(coords_valid__isnull=False)
elif has_valid == "0":
sources = sources.filter(coords_valid__isnull=True)
if has_reference == "1":
sources = sources.filter(coords_reference__isnull=False)
elif has_reference == "0":
sources = sources.filter(coords_reference__isnull=True)
return sources.order_by('-id')
def _get_objitems_queryset(self, selected_satellites, date_from, date_to,
freq_min, freq_max, range_min, range_max,
selected_modulations, selected_polarizations,
has_geo, has_lyngsat):
"""Build queryset for objitems with filters."""
objitems = ObjItem.objects.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__modulation',
'parameter_obj__polarization',
'geo_obj',
'source',
'lyngsat_source'
)
# Filter by satellites
if selected_satellites:
objitems = objitems.filter(parameter_obj__id_satellite_id__in=selected_satellites)
# Filter by date range
if date_from:
try:
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
objitems = objitems.filter(geo_obj__timestamp__gte=date_from_obj)
except (ValueError, TypeError):
pass
if date_to:
try:
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
objitems = objitems.filter(geo_obj__timestamp__lt=date_to_obj)
except (ValueError, TypeError):
pass
# Filter by frequency
if freq_min:
try:
objitems = objitems.filter(parameter_obj__frequency__gte=float(freq_min))
except ValueError:
pass
if freq_max:
try:
objitems = objitems.filter(parameter_obj__frequency__lte=float(freq_max))
except ValueError:
pass
# Filter by frequency range
if range_min:
try:
objitems = objitems.filter(parameter_obj__freq_range__gte=float(range_min))
except ValueError:
pass
if range_max:
try:
objitems = objitems.filter(parameter_obj__freq_range__lte=float(range_max))
except ValueError:
pass
# Filter by modulation
if selected_modulations:
objitems = objitems.filter(parameter_obj__modulation_id__in=selected_modulations)
# Filter by polarization
if selected_polarizations:
objitems = objitems.filter(parameter_obj__polarization_id__in=selected_polarizations)
# Filter by coordinates presence
if has_geo == "1":
objitems = objitems.filter(geo_obj__isnull=False)
elif has_geo == "0":
objitems = objitems.filter(geo_obj__isnull=True)
# Filter by LyngSat connection
if has_lyngsat == "1":
objitems = objitems.filter(lyngsat_source__isnull=False)
elif has_lyngsat == "0":
objitems = objitems.filter(lyngsat_source__isnull=True)
return objitems.order_by('-id')
def _process_sources(self, page_obj, show_marks="0", marks_date_from="", marks_date_to="", marks_status=""):
"""Process sources for display."""
processed = []
for source in page_obj:
# Get satellites
satellite_names = set()
for objitem in source.source_objitems.all():
if objitem.parameter_obj and objitem.parameter_obj.id_satellite:
satellite_names.add(objitem.parameter_obj.id_satellite.name)
# Format coordinates
def format_coords(point):
if point:
lon, lat = point.coords[0], point.coords[1]
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
return f"{lat_str} {lon_str}"
return "-"
# Get marks if requested
marks_data = []
if show_marks == "1":
marks_qs = source.marks.select_related('created_by__user').all()
# Filter marks by date
if marks_date_from:
try:
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%dT%H:%M")
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
except (ValueError, TypeError):
try:
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%d")
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
except (ValueError, TypeError):
pass
if marks_date_to:
try:
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%dT%H:%M")
marks_qs = marks_qs.filter(timestamp__lte=date_to_obj)
except (ValueError, TypeError):
try:
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%d") + timedelta(days=1)
marks_qs = marks_qs.filter(timestamp__lt=date_to_obj)
except (ValueError, TypeError):
pass
# Filter marks by status
if marks_status == "present":
marks_qs = marks_qs.filter(mark=True)
elif marks_status == "absent":
marks_qs = marks_qs.filter(mark=False)
# Process marks
for mark in marks_qs:
marks_data.append({
'id': mark.id,
'mark': mark.mark,
'timestamp': mark.timestamp,
'created_by': str(mark.created_by) if mark.created_by else "-",
'can_edit': mark.can_edit(),
})
processed.append({
'id': source.id,
'satellites': ", ".join(sorted(satellite_names)) if satellite_names else "-",
'objitem_count': source.objitem_count,
'coords_average': format_coords(source.coords_average),
'coords_kupsat': format_coords(source.coords_kupsat),
'coords_valid': format_coords(source.coords_valid),
'coords_reference': format_coords(source.coords_reference),
'created_at': source.created_at,
'marks': marks_data,
})
return processed
def _process_objitems(self, page_obj, show_marks="0", marks_date_from="", marks_date_to="", marks_status=""):
"""Process objitems for display."""
processed = []
for objitem in page_obj:
param = objitem.parameter_obj
geo = objitem.geo_obj
source = objitem.source
# Format geo coordinates
geo_coords = "-"
geo_date = "-"
if geo and geo.coords:
lon, lat = geo.coords.coords[0], geo.coords.coords[1]
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
geo_coords = f"{lat_str} {lon_str}"
if geo.timestamp:
geo_date = geo.timestamp.strftime("%Y-%m-%d")
# Format source coordinates
def format_coords(point):
if point:
lon, lat = point.coords[0], point.coords[1]
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
return f"{lat_str} {lon_str}"
return "-"
kupsat_coords = format_coords(source.coords_kupsat) if source else "-"
valid_coords = format_coords(source.coords_valid) if source else "-"
# Get marks if requested
marks_data = []
if show_marks == "1":
marks_qs = objitem.marks.select_related('created_by__user').all()
# Filter marks by date
if marks_date_from:
try:
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%d")
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
except (ValueError, TypeError):
pass
if marks_date_to:
try:
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%d") + timedelta(days=1)
marks_qs = marks_qs.filter(timestamp__lt=date_to_obj)
except (ValueError, TypeError):
pass
# Filter marks by status
if marks_status == "present":
marks_qs = marks_qs.filter(mark=True)
elif marks_status == "absent":
marks_qs = marks_qs.filter(mark=False)
# Process marks
for mark in marks_qs:
marks_data.append({
'id': mark.id,
'mark': mark.mark,
'timestamp': mark.timestamp,
'created_by': str(mark.created_by) if mark.created_by else "-",
'can_edit': mark.can_edit(),
})
processed.append({
'id': objitem.id,
'name': objitem.name or "-",
'satellite': param.id_satellite.name if param and param.id_satellite else "-",
'frequency': param.frequency if param else "-",
'freq_range': param.freq_range if param else "-",
'polarization': param.polarization.name if param and param.polarization else "-",
'modulation': param.modulation.name if param and param.modulation else "-",
'bod_velocity': param.bod_velocity if param else "-",
'snr': param.snr if param else "-",
'geo_coords': geo_coords,
'geo_date': geo_date,
'kupsat_coords': kupsat_coords,
'valid_coords': valid_coords,
'source_id': source.id if source else None,
'lyngsat_id': objitem.lyngsat_source.id if objitem.lyngsat_source else None,
'marks': marks_data,
})
return processed
class ActionsPageView(View): class ActionsPageView(View):
"""View for displaying the actions page.""" """View for displaying the actions page."""

View File

@@ -46,22 +46,30 @@ class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
param = objitem.parameter_obj param = objitem.parameter_obj
# Round object frequency # Round object frequency to 1 decimal place
if param.frequency: if param.frequency:
rounded_freq = round(param.frequency, 0) # Round to integer rounded_freq = round(param.frequency, 1) # Round to 1 decimal place
# Find matching LyngSat source # Find matching LyngSat source
# Compare by rounded frequency and polarization # Compare by rounded frequency (with tolerance) and polarization
# LyngSat frequencies are also rounded to 1 decimal place for comparison
lyngsat_sources = LyngSat.objects.filter( lyngsat_sources = LyngSat.objects.filter(
id_satellite=param.id_satellite, id_satellite=param.id_satellite,
polarization=param.polarization, polarization=param.polarization
frequency__gte=rounded_freq - frequency_tolerance, ).select_related('id_satellite', 'polarization')
frequency__lte=rounded_freq + frequency_tolerance
).order_by('frequency')
if lyngsat_sources.exists(): # Filter by rounded frequency with tolerance
# Take first matching source matching_sources = []
objitem.lyngsat_source = lyngsat_sources.first() for lyngsat in lyngsat_sources:
if lyngsat.frequency:
rounded_lyngsat_freq = round(lyngsat.frequency, 1)
if abs(rounded_lyngsat_freq - rounded_freq) <= frequency_tolerance:
matching_sources.append(lyngsat)
if matching_sources:
# Take first matching source (sorted by frequency difference)
matching_sources.sort(key=lambda x: abs(round(x.frequency, 1) - rounded_freq))
objitem.lyngsat_source = matching_sources[0]
objitem.save(update_fields=['lyngsat_source']) objitem.save(update_fields=['lyngsat_source'])
linked_count += 1 linked_count += 1
@@ -159,3 +167,35 @@ class ClearLyngsatCacheView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
"""Cache management page.""" """Cache management page."""
return render(request, 'mainapp/clear_lyngsat_cache.html') return render(request, 'mainapp/clear_lyngsat_cache.html')
class UnlinkAllLyngsatSourcesView(LoginRequiredMixin, View):
"""View for unlinking all LyngSat sources from ObjItems."""
def post(self, request):
"""Unlink all LyngSat sources."""
try:
# Count objects with LyngSat sources before unlinking
linked_count = ObjItem.objects.filter(lyngsat_source__isnull=False).count()
# Unlink all LyngSat sources
ObjItem.objects.filter(lyngsat_source__isnull=False).update(lyngsat_source=None)
messages.success(
request,
f"Успешно отвязано {linked_count} объектов от источников LyngSat"
)
except Exception as e:
messages.error(request, f"Ошибка при отвязке источников: {str(e)}")
return redirect('mainapp:actions')
def get(self, request):
"""Show confirmation page."""
# Count objects with LyngSat sources
linked_count = ObjItem.objects.filter(lyngsat_source__isnull=False).count()
context = {
'linked_count': linked_count
}
return render(request, 'mainapp/unlink_lyngsat_confirm.html', context)

View File

@@ -0,0 +1,142 @@
"""
Views для управления отметками объектов.
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Prefetch
from django.http import JsonResponse
from django.views.generic import ListView, View
from django.shortcuts import get_object_or_404
from mainapp.models import Source, ObjectMark, CustomUser
class ObjectMarksListView(LoginRequiredMixin, ListView):
"""
Представление списка источников с отметками.
"""
model = Source
template_name = "mainapp/object_marks.html"
context_object_name = "sources"
paginate_by = 50
def get_queryset(self):
"""Получить queryset с предзагруженными связанными данными"""
queryset = Source.objects.prefetch_related(
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
Prefetch(
'marks',
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')
)
).order_by('-updated_at')
# Фильтрация по спутнику
satellite_id = self.request.GET.get('satellite')
if satellite_id:
queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id=satellite_id).distinct()
return queryset
def get_context_data(self, **kwargs):
"""Добавить дополнительные данные в контекст"""
context = super().get_context_data(**kwargs)
from mainapp.models import Satellite
context['satellites'] = Satellite.objects.all().order_by('name')
# Добавить информацию о возможности редактирования для каждой отметки
for source in context['sources']:
for mark in source.marks.all():
mark.editable = mark.can_edit()
return context
class AddObjectMarkView(LoginRequiredMixin, View):
"""
API endpoint для добавления отметки источника.
"""
def post(self, request, *args, **kwargs):
"""Создать новую отметку"""
from datetime import timedelta
from django.utils import timezone
source_id = request.POST.get('source_id')
mark = request.POST.get('mark') == 'true'
if not source_id:
return JsonResponse({'success': False, 'error': 'Не указан ID источника'}, status=400)
source = get_object_or_404(Source, pk=source_id)
# Проверить последнюю отметку источника
last_mark = source.marks.first()
if last_mark:
time_diff = timezone.now() - last_mark.timestamp
if time_diff < timedelta(minutes=5):
minutes_left = 5 - int(time_diff.total_seconds() / 60)
return JsonResponse({
'success': False,
'error': f'Нельзя добавить отметку. Подождите ещё {minutes_left} мин.'
}, status=400)
# Получить или создать CustomUser для текущего пользователя
custom_user, _ = CustomUser.objects.get_or_create(user=request.user)
# Создать отметку
object_mark = ObjectMark.objects.create(
source=source,
mark=mark,
created_by=custom_user
)
return JsonResponse({
'success': True,
'mark': {
'id': object_mark.id,
'mark': object_mark.mark,
'timestamp': object_mark.timestamp.strftime('%d.%m.%Y %H:%M'),
'created_by': str(object_mark.created_by) if object_mark.created_by else 'Неизвестно',
'can_edit': object_mark.can_edit()
}
})
class UpdateObjectMarkView(LoginRequiredMixin, View):
"""
API endpoint для обновления отметки объекта (в течение 5 минут).
"""
def post(self, request, *args, **kwargs):
"""Обновить существующую отметку"""
mark_id = request.POST.get('mark_id')
new_mark_value = request.POST.get('mark') == 'true'
if not mark_id:
return JsonResponse({'success': False, 'error': 'Не указан ID отметки'}, status=400)
object_mark = get_object_or_404(ObjectMark, pk=mark_id)
# Проверить возможность редактирования
if not object_mark.can_edit():
return JsonResponse({
'success': False,
'error': 'Время редактирования истекло (более 5 минут)'
}, status=400)
# Обновить отметку
object_mark.mark = new_mark_value
object_mark.save()
return JsonResponse({
'success': True,
'mark': {
'id': object_mark.id,
'mark': object_mark.mark,
'timestamp': object_mark.timestamp.strftime('%d.%m.%Y %H:%M'),
'created_by': str(object_mark.created_by) if object_mark.created_by else 'Неизвестно',
'can_edit': object_mark.can_edit()
}
})

View File

@@ -35,10 +35,13 @@ class SourceListView(LoginRequiredMixin, View):
has_coords_kupsat = request.GET.get("has_coords_kupsat") has_coords_kupsat = request.GET.get("has_coords_kupsat")
has_coords_valid = request.GET.get("has_coords_valid") has_coords_valid = request.GET.get("has_coords_valid")
has_coords_reference = request.GET.get("has_coords_reference") has_coords_reference = request.GET.get("has_coords_reference")
has_lyngsat = request.GET.get("has_lyngsat")
objitem_count_min = request.GET.get("objitem_count_min", "").strip() objitem_count_min = request.GET.get("objitem_count_min", "").strip()
objitem_count_max = request.GET.get("objitem_count_max", "").strip() objitem_count_max = request.GET.get("objitem_count_max", "").strip()
date_from = request.GET.get("date_from", "").strip() date_from = request.GET.get("date_from", "").strip()
date_to = request.GET.get("date_to", "").strip() date_to = request.GET.get("date_to", "").strip()
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_satellites = request.GET.getlist("satellite_id")
# Get all satellites for filter # Get all satellites for filter
@@ -49,6 +52,28 @@ class SourceListView(LoginRequiredMixin, View):
.order_by("name") .order_by("name")
) )
# Build Q object for geo date filtering
geo_date_q = Q()
has_geo_date_filter = False
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
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")
# 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
except (ValueError, TypeError):
pass
# Get all Source objects with query optimization # Get all Source objects with query optimization
# Using annotate to count ObjItems efficiently (single query with GROUP BY) # Using annotate to count ObjItems efficiently (single query with GROUP BY)
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries # Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
@@ -56,9 +81,11 @@ class SourceListView(LoginRequiredMixin, View):
'source_objitems', 'source_objitems',
'source_objitems__parameter_obj', 'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite', 'source_objitems__parameter_obj__id_satellite',
'source_objitems__geo_obj' 'source_objitems__geo_obj',
'marks',
'marks__created_by__user'
).annotate( ).annotate(
objitem_count=Count('source_objitems') objitem_count=Count('source_objitems', filter=geo_date_q) if has_geo_date_filter else Count('source_objitems')
) )
# Apply filters # Apply filters
@@ -86,6 +113,14 @@ class SourceListView(LoginRequiredMixin, View):
elif has_coords_reference == "0": elif has_coords_reference == "0":
sources = sources.filter(coords_reference__isnull=True) sources = sources.filter(coords_reference__isnull=True)
# Filter by LyngSat presence
if has_lyngsat == "1":
sources = sources.filter(source_objitems__lyngsat_source__isnull=False).distinct()
elif has_lyngsat == "0":
sources = sources.filter(
~Q(source_objitems__lyngsat_source__isnull=False)
).distinct()
# Filter by ObjItem count # Filter by ObjItem count
if objitem_count_min: if objitem_count_min:
try: try:
@@ -119,6 +154,10 @@ class SourceListView(LoginRequiredMixin, View):
except (ValueError, TypeError): except (ValueError, TypeError):
pass 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()
# Search by ID # Search by ID
if search_query: if search_query:
try: try:
@@ -155,6 +194,8 @@ class SourceListView(LoginRequiredMixin, View):
# Prepare data for display # Prepare data for display
processed_sources = [] processed_sources = []
has_any_lyngsat = False # Track if any source has LyngSat data
for source in page_obj: for source in page_obj:
# Format coordinates # Format coordinates
def format_coords(point): def format_coords(point):
@@ -171,18 +212,54 @@ class SourceListView(LoginRequiredMixin, View):
coords_valid_str = format_coords(source.coords_valid) coords_valid_str = format_coords(source.coords_valid)
coords_reference_str = format_coords(source.coords_reference) coords_reference_str = format_coords(source.coords_reference)
# Get count of related ObjItems # Filter objitems by geo date if filter is applied
objitem_count = source.objitem_count objitems_to_display = source.source_objitems.all()
if geo_date_from or geo_date_to:
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
# Get satellites for this source # Get count of related ObjItems (filtered)
objitem_count = objitems_to_display.count()
# Get satellites for this source and check for LyngSat
satellite_names = set() satellite_names = set()
for objitem in source.source_objitems.all(): has_lyngsat = False
lyngsat_id = None
for objitem in objitems_to_display:
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj: if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite: if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite:
satellite_names.add(objitem.parameter_obj.id_satellite.name) satellite_names.add(objitem.parameter_obj.id_satellite.name)
# 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 "-" satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
# Get all marks (presence/absence)
marks_data = []
for mark in source.marks.all():
marks_data.append({
'mark': mark.mark,
'timestamp': mark.timestamp,
'created_by': str(mark.created_by) if mark.created_by else '-',
})
processed_sources.append({ processed_sources.append({
'id': source.id, 'id': source.id,
'coords_average': coords_average_str, 'coords_average': coords_average_str,
@@ -193,6 +270,9 @@ class SourceListView(LoginRequiredMixin, View):
'satellite': satellite_str, 'satellite': satellite_str,
'created_at': source.created_at, 'created_at': source.created_at,
'updated_at': source.updated_at, 'updated_at': source.updated_at,
'has_lyngsat': has_lyngsat,
'lyngsat_id': lyngsat_id,
'marks': marks_data,
}) })
# Prepare context for template # Prepare context for template
@@ -207,10 +287,14 @@ class SourceListView(LoginRequiredMixin, View):
'has_coords_kupsat': has_coords_kupsat, 'has_coords_kupsat': has_coords_kupsat,
'has_coords_valid': has_coords_valid, 'has_coords_valid': has_coords_valid,
'has_coords_reference': has_coords_reference, 'has_coords_reference': has_coords_reference,
'has_lyngsat': has_lyngsat,
'has_any_lyngsat': has_any_lyngsat,
'objitem_count_min': objitem_count_min, 'objitem_count_min': objitem_count_min,
'objitem_count_max': objitem_count_max, 'objitem_count_max': objitem_count_max,
'date_from': date_from, 'date_from': date_from,
'date_to': date_to, 'date_to': date_to,
'geo_date_from': geo_date_from,
'geo_date_to': geo_date_to,
'satellites': satellites, 'satellites': satellites,
'selected_satellites': [ 'selected_satellites': [
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit())) int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-15 21:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='snr',
field=models.FloatField(blank=True, help_text='Отношение сигнал/шум в децибелах', null=True, verbose_name='ОСШ, дБ'),
),
]