Переделки и улучшения
This commit is contained in:
@@ -478,6 +478,9 @@ class SourceForm(forms.ModelForm):
|
||||
'info': 'Тип объекта',
|
||||
'ownership': 'Принадлежность объекта',
|
||||
}
|
||||
help_texts = {
|
||||
'info': 'Стационарные: координата усредняется. Подвижные: координата = последняя точка. При изменении типа координата пересчитывается автоматически.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
38
dbapp/mainapp/migrations/0010_set_default_source_type.py
Normal file
38
dbapp/mainapp/migrations/0010_set_default_source_type.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-21 07:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_default_source_type(apps, schema_editor):
|
||||
"""
|
||||
Устанавливает тип "Стационарные" для всех Source, у которых не указан тип.
|
||||
"""
|
||||
Source = apps.get_model('mainapp', 'Source')
|
||||
ObjectInfo = apps.get_model('mainapp', 'ObjectInfo')
|
||||
|
||||
# Создаем или получаем тип "Стационарные"
|
||||
stationary_info, _ = ObjectInfo.objects.get_or_create(name="Стационарные")
|
||||
|
||||
# Обновляем все Source без типа
|
||||
sources_without_type = Source.objects.filter(info__isnull=True)
|
||||
count = sources_without_type.update(info=stationary_info)
|
||||
|
||||
print(f"Обновлено {count} источников с типом 'Стационарные'")
|
||||
|
||||
|
||||
def reverse_set_default_source_type(apps, schema_editor):
|
||||
"""
|
||||
Обратная миграция - ничего не делаем, так как это безопасная операция.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0009_objectownership_alter_source_info_source_ownership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_default_source_type, reverse_set_default_source_type),
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-21 07:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def fix_capitalization(apps, schema_editor):
|
||||
"""
|
||||
Исправляет регистр типов объектов: "стационарные" -> "Стационарные", "подвижные" -> "Подвижные"
|
||||
"""
|
||||
ObjectInfo = apps.get_model('mainapp', 'ObjectInfo')
|
||||
Source = apps.get_model('mainapp', 'Source')
|
||||
|
||||
# Создаем правильные типы с большой буквы
|
||||
stationary_new, _ = ObjectInfo.objects.get_or_create(name="Стационарные")
|
||||
mobile_new, _ = ObjectInfo.objects.get_or_create(name="Подвижные")
|
||||
|
||||
# Находим старые типы с маленькой буквы
|
||||
try:
|
||||
stationary_old = ObjectInfo.objects.get(name="стационарные")
|
||||
# Обновляем все Source, которые используют старый тип
|
||||
count = Source.objects.filter(info=stationary_old).update(info=stationary_new)
|
||||
print(f"Обновлено {count} источников: 'стационарные' -> 'Стационарные'")
|
||||
# Удаляем старый тип
|
||||
stationary_old.delete()
|
||||
except ObjectInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
mobile_old = ObjectInfo.objects.get(name="подвижные")
|
||||
# Обновляем все Source, которые используют старый тип
|
||||
count = Source.objects.filter(info=mobile_old).update(info=mobile_new)
|
||||
print(f"Обновлено {count} источников: 'подвижные' -> 'Подвижные'")
|
||||
# Удаляем старый тип
|
||||
mobile_old.delete()
|
||||
except ObjectInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
def reverse_fix_capitalization(apps, schema_editor):
|
||||
"""
|
||||
Обратная миграция - возвращаем маленькие буквы
|
||||
"""
|
||||
ObjectInfo = apps.get_model('mainapp', 'ObjectInfo')
|
||||
Source = apps.get_model('mainapp', 'Source')
|
||||
|
||||
# Создаем типы с маленькой буквы
|
||||
stationary_old, _ = ObjectInfo.objects.get_or_create(name="стационарные")
|
||||
mobile_old, _ = ObjectInfo.objects.get_or_create(name="подвижные")
|
||||
|
||||
# Находим типы с большой буквы
|
||||
try:
|
||||
stationary_new = ObjectInfo.objects.get(name="Стационарные")
|
||||
Source.objects.filter(info=stationary_new).update(info=stationary_old)
|
||||
stationary_new.delete()
|
||||
except ObjectInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
mobile_new = ObjectInfo.objects.get(name="Подвижные")
|
||||
Source.objects.filter(info=mobile_new).update(info=mobile_old)
|
||||
mobile_new.delete()
|
||||
except ObjectInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0010_set_default_source_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_capitalization, reverse_fix_capitalization),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-21 12:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0011_fix_source_type_capitalization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='source',
|
||||
name='confirm_at',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время добавления последней полученной точки ГЛ', null=True, verbose_name='Дата подтверждения'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='source',
|
||||
name='last_signal_at',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время последней отметки о наличии сигнала', null=True, verbose_name='Последний сигнал'),
|
||||
),
|
||||
]
|
||||
@@ -491,6 +491,18 @@ class Source(models.Model):
|
||||
verbose_name="Принадлежность объекта",
|
||||
help_text="Принадлежность объекта (страна, организация и т.д.)",
|
||||
)
|
||||
confirm_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Дата подтверждения",
|
||||
help_text="Дата и время добавления последней полученной точки ГЛ",
|
||||
)
|
||||
last_signal_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Последний сигнал",
|
||||
help_text="Дата и время последней отметки о наличии сигнала",
|
||||
)
|
||||
|
||||
coords_average = gis.PointField(
|
||||
srid=4326,
|
||||
@@ -550,6 +562,135 @@ class Source(models.Model):
|
||||
help_text="Пользователь, последним изменивший запись",
|
||||
)
|
||||
|
||||
def update_coords_average(self, new_coord_tuple):
|
||||
"""
|
||||
Обновляет coords_average в зависимости от типа объекта (info).
|
||||
|
||||
Логика:
|
||||
- Если info == "Подвижные": coords_average = последняя добавленная координата
|
||||
- Иначе (Стационарные и др.): coords_average = инкрементальное среднее
|
||||
|
||||
Args:
|
||||
new_coord_tuple: кортеж (longitude, latitude) новой координаты
|
||||
"""
|
||||
from django.contrib.gis.geos import Point
|
||||
from .utils import calculate_mean_coords
|
||||
|
||||
# Если тип объекта "Подвижные" - просто устанавливаем последнюю координату
|
||||
if self.info and self.info.name == "Подвижные":
|
||||
self.coords_average = Point(new_coord_tuple, srid=4326)
|
||||
else:
|
||||
# Для стационарных объектов - вычисляем среднее
|
||||
if self.coords_average:
|
||||
# Есть предыдущее среднее - вычисляем новое среднее
|
||||
current_coord = (self.coords_average.x, self.coords_average.y)
|
||||
new_avg, _ = calculate_mean_coords(current_coord, new_coord_tuple)
|
||||
self.coords_average = Point(new_avg, srid=4326)
|
||||
else:
|
||||
# Первая координата - просто устанавливаем её
|
||||
self.coords_average = Point(new_coord_tuple, srid=4326)
|
||||
|
||||
def get_last_geo_coords(self):
|
||||
"""
|
||||
Получает координаты последней добавленной точки ГЛ для этого источника.
|
||||
Сортировка по ID (последняя добавленная в базу).
|
||||
|
||||
Returns:
|
||||
tuple: (longitude, latitude) или None если точек нет
|
||||
"""
|
||||
# Получаем последний ObjItem для этого Source (по ID)
|
||||
last_objitem = self.source_objitems.filter(
|
||||
geo_obj__coords__isnull=False
|
||||
).select_related('geo_obj').order_by('-id').first()
|
||||
|
||||
if last_objitem and last_objitem.geo_obj and last_objitem.geo_obj.coords:
|
||||
return (last_objitem.geo_obj.coords.x, last_objitem.geo_obj.coords.y)
|
||||
|
||||
return None
|
||||
|
||||
def update_confirm_at(self):
|
||||
"""
|
||||
Обновляет дату confirm_at на дату создания последней добавленной точки ГЛ.
|
||||
"""
|
||||
last_objitem = self.source_objitems.order_by('-created_at').first()
|
||||
if last_objitem:
|
||||
self.confirm_at = last_objitem.created_at
|
||||
|
||||
def update_last_signal_at(self):
|
||||
"""
|
||||
Обновляет дату last_signal_at на дату последней отметки о наличии сигнала (mark=True).
|
||||
"""
|
||||
last_signal_mark = self.marks.filter(mark=True).order_by('-timestamp').first()
|
||||
if last_signal_mark:
|
||||
self.last_signal_at = last_signal_mark.timestamp
|
||||
else:
|
||||
self.last_signal_at = None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Переопределенный метод save для автоматического обновления coords_average
|
||||
при изменении типа объекта.
|
||||
"""
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
# Проверяем, изменился ли тип объекта
|
||||
if self.pk: # Объект уже существует
|
||||
try:
|
||||
old_instance = Source.objects.get(pk=self.pk)
|
||||
old_info = old_instance.info
|
||||
new_info = self.info
|
||||
|
||||
# Если тип изменился на "Подвижные"
|
||||
if new_info and new_info.name == "Подвижные" and (not old_info or old_info.name != "Подвижные"):
|
||||
# Устанавливаем координату последней точки
|
||||
last_coords = self.get_last_geo_coords()
|
||||
if last_coords:
|
||||
self.coords_average = Point(last_coords, srid=4326)
|
||||
|
||||
# Если тип изменился с "Подвижные" на что-то другое
|
||||
elif old_info and old_info.name == "Подвижные" and (not new_info or new_info.name != "Подвижные"):
|
||||
# Пересчитываем среднюю координату по всем точкам
|
||||
self._recalculate_average_coords()
|
||||
|
||||
except Source.DoesNotExist:
|
||||
pass
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _recalculate_average_coords(self):
|
||||
"""
|
||||
Пересчитывает среднюю координату по всем точкам источника.
|
||||
Используется при переключении с "Подвижные" на "Стационарные".
|
||||
|
||||
Сортировка по ID (порядок добавления в базу), инкрементальное усреднение
|
||||
как в функциях импорта.
|
||||
"""
|
||||
from django.contrib.gis.geos import Point
|
||||
from .utils import calculate_mean_coords
|
||||
|
||||
# Получаем все точки для этого источника, сортируем по ID (порядок добавления)
|
||||
objitems = self.source_objitems.filter(
|
||||
geo_obj__coords__isnull=False
|
||||
).select_related('geo_obj').order_by('id')
|
||||
|
||||
if not objitems.exists():
|
||||
return
|
||||
|
||||
# Вычисляем среднюю координату инкрементально (как в функциях импорта)
|
||||
coords_average = None
|
||||
for objitem in objitems:
|
||||
if objitem.geo_obj and objitem.geo_obj.coords:
|
||||
coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
|
||||
if coords_average is None:
|
||||
# Первая точка - просто устанавливаем её
|
||||
coords_average = coord
|
||||
else:
|
||||
# Последующие точки - вычисляем среднее между текущим средним и новой точкой
|
||||
coords_average, _ = calculate_mean_coords(coords_average, coord)
|
||||
|
||||
if coords_average:
|
||||
self.coords_average = Point(coords_average, srid=4326)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Источник"
|
||||
verbose_name_plural = "Источники"
|
||||
|
||||
@@ -12,10 +12,15 @@
|
||||
}
|
||||
|
||||
.source-info-cell {
|
||||
min-width: 250px;
|
||||
min-width: 200px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.param-cell {
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.marks-cell {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
@@ -82,11 +87,24 @@
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.satellite-selector {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.satellite-selector h5 {
|
||||
margin-bottom: 1rem;
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="{% if full_width_page %}container-fluid{% else %}container{% endif %} px-3">
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
@@ -94,6 +112,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satellite Selector -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="satellite-selector">
|
||||
<h5>Выберите спутник:</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<select id="satellite-select" class="form-select" onchange="selectSatellite()">
|
||||
<option value="">-- Выберите спутник --</option>
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id == selected_satellite_id %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if selected_satellite_id %}
|
||||
<!-- Toolbar with search, pagination, and filters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
@@ -111,7 +151,18 @@
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="source-info-cell">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='id' label='Информация об объекте' current_sort=sort %}
|
||||
{% include 'mainapp/components/_sort_header.html' with field='id' label='ID / Имя' current_sort=sort %}
|
||||
</th>
|
||||
<th class="param-cell">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='frequency' label='Частота, МГц' current_sort=sort %}
|
||||
</th>
|
||||
<th class="param-cell">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='freq_range' label='Полоса, МГц' current_sort=sort %}
|
||||
</th>
|
||||
<th class="param-cell">Поляризация</th>
|
||||
<th class="param-cell">Модуляция</th>
|
||||
<th class="param-cell">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='bod_velocity' label='Бодовая скорость' current_sort=sort %}
|
||||
</th>
|
||||
<th class="marks-cell">Наличие</th>
|
||||
<th class="marks-cell">
|
||||
@@ -128,19 +179,13 @@
|
||||
<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.objitem_name }}</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 %}
|
||||
<div><strong>Имя:</strong> {{ source.objitem_name }}</div>
|
||||
</td>
|
||||
<td class="param-cell" rowspan="{{ marks.count }}">{{ source.frequency }}</td>
|
||||
<td class="param-cell" rowspan="{{ marks.count }}">{{ source.freq_range }}</td>
|
||||
<td class="param-cell" rowspan="{{ marks.count }}">{{ source.polarization }}</td>
|
||||
<td class="param-cell" rowspan="{{ marks.count }}">{{ source.modulation }}</td>
|
||||
<td class="param-cell" rowspan="{{ marks.count }}">{{ source.bod_velocity }}</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 %}">
|
||||
@@ -197,19 +242,13 @@
|
||||
<tr data-source-id="{{ source.id }}">
|
||||
<td class="source-info-cell">
|
||||
<div><strong>ID:</strong> {{ source.id }}</div>
|
||||
<div><strong>Имя объекта:</strong> {{ source.objitem_name }}</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 %}
|
||||
<div><strong>Имя:</strong> {{ source.objitem_name }}</div>
|
||||
</td>
|
||||
<td class="param-cell">{{ source.frequency }}</td>
|
||||
<td class="param-cell">{{ source.freq_range }}</td>
|
||||
<td class="param-cell">{{ source.polarization }}</td>
|
||||
<td class="param-cell">{{ source.modulation }}</td>
|
||||
<td class="param-cell">{{ source.bod_velocity }}</td>
|
||||
<td colspan="2" class="no-marks">Отметок нет</td>
|
||||
<td class="actions-cell">
|
||||
<div class="action-buttons" id="actions-{{ source.id }}">
|
||||
@@ -228,8 +267,8 @@
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4">
|
||||
<p class="text-muted mb-0">Объекты не найдены</p>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<p class="text-muted mb-0">Объекты не найдены для выбранного спутника</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -240,6 +279,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- No satellite selected message -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info text-center">
|
||||
<h5>Пожалуйста, выберите спутник для просмотра объектов</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Offcanvas Filter Panel -->
|
||||
@@ -250,24 +299,6 @@
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<form method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Mark Status Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Статус отметок:</label>
|
||||
@@ -307,7 +338,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Сохраняем параметры сортировки и поиска при применении фильтров #}
|
||||
{# Сохраняем параметры сортировки, поиска и спутника при применении фильтров #}
|
||||
{% if selected_satellite_id %}
|
||||
<input type="hidden" name="satellite_id" value="{{ selected_satellite_id }}">
|
||||
{% endif %}
|
||||
{% if request.GET.sort %}
|
||||
<input type="hidden" name="sort" value="{{ request.GET.sort }}">
|
||||
{% endif %}
|
||||
@@ -322,7 +356,7 @@
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
Применить
|
||||
</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">
|
||||
<a href="?{% if selected_satellite_id %}satellite_id={{ selected_satellite_id }}{% endif %}" class="btn btn-secondary btn-sm">
|
||||
Сбросить
|
||||
</a>
|
||||
</div>
|
||||
@@ -331,6 +365,25 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Satellite selection
|
||||
function selectSatellite() {
|
||||
const select = document.getElementById('satellite-select');
|
||||
const satelliteId = select.value;
|
||||
|
||||
if (satelliteId) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set('satellite_id', satelliteId);
|
||||
|
||||
// Reset page when changing satellite
|
||||
urlParams.delete('page');
|
||||
|
||||
window.location.search = urlParams.toString();
|
||||
} else {
|
||||
// Clear all params if no satellite selected
|
||||
window.location.search = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-select helper function
|
||||
function selectAllOptions(selectName, select) {
|
||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||
@@ -347,8 +400,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const filterCounter = document.getElementById('filterCounter');
|
||||
|
||||
if (filterCounter) {
|
||||
// Count active filters (excluding pagination, sort, search, and items_per_page)
|
||||
const excludedParams = ['page', 'sort', 'search', 'items_per_page'];
|
||||
// Count active filters (excluding pagination, sort, search, items_per_page, and satellite_id)
|
||||
const excludedParams = ['page', 'sort', 'search', 'items_per_page', 'satellite_id'];
|
||||
let activeFilters = 0;
|
||||
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
|
||||
@@ -193,6 +193,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата подтверждения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.confirm_at %}{{ object.confirm_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата наличия:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.last_signal_at %}{{ object.last_signal_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -118,6 +118,40 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Column visibility toggle button -->
|
||||
<div>
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-gear"></i> Колонки
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="columnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" id="select-all-columns" onchange="toggleAllColumns(this)"> Выбрать всё
|
||||
</label>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> ID</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Имя</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Спутник</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Тип объекта</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Принадлежность объекта</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Координаты ГЛ</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Кол-во ГЛ(точек)</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> Координаты Кубсата</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Координаты визуального наблюдения</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Координаты справочные</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="11" onchange="toggleColumn(this)"> Создано</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="12" onchange="toggleColumn(this)"> Обновлено</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Дата подтверждения</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Последний сигнал</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Действия</label></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="ms-auto">
|
||||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||
@@ -281,32 +315,6 @@
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Signal Mark Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Наличие сигнала:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_signal_mark" id="has_signal_mark_1"
|
||||
value="1" {% if has_signal_mark == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_signal_mark_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_signal_mark" id="has_signal_mark_0"
|
||||
value="0" {% if has_signal_mark == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_signal_mark_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mark Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата отметки сигнала:</label>
|
||||
<input type="date" name="mark_date_from" id="mark_date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ mark_date_from|default:'' }}">
|
||||
<input type="date" name="mark_date_to" id="mark_date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ mark_date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
<h6 class="text-muted mb-2"><i class="bi bi-sliders"></i> Фильтры по параметрам точек</h6>
|
||||
|
||||
@@ -444,13 +452,14 @@
|
||||
<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>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='created_at' label='Создано' current_sort=sort %}
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
|
||||
</th>
|
||||
<th scope="col" style="min-width: 150px;">Дата подтверждения</th>
|
||||
<th scope="col" style="min-width: 150px;">Последний сигнал</th>
|
||||
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -489,32 +498,10 @@
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
<td>{{ source.coords_valid }}</td>
|
||||
<td>{{ source.coords_reference }}</td>
|
||||
<td style="padding: 0.3rem; vertical-align: top;">
|
||||
{% if source.marks %}
|
||||
<div style="font-size: 0.75rem; line-height: 1.3;">
|
||||
{% for mark in source.marks %}
|
||||
<div style="{% if not forloop.last %}border-bottom: 1px solid #dee2e6; padding-bottom: 3px; margin-bottom: 3px;{% endif %}">
|
||||
<div style="margin-bottom: 1px;">
|
||||
{% if mark.mark %}
|
||||
<span class="badge bg-success" style="font-size: 0.7rem;">Есть</span>
|
||||
{% elif mark.mark == False %}
|
||||
<span class="badge bg-danger" style="font-size: 0.7rem;">Нет</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary" style="font-size: 0.7rem;">-</span>
|
||||
{% endif %}
|
||||
<span class="text-muted" style="font-size: 0.7rem;">{{ mark.timestamp|date:"d.m.y H:i" }}</span>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.65rem;">{{ mark.created_by }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}</td>
|
||||
<td>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
{% if source.objitem_count > 0 %}
|
||||
@@ -566,7 +553,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="15" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -1062,6 +1049,47 @@ function updateFilterCounter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Column visibility functions
|
||||
function toggleColumn(checkbox) {
|
||||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||||
const table = document.querySelector('.table');
|
||||
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
|
||||
|
||||
if (checkbox.checked) {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = '';
|
||||
});
|
||||
} else {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllColumns(selectAllCheckbox) {
|
||||
const columnCheckboxes = document.querySelectorAll('.column-toggle');
|
||||
columnCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
toggleColumn(checkbox);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize column visibility - hide Создано and Обновлено columns by default
|
||||
function initColumnVisibility() {
|
||||
const createdAtCheckbox = document.querySelector('input[data-column="11"]');
|
||||
const updatedAtCheckbox = document.querySelector('input[data-column="12"]');
|
||||
|
||||
if (createdAtCheckbox) {
|
||||
createdAtCheckbox.checked = false;
|
||||
toggleColumn(createdAtCheckbox);
|
||||
}
|
||||
|
||||
if (updatedAtCheckbox) {
|
||||
updatedAtCheckbox.checked = false;
|
||||
toggleColumn(updatedAtCheckbox);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Setup select-all checkbox
|
||||
@@ -1092,7 +1120,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setupRadioLikeCheckboxes('has_coords_kupsat');
|
||||
setupRadioLikeCheckboxes('has_coords_valid');
|
||||
setupRadioLikeCheckboxes('has_coords_reference');
|
||||
setupRadioLikeCheckboxes('has_signal_mark');
|
||||
|
||||
// Update filter counter on page load
|
||||
updateFilterCounter();
|
||||
@@ -1122,6 +1149,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
|
||||
// Initialize column visibility
|
||||
setTimeout(initColumnVisibility, 100);
|
||||
});
|
||||
|
||||
// Show source details in modal
|
||||
|
||||
@@ -210,9 +210,9 @@ def _find_or_create_source_by_name_and_distance(
|
||||
- Совпадает имя (source_name)
|
||||
2. Для каждого найденного Source проверяет расстояние до новой координаты
|
||||
3. Если найден Source в радиусе ≤56 км:
|
||||
- Возвращает его и обновляет coords_average инкрементально
|
||||
- Возвращает его и обновляет coords_average через метод update_coords_average
|
||||
4. Если не найден подходящий Source:
|
||||
- Создает новый Source
|
||||
- Создает новый Source с типом "стационарные"
|
||||
|
||||
Важно: Может существовать несколько Source с одинаковым именем и спутником,
|
||||
но они должны быть географически разделены (>56 км друг от друга).
|
||||
@@ -232,7 +232,7 @@ def _find_or_create_source_by_name_and_distance(
|
||||
parameter_obj__id_satellite=sat,
|
||||
source__isnull=False,
|
||||
source__coords_average__isnull=False
|
||||
).select_related('source', 'parameter_obj')
|
||||
).select_related('source', 'parameter_obj', 'source__info')
|
||||
|
||||
# Собираем уникальные Source из найденных ObjItem
|
||||
existing_sources = {}
|
||||
@@ -243,28 +243,30 @@ def _find_or_create_source_by_name_and_distance(
|
||||
# Проверяем расстояние до каждого существующего Source
|
||||
closest_source = None
|
||||
min_distance = float('inf')
|
||||
best_new_avg = None
|
||||
|
||||
for source in existing_sources.values():
|
||||
if source.coords_average:
|
||||
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||
new_avg, distance = calculate_mean_coords(source_coord, coord)
|
||||
_, distance = calculate_mean_coords(source_coord, coord)
|
||||
|
||||
if distance <= RANGE_DISTANCE and distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_source = source
|
||||
best_new_avg = new_avg
|
||||
|
||||
# Если найден близкий Source (≤56 км)
|
||||
if closest_source:
|
||||
# Обновляем coords_average инкрементально
|
||||
closest_source.coords_average = Point(best_new_avg, srid=4326)
|
||||
# Обновляем coords_average через метод модели
|
||||
closest_source.update_coords_average(coord)
|
||||
closest_source.save()
|
||||
return closest_source
|
||||
|
||||
# Если не найден подходящий Source - создаем новый
|
||||
# Если не найден подходящий Source - создаем новый с типом "Стационарные"
|
||||
from .models import ObjectInfo
|
||||
stationary_info, _ = ObjectInfo.objects.get_or_create(name="Стационарные")
|
||||
|
||||
source = Source.objects.create(
|
||||
coords_average=Point(coord, srid=4326),
|
||||
info=stationary_info,
|
||||
created_by=user
|
||||
)
|
||||
return source
|
||||
@@ -306,7 +308,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
|
||||
consts = get_all_constants()
|
||||
df.fillna(-1, inplace=True)
|
||||
|
||||
df.sort_values('Дата')
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
new_sources_count = 0
|
||||
added_count = 0
|
||||
@@ -324,7 +326,6 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
# Извлекаем имя источника
|
||||
source_name = row["Объект наблюдения"]
|
||||
|
||||
# Проверяем кэш: ищем подходящий Source среди закэшированных
|
||||
found_in_cache = False
|
||||
for cache_key, cached_source in sources_cache.items():
|
||||
cached_name, cached_id = cache_key
|
||||
@@ -336,11 +337,11 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.coords_average = Point(new_avg, srid=4326)
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
@@ -500,6 +501,10 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
geo.objitem = obj_item
|
||||
geo.save()
|
||||
|
||||
# Обновляем дату подтверждения источника
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
|
||||
|
||||
def add_satellite_list():
|
||||
sats = [
|
||||
@@ -624,7 +629,7 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
.astype(float)
|
||||
)
|
||||
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
|
||||
|
||||
df.sort_values('time')
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
new_sources_count = 0
|
||||
added_count = 0
|
||||
@@ -665,11 +670,11 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.coords_average = Point(new_avg, srid=4326)
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
@@ -835,6 +840,10 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
geo_obj.objitem = obj_item
|
||||
geo_obj.save()
|
||||
|
||||
# Обновляем дату подтверждения источника
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
|
||||
|
||||
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
filename = file.name.split("_")
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
from mainapp.models import Source, ObjectMark, CustomUser, Satellite
|
||||
|
||||
|
||||
class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
@@ -27,25 +27,35 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
"""Получить queryset с предзагруженными связанными данными"""
|
||||
from django.db.models import Count, Max
|
||||
from django.db.models import Count, Max, Min
|
||||
|
||||
# Проверяем, выбран ли спутник
|
||||
satellite_id = self.request.GET.get('satellite_id')
|
||||
if not satellite_id:
|
||||
# Если спутник не выбран, возвращаем пустой queryset
|
||||
return Source.objects.none()
|
||||
|
||||
queryset = Source.objects.prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
'source_objitems__parameter_obj__polarization',
|
||||
'source_objitems__parameter_obj__modulation',
|
||||
Prefetch(
|
||||
'marks',
|
||||
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')
|
||||
)
|
||||
).annotate(
|
||||
mark_count=Count('marks'),
|
||||
last_mark_date=Max('marks__timestamp')
|
||||
last_mark_date=Max('marks__timestamp'),
|
||||
# Аннотации для сортировки по параметрам (берем минимальное значение из связанных объектов)
|
||||
min_frequency=Min('source_objitems__parameter_obj__frequency'),
|
||||
min_freq_range=Min('source_objitems__parameter_obj__freq_range'),
|
||||
min_bod_velocity=Min('source_objitems__parameter_obj__bod_velocity')
|
||||
)
|
||||
|
||||
# Фильтрация по спутникам (мультивыбор)
|
||||
satellite_ids = self.request.GET.getlist('satellite_id')
|
||||
if satellite_ids:
|
||||
queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id__in=satellite_ids).distinct()
|
||||
# Фильтрация по выбранному спутнику (обязательно)
|
||||
queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id=satellite_id).distinct()
|
||||
|
||||
# Фильтрация по статусу (есть/нет отметок)
|
||||
mark_status = self.request.GET.get('mark_status')
|
||||
@@ -87,7 +97,16 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
|
||||
# Сортировка
|
||||
sort = self.request.GET.get('sort', '-id')
|
||||
allowed_sorts = ['id', '-id', 'created_at', '-created_at', 'last_mark_date', '-last_mark_date', 'mark_count', '-mark_count']
|
||||
allowed_sorts = [
|
||||
'id', '-id',
|
||||
'created_at', '-created_at',
|
||||
'last_mark_date', '-last_mark_date',
|
||||
'mark_count', '-mark_count',
|
||||
'frequency', '-frequency',
|
||||
'freq_range', '-freq_range',
|
||||
'bod_velocity', '-bod_velocity'
|
||||
]
|
||||
|
||||
if sort in allowed_sorts:
|
||||
# Для сортировки по last_mark_date нужно обработать NULL значения
|
||||
if 'last_mark_date' in sort:
|
||||
@@ -96,6 +115,21 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
queryset = queryset.order_by(
|
||||
Coalesce(F('last_mark_date'), F('created_at')).desc() if sort.startswith('-') else Coalesce(F('last_mark_date'), F('created_at')).asc()
|
||||
)
|
||||
# Сортировка по частоте
|
||||
elif sort == 'frequency':
|
||||
queryset = queryset.order_by('min_frequency')
|
||||
elif sort == '-frequency':
|
||||
queryset = queryset.order_by('-min_frequency')
|
||||
# Сортировка по полосе
|
||||
elif sort == 'freq_range':
|
||||
queryset = queryset.order_by('min_freq_range')
|
||||
elif sort == '-freq_range':
|
||||
queryset = queryset.order_by('-min_freq_range')
|
||||
# Сортировка по бодовой скорости
|
||||
elif sort == 'bod_velocity':
|
||||
queryset = queryset.order_by('min_bod_velocity')
|
||||
elif sort == '-bod_velocity':
|
||||
queryset = queryset.order_by('-min_bod_velocity')
|
||||
else:
|
||||
queryset = queryset.order_by(sort)
|
||||
else:
|
||||
@@ -106,14 +140,17 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Добавить дополнительные данные в контекст"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
from mainapp.models import Satellite
|
||||
from mainapp.utils import parse_pagination_params
|
||||
|
||||
# Данные для фильтров - только спутники, у которых есть источники
|
||||
# Все спутники для выбора
|
||||
context['satellites'] = Satellite.objects.filter(
|
||||
parameters__objitem__source__isnull=False
|
||||
).distinct().order_by('name')
|
||||
|
||||
# Выбранный спутник
|
||||
satellite_id = self.request.GET.get('satellite_id')
|
||||
context['selected_satellite_id'] = int(satellite_id) if satellite_id and satellite_id.isdigit() else None
|
||||
|
||||
context['users'] = CustomUser.objects.select_related('user').filter(
|
||||
marks_created__isnull=False
|
||||
).distinct().order_by('user__username')
|
||||
@@ -121,26 +158,56 @@ class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
# Параметры пагинации
|
||||
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
|
||||
context['items_per_page'] = items_per_page
|
||||
context['available_items_per_page'] = [25, 50, 100, 200]
|
||||
context['available_items_per_page'] = [25, 50, 100, 200, 500]
|
||||
|
||||
# Параметры поиска и сортировки
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
context['sort'] = self.request.GET.get('sort', '-id')
|
||||
|
||||
# Параметры фильтров для отображения в UI (мультивыбор)
|
||||
context['selected_satellites'] = [int(x) for x in self.request.GET.getlist('satellite_id') if x.isdigit()]
|
||||
# Параметры фильтров для отображения в UI
|
||||
context['selected_users'] = [int(x) for x in self.request.GET.getlist('user_id') if x.isdigit()]
|
||||
context['filter_mark_status'] = self.request.GET.get('mark_status', '')
|
||||
context['filter_date_from'] = self.request.GET.get('date_from', '')
|
||||
context['filter_date_to'] = self.request.GET.get('date_to', '')
|
||||
|
||||
# Добавить информацию о возможности редактирования для каждой отметки
|
||||
# и получить имя первого объекта для каждого источника
|
||||
for source in context['sources']:
|
||||
# Получить имя первого объекта
|
||||
first_objitem = source.source_objitems.first()
|
||||
source.objitem_name = first_objitem.name if first_objitem and first_objitem.name else '-'
|
||||
# Полноэкранный режим
|
||||
context['full_width_page'] = True
|
||||
|
||||
# Добавить информацию о параметрах для каждого источника
|
||||
for source in context['sources']:
|
||||
# Получить первый объект для параметров (они должны быть одинаковыми)
|
||||
first_objitem = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation'
|
||||
).first()
|
||||
|
||||
if first_objitem:
|
||||
source.objitem_name = first_objitem.name if first_objitem.name else '-'
|
||||
|
||||
# Получить параметры
|
||||
if first_objitem.parameter_obj:
|
||||
param = first_objitem.parameter_obj
|
||||
source.frequency = param.frequency if param.frequency else '-'
|
||||
source.freq_range = param.freq_range if param.freq_range else '-'
|
||||
source.polarization = param.polarization.name if param.polarization else '-'
|
||||
source.modulation = param.modulation.name if param.modulation else '-'
|
||||
source.bod_velocity = param.bod_velocity if param.bod_velocity else '-'
|
||||
else:
|
||||
source.frequency = '-'
|
||||
source.freq_range = '-'
|
||||
source.polarization = '-'
|
||||
source.modulation = '-'
|
||||
source.bod_velocity = '-'
|
||||
else:
|
||||
source.objitem_name = '-'
|
||||
source.frequency = '-'
|
||||
source.freq_range = '-'
|
||||
source.polarization = '-'
|
||||
source.modulation = '-'
|
||||
source.bod_velocity = '-'
|
||||
|
||||
# Проверка возможности редактирования отметок
|
||||
for mark in source.marks.all():
|
||||
mark.editable = mark.can_edit()
|
||||
|
||||
@@ -187,6 +254,10 @@ class AddObjectMarkView(LoginRequiredMixin, View):
|
||||
created_by=custom_user
|
||||
)
|
||||
|
||||
# Обновляем дату последнего сигнала источника
|
||||
source.update_last_signal_at()
|
||||
source.save()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'mark': {
|
||||
@@ -225,6 +296,10 @@ class UpdateObjectMarkView(LoginRequiredMixin, View):
|
||||
object_mark.mark = new_mark_value
|
||||
object_mark.save()
|
||||
|
||||
# Обновляем дату последнего сигнала источника
|
||||
object_mark.source.update_last_signal_at()
|
||||
object_mark.source.save()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'mark': {
|
||||
|
||||
@@ -666,6 +666,8 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'satellite_id': first_satellite_id,
|
||||
'created_at': source.created_at,
|
||||
'updated_at': source.updated_at,
|
||||
'confirm_at': source.confirm_at,
|
||||
'last_signal_at': source.last_signal_at,
|
||||
'has_lyngsat': has_lyngsat,
|
||||
'lyngsat_id': lyngsat_id,
|
||||
'marks': marks_data,
|
||||
|
||||
Reference in New Issue
Block a user