Правки и улучшения визуала. Добавил функционал отметок.

This commit is contained in:
2025-11-16 23:32:29 +03:00
parent d9cb243388
commit 8994a0e500
30 changed files with 2495 additions and 510 deletions

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

@@ -13,11 +13,14 @@
<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>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Источники</a> <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>
@@ -28,6 +31,9 @@
<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">
<div class="card">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3">
<div style="min-width: 300px; flex-grow: 1;">
<label for="toolbar-search" class="form-label mb-0">Поиск:</label>
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по имени, местоположению..." value="{{ search_query|default:'' }}">
</div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Filters Sidebar - Made narrower -->
<div class="col-md-2">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Фильтры</h5>
<form method="get" id="filter-form"> <form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select --> <div class="row">
<div class="mb-2"> <!-- Основной выбор: Объекти или Объекты -->
<label class="form-label">Спутник:</label> <div class="col-12">
<div class="d-flex justify-content-between mb-1"> <div class="filter-group">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button> <div class="filter-group-title">1. Тип отображения</div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button> <div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="display_mode" id="mode_sources" value="sources"
{% if display_mode == 'sources' %}checked{% endif %} onchange="updateConditionalFilters()">
<label class="btn btn-outline-primary" for="mode_sources">Объекти (Source)</label>
<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>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4"> </div>
{% for satellite in satellites %} </div>
<option value="{{ satellite.id }}" </div>
{% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }} <div class="row">
<!-- Общие фильтры -->
<div class="col-md-6">
<div class="filter-group">
<div class="filter-group-title">2. Общие фильтры</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('satellite_id', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm" multiple size="5">
{% for sat in satellites %}
<option value="{{ sat.id }}" {% if sat.id in selected_satellites %}selected{% endif %}>
{{ sat.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<!-- Frequency Filter --> </div>
<div class="mb-2"> </div>
<!-- Условные фильтры (зависят от типа отображения) -->
<div class="col-md-6">
<!-- Фильтры для Объектов -->
<div class="filter-group" id="sources-filters" style="display: none;">
<div class="filter-group-title">3. Фильтры для Объектов</div>
<div class="mb-3">
<label class="form-label">Усреднённые координаты:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_all" value="" {% if not has_coords_average %}checked{% endif %}>
<label class="form-check-label" for="avg_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_yes" value="1" {% if has_coords_average == '1' %}checked{% endif %}>
<label class="form-check-label" for="avg_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_no" value="0" {% if has_coords_average == '0' %}checked{% endif %}>
<label class="form-check-label" for="avg_no">Нет</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Координаты Кубсата:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_all" value="" {% if not has_kupsat %}checked{% endif %}>
<label class="form-check-label" for="kup_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_yes" value="1" {% if has_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="kup_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_no" value="0" {% if has_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="kup_no">Нет</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Координаты оперативников:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_valid" id="val_all" value="" {% if not has_valid %}checked{% endif %}>
<label class="form-check-label" for="val_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_valid" id="val_yes" value="1" {% if has_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="val_yes">Есть</label>
</div>
<div class="form-check">
<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="val_no">Нет</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Справочные координаты:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_reference" id="ref_all" value="" {% if not has_reference %}checked{% endif %}>
<label class="form-check-label" for="ref_all">Все</label>
</div>
<div class="form-check">
<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> <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_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|default:'' }}"> <input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max }}">
</div> </div>
<!-- Range Filter --> <!-- Полоса -->
<div class="mb-2"> <div class="mb-3">
<label class="form-label">Полоса, МГц:</label> <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:'' }}"> <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|default:'' }}"> <input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max }}">
</div> </div>
<!-- SNR Filter --> <!-- Модуляция -->
<div class="mb-2"> <div class="mb-3">
<label class="form-label">ОСШ:</label>
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
</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> <label class="form-label">Модуляция:</label>
<div class="d-flex justify-content-between mb-1"> <div class="d-flex gap-2 mb-2">
<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="selectAll('modulation', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', false)">Снять</button>
</div> </div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4"> <select name="modulation" class="form-select form-select-sm" multiple size="4">
{% for mod in modulations %} {% for mod in modulations %}
<option value="{{ mod.id }}" <option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
{% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }} {{ mod.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<!-- Polarization Filter --> <!-- Поляризация -->
<div class="mb-2"> <div class="mb-3">
<label class="form-label">Поляризация:</label> <label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1"> <div class="d-flex gap-2 mb-2">
<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="selectAll('polarization', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('polarization', false)">Снять</button>
</div> </div>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4"> <select name="polarization" class="form-select form-select-sm" multiple size="4">
{% for pol in polarizations %} {% for pol in polarizations %}
<option value="{{ pol.id }}" <option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }} {{ pol.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </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 class="form-check">
<div> <input class="form-check-input" type="radio" name="has_geo" id="geo_all" value="" {% if not has_geo %}checked{% endif %}>
<div class="form-check form-check-inline"> <label class="form-check-label" for="geo_all">Все</label>
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1" </div>
{% if has_kupsat == '1' %}checked{% endif %}> <div class="form-check">
<label class="form-check-label" for="has_kupsat_1">Есть</label> <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 class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" 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>
</div> </div>
<!-- Valid Coordinates Filter --> <!-- Настройки отображения -->
<div class="mb-2"> <div class="row">
<label class="form-label">Координаты опер. отдела:</label> <div class="col-12">
<div> <div class="filter-group">
<div class="form-check form-check-inline"> <div class="filter-group-title">4. Настройки отображения</div>
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1" <div class="row">
{% if has_valid == '1' %}checked{% endif %}> <div class="col-md-6">
<label class="form-check-label" for="has_valid_1">Есть</label> <label class="form-label">Элементов на странице:</label>
</div> <select name="items_per_page" class="form-select form-select-sm">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" 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-2">
<label for="items-per-page" class="form-label">Элементов:</label>
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();">
{% for option in available_items_per_page %} {% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}> <option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>{{ option }}</option>
{{ option }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div>
</div>
</div>
</div>
<!-- Apply Filters and Reset Buttons --> <!-- Кнопки -->
<div class="d-grid gap-2 mt-2"> <div class="row">
<button type="submit" class="btn btn-primary btn-sm">Применить</button> <div class="col-12 text-center">
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a> <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> </div>
</form> </form>
</div> </div>
</div>
</div>
<!-- Main Table --> <!-- Активные фильтры -->
<div class="col-md-10">
<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 %}
</tbody>
</table>
</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 %} {% if page_obj %}
<div class="px-3 pb-3 d-flex justify-content-between align-items-center"> <div class="active-filters">
<div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div> <strong>Активные фильтры:</strong>
</div> {% 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 %} {% endif %}
</div> </div>
</div> {% endif %}
</div>
<!-- Таблица (показывается только после генерации) -->
<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>
@@ -194,7 +194,7 @@
<!-- LyngSat Filter --> <!-- LyngSat Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Тип источника (ТВ):</label> <label class="form-label">Тип объекта (ТВ):</label>
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1" <input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
@@ -242,7 +242,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%;">
@@ -263,8 +263,9 @@
<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 %} {% if has_any_lyngsat %}
<th scope="col" class="text-center" style="min-width: 80px;">Тип источника</th> <th scope="col" class="text-center" style="min-width: 80px;">Тип объекта</th>
{% endif %} {% 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">
@@ -312,6 +313,29 @@
<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 %} {% if has_any_lyngsat %}
<td class="text-center"> <td class="text-center">
{% if source.has_lyngsat %} {% if source.has_lyngsat %}
@@ -333,7 +357,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>
@@ -365,7 +389,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 %}
@@ -378,7 +402,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>
@@ -395,7 +419,7 @@
<div class="modal-dialog modal-fullscreen"> <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">
@@ -406,6 +430,25 @@
</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;">
<!-- Marks Section -->
<div id="marksSection" class="mb-3" style="display: none;">
<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"> <div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6> <h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
<div class="dropdown"> <div class="dropdown">
@@ -441,14 +484,14 @@
<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="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="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="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="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="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> <li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;"> <div class="table-responsive" style="max-height: 80vh; 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-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%;">
@@ -474,7 +517,7 @@
<th style="min-width: 150px;">Комментарий</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: 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;">Sigma</th>
<th style="min-width: 80px;">Зеркала</th> <th style="min-width: 80px;">Зеркала</th>
</tr> </tr>
@@ -534,7 +577,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;
} }
@@ -555,7 +598,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;
} }
@@ -774,7 +817,7 @@ function showSourceDetails(sourceId) {
.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('Ошибка при загрузке данных');
} }
@@ -785,6 +828,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';
@@ -981,7 +1051,7 @@ function showLyngsatModal(lyngsatId) {
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' + '<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="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"><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>' + '<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">' + '<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') + '<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
'</div></div></div></div></div></div></div>'; '</div></div></div></div></div></div></div>';
@@ -1048,7 +1118,7 @@ function showTransponderModal(transponderId) {
<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>

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

@@ -12,6 +12,7 @@ from .views import (
DeleteSelectedTranspondersView, DeleteSelectedTranspondersView,
FillLyngsatDataView, FillLyngsatDataView,
GetLocationsView, GetLocationsView,
HomeView,
LinkLyngsatSourcesView, LinkLyngsatSourcesView,
LinkVchSigmaView, LinkVchSigmaView,
LoadCsvDataView, LoadCsvDataView,
@@ -43,11 +44,13 @@ from .views import (
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'),
@@ -87,5 +90,8 @@ urlpatterns = [
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('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

@@ -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,
@@ -51,6 +51,7 @@ from .map import (
__all__ = [ __all__ = [
# Base # Base
'ActionsPageView', 'ActionsPageView',
'HomeView',
'custom_logout', 'custom_logout',
# ObjItem # ObjItem
'ObjItemListView', 'ObjItemListView',

View File

@@ -192,7 +192,9 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'source_objitems__lyngsat_source', 'source_objitems__lyngsat_source',
'source_objitems__transponder', 'source_objitems__transponder',
'source_objitems__created_by__user', 'source_objitems__created_by__user',
'source_objitems__updated_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
@@ -327,9 +329,25 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'mirrors': mirrors, '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

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

@@ -57,7 +57,9 @@ 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')
) )
@@ -203,6 +205,15 @@ class SourceListView(LoginRequiredMixin, View):
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,
@@ -215,6 +226,7 @@ class SourceListView(LoginRequiredMixin, View):
'updated_at': source.updated_at, 'updated_at': source.updated_at,
'has_lyngsat': has_lyngsat, 'has_lyngsat': has_lyngsat,
'lyngsat_id': lyngsat_id, 'lyngsat_id': lyngsat_id,
'marks': marks_data,
}) })
# Prepare context for template # Prepare context for template

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='ОСШ, дБ'),
),
]