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

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,
Standard,
SigmaParMark,
ObjectMark,
SigmaParameter,
Parameter,
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)
class SigmaParMarkAdmin(BaseAdmin):
"""Админ-панель для модели SigmaParMark."""
@@ -1023,6 +1041,7 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
("created_at", DateRangeQuickSelectListFilterBuilder()),
("updated_at", DateRangeQuickSelectListFilterBuilder()),
)
search_fields = ("id",)
ordering = ("-created_at",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
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"]
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):
"""
Модель отметки о наличии сигнала.
Модель отметки о наличии сигнала (для Sigma).
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
"""
@@ -97,8 +163,8 @@ class SigmaParMark(models.Model):
return "Отметка без времени"
class Meta:
verbose_name = "Отметка"
verbose_name_plural = "Отметки"
verbose_name = "Отметка сигнала"
verbose_name_plural = "Отметки сигналов"
ordering = ["-timestamp"]

View File

@@ -13,11 +13,14 @@
<div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %}
<ul class="navbar-nav me-auto">
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Главная</a>
</li> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a>
<a class="nav-link" href="{% url 'mainapp:source_list' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Источники</a>
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Точки</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
@@ -28,6 +31,9 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:object_marks' %}">Отметки</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</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 -->
<div class="flex-grow-1 overflow-auto">
<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">
<tr>
<th scope="col" class="text-center" style="width: 3%;">

View File

@@ -180,10 +180,10 @@ function showSigmaParameterModal(parameterId) {
if (sigma.marks.length > 0) {
html += `
<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">
<tr>
<th style="width: 20%;">Отметка</th>
<th style="width: 20%;">Наличие сигнала</th>
<th>Дата</th>
</tr>
</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' %}
{% 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 %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Список объектов</h2>
</div>
</div>
<div class="container-fluid px-4 py-3">
<h2 class="mb-4">Главная страница - Динамический отчёт</h2>
<!-- Toolbar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3">
<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 class="filter-section">
<form method="get" id="filter-form">
<div class="row">
<!-- Основной выбор: Объекти или Объекты -->
<div class="col-12">
<div class="filter-group">
<div class="filter-group-title">1. Тип отображения</div>
<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>
</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">
<!-- Satellite Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
<div 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 mb-2" multiple size="4">
{% for satellite in satellites %}
<option value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
<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>
{% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
</div>
</div>
</div>
<!-- Условные фильтры (зависят от типа отображения) -->
<div class="col-md-6">
<!-- Фильтры для Объектов -->
<div class="filter-group" id="sources-filters" style="display: none;">
<div class="filter-group-title">3. Фильтры для Объектов</div>
<!-- Range Filter -->
<div class="mb-2">
<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_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
</div>
<!-- SNR Filter -->
<div class="mb-2">
<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>
<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 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>
<select name="modulation" class="form-select form-select-sm mb-2" 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-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
<div 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>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}"
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Kubsat Coordinates Filter -->
<div class="mb-2">
<div class="mb-3">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" 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 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 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>
<!-- Valid Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты опер. отдела:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_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 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 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>
<!-- 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 %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
{{ option }}
</option>
<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>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max }}">
</div>
<!-- Полоса -->
<div class="mb-3">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max }}">
</div>
<!-- Модуляция -->
<div class="mb-3">
<label class="form-label">Модуляция:</label>
<div class="d-flex gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm" multiple size="4">
{% for mod in modulations %}
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</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>
<!-- Поляризация -->
<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('polarization', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('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 %}
</tbody>
</table>
</select>
</div>
<div class="mb-3">
<label class="form-label">Координаты геолокации:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_all" value="" {% if not has_geo %}checked{% endif %}>
<label class="form-check-label" for="geo_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_yes" value="1" {% if has_geo == '1' %}checked{% endif %}>
<label class="form-check-label" for="geo_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_geo" id="geo_no" value="0" {% if has_geo == '0' %}checked{% endif %}>
<label class="form-check-label" for="geo_no">Нет</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Связь с LyngSat:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_all" value="" {% if not has_lyngsat %}checked{% endif %}>
<label class="form-check-label" for="lyng_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_yes" value="1" {% if has_lyngsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="lyng_yes">Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_no" value="0" {% if has_lyngsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="lyng_no">Нет</label>
</div>
</div>
<!-- Наличие сигнала -->
<div class="conditional-filters">
<div class="mb-3">
<label class="form-label fw-bold">Отображать наличие сигнала:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="show_marks" id="marks_obj" value="1"
{% if show_marks == '1' %}checked{% endif %} onchange="toggleMarksFilters()">
<label class="form-check-label" for="marks_obj">Да</label>
</div>
</div>
<div id="marks-additional-filters" style="{% if show_marks != '1' %}display:none;{% endif %}">
<div class="mb-3">
<label class="form-label">Период отметок:</label>
<input type="datetime-local" name="marks_date_from" class="form-control form-control-sm mb-1" value="{{ marks_date_from }}">
<input type="datetime-local" name="marks_date_to" class="form-control form-control-sm" value="{{ marks_date_to }}">
</div>
<div class="mb-3">
<label class="form-label">Статус отметок:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_all" value="" {% if not marks_status %}checked{% endif %}>
<label class="form-check-label" for="marks_all">Все</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_present" value="present" {% if marks_status == 'present' %}checked{% endif %}>
<label class="form-check-label" for="marks_present">✓ Есть</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marks_status" id="marks_absent" value="absent" {% if marks_status == 'absent' %}checked{% endif %}>
<label class="form-check-label" for="marks_absent">✗ Нет</label>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="px-3 pb-3">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- Pagination Info -->
{% if page_obj %}
<div class="px-3 pb-3 d-flex justify-content-between align-items-center">
<div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Настройки отображения -->
<div class="row">
<div class="col-12">
<div class="filter-group">
<div class="filter-group-title">4. Настройки отображения</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">Элементов на странице:</label>
<select name="items_per_page" class="form-select form-select-sm">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Кнопки -->
<div class="row">
<div class="col-12 text-center">
<button type="submit" class="btn btn-primary btn-generate">
<i class="bi bi-table"></i> Сформировать таблицу
</button>
<a href="?" class="btn btn-secondary ms-2">Сбросить фильтры</a>
</div>
</div>
</form>
</div>
<!-- Активные фильтры -->
{% if page_obj %}
<div class="active-filters">
<strong>Активные фильтры:</strong>
{% if display_mode == 'sources' %}
<span class="filter-badge">Объекти</span>
{% else %}
<span class="filter-badge">Объекты</span>
{% endif %}
{% if selected_satellites %}
<span class="filter-badge">Спутники: {{ selected_satellites|length }}</span>
{% endif %}
{% if date_from or date_to %}
<span class="filter-badge">Дата геолокации</span>
{% endif %}
{% if show_marks == '1' %}
<span class="filter-badge">С наличие сигналами</span>
{% endif %}
</div>
{% endif %}
<!-- Таблица (показывается только после генерации) -->
<div class="table-container {% if page_obj %}show{% endif %}">
{% if display_mode == 'sources' %}
{% include 'mainapp/components/_sources_table.html' %}
{% else %}
{% include 'mainapp/components/_objitems_table.html' %}
{% endif %}
</div>
</div>
{% endblock %}
<!-- JavaScript for checkbox functionality and filters -->
{% block extra_js %}
<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() {
// Select/Deselect all checkboxes
const selectAllCheckbox = document.getElementById('select-all');
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();
});
}
updateConditionalFilters();
toggleMarksFilters();
});
</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="row mb-3">
<div class="col-12">
<h2>Список объектов</h2>
<h2>Список точек</h2>
</div>
</div>
@@ -207,7 +207,7 @@
<!-- Source Type Filter -->
<div class="mb-2">
<label class="form-label">Тип источника:</label>
<label class="form-label">Тип точки:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type"
@@ -277,7 +277,7 @@
<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;">
<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" 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="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="Зеркала" field="" sortable=False %}
</tr>
@@ -1036,7 +1036,7 @@
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat
<i class="bi bi-tv"></i> Данные объекта LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
@@ -1160,7 +1160,7 @@
<div class="col-md-6">
${data.url ? `
<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">
<i class="bi bi-link-45deg"></i> Открыть на LyngSat
</a>

View File

@@ -30,7 +30,7 @@
<h5 class="mt-4 mb-3">Детали удаления:</h5>
<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">
<tr>
<th class="text-center" style="width: 15%;">ID источника</th>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{% extends 'mainapp/base.html' %}
{% block title %}Список источников{% endblock %}
{% block title %}Список объектов{% endblock %}
{% block extra_css %}
<style>
@@ -29,7 +29,7 @@
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Список источников</h2>
<h2>Список объектов</h2>
</div>
</div>
@@ -194,7 +194,7 @@
<!-- LyngSat Filter -->
<div class="mb-2">
<label class="form-label">Тип источника (ТВ):</label>
<label class="form-label">Тип объекта (ТВ):</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
@@ -242,7 +242,7 @@
<div class="card">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<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" 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: 180px;">Наличие сигнала</th>
{% if has_any_lyngsat %}
<th scope="col" class="text-center" style="min-width: 80px;">Тип источника</th>
<th scope="col" class="text-center" style="min-width: 80px;">Тип объекта</th>
{% endif %}
<th scope="col" class="text-center" style="min-width: 100px;">
<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_valid }}</td>
<td>{{ source.coords_reference }}</td>
<td style="padding: 0.3rem; vertical-align: top;">
{% if source.marks %}
<div style="font-size: 0.75rem; line-height: 1.3;">
{% for mark in source.marks %}
<div style="{% if not forloop.last %}border-bottom: 1px solid #dee2e6; padding-bottom: 3px; margin-bottom: 3px;{% endif %}">
<div style="margin-bottom: 1px;">
{% if mark.mark %}
<span class="badge bg-success" style="font-size: 0.7rem;">Есть</span>
{% elif mark.mark == False %}
<span class="badge bg-danger" style="font-size: 0.7rem;">Нет</span>
{% else %}
<span class="badge bg-secondary" style="font-size: 0.7rem;">-</span>
{% endif %}
<span class="text-muted" style="font-size: 0.7rem;">{{ mark.timestamp|date:"d.m.y H:i" }}</span>
</div>
<div class="text-muted" style="font-size: 0.65rem;">{{ mark.created_by }}</div>
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
{% if has_any_lyngsat %}
<td class="text-center">
{% if source.has_lyngsat %}
@@ -333,7 +357,7 @@
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
target="_blank"
class="btn btn-sm btn-outline-success"
title="Показать источник с точками на карте">
title="Показать объект с точками на карте">
<i class="bi bi-geo-alt"></i>
<span class="badge bg-success">{{ source.objitem_count }}</span>
</a>
@@ -365,7 +389,7 @@
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:source_update' source.id %}"
class="btn btn-sm btn-outline-warning"
title="Редактировать источник">
title="Редактировать объект">
<i class="bi bi-pencil"></i>
</a>
{% else %}
@@ -378,7 +402,7 @@
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
<td colspan="12" class="text-center text-muted">Нет данных для отображения</td>
</tr>
{% endfor %}
</tbody>
@@ -395,7 +419,7 @@
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<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>
</div>
<div class="modal-body">
@@ -406,6 +430,25 @@
</div>
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
<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">
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
<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="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип источника</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип объекта</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
</ul>
</div>
</div>
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" 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">
<tr>
<th class="text-center" style="width: 3%;">
@@ -474,7 +517,7 @@
<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: 80px;">Sigma</th>
<th style="min-width: 80px;">Зеркала</th>
</tr>
@@ -534,7 +577,7 @@ function showSelectedOnMap() {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
return;
}
@@ -555,7 +598,7 @@ function deleteSelectedSources() {
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для удаления');
alert('Пожалуйста, выберите хотя бы один объект для удаления');
return;
}
@@ -774,7 +817,7 @@ function showSourceDetails(sourceId) {
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('Источник не найден');
throw new Error('Объект не найден');
} else {
throw new Error('Ошибка при загрузке данных');
}
@@ -785,6 +828,33 @@ function showSourceDetails(sourceId) {
// Hide loading spinner
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) {
// Show content
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-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на источник:</span><br>' +
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
'</div></div></div></div></div></div></div>';
@@ -1048,7 +1118,7 @@ function showTransponderModal(transponderId) {
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat
<i class="bi bi-tv"></i> Данные объекта LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>

View File

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

View File

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

View File

@@ -30,7 +30,7 @@
<h5 class="mt-4 mb-3">Детали удаления:</h5>
<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">
<tr>
<th class="text-center" style="width: 10%;">ID</th>

View File

@@ -191,7 +191,7 @@
<div class="card">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<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" class="text-center" style="width: 3%;">

View File

@@ -12,6 +12,7 @@ from .views import (
DeleteSelectedTranspondersView,
FillLyngsatDataView,
GetLocationsView,
HomeView,
LinkLyngsatSourcesView,
LinkVchSigmaView,
LoadCsvDataView,
@@ -43,11 +44,13 @@ from .views import (
UploadVchLoadView,
custom_logout,
)
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
app_name = 'mainapp'
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>/delete/', SourceDeleteView.as_view(), name='source_delete'),
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('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'),
path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'),
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
path('logout/', custom_logout, name='logout'),
]

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

View File

@@ -192,7 +192,9 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'source_objitems__lyngsat_source',
'source_objitems__transponder',
'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 all related ObjItems, sorted by created_at
@@ -327,9 +329,25 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'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({
'source_id': source_id,
'objitems': objitems_data
'objitems': objitems_data,
'marks': marks_data
})
except Source.DoesNotExist:
return JsonResponse({'error': 'Источник не найден'}, status=404)

View File

@@ -1,10 +1,491 @@
"""
Base views and utilities.
"""
from datetime import datetime, timedelta
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.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):
"""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__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__geo_obj'
'source_objitems__geo_obj',
'marks',
'marks__created_by__user'
).annotate(
objitem_count=Count('source_objitems')
)
@@ -203,6 +205,15 @@ class SourceListView(LoginRequiredMixin, View):
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({
'id': source.id,
'coords_average': coords_average_str,
@@ -215,6 +226,7 @@ class SourceListView(LoginRequiredMixin, View):
'updated_at': source.updated_at,
'has_lyngsat': has_lyngsat,
'lyngsat_id': lyngsat_id,
'marks': marks_data,
})
# Prepare context for template