Правки и улучшения визуала. Добавил функционал отметок.
This commit is contained in:
@@ -24,6 +24,7 @@ from .models import (
|
|||||||
Modulation,
|
Modulation,
|
||||||
Standard,
|
Standard,
|
||||||
SigmaParMark,
|
SigmaParMark,
|
||||||
|
ObjectMark,
|
||||||
SigmaParameter,
|
SigmaParameter,
|
||||||
Parameter,
|
Parameter,
|
||||||
Satellite,
|
Satellite,
|
||||||
@@ -339,6 +340,23 @@ class ParameterInline(admin.StackedInline):
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ObjectMark)
|
||||||
|
class ObjectMarkAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели ObjectMark."""
|
||||||
|
|
||||||
|
list_display = ("source", "mark", "timestamp", "created_by")
|
||||||
|
list_select_related = ("source", "created_by__user")
|
||||||
|
search_fields = ("source__id",)
|
||||||
|
ordering = ("-timestamp",)
|
||||||
|
list_filter = (
|
||||||
|
"mark",
|
||||||
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
("source", MultiSelectRelatedDropdownFilter),
|
||||||
|
)
|
||||||
|
readonly_fields = ("timestamp", "created_by")
|
||||||
|
autocomplete_fields = ("source",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SigmaParMark)
|
@admin.register(SigmaParMark)
|
||||||
class SigmaParMarkAdmin(BaseAdmin):
|
class SigmaParMarkAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели SigmaParMark."""
|
"""Админ-панель для модели SigmaParMark."""
|
||||||
@@ -1023,6 +1041,7 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
)
|
)
|
||||||
|
search_fields = ("id",)
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
inlines = [ObjItemInline]
|
inlines = [ObjItemInline]
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
27
dbapp/mainapp/migrations/0006_change_objectmark_to_source.py
Normal file
27
dbapp/mainapp/migrations/0006_change_objectmark_to_source.py
Normal 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='Источник'),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
dbapp/mainapp/migrations/0007_make_source_required.py
Normal file
19
dbapp/mainapp/migrations/0007_make_source_required.py
Normal 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='Источник'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -68,9 +68,75 @@ class CustomUser(models.Model):
|
|||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectMark(models.Model):
|
||||||
|
"""
|
||||||
|
Модель отметки о наличии объекта.
|
||||||
|
|
||||||
|
Используется для фиксации моментов времени когда объект был обнаружен или отсутствовал.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
mark = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Наличие объекта",
|
||||||
|
help_text="True - объект обнаружен, False - объект отсутствует",
|
||||||
|
)
|
||||||
|
timestamp = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Время",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Время фиксации отметки",
|
||||||
|
)
|
||||||
|
source = models.ForeignKey(
|
||||||
|
'Source',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="marks",
|
||||||
|
verbose_name="Источник",
|
||||||
|
help_text="Связанный источник",
|
||||||
|
)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
CustomUser,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="marks_created",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Создан пользователем",
|
||||||
|
help_text="Пользователь, создавший отметку",
|
||||||
|
)
|
||||||
|
|
||||||
|
def can_edit(self):
|
||||||
|
"""Проверка возможности редактирования отметки (в течение 5 минут)"""
|
||||||
|
from datetime import timedelta
|
||||||
|
if not self.timestamp:
|
||||||
|
return False
|
||||||
|
time_diff = timezone.now() - self.timestamp
|
||||||
|
return time_diff < timedelta(minutes=5)
|
||||||
|
|
||||||
|
def can_add_new_mark_for_object(self):
|
||||||
|
"""Проверка возможности добавления новой отметки для объекта (прошло 5 минут с последней)"""
|
||||||
|
from datetime import timedelta
|
||||||
|
if not self.timestamp:
|
||||||
|
return True
|
||||||
|
time_diff = timezone.now() - self.timestamp
|
||||||
|
return time_diff >= timedelta(minutes=5)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.timestamp:
|
||||||
|
timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
|
||||||
|
return f"+ {timestamp}" if self.mark else f"- {timestamp}"
|
||||||
|
return "Отметка без времени"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Отметка источника"
|
||||||
|
verbose_name_plural = "Отметки источников"
|
||||||
|
ordering = ["-timestamp"]
|
||||||
|
|
||||||
|
|
||||||
|
# Для обратной совместимости с SigmaParameter
|
||||||
class SigmaParMark(models.Model):
|
class SigmaParMark(models.Model):
|
||||||
"""
|
"""
|
||||||
Модель отметки о наличии сигнала.
|
Модель отметки о наличии сигнала (для Sigma).
|
||||||
|
|
||||||
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
|
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
|
||||||
"""
|
"""
|
||||||
@@ -97,8 +163,8 @@ class SigmaParMark(models.Model):
|
|||||||
return "Отметка без времени"
|
return "Отметка без времени"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Отметка"
|
verbose_name = "Отметка сигнала"
|
||||||
verbose_name_plural = "Отметки"
|
verbose_name_plural = "Отметки сигналов"
|
||||||
ordering = ["-timestamp"]
|
ordering = ["-timestamp"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,14 @@
|
|||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mainapp:home' %}">Главная</a>
|
||||||
|
</li> -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a>
|
<a class="nav-link" href="{% url 'mainapp:source_list' %}">Объекты</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:home' %}">Источники</a>
|
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Точки</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
|
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mainapp:object_marks' %}">Отметки</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
126
dbapp/mainapp/templates/mainapp/components/_objitems_table.html
Normal file
126
dbapp/mainapp/templates/mainapp/components/_objitems_table.html
Normal 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>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<!-- Table container -->
|
<!-- Table container -->
|
||||||
<div class="flex-grow-1 overflow-auto">
|
<div class="flex-grow-1 overflow-auto">
|
||||||
<div class="table-responsive" style="height: 100%;">
|
<div class="table-responsive" style="height: 100%;">
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="text-center" style="width: 3%;">
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
|
|||||||
@@ -180,10 +180,10 @@ function showSigmaParameterModal(parameterId) {
|
|||||||
if (sigma.marks.length > 0) {
|
if (sigma.marks.length > 0) {
|
||||||
html += `
|
html += `
|
||||||
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped table-bordered mb-0">
|
||||||
<thead class="table-light sticky-top">
|
<thead class="table-light sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 20%;">Отметка</th>
|
<th style="width: 20%;">Наличие сигнала</th>
|
||||||
<th>Дата</th>
|
<th>Дата</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
122
dbapp/mainapp/templates/mainapp/components/_sources_table.html
Normal file
122
dbapp/mainapp/templates/mainapp/components/_sources_table.html
Normal 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>
|
||||||
@@ -1,422 +1,483 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Список объектов{% endblock %}
|
{% block title %}Главная страница{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.filter-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-filters {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-generate {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 12px 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filters {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-present {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-absent {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid px-3">
|
<div class="container-fluid px-4 py-3">
|
||||||
<div class="row mb-3">
|
<h2 class="mb-4">Главная страница - Динамический отчёт</h2>
|
||||||
<div class="col-12">
|
|
||||||
<h2>Список объектов</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Фильтры -->
|
||||||
<div class="row mb-3">
|
<div class="filter-section">
|
||||||
<div class="col-12">
|
<form method="get" id="filter-form">
|
||||||
<div class="card">
|
<div class="row">
|
||||||
<div class="card-body">
|
<!-- Основной выбор: Объекти или Объекты -->
|
||||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
<div class="col-12">
|
||||||
<div style="min-width: 300px; flex-grow: 1;">
|
<div class="filter-group">
|
||||||
<label for="toolbar-search" class="form-label mb-0">Поиск:</label>
|
<div class="filter-group-title">1. Тип отображения</div>
|
||||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по имени, местоположению..." value="{{ search_query|default:'' }}">
|
<div class="btn-group w-100" role="group">
|
||||||
</div>
|
<input type="radio" class="btn-check" name="display_mode" id="mode_sources" value="sources"
|
||||||
<div class="ms-auto">
|
{% if display_mode == 'sources' %}checked{% endif %} onchange="updateConditionalFilters()">
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
<label class="btn btn-outline-primary" for="mode_sources">Объекти (Source)</label>
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
|
||||||
|
<input type="radio" class="btn-check" name="display_mode" id="mode_objitems" value="objitems"
|
||||||
|
{% if display_mode == 'objitems' %}checked{% endif %} onchange="updateConditionalFilters()">
|
||||||
|
<label class="btn btn-outline-primary" for="mode_objitems">Объекты (ObjItem)</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row">
|
||||||
<!-- Filters Sidebar - Made narrower -->
|
<!-- Общие фильтры -->
|
||||||
<div class="col-md-2">
|
<div class="col-md-6">
|
||||||
<div class="card h-100">
|
<div class="filter-group">
|
||||||
<div class="card-body">
|
<div class="filter-group-title">2. Общие фильтры</div>
|
||||||
<h5 class="card-title">Фильтры</h5>
|
|
||||||
<form method="get" id="filter-form">
|
<!-- Спутники -->
|
||||||
<!-- Satellite Selection - Multi-select -->
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<label class="form-label">Спутники:</label>
|
||||||
<label class="form-label">Спутник:</label>
|
<div class="d-flex gap-2 mb-2">
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('satellite_id', true)">Все</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('satellite_id', false)">Снять</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
|
||||||
</div>
|
</div>
|
||||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4">
|
<select name="satellite_id" class="form-select form-select-sm" multiple size="5">
|
||||||
{% for satellite in satellites %}
|
{% for sat in satellites %}
|
||||||
<option value="{{ satellite.id }}"
|
<option value="{{ sat.id }}" {% if sat.id in selected_satellites %}selected{% endif %}>
|
||||||
{% if satellite.id in selected_satellites %}selected{% endif %}>
|
{{ sat.name }}
|
||||||
{{ satellite.name }}
|
</option>
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Frequency Filter -->
|
</div>
|
||||||
<div class="mb-2">
|
</div>
|
||||||
<label class="form-label">Частота, МГц:</label>
|
|
||||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
|
|
||||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Range Filter -->
|
<!-- Условные фильтры (зависят от типа отображения) -->
|
||||||
<div class="mb-2">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Полоса, МГц:</label>
|
<!-- Фильтры для Объектов -->
|
||||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
|
<div class="filter-group" id="sources-filters" style="display: none;">
|
||||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
|
<div class="filter-group-title">3. Фильтры для Объектов</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SNR Filter -->
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<label class="form-label">Усреднённые координаты:</label>
|
||||||
<label class="form-label">ОСШ:</label>
|
<div class="form-check">
|
||||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
|
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_all" value="" {% if not has_coords_average %}checked{% endif %}>
|
||||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
|
<label class="form-check-label" for="avg_all">Все</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Symbol Rate Filter -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Сим. v, БОД:</label>
|
|
||||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
|
|
||||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Removed old search input as it's now in the toolbar -->
|
|
||||||
|
|
||||||
<!-- Modulation Filter -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Модуляция:</label>
|
|
||||||
<div class="d-flex justify-content-between mb-1">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
|
|
||||||
</div>
|
</div>
|
||||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4">
|
<div class="form-check">
|
||||||
{% for mod in modulations %}
|
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_yes" value="1" {% if has_coords_average == '1' %}checked{% endif %}>
|
||||||
<option value="{{ mod.id }}"
|
<label class="form-check-label" for="avg_yes">Есть</label>
|
||||||
{% if mod.id in selected_modulations %}selected{% endif %}>
|
</div>
|
||||||
{{ mod.name }}
|
<div class="form-check">
|
||||||
</option>
|
<input class="form-check-input" type="radio" name="has_coords_average" id="avg_no" value="0" {% if has_coords_average == '0' %}checked{% endif %}>
|
||||||
{% endfor %}
|
<label class="form-check-label" for="avg_no">Нет</label>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Polarization Filter -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Поляризация:</label>
|
|
||||||
<div class="d-flex justify-content-between mb-1">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
|
|
||||||
</div>
|
</div>
|
||||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
|
||||||
{% for pol in polarizations %}
|
|
||||||
<option value="{{ pol.id }}"
|
|
||||||
{% if pol.id in selected_polarizations %}selected{% endif %}>
|
|
||||||
{{ pol.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kubsat Coordinates Filter -->
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Координаты Кубсата:</label>
|
<label class="form-label">Координаты Кубсата:</label>
|
||||||
<div>
|
<div class="form-check">
|
||||||
<div class="form-check form-check-inline">
|
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_all" value="" {% if not has_kupsat %}checked{% endif %}>
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
|
<label class="form-check-label" for="kup_all">Все</label>
|
||||||
{% if has_kupsat == '1' %}checked{% endif %}>
|
</div>
|
||||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
<div class="form-check">
|
||||||
</div>
|
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_yes" value="1" {% if has_kupsat == '1' %}checked{% endif %}>
|
||||||
<div class="form-check form-check-inline">
|
<label class="form-check-label" for="kup_yes">Есть</label>
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
|
</div>
|
||||||
{% if has_kupsat == '0' %}checked{% endif %}>
|
<div class="form-check">
|
||||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
<input class="form-check-input" type="radio" name="has_kupsat" id="kup_no" value="0" {% if has_kupsat == '0' %}checked{% endif %}>
|
||||||
</div>
|
<label class="form-check-label" for="kup_no">Нет</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Valid Coordinates Filter -->
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<label class="form-label">Координаты оперативников:</label>
|
||||||
<label class="form-label">Координаты опер. отдела:</label>
|
<div class="form-check">
|
||||||
<div>
|
<input class="form-check-input" type="radio" name="has_valid" id="val_all" value="" {% if not has_valid %}checked{% endif %}>
|
||||||
<div class="form-check form-check-inline">
|
<label class="form-check-label" for="val_all">Все</label>
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
|
</div>
|
||||||
{% if has_valid == '1' %}checked{% endif %}>
|
<div class="form-check">
|
||||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
<input class="form-check-input" type="radio" name="has_valid" id="val_yes" value="1" {% if has_valid == '1' %}checked{% endif %}>
|
||||||
</div>
|
<label class="form-check-label" for="val_yes">Есть</label>
|
||||||
<div class="form-check form-check-inline">
|
</div>
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
|
<div class="form-check">
|
||||||
{% if has_valid == '0' %}checked{% endif %}>
|
<input class="form-check-input" type="radio" name="has_valid" id="val_no" value="0" {% if has_valid == '0' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
<label class="form-check-label" for="val_no">Нет</label>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Items Per Page -->
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<label class="form-label">Справочные координаты:</label>
|
||||||
<label for="items-per-page" class="form-label">Элементов:</label>
|
<div class="form-check">
|
||||||
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();">
|
<input class="form-check-input" type="radio" name="has_reference" id="ref_all" value="" {% if not has_reference %}checked{% endif %}>
|
||||||
{% for option in available_items_per_page %}
|
<label class="form-check-label" for="ref_all">Все</label>
|
||||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
</div>
|
||||||
{{ option }}
|
<div class="form-check">
|
||||||
</option>
|
<input class="form-check-input" type="radio" name="has_reference" id="ref_yes" value="1" {% if has_reference == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="ref_yes">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_reference" id="ref_no" value="0" {% if has_reference == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="ref_no">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Наличие сигнала для объектов -->
|
||||||
|
<div class="conditional-filters">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Отображать наличие сигнала:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="show_marks" id="marks_src" value="1"
|
||||||
|
{% if show_marks == '1' %}checked{% endif %} onchange="toggleMarksFilters()">
|
||||||
|
<label class="form-check-label" for="marks_src">Да</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="marks-additional-filters-src" style="{% if show_marks != '1' %}display:none;{% endif %}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Период отметок:</label>
|
||||||
|
<input type="datetime-local" name="marks_date_from" class="form-control form-control-sm mb-1" value="{{ marks_date_from }}">
|
||||||
|
<input type="datetime-local" name="marks_date_to" class="form-control form-control-sm" value="{{ marks_date_to }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Статус отметок:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_all_src" value="" {% if not marks_status %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_all_src">Все</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_present_src" value="present" {% if marks_status == 'present' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_present_src">✓ Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_absent_src" value="absent" {% if marks_status == 'absent' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_absent_src">✗ Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Фильтры для Объектов -->
|
||||||
|
<div class="filter-group" id="objitems-filters" style="display: none;">
|
||||||
|
<div class="filter-group-title">3. Фильтры для Объектов</div>
|
||||||
|
|
||||||
|
<!-- Дата геолокации -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Дата геолокации:</label>
|
||||||
|
<input type="date" name="date_from" class="form-control form-control-sm mb-1" value="{{ date_from }}">
|
||||||
|
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Частота -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Частота, МГц:</label>
|
||||||
|
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min }}">
|
||||||
|
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Полоса -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Полоса, МГц:</label>
|
||||||
|
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min }}">
|
||||||
|
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модуляция -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Модуляция:</label>
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', true)">Все</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('modulation', false)">Снять</button>
|
||||||
|
</div>
|
||||||
|
<select name="modulation" class="form-select form-select-sm" multiple size="4">
|
||||||
|
{% for mod in modulations %}
|
||||||
|
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
|
||||||
|
{{ mod.name }}
|
||||||
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apply Filters and Reset Buttons -->
|
<!-- Поляризация -->
|
||||||
<div class="d-grid gap-2 mt-2">
|
<div class="mb-3">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
<label class="form-label">Поляризация:</label>
|
||||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
<div class="d-flex gap-2 mb-2">
|
||||||
</div>
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('polarization', true)">Все</button>
|
||||||
</form>
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll('polarization', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<select name="polarization" class="form-select form-select-sm" multiple size="4">
|
||||||
</div>
|
{% for pol in polarizations %}
|
||||||
|
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||||
<!-- Main Table -->
|
{{ pol.name }}
|
||||||
<div class="col-md-10">
|
</option>
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
|
||||||
<thead class="table-dark sticky-top">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center" style="width: 3%;">
|
|
||||||
<input type="checkbox" id="select-all" class="form-check-input">
|
|
||||||
</th>
|
|
||||||
<th scope="col">Имя</th>
|
|
||||||
<th scope="col">Спутник</th>
|
|
||||||
<th scope="col">Част, МГц</th>
|
|
||||||
<th scope="col">Полоса, МГц</th>
|
|
||||||
<th scope="col">Поляр</th>
|
|
||||||
<th scope="col">Сим. v</th>
|
|
||||||
<th scope="col">Модул</th>
|
|
||||||
<th scope="col">ОСШ</th>
|
|
||||||
<th scope="col">Геолокация</th>
|
|
||||||
<th scope="col">Кубсат</th>
|
|
||||||
<th scope="col">Опер. отд</th>
|
|
||||||
<th scope="col">Гео-куб, км</th>
|
|
||||||
<th scope="col">Гео-опер, км</th>
|
|
||||||
<th scope="col">Куб-опер, км</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in processed_objects %}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center">
|
|
||||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
|
||||||
</td>
|
|
||||||
<td>{{ item.name }}</td>
|
|
||||||
<td>{{ item.satellite_name }}</td>
|
|
||||||
<td>{{ item.frequency }}</td>
|
|
||||||
<td>{{ item.freq_range }}</td>
|
|
||||||
<td>{{ item.polarization }}</td>
|
|
||||||
<td>{{ item.bod_velocity }}</td>
|
|
||||||
<td>{{ item.modulation }}</td>
|
|
||||||
<td>{{ item.snr }}</td>
|
|
||||||
<td>{{ item.geo_coords }}</td>
|
|
||||||
<td>{{ item.kupsat_coords }}</td>
|
|
||||||
<td>{{ item.valid_coords }}</td>
|
|
||||||
<td>{{ item.distance_geo_kup }}</td>
|
|
||||||
<td>{{ item.distance_geo_valid }}</td>
|
|
||||||
<td>{{ item.distance_kup_valid }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="15" class="text-center py-4">
|
|
||||||
{% if selected_satellite_id %}
|
|
||||||
Нет данных для выбранных фильтров
|
|
||||||
{% else %}
|
|
||||||
Пожалуйста, выберите спутник для отображения данных
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</select>
|
||||||
</table>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Координаты геолокации:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_geo" id="geo_all" value="" {% if not has_geo %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="geo_all">Все</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_geo" id="geo_yes" value="1" {% if has_geo == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="geo_yes">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_geo" id="geo_no" value="0" {% if has_geo == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="geo_no">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Связь с LyngSat:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_all" value="" {% if not has_lyngsat %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="lyng_all">Все</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_yes" value="1" {% if has_lyngsat == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="lyng_yes">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="has_lyngsat" id="lyng_no" value="0" {% if has_lyngsat == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="lyng_no">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Наличие сигнала -->
|
||||||
|
<div class="conditional-filters">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Отображать наличие сигнала:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="show_marks" id="marks_obj" value="1"
|
||||||
|
{% if show_marks == '1' %}checked{% endif %} onchange="toggleMarksFilters()">
|
||||||
|
<label class="form-check-label" for="marks_obj">Да</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="marks-additional-filters" style="{% if show_marks != '1' %}display:none;{% endif %}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Период отметок:</label>
|
||||||
|
<input type="datetime-local" name="marks_date_from" class="form-control form-control-sm mb-1" value="{{ marks_date_from }}">
|
||||||
|
<input type="datetime-local" name="marks_date_to" class="form-control form-control-sm" value="{{ marks_date_to }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Статус отметок:</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_all" value="" {% if not marks_status %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_all">Все</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_present" value="present" {% if marks_status == 'present' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_present">✓ Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="marks_status" id="marks_absent" value="absent" {% if marks_status == 'absent' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="marks_absent">✗ Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if page_obj.paginator.num_pages > 1 %}
|
|
||||||
<nav aria-label="Page navigation" class="px-3 pb-3">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
|
||||||
{% if page_obj.number == num %}
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">{{ num }}</span>
|
|
||||||
</li>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Pagination Info -->
|
|
||||||
{% if page_obj %}
|
|
||||||
<div class="px-3 pb-3 d-flex justify-content-between align-items-center">
|
|
||||||
<div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Настройки отображения -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="filter-group-title">4. Настройки отображения</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Элементов на странице:</label>
|
||||||
|
<select name="items_per_page" class="form-select form-select-sm">
|
||||||
|
{% for option in available_items_per_page %}
|
||||||
|
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>{{ option }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопки -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<button type="submit" class="btn btn-primary btn-generate">
|
||||||
|
<i class="bi bi-table"></i> Сформировать таблицу
|
||||||
|
</button>
|
||||||
|
<a href="?" class="btn btn-secondary ms-2">Сбросить фильтры</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Активные фильтры -->
|
||||||
|
{% if page_obj %}
|
||||||
|
<div class="active-filters">
|
||||||
|
<strong>Активные фильтры:</strong>
|
||||||
|
{% if display_mode == 'sources' %}
|
||||||
|
<span class="filter-badge">Объекти</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="filter-badge">Объекты</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected_satellites %}
|
||||||
|
<span class="filter-badge">Спутники: {{ selected_satellites|length }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if date_from or date_to %}
|
||||||
|
<span class="filter-badge">Дата геолокации</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_marks == '1' %}
|
||||||
|
<span class="filter-badge">С наличие сигналами</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Таблица (показывается только после генерации) -->
|
||||||
|
<div class="table-container {% if page_obj %}show{% endif %}">
|
||||||
|
{% if display_mode == 'sources' %}
|
||||||
|
{% include 'mainapp/components/_sources_table.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'mainapp/components/_objitems_table.html' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- JavaScript for checkbox functionality and filters -->
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
|
function selectAll(name, select) {
|
||||||
|
const element = document.querySelector(`select[name="${name}"]`);
|
||||||
|
if (element) {
|
||||||
|
for (let option of element.options) {
|
||||||
|
option.selected = select;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConditionalFilters() {
|
||||||
|
const mode = document.querySelector('input[name="display_mode"]:checked').value;
|
||||||
|
const sourcesFilters = document.getElementById('sources-filters');
|
||||||
|
const objitemsFilters = document.getElementById('objitems-filters');
|
||||||
|
|
||||||
|
if (mode === 'sources') {
|
||||||
|
sourcesFilters.style.display = 'block';
|
||||||
|
objitemsFilters.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
sourcesFilters.style.display = 'none';
|
||||||
|
objitemsFilters.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMarksFilters() {
|
||||||
|
const mode = document.querySelector('input[name="display_mode"]:checked')?.value;
|
||||||
|
|
||||||
|
if (mode === 'sources') {
|
||||||
|
const checkbox = document.getElementById('marks_src');
|
||||||
|
const filters = document.getElementById('marks-additional-filters-src');
|
||||||
|
if (checkbox && filters) {
|
||||||
|
filters.style.display = checkbox.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
} else if (mode === 'objitems') {
|
||||||
|
const checkbox = document.getElementById('marks_obj');
|
||||||
|
const filters = document.getElementById('marks-additional-filters');
|
||||||
|
if (checkbox && filters) {
|
||||||
|
filters.style.display = checkbox.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Select/Deselect all checkboxes
|
updateConditionalFilters();
|
||||||
const selectAllCheckbox = document.getElementById('select-all');
|
toggleMarksFilters();
|
||||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
|
||||||
|
|
||||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
|
||||||
selectAllCheckbox.addEventListener('change', function() {
|
|
||||||
itemCheckboxes.forEach(checkbox => {
|
|
||||||
checkbox.checked = selectAllCheckbox.checked;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update select all checkbox state based on individual selections
|
|
||||||
itemCheckboxes.forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', function() {
|
|
||||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
|
||||||
selectAllCheckbox.checked = allChecked;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle multiple selection for modulations and polarizations
|
|
||||||
const modulationSelect = document.querySelector('select[name="modulation"]');
|
|
||||||
const polarizationSelect = document.querySelector('select[name="polarization"]');
|
|
||||||
|
|
||||||
// Prevent deselecting all options when Ctrl+click is used
|
|
||||||
if (modulationSelect) {
|
|
||||||
modulationSelect.addEventListener('change', function(e) {
|
|
||||||
document.getElementById('filter-form').submit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polarizationSelect) {
|
|
||||||
polarizationSelect.addEventListener('change', function(e) {
|
|
||||||
document.getElementById('filter-form').submit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle kubsat and valid coords checkboxes (mutually exclusive)
|
|
||||||
// Add a function to handle radio-like behavior for these checkboxes
|
|
||||||
function setupRadioLikeCheckboxes(name) {
|
|
||||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
|
||||||
checkboxes.forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', function() {
|
|
||||||
// If this checkbox is checked, uncheck the other
|
|
||||||
if (this.checked) {
|
|
||||||
checkboxes.forEach(other => {
|
|
||||||
if (other !== this) {
|
|
||||||
other.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If both are unchecked, no action needed
|
|
||||||
}
|
|
||||||
document.getElementById('filter-form').submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupRadioLikeCheckboxes('has_kupsat');
|
|
||||||
setupRadioLikeCheckboxes('has_valid');
|
|
||||||
|
|
||||||
// Function to select/deselect all options in a select element
|
|
||||||
window.selectAllOptions = function(selectName, selectAll) {
|
|
||||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
|
||||||
if (selectElement) {
|
|
||||||
for (let i = 0; i < selectElement.options.length; i++) {
|
|
||||||
selectElement.options[i].selected = selectAll;
|
|
||||||
}
|
|
||||||
document.getElementById('filter-form').submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to update the page when satellite selection changes
|
|
||||||
function updateSatelliteSelection() {
|
|
||||||
document.getElementById('filter-form').submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all current filter values and return as URL parameters
|
|
||||||
function getAllFilterParams() {
|
|
||||||
const form = document.getElementById('filter-form');
|
|
||||||
const searchValue = document.getElementById('toolbar-search').value;
|
|
||||||
|
|
||||||
// Create URLSearchParams object from the form
|
|
||||||
const params = new URLSearchParams(new FormData(form));
|
|
||||||
|
|
||||||
// Add search value from toolbar if present
|
|
||||||
if (searchValue.trim() !== '') {
|
|
||||||
params.set('search', searchValue);
|
|
||||||
} else {
|
|
||||||
// Remove search parameter if empty
|
|
||||||
params.delete('search');
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to perform search
|
|
||||||
window.performSearch = function() {
|
|
||||||
const filterParams = getAllFilterParams();
|
|
||||||
window.location.search = filterParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to clear search
|
|
||||||
window.clearSearch = function() {
|
|
||||||
// Clear only the search input in the toolbar
|
|
||||||
document.getElementById('toolbar-search').value = '';
|
|
||||||
// Submit the form to update the results
|
|
||||||
const filterParams = getAllFilterParams();
|
|
||||||
window.location.search = filterParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Enter key in toolbar search
|
|
||||||
const toolbarSearch = document.getElementById('toolbar-search');
|
|
||||||
if (toolbarSearch) {
|
|
||||||
toolbarSearch.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
performSearch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listener to satellite select for immediate update
|
|
||||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
|
||||||
if (satelliteSelect) {
|
|
||||||
satelliteSelect.addEventListener('change', function() {
|
|
||||||
updateSatelliteSelection();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
450
dbapp/mainapp/templates/mainapp/home_old.html
Normal file
450
dbapp/mainapp/templates/mainapp/home_old.html
Normal 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 %}
|
||||||
383
dbapp/mainapp/templates/mainapp/object_marks.html
Normal file
383
dbapp/mainapp/templates/mainapp/object_marks.html
Normal 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 %}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="container-fluid px-3">
|
<div class="container-fluid px-3">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2>Список объектов</h2>
|
<h2>Список точек</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
|
|
||||||
<!-- Source Type Filter -->
|
<!-- Source Type Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Тип источника:</label>
|
<label class="form-label">Тип точки:</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_source_type"
|
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="text-center" style="width: 3%;">
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Тип точки" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1036,7 +1036,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-primary text-white">
|
<div class="modal-header bg-primary text-white">
|
||||||
<h5 class="modal-title" id="lyngsatModalLabel">
|
<h5 class="modal-title" id="lyngsatModalLabel">
|
||||||
<i class="bi bi-tv"></i> Данные источника LyngSat
|
<i class="bi bi-tv"></i> Данные объекта LyngSat
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||||
aria-label="Закрыть"></button>
|
aria-label="Закрыть"></button>
|
||||||
@@ -1160,7 +1160,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
${data.url ? `
|
${data.url ? `
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<span class="text-muted">Ссылка на источник:</span><br>
|
<span class="text-muted">Ссылка на объект:</span><br>
|
||||||
<a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary">
|
<a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-link-45deg"></i> Открыть на LyngSat
|
<i class="bi bi-link-45deg"></i> Открыть на LyngSat
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||||
|
|
||||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm">
|
<table class="table table-striped table-hover table-sm table-bordered">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" style="width: 15%;">ID источника</th>
|
<th class="text-center" style="width: 15%;">ID источника</th>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Удалить источник #{{ object.id }}{% endblock %}
|
{% block title %}Удалить объект #{{ object.id }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||||
<strong>Внимание!</strong> Вы собираетесь удалить источник.
|
<strong>Внимание!</strong> Вы собираетесь удалить объект.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Информация об источнике:</h5>
|
<h5>Информация об объекте:</h5>
|
||||||
<ul class="list-group mb-3">
|
<ul class="list-group mb-3">
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<strong>ID:</strong> {{ object.id }}
|
<strong>ID:</strong> {{ object.id }}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
{% if objitems_count > 0 %}
|
{% if objitems_count > 0 %}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<i class="bi bi-exclamation-circle-fill"></i>
|
<i class="bi bi-exclamation-circle-fill"></i>
|
||||||
<strong>Важно!</strong> При удалении источника будут также удалены все {{ objitems_count }} привязанных объектов!
|
<strong>Важно!</strong> При удалении объекта будут также удалены все {{ objitems_count }} привязанных объектов!
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% load static leaflet_tags %}
|
{% load static leaflet_tags %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
|
|
||||||
{% block title %}Редактировать источник #{{ object.id }}{% endblock %}
|
{% block title %}Редактировать объект #{{ object.id }}{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<div class="container-fluid px-3">
|
<div class="container-fluid px-3">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
<h2>Редактировать источник #{{ object.id }}</h2>
|
<h2>Редактировать объект #{{ object.id }}</h2>
|
||||||
<div>
|
<div>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
|
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">ID источника:</label>
|
<label class="form-label">ID объекта:</label>
|
||||||
<div class="readonly-field">{{ object.id }}</div>
|
<div class="readonly-field">{{ object.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
{% block title %}Список источников{% endblock %}
|
{% block title %}Список объектов{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="container-fluid px-3">
|
<div class="container-fluid px-3">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2>Список источников</h2>
|
<h2>Список объектов</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
|
|
||||||
<!-- LyngSat Filter -->
|
<!-- LyngSat Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Тип источника (ТВ):</label>
|
<label class="form-label">Тип объекта (ТВ):</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
|
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_1"
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="text-center" style="width: 3%;">
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
@@ -263,8 +263,9 @@
|
|||||||
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
||||||
<th scope="col" style="min-width: 150px;">Координаты оперативников</th>
|
<th scope="col" style="min-width: 150px;">Координаты оперативников</th>
|
||||||
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
||||||
|
<th scope="col" style="min-width: 180px;">Наличие сигнала</th>
|
||||||
{% if has_any_lyngsat %}
|
{% if has_any_lyngsat %}
|
||||||
<th scope="col" class="text-center" style="min-width: 80px;">Тип источника</th>
|
<th scope="col" class="text-center" style="min-width: 80px;">Тип объекта</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th scope="col" class="text-center" style="min-width: 100px;">
|
<th scope="col" class="text-center" style="min-width: 100px;">
|
||||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||||
@@ -312,6 +313,29 @@
|
|||||||
<td>{{ source.coords_kupsat }}</td>
|
<td>{{ source.coords_kupsat }}</td>
|
||||||
<td>{{ source.coords_valid }}</td>
|
<td>{{ source.coords_valid }}</td>
|
||||||
<td>{{ source.coords_reference }}</td>
|
<td>{{ source.coords_reference }}</td>
|
||||||
|
<td style="padding: 0.3rem; vertical-align: top;">
|
||||||
|
{% if source.marks %}
|
||||||
|
<div style="font-size: 0.75rem; line-height: 1.3;">
|
||||||
|
{% for mark in source.marks %}
|
||||||
|
<div style="{% if not forloop.last %}border-bottom: 1px solid #dee2e6; padding-bottom: 3px; margin-bottom: 3px;{% endif %}">
|
||||||
|
<div style="margin-bottom: 1px;">
|
||||||
|
{% if mark.mark %}
|
||||||
|
<span class="badge bg-success" style="font-size: 0.7rem;">Есть</span>
|
||||||
|
{% elif mark.mark == False %}
|
||||||
|
<span class="badge bg-danger" style="font-size: 0.7rem;">Нет</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary" style="font-size: 0.7rem;">-</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-muted" style="font-size: 0.7rem;">{{ mark.timestamp|date:"d.m.y H:i" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted" style="font-size: 0.65rem;">{{ mark.created_by }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% if has_any_lyngsat %}
|
{% if has_any_lyngsat %}
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if source.has_lyngsat %}
|
{% if source.has_lyngsat %}
|
||||||
@@ -333,7 +357,7 @@
|
|||||||
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-sm btn-outline-success"
|
class="btn btn-sm btn-outline-success"
|
||||||
title="Показать источник с точками на карте">
|
title="Показать объект с точками на карте">
|
||||||
<i class="bi bi-geo-alt"></i>
|
<i class="bi bi-geo-alt"></i>
|
||||||
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -365,7 +389,7 @@
|
|||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<a href="{% url 'mainapp:source_update' source.id %}"
|
<a href="{% url 'mainapp:source_update' source.id %}"
|
||||||
class="btn btn-sm btn-outline-warning"
|
class="btn btn-sm btn-outline-warning"
|
||||||
title="Редактировать источник">
|
title="Редактировать объект">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -378,7 +402,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
|
<td colspan="12" class="text-center text-muted">Нет данных для отображения</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -395,7 +419,7 @@
|
|||||||
<div class="modal-dialog modal-fullscreen">
|
<div class="modal-dialog modal-fullscreen">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника #<span id="modalSourceId"></span></h5>
|
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта #<span id="modalSourceId"></span></h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -406,6 +430,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||||
<div id="modalContent" style="display: none;">
|
<div id="modalContent" style="display: none;">
|
||||||
|
<!-- Marks Section -->
|
||||||
|
<div id="marksSection" class="mb-3" style="display: none;">
|
||||||
|
<h6 class="mb-2">Наличие сигнала объекта (<span id="marksCount">0</span>):</h6>
|
||||||
|
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 20%;">Наличие сигнала</th>
|
||||||
|
<th style="width: 40%;">Дата и время</th>
|
||||||
|
<th style="width: 40%;">Пользователь</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="marksTableBody">
|
||||||
|
<!-- Marks will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
|
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
@@ -441,14 +484,14 @@
|
|||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="18" onchange="toggleModalColumn(this)"> Комментарий</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="18" onchange="toggleModalColumn(this)"> Комментарий</label></li>
|
||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
|
||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
|
||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип источника</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип объекта</label></li>
|
||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
|
||||||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
|
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||||||
<thead class="table-light sticky-top">
|
<thead class="table-light sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" style="width: 3%;">
|
<th class="text-center" style="width: 3%;">
|
||||||
@@ -474,7 +517,7 @@
|
|||||||
<th style="min-width: 150px;">Комментарий</th>
|
<th style="min-width: 150px;">Комментарий</th>
|
||||||
<th style="min-width: 100px;">Усреднённое</th>
|
<th style="min-width: 100px;">Усреднённое</th>
|
||||||
<th style="min-width: 100px;">Стандарт</th>
|
<th style="min-width: 100px;">Стандарт</th>
|
||||||
<th style="min-width: 100px;">Тип источника</th>
|
<th style="min-width: 100px;">Тип объекта</th>
|
||||||
<th style="min-width: 80px;">Sigma</th>
|
<th style="min-width: 80px;">Sigma</th>
|
||||||
<th style="min-width: 80px;">Зеркала</th>
|
<th style="min-width: 80px;">Зеркала</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -534,7 +577,7 @@ function showSelectedOnMap() {
|
|||||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
if (checkedCheckboxes.length === 0) {
|
if (checkedCheckboxes.length === 0) {
|
||||||
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
|
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +598,7 @@ function deleteSelectedSources() {
|
|||||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
if (checkedCheckboxes.length === 0) {
|
if (checkedCheckboxes.length === 0) {
|
||||||
alert('Пожалуйста, выберите хотя бы один источник для удаления');
|
alert('Пожалуйста, выберите хотя бы один объект для удаления');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,7 +817,7 @@ function showSourceDetails(sourceId) {
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
throw new Error('Источник не найден');
|
throw new Error('Объект не найден');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Ошибка при загрузке данных');
|
throw new Error('Ошибка при загрузке данных');
|
||||||
}
|
}
|
||||||
@@ -785,6 +828,33 @@ function showSourceDetails(sourceId) {
|
|||||||
// Hide loading spinner
|
// Hide loading spinner
|
||||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
|
||||||
|
// Show marks if available
|
||||||
|
if (data.marks && data.marks.length > 0) {
|
||||||
|
document.getElementById('marksSection').style.display = 'block';
|
||||||
|
document.getElementById('marksCount').textContent = data.marks.length;
|
||||||
|
|
||||||
|
const marksTableBody = document.getElementById('marksTableBody');
|
||||||
|
marksTableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.marks.forEach(mark => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
let markBadge = '<span class="badge bg-secondary">-</span>';
|
||||||
|
if (mark.mark === true) {
|
||||||
|
markBadge = '<span class="badge bg-success">Есть</span>';
|
||||||
|
} else if (mark.mark === false) {
|
||||||
|
markBadge = '<span class="badge bg-danger">Нет</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = '<td class="text-center">' + markBadge + '</td>' +
|
||||||
|
'<td>' + mark.timestamp + '</td>' +
|
||||||
|
'<td>' + mark.created_by + '</td>';
|
||||||
|
marksTableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById('marksSection').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
if (data.objitems && data.objitems.length > 0) {
|
if (data.objitems && data.objitems.length > 0) {
|
||||||
// Show content
|
// Show content
|
||||||
document.getElementById('modalContent').style.display = 'block';
|
document.getElementById('modalContent').style.display = 'block';
|
||||||
@@ -981,7 +1051,7 @@ function showLyngsatModal(lyngsatId) {
|
|||||||
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
|
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
|
||||||
'<div class="card-body"><div class="row">' +
|
'<div class="card-body"><div class="row">' +
|
||||||
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
|
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
|
||||||
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на источник:</span><br>' +
|
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
|
||||||
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
|
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
|
||||||
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
|
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
|
||||||
'</div></div></div></div></div></div></div>';
|
'</div></div></div></div></div></div></div>';
|
||||||
@@ -1048,7 +1118,7 @@ function showTransponderModal(transponderId) {
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-primary text-white">
|
<div class="modal-header bg-primary text-white">
|
||||||
<h5 class="modal-title" id="lyngsatModalLabel">
|
<h5 class="modal-title" id="lyngsatModalLabel">
|
||||||
<i class="bi bi-tv"></i> Данные источника LyngSat
|
<i class="bi bi-tv"></i> Данные объекта LyngSat
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||||
aria-label="Закрыть"></button>
|
aria-label="Закрыть"></button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "mainapp/base.html" %}
|
{% extends "mainapp/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}Карта источников{% endblock title %}
|
{% block title %}Карта объектов{% endblock title %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<!-- Leaflet CSS -->
|
<!-- Leaflet CSS -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "mainapp/base.html" %}
|
{% extends "mainapp/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %}
|
{% block title %}Карта объекта #{{ source_id }} с точками{% endblock title %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<!-- Leaflet CSS -->
|
<!-- Leaflet CSS -->
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
var sourceOverlays = [];
|
var sourceOverlays = [];
|
||||||
var glPointLayers = [];
|
var glPointLayers = [];
|
||||||
|
|
||||||
// Создаём слои для координат источника и точек ГЛ
|
// Создаём слои для координат объекта и точек ГЛ
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
var groupName = '{{ group.name|escapejs }}';
|
var groupName = '{{ group.name|escapejs }}';
|
||||||
var colorName = '{{ group.color }}';
|
var colorName = '{{ group.color }}';
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
|
|
||||||
{% for point_data in group.points %}
|
{% for point_data in group.points %}
|
||||||
{% if point_data.source_id %}
|
{% if point_data.source_id %}
|
||||||
// Это координата источника
|
// Это координата объекта
|
||||||
var pointName = "{{ point_data.source_id|escapejs }}";
|
var pointName = "{{ point_data.source_id|escapejs }}";
|
||||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||||
icon: groupIcon
|
icon: groupIcon
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
// Для координат источника добавляем как отдельный слой без вложенности
|
// Для координат объекта добавляем как отдельный слой без вложенности
|
||||||
{% if group.color in 'blue,orange,green,violet' %}
|
{% if group.color in 'blue,orange,green,violet' %}
|
||||||
sourceOverlays.push({
|
sourceOverlays.push({
|
||||||
label: groupName,
|
label: groupName,
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
|
|
||||||
if (sourceOverlays.length > 0) {
|
if (sourceOverlays.length > 0) {
|
||||||
treeOverlays.push({
|
treeOverlays.push({
|
||||||
label: "Координаты источника #{{ source_id }}",
|
label: "Координаты объекта #{{ source_id }}",
|
||||||
selectAllCheckbox: true,
|
selectAllCheckbox: true,
|
||||||
children: sourceOverlays,
|
children: sourceOverlays,
|
||||||
layer: L.layerGroup()
|
layer: L.layerGroup()
|
||||||
@@ -205,13 +205,13 @@
|
|||||||
var div = L.DomUtil.create('div', 'legend');
|
var div = L.DomUtil.create('div', 'legend');
|
||||||
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
||||||
|
|
||||||
// Координаты источника
|
// Координаты объекта
|
||||||
var hasSourceCoords = false;
|
var hasSourceCoords = false;
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
{% if group.color in 'blue,orange,green,violet' %}
|
{% if group.color in 'blue,orange,green,violet' %}
|
||||||
{% if not hasSourceCoords %}
|
{% if not hasSourceCoords %}
|
||||||
if (!hasSourceCoords) {
|
if (!hasSourceCoords) {
|
||||||
div.innerHTML += '<div class="legend-section-title">Координаты источника:</div>';
|
div.innerHTML += '<div class="legend-section-title">Координаты объекта:</div>';
|
||||||
hasSourceCoords = true;
|
hasSourceCoords = true;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||||
|
|
||||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm">
|
<table class="table table-striped table-hover table-sm table-bordered">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" style="width: 10%;">ID</th>
|
<th class="text-center" style="width: 10%;">ID</th>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||||||
<thead class="table-dark sticky-top">
|
<thead class="table-dark sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="text-center" style="width: 3%;">
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .views import (
|
|||||||
DeleteSelectedTranspondersView,
|
DeleteSelectedTranspondersView,
|
||||||
FillLyngsatDataView,
|
FillLyngsatDataView,
|
||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
|
HomeView,
|
||||||
LinkLyngsatSourcesView,
|
LinkLyngsatSourcesView,
|
||||||
LinkVchSigmaView,
|
LinkVchSigmaView,
|
||||||
LoadCsvDataView,
|
LoadCsvDataView,
|
||||||
@@ -43,11 +44,13 @@ from .views import (
|
|||||||
UploadVchLoadView,
|
UploadVchLoadView,
|
||||||
custom_logout,
|
custom_logout,
|
||||||
)
|
)
|
||||||
|
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', SourceListView.as_view(), name='home'),
|
path('', HomeView.as_view(), name='home'),
|
||||||
|
path('sources/', SourceListView.as_view(), name='source_list'),
|
||||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||||
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
||||||
@@ -87,5 +90,8 @@ urlpatterns = [
|
|||||||
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
||||||
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
||||||
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
|
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
|
||||||
|
path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'),
|
||||||
|
path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'),
|
||||||
|
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
|
||||||
path('logout/', custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
]
|
]
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Import all views for easy access
|
# Import all views for easy access
|
||||||
from .base import ActionsPageView, custom_logout
|
from .base import ActionsPageView, HomeView, custom_logout
|
||||||
from .objitem import (
|
from .objitem import (
|
||||||
ObjItemListView,
|
ObjItemListView,
|
||||||
ObjItemCreateView,
|
ObjItemCreateView,
|
||||||
@@ -51,6 +51,7 @@ from .map import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
# Base
|
# Base
|
||||||
'ActionsPageView',
|
'ActionsPageView',
|
||||||
|
'HomeView',
|
||||||
'custom_logout',
|
'custom_logout',
|
||||||
# ObjItem
|
# ObjItem
|
||||||
'ObjItemListView',
|
'ObjItemListView',
|
||||||
|
|||||||
@@ -192,7 +192,9 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
|||||||
'source_objitems__lyngsat_source',
|
'source_objitems__lyngsat_source',
|
||||||
'source_objitems__transponder',
|
'source_objitems__transponder',
|
||||||
'source_objitems__created_by__user',
|
'source_objitems__created_by__user',
|
||||||
'source_objitems__updated_by__user'
|
'source_objitems__updated_by__user',
|
||||||
|
'marks',
|
||||||
|
'marks__created_by__user'
|
||||||
).get(id=source_id)
|
).get(id=source_id)
|
||||||
|
|
||||||
# Get all related ObjItems, sorted by created_at
|
# Get all related ObjItems, sorted by created_at
|
||||||
@@ -327,9 +329,25 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
|||||||
'mirrors': mirrors,
|
'mirrors': mirrors,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Get marks for the source
|
||||||
|
marks_data = []
|
||||||
|
for mark in source.marks.all().order_by('-timestamp'):
|
||||||
|
mark_timestamp = '-'
|
||||||
|
if mark.timestamp:
|
||||||
|
local_time = timezone.localtime(mark.timestamp)
|
||||||
|
mark_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
marks_data.append({
|
||||||
|
'id': mark.id,
|
||||||
|
'mark': mark.mark,
|
||||||
|
'timestamp': mark_timestamp,
|
||||||
|
'created_by': str(mark.created_by) if mark.created_by else '-',
|
||||||
|
})
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'source_id': source_id,
|
'source_id': source_id,
|
||||||
'objitems': objitems_data
|
'objitems': objitems_data,
|
||||||
|
'marks': marks_data
|
||||||
})
|
})
|
||||||
except Source.DoesNotExist:
|
except Source.DoesNotExist:
|
||||||
return JsonResponse({'error': 'Источник не найден'}, status=404)
|
return JsonResponse({'error': 'Источник не найден'}, status=404)
|
||||||
|
|||||||
@@ -1,10 +1,491 @@
|
|||||||
"""
|
"""
|
||||||
Base views and utilities.
|
Base views and utilities.
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Count, Q
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
|
from ..models import Source, ObjItem, Satellite, Modulation, Polarization, ObjectMark
|
||||||
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
|
class HomeView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Main page with filters for displaying sources or objitems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Get pagination parameters
|
||||||
|
page_number, items_per_page = parse_pagination_params(request)
|
||||||
|
|
||||||
|
# Get display mode: 'sources' or 'objitems'
|
||||||
|
display_mode = request.GET.get("display_mode", "sources")
|
||||||
|
|
||||||
|
# Get filter parameters
|
||||||
|
selected_satellites = request.GET.getlist("satellite_id")
|
||||||
|
date_from = request.GET.get("date_from", "").strip()
|
||||||
|
date_to = request.GET.get("date_to", "").strip()
|
||||||
|
freq_min = request.GET.get("freq_min", "").strip()
|
||||||
|
freq_max = request.GET.get("freq_max", "").strip()
|
||||||
|
range_min = request.GET.get("range_min", "").strip()
|
||||||
|
range_max = request.GET.get("range_max", "").strip()
|
||||||
|
selected_modulations = request.GET.getlist("modulation")
|
||||||
|
selected_polarizations = request.GET.getlist("polarization")
|
||||||
|
|
||||||
|
# Source-specific filters
|
||||||
|
has_coords_average = request.GET.get("has_coords_average")
|
||||||
|
has_kupsat = request.GET.get("has_kupsat")
|
||||||
|
has_valid = request.GET.get("has_valid")
|
||||||
|
has_reference = request.GET.get("has_reference")
|
||||||
|
|
||||||
|
# ObjItem-specific filters
|
||||||
|
has_geo = request.GET.get("has_geo")
|
||||||
|
has_lyngsat = request.GET.get("has_lyngsat")
|
||||||
|
|
||||||
|
# Marks filters
|
||||||
|
show_marks = request.GET.get("show_marks", "0")
|
||||||
|
marks_date_from = request.GET.get("marks_date_from", "").strip()
|
||||||
|
marks_date_to = request.GET.get("marks_date_to", "").strip()
|
||||||
|
marks_status = request.GET.get("marks_status", "") # all, present, absent
|
||||||
|
|
||||||
|
# Get all satellites, modulations, polarizations for filters
|
||||||
|
satellites = Satellite.objects.all().order_by("name")
|
||||||
|
modulations = Modulation.objects.all().order_by("name")
|
||||||
|
polarizations = Polarization.objects.all().order_by("name")
|
||||||
|
|
||||||
|
# Prepare context
|
||||||
|
context = {
|
||||||
|
'display_mode': display_mode,
|
||||||
|
'satellites': satellites,
|
||||||
|
'modulations': modulations,
|
||||||
|
'polarizations': polarizations,
|
||||||
|
'selected_satellites': [int(x) for x in selected_satellites if x.isdigit()],
|
||||||
|
'selected_modulations': [int(x) for x in selected_modulations if x.isdigit()],
|
||||||
|
'selected_polarizations': [int(x) for x in selected_polarizations if x.isdigit()],
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'freq_min': freq_min,
|
||||||
|
'freq_max': freq_max,
|
||||||
|
'range_min': range_min,
|
||||||
|
'range_max': range_max,
|
||||||
|
'has_coords_average': has_coords_average,
|
||||||
|
'has_kupsat': has_kupsat,
|
||||||
|
'has_valid': has_valid,
|
||||||
|
'has_reference': has_reference,
|
||||||
|
'has_geo': has_geo,
|
||||||
|
'has_lyngsat': has_lyngsat,
|
||||||
|
'show_marks': show_marks,
|
||||||
|
'marks_date_from': marks_date_from,
|
||||||
|
'marks_date_to': marks_date_to,
|
||||||
|
'marks_status': marks_status,
|
||||||
|
'items_per_page': items_per_page,
|
||||||
|
'available_items_per_page': [50, 100, 500, 1000],
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if display_mode == "objitems":
|
||||||
|
# Display ObjItems
|
||||||
|
queryset = self._get_objitems_queryset(
|
||||||
|
selected_satellites, date_from, date_to,
|
||||||
|
freq_min, freq_max, range_min, range_max,
|
||||||
|
selected_modulations, selected_polarizations,
|
||||||
|
has_geo, has_lyngsat
|
||||||
|
)
|
||||||
|
|
||||||
|
paginator = Paginator(queryset, items_per_page)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
processed_objitems = self._process_objitems(
|
||||||
|
page_obj, show_marks, marks_date_from, marks_date_to, marks_status
|
||||||
|
)
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'processed_objitems': processed_objitems,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Display Sources
|
||||||
|
queryset = self._get_sources_queryset(
|
||||||
|
selected_satellites, date_from, date_to,
|
||||||
|
freq_min, freq_max, range_min, range_max,
|
||||||
|
selected_modulations, selected_polarizations,
|
||||||
|
has_coords_average, has_kupsat, has_valid, has_reference
|
||||||
|
)
|
||||||
|
|
||||||
|
paginator = Paginator(queryset, items_per_page)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
processed_sources = self._process_sources(
|
||||||
|
page_obj, show_marks, marks_date_from, marks_date_to, marks_status
|
||||||
|
)
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'processed_sources': processed_sources,
|
||||||
|
})
|
||||||
|
|
||||||
|
return render(request, "mainapp/home.html", context)
|
||||||
|
|
||||||
|
def _get_sources_queryset(self, selected_satellites, date_from, date_to,
|
||||||
|
freq_min, freq_max, range_min, range_max,
|
||||||
|
selected_modulations, selected_polarizations,
|
||||||
|
has_coords_average, has_kupsat, has_valid, has_reference):
|
||||||
|
"""Build queryset for sources with filters."""
|
||||||
|
sources = Source.objects.prefetch_related(
|
||||||
|
'source_objitems',
|
||||||
|
'source_objitems__parameter_obj',
|
||||||
|
'source_objitems__parameter_obj__id_satellite',
|
||||||
|
'source_objitems__geo_obj'
|
||||||
|
).annotate(objitem_count=Count('source_objitems'))
|
||||||
|
|
||||||
|
# Filter by satellites
|
||||||
|
if selected_satellites:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# Filter by date range (using Geo timestamps)
|
||||||
|
if date_from or date_to:
|
||||||
|
geo_filter = Q()
|
||||||
|
if date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||||
|
geo_filter &= Q(source_objitems__geo_obj__timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
try:
|
||||||
|
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
|
||||||
|
geo_filter &= Q(source_objitems__geo_obj__timestamp__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if geo_filter:
|
||||||
|
sources = sources.filter(geo_filter).distinct()
|
||||||
|
|
||||||
|
# Filter by frequency
|
||||||
|
if freq_min:
|
||||||
|
try:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__frequency__gte=float(freq_min)
|
||||||
|
).distinct()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if freq_max:
|
||||||
|
try:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__frequency__lte=float(freq_max)
|
||||||
|
).distinct()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by frequency range
|
||||||
|
if range_min:
|
||||||
|
try:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__freq_range__gte=float(range_min)
|
||||||
|
).distinct()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if range_max:
|
||||||
|
try:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__freq_range__lte=float(range_max)
|
||||||
|
).distinct()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by modulation
|
||||||
|
if selected_modulations:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__modulation_id__in=selected_modulations
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# Filter by polarization
|
||||||
|
if selected_polarizations:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__parameter_obj__polarization_id__in=selected_polarizations
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# Filter by coordinates presence
|
||||||
|
if has_coords_average == "1":
|
||||||
|
sources = sources.filter(coords_average__isnull=False)
|
||||||
|
elif has_coords_average == "0":
|
||||||
|
sources = sources.filter(coords_average__isnull=True)
|
||||||
|
|
||||||
|
if has_kupsat == "1":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=False)
|
||||||
|
elif has_kupsat == "0":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=True)
|
||||||
|
|
||||||
|
if has_valid == "1":
|
||||||
|
sources = sources.filter(coords_valid__isnull=False)
|
||||||
|
elif has_valid == "0":
|
||||||
|
sources = sources.filter(coords_valid__isnull=True)
|
||||||
|
|
||||||
|
if has_reference == "1":
|
||||||
|
sources = sources.filter(coords_reference__isnull=False)
|
||||||
|
elif has_reference == "0":
|
||||||
|
sources = sources.filter(coords_reference__isnull=True)
|
||||||
|
|
||||||
|
return sources.order_by('-id')
|
||||||
|
|
||||||
|
def _get_objitems_queryset(self, selected_satellites, date_from, date_to,
|
||||||
|
freq_min, freq_max, range_min, range_max,
|
||||||
|
selected_modulations, selected_polarizations,
|
||||||
|
has_geo, has_lyngsat):
|
||||||
|
"""Build queryset for objitems with filters."""
|
||||||
|
objitems = ObjItem.objects.select_related(
|
||||||
|
'parameter_obj',
|
||||||
|
'parameter_obj__id_satellite',
|
||||||
|
'parameter_obj__modulation',
|
||||||
|
'parameter_obj__polarization',
|
||||||
|
'geo_obj',
|
||||||
|
'source',
|
||||||
|
'lyngsat_source'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter by satellites
|
||||||
|
if selected_satellites:
|
||||||
|
objitems = objitems.filter(parameter_obj__id_satellite_id__in=selected_satellites)
|
||||||
|
|
||||||
|
# Filter by date range
|
||||||
|
if date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||||
|
objitems = objitems.filter(geo_obj__timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
try:
|
||||||
|
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
|
||||||
|
objitems = objitems.filter(geo_obj__timestamp__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by frequency
|
||||||
|
if freq_min:
|
||||||
|
try:
|
||||||
|
objitems = objitems.filter(parameter_obj__frequency__gte=float(freq_min))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if freq_max:
|
||||||
|
try:
|
||||||
|
objitems = objitems.filter(parameter_obj__frequency__lte=float(freq_max))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by frequency range
|
||||||
|
if range_min:
|
||||||
|
try:
|
||||||
|
objitems = objitems.filter(parameter_obj__freq_range__gte=float(range_min))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if range_max:
|
||||||
|
try:
|
||||||
|
objitems = objitems.filter(parameter_obj__freq_range__lte=float(range_max))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by modulation
|
||||||
|
if selected_modulations:
|
||||||
|
objitems = objitems.filter(parameter_obj__modulation_id__in=selected_modulations)
|
||||||
|
|
||||||
|
# Filter by polarization
|
||||||
|
if selected_polarizations:
|
||||||
|
objitems = objitems.filter(parameter_obj__polarization_id__in=selected_polarizations)
|
||||||
|
|
||||||
|
# Filter by coordinates presence
|
||||||
|
if has_geo == "1":
|
||||||
|
objitems = objitems.filter(geo_obj__isnull=False)
|
||||||
|
elif has_geo == "0":
|
||||||
|
objitems = objitems.filter(geo_obj__isnull=True)
|
||||||
|
|
||||||
|
# Filter by LyngSat connection
|
||||||
|
if has_lyngsat == "1":
|
||||||
|
objitems = objitems.filter(lyngsat_source__isnull=False)
|
||||||
|
elif has_lyngsat == "0":
|
||||||
|
objitems = objitems.filter(lyngsat_source__isnull=True)
|
||||||
|
|
||||||
|
return objitems.order_by('-id')
|
||||||
|
|
||||||
|
def _process_sources(self, page_obj, show_marks="0", marks_date_from="", marks_date_to="", marks_status=""):
|
||||||
|
"""Process sources for display."""
|
||||||
|
processed = []
|
||||||
|
|
||||||
|
for source in page_obj:
|
||||||
|
# Get satellites
|
||||||
|
satellite_names = set()
|
||||||
|
for objitem in source.source_objitems.all():
|
||||||
|
if objitem.parameter_obj and objitem.parameter_obj.id_satellite:
|
||||||
|
satellite_names.add(objitem.parameter_obj.id_satellite.name)
|
||||||
|
|
||||||
|
# Format coordinates
|
||||||
|
def format_coords(point):
|
||||||
|
if point:
|
||||||
|
lon, lat = point.coords[0], point.coords[1]
|
||||||
|
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
|
||||||
|
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||||
|
return f"{lat_str} {lon_str}"
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
# Get marks if requested
|
||||||
|
marks_data = []
|
||||||
|
if show_marks == "1":
|
||||||
|
marks_qs = source.marks.select_related('created_by__user').all()
|
||||||
|
|
||||||
|
# Filter marks by date
|
||||||
|
if marks_date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%dT%H:%M")
|
||||||
|
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%d")
|
||||||
|
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if marks_date_to:
|
||||||
|
try:
|
||||||
|
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%dT%H:%M")
|
||||||
|
marks_qs = marks_qs.filter(timestamp__lte=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%d") + timedelta(days=1)
|
||||||
|
marks_qs = marks_qs.filter(timestamp__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter marks by status
|
||||||
|
if marks_status == "present":
|
||||||
|
marks_qs = marks_qs.filter(mark=True)
|
||||||
|
elif marks_status == "absent":
|
||||||
|
marks_qs = marks_qs.filter(mark=False)
|
||||||
|
|
||||||
|
# Process marks
|
||||||
|
for mark in marks_qs:
|
||||||
|
marks_data.append({
|
||||||
|
'id': mark.id,
|
||||||
|
'mark': mark.mark,
|
||||||
|
'timestamp': mark.timestamp,
|
||||||
|
'created_by': str(mark.created_by) if mark.created_by else "-",
|
||||||
|
'can_edit': mark.can_edit(),
|
||||||
|
})
|
||||||
|
|
||||||
|
processed.append({
|
||||||
|
'id': source.id,
|
||||||
|
'satellites': ", ".join(sorted(satellite_names)) if satellite_names else "-",
|
||||||
|
'objitem_count': source.objitem_count,
|
||||||
|
'coords_average': format_coords(source.coords_average),
|
||||||
|
'coords_kupsat': format_coords(source.coords_kupsat),
|
||||||
|
'coords_valid': format_coords(source.coords_valid),
|
||||||
|
'coords_reference': format_coords(source.coords_reference),
|
||||||
|
'created_at': source.created_at,
|
||||||
|
'marks': marks_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return processed
|
||||||
|
|
||||||
|
def _process_objitems(self, page_obj, show_marks="0", marks_date_from="", marks_date_to="", marks_status=""):
|
||||||
|
"""Process objitems for display."""
|
||||||
|
processed = []
|
||||||
|
|
||||||
|
for objitem in page_obj:
|
||||||
|
param = objitem.parameter_obj
|
||||||
|
geo = objitem.geo_obj
|
||||||
|
source = objitem.source
|
||||||
|
|
||||||
|
# Format geo coordinates
|
||||||
|
geo_coords = "-"
|
||||||
|
geo_date = "-"
|
||||||
|
if geo and geo.coords:
|
||||||
|
lon, lat = geo.coords.coords[0], geo.coords.coords[1]
|
||||||
|
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
|
||||||
|
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||||
|
geo_coords = f"{lat_str} {lon_str}"
|
||||||
|
if geo.timestamp:
|
||||||
|
geo_date = geo.timestamp.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Format source coordinates
|
||||||
|
def format_coords(point):
|
||||||
|
if point:
|
||||||
|
lon, lat = point.coords[0], point.coords[1]
|
||||||
|
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
|
||||||
|
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||||
|
return f"{lat_str} {lon_str}"
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
kupsat_coords = format_coords(source.coords_kupsat) if source else "-"
|
||||||
|
valid_coords = format_coords(source.coords_valid) if source else "-"
|
||||||
|
|
||||||
|
# Get marks if requested
|
||||||
|
marks_data = []
|
||||||
|
if show_marks == "1":
|
||||||
|
marks_qs = objitem.marks.select_related('created_by__user').all()
|
||||||
|
|
||||||
|
# Filter marks by date
|
||||||
|
if marks_date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(marks_date_from, "%Y-%m-%d")
|
||||||
|
marks_qs = marks_qs.filter(timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if marks_date_to:
|
||||||
|
try:
|
||||||
|
date_to_obj = datetime.strptime(marks_date_to, "%Y-%m-%d") + timedelta(days=1)
|
||||||
|
marks_qs = marks_qs.filter(timestamp__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter marks by status
|
||||||
|
if marks_status == "present":
|
||||||
|
marks_qs = marks_qs.filter(mark=True)
|
||||||
|
elif marks_status == "absent":
|
||||||
|
marks_qs = marks_qs.filter(mark=False)
|
||||||
|
|
||||||
|
# Process marks
|
||||||
|
for mark in marks_qs:
|
||||||
|
marks_data.append({
|
||||||
|
'id': mark.id,
|
||||||
|
'mark': mark.mark,
|
||||||
|
'timestamp': mark.timestamp,
|
||||||
|
'created_by': str(mark.created_by) if mark.created_by else "-",
|
||||||
|
'can_edit': mark.can_edit(),
|
||||||
|
})
|
||||||
|
|
||||||
|
processed.append({
|
||||||
|
'id': objitem.id,
|
||||||
|
'name': objitem.name or "-",
|
||||||
|
'satellite': param.id_satellite.name if param and param.id_satellite else "-",
|
||||||
|
'frequency': param.frequency if param else "-",
|
||||||
|
'freq_range': param.freq_range if param else "-",
|
||||||
|
'polarization': param.polarization.name if param and param.polarization else "-",
|
||||||
|
'modulation': param.modulation.name if param and param.modulation else "-",
|
||||||
|
'bod_velocity': param.bod_velocity if param else "-",
|
||||||
|
'snr': param.snr if param else "-",
|
||||||
|
'geo_coords': geo_coords,
|
||||||
|
'geo_date': geo_date,
|
||||||
|
'kupsat_coords': kupsat_coords,
|
||||||
|
'valid_coords': valid_coords,
|
||||||
|
'source_id': source.id if source else None,
|
||||||
|
'lyngsat_id': objitem.lyngsat_source.id if objitem.lyngsat_source else None,
|
||||||
|
'marks': marks_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return processed
|
||||||
|
|
||||||
|
|
||||||
class ActionsPageView(View):
|
class ActionsPageView(View):
|
||||||
"""View for displaying the actions page."""
|
"""View for displaying the actions page."""
|
||||||
|
|||||||
142
dbapp/mainapp/views/marks.py
Normal file
142
dbapp/mainapp/views/marks.py
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -57,7 +57,9 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
'source_objitems',
|
'source_objitems',
|
||||||
'source_objitems__parameter_obj',
|
'source_objitems__parameter_obj',
|
||||||
'source_objitems__parameter_obj__id_satellite',
|
'source_objitems__parameter_obj__id_satellite',
|
||||||
'source_objitems__geo_obj'
|
'source_objitems__geo_obj',
|
||||||
|
'marks',
|
||||||
|
'marks__created_by__user'
|
||||||
).annotate(
|
).annotate(
|
||||||
objitem_count=Count('source_objitems')
|
objitem_count=Count('source_objitems')
|
||||||
)
|
)
|
||||||
@@ -203,6 +205,15 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
|
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
|
||||||
|
|
||||||
|
# Get all marks (presence/absence)
|
||||||
|
marks_data = []
|
||||||
|
for mark in source.marks.all():
|
||||||
|
marks_data.append({
|
||||||
|
'mark': mark.mark,
|
||||||
|
'timestamp': mark.timestamp,
|
||||||
|
'created_by': str(mark.created_by) if mark.created_by else '-',
|
||||||
|
})
|
||||||
|
|
||||||
processed_sources.append({
|
processed_sources.append({
|
||||||
'id': source.id,
|
'id': source.id,
|
||||||
'coords_average': coords_average_str,
|
'coords_average': coords_average_str,
|
||||||
@@ -215,6 +226,7 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
'updated_at': source.updated_at,
|
'updated_at': source.updated_at,
|
||||||
'has_lyngsat': has_lyngsat,
|
'has_lyngsat': has_lyngsat,
|
||||||
'lyngsat_id': lyngsat_id,
|
'lyngsat_id': lyngsat_id,
|
||||||
|
'marks': marks_data,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Prepare context for template
|
# Prepare context for template
|
||||||
|
|||||||
18
dbapp/mapsapp/migrations/0002_alter_transponders_snr.py
Normal file
18
dbapp/mapsapp/migrations/0002_alter_transponders_snr.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-15 21:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mapsapp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='snr',
|
||||||
|
field=models.FloatField(blank=True, help_text='Отношение сигнал/шум в децибелах', null=True, verbose_name='ОСШ, дБ'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user