Привязка данных LyngSat

This commit is contained in:
2025-11-11 22:40:52 +03:00
parent a3c381b9c7
commit 5e94086bf0
9 changed files with 455 additions and 36 deletions

View File

@@ -25,7 +25,6 @@ from .models import (
Standard, Standard,
SigmaParMark, SigmaParMark,
SigmaParameter, SigmaParameter,
SourceType,
Parameter, Parameter,
Satellite, Satellite,
Mirror, Mirror,
@@ -336,14 +335,6 @@ class ModulationAdmin(BaseAdmin):
ordering = ("name",) ordering = ("name",)
@admin.register(SourceType)
class SourceTypeAdmin(BaseAdmin):
"""Админ-панель для модели SourceType."""
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@admin.register(Standard) @admin.register(Standard)
class StandardAdmin(BaseAdmin): class StandardAdmin(BaseAdmin):
"""Админ-панель для модели Standard.""" """Админ-панель для модели Standard."""

View File

@@ -161,6 +161,32 @@ class FillLyngsatDataForm(forms.Form):
}), }),
help_text="Игнорировать кеш и получить свежие данные с сайта" help_text="Игнорировать кеш и получить свежие данные с сайта"
) )
class LinkLyngsatForm(forms.Form):
"""Форма для привязки источников LyngSat к объектам"""
satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '10'
}),
required=False,
help_text="Оставьте пустым для обработки всех спутников"
)
frequency_tolerance = forms.FloatField(
label="Допуск по частоте (МГц)",
initial=0.5,
min_value=0,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.1'
}),
help_text="Допустимое отклонение частоты при сравнении"
)
class ParameterForm(forms.ModelForm): class ParameterForm(forms.ModelForm):
""" """
Форма для создания и редактирования параметров ВЧ загрузки. Форма для создания и редактирования параметров ВЧ загрузки.

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2025-11-11 19:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyngsatapp', '0002_alter_lyngsat_last_update'),
('mainapp', '0008_remove_sourcetype_objitem_objitem_source_type_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='objitem',
name='source_type_id',
),
migrations.AddField(
model_name='objitem',
name='lyngsat_source',
field=models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat'),
),
migrations.DeleteModel(
name='SourceType',
),
]

View File

@@ -233,27 +233,7 @@ class Satellite(models.Model):
ordering = ["name"] ordering = ["name"]
class SourceType(models.Model):
"""
Модель типа источника сигнала.
Классифицирует источники по типам (наземный, морской, воздушный и т.д.).
"""
# Основные поля
name = models.CharField(
max_length=50,
unique=True,
verbose_name="Тип источника",
db_index=True,
help_text="Тип источника сигнала",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Тип источника"
verbose_name_plural = "Типы источников"
ordering = ["name"]
class ObjItemQuerySet(models.QuerySet): class ObjItemQuerySet(models.QuerySet):
"""Custom QuerySet для модели ObjItem с оптимизированными запросами""" """Custom QuerySet для модели ObjItem с оптимизированными запросами"""
@@ -264,7 +244,7 @@ class ObjItemQuerySet(models.QuerySet):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"source_type_obj", "lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
@@ -349,14 +329,14 @@ class ObjItem(models.Model):
verbose_name="Изменен пользователем", verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись", help_text="Пользователь, последним изменивший запись",
) )
source_type_id = models.ForeignKey( lyngsat_source = models.ForeignKey(
SourceType, "lyngsatapp.LyngSat",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="objitems_sourcetype", related_name="objitems",
null=True, null=True,
blank=True, blank=True,
verbose_name="Тип источника", verbose_name="Источник LyngSat",
help_text="Тип источника сигнала", help_text="Связанный источник из базы LyngSat (ТВ)",
) )
# Custom manager # Custom manager

View File

@@ -184,6 +184,27 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Link LyngSat Sources Card -->
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-link-45deg text-primary" viewBox="0 0 16 16">
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
</svg>
</div>
<h3 class="card-title mb-0">Привязка источников LyngSat</h3>
</div>
<p class="card-text">Автоматическая привязка источников из базы LyngSat к объектам по частоте и поляризации. Объекты с привязанными источниками отображаются как "ТВ".</p>
<a href="{% url 'mainapp:link_lyngsat' %}" class="btn btn-primary">
Привязать источники
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,90 @@
{% extends 'mainapp/base.html' %}
{% block title %}Привязка источников LyngSat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<i class="bi bi-link-45deg"></i> Привязка источников LyngSat к объектам
</h3>
</div>
<div class="card-body">
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i>
<strong>Информация:</strong> Эта функция автоматически привязывает источники из базы LyngSat к объектам
на основе совпадения частоты (с округлением) и поляризации. Объекты с привязанными источниками LyngSat
будут отмечены как "ТВ" в списке объектов.
</div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label">
{{ form.satellites.label }}
</label>
{{ form.satellites }}
{% if form.satellites.help_text %}
<div class="form-text">{{ form.satellites.help_text }}</div>
{% endif %}
{% if form.satellites.errors %}
<div class="invalid-feedback d-block">
{{ form.satellites.errors }}
</div>
{% endif %}
</div>
<div class="mb-4">
<label for="{{ form.frequency_tolerance.id_for_label }}" class="form-label">
{{ form.frequency_tolerance.label }}
</label>
{{ form.frequency_tolerance }}
{% if form.frequency_tolerance.help_text %}
<div class="form-text">{{ form.frequency_tolerance.help_text }}</div>
{% endif %}
{% if form.frequency_tolerance.errors %}
<div class="invalid-feedback d-block">
{{ form.frequency_tolerance.errors }}
</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-link-45deg"></i> Привязать источники
</button>
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Назад к действиям
</a>
</div>
</form>
</div>
</div>
<!-- Help section -->
<div class="card mt-4 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="bi bi-question-circle"></i> Как это работает?
</h5>
</div>
<div class="card-body">
<ol class="mb-0">
<li>Система округляет частоту каждого объекта до целого числа</li>
<li>Ищет источники LyngSat с той же поляризацией и близкой частотой (в пределах допуска)</li>
<li>При нахождении совпадения создается связь между объектом и источником LyngSat</li>
<li>Объекты с привязанными источниками отображаются как "ТВ" в списке</li>
</ol>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -251,6 +251,12 @@
onchange="toggleColumn(this)"> Стандарт onchange="toggleColumn(this)"> Стандарт
</label> </label>
</li> </li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="24" checked
onchange="toggleColumn(this)"> Тип источника
</label>
</li>
</ul> </ul>
</div> </div>
@@ -463,6 +469,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 %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -495,10 +502,19 @@
<td>{{ item.comment }}</td> <td>{{ item.comment }}</td>
<td>{{ item.is_average }}</td> <td>{{ item.is_average }}</td>
<td>{{ item.standard }}</td> <td>{{ item.standard }}</td>
<td>
{% if item.obj.lyngsat_source %}
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
<i class="bi bi-tv"></i> ТВ
</a>
{% else %}
-
{% endif %}
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="22" class="text-center py-4"> <td colspan="25" class="text-center py-4">
{% if selected_satellite_id %} {% if selected_satellite_id %}
Нет данных для выбранных фильтров Нет данных для выбранных фильтров
{% else %} {% else %}
@@ -1165,4 +1181,160 @@
<!-- Include the selected items offcanvas component --> <!-- Include the selected items offcanvas component -->
{% include 'mainapp/components/_selected_items_offcanvas.html' %} {% include 'mainapp/components/_selected_items_offcanvas.html' %}
<!-- LyngSat Data Modal -->
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
function showLyngsatModal(lyngsatId) {
// Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
// Показываем индикатор загрузки
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
// Загружаем данные
fetch(`/api/lyngsat/${lyngsatId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
// Формируем HTML с данными
let html = `
<div class="container-fluid">
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Спутник:</td>
<td><strong>${data.satellite}</strong></td>
</tr>
<tr>
<td class="text-muted">Частота:</td>
<td><strong>${data.frequency} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Поляризация:</td>
<td><span class="badge bg-info">${data.polarization}</span></td>
</tr>
<tr>
<td class="text-muted">Канал:</td>
<td>${data.channel_info}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-gear"></i> Технические параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Модуляция:</td>
<td><span class="badge bg-secondary">${data.modulation}</span></td>
</tr>
<tr>
<td class="text-muted">Стандарт:</td>
<td><span class="badge bg-secondary">${data.standard}</span></td>
</tr>
<tr>
<td class="text-muted">Сим. скорость:</td>
<td><strong>${data.sym_velocity} БОД</strong></td>
</tr>
<tr>
<td class="text-muted">FEC:</td>
<td>${data.fec}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p class="mb-2">
<span class="text-muted">Последнее обновление:</span><br>
<strong>${data.last_update}</strong>
</p>
</div>
<div class="col-md-6">
${data.url ? `
<p class="mb-2">
<span class="text-muted">Ссылка на источник:</span><br>
<a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-link-45deg"></i> Открыть на LyngSat
</a>
</p>
` : ''}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</div>
`;
});
}
</script>
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,8 @@ urlpatterns = [
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'), path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'), path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
from io import BytesIO from io import BytesIO
# Django imports # Django imports
from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth import logout from django.contrib.auth import logout
@@ -38,6 +39,7 @@ from .forms import (
UploadVchLoad, UploadVchLoad,
VchLinkForm, VchLinkForm,
FillLyngsatDataForm, FillLyngsatDataForm,
LinkLyngsatForm,
) )
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
from .models import Geo, Modulation, ObjItem, Polarization, Satellite from .models import Geo, Modulation, ObjItem, Polarization, Satellite
@@ -375,6 +377,108 @@ class LinkVchSigmaView(LoginRequiredMixin, FormView):
return self.render_to_response(self.get_context_data(form=form)) return self.render_to_response(self.get_context_data(form=form))
class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
"""Представление для привязки источников LyngSat к объектам"""
template_name = "mainapp/link_lyngsat.html"
form_class = LinkLyngsatForm
success_message = "Привязка источников LyngSat завершена"
error_message = "Ошибка при привязке источников"
def form_valid(self, form):
from lyngsatapp.models import LyngSat
satellites = form.cleaned_data.get("satellites")
frequency_tolerance = form.cleaned_data.get("frequency_tolerance", 0.5)
# Если спутники не выбраны, обрабатываем все
if satellites:
objitems = ObjItem.objects.filter(
parameter_obj__id_satellite__in=satellites
).select_related('parameter_obj', 'parameter_obj__polarization')
else:
objitems = ObjItem.objects.filter(
parameter_obj__isnull=False
).select_related('parameter_obj', 'parameter_obj__polarization')
linked_count = 0
total_count = objitems.count()
for objitem in objitems:
if not hasattr(objitem, 'parameter_obj') or not objitem.parameter_obj:
continue
param = objitem.parameter_obj
# Округляем частоту объекта
if param.frequency:
rounded_freq = round(param.frequency, 0) # Округление до целого
# Ищем подходящий источник LyngSat
# Сравниваем по округленной частоте и поляризации
lyngsat_sources = LyngSat.objects.filter(
id_satellite=param.id_satellite,
polarization=param.polarization,
frequency__gte=rounded_freq - frequency_tolerance,
frequency__lte=rounded_freq + frequency_tolerance
).order_by('frequency')
if lyngsat_sources.exists():
# Берем первый подходящий источник
objitem.lyngsat_source = lyngsat_sources.first()
objitem.save(update_fields=['lyngsat_source'])
linked_count += 1
messages.success(
self.request,
f"Привязано {linked_count} из {total_count} объектов к источникам LyngSat"
)
return redirect("mainapp:link_lyngsat")
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
class LyngsatDataAPIView(LoginRequiredMixin, View):
"""API для получения данных LyngSat источника"""
def get(self, request, lyngsat_id):
from lyngsatapp.models import LyngSat
try:
lyngsat = LyngSat.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).get(id=lyngsat_id)
# Форматируем дату с учетом локального времени
last_update_str = '-'
if lyngsat.last_update:
local_time = timezone.localtime(lyngsat.last_update)
last_update_str = local_time.strftime("%d.%m.%Y")
data = {
'id': lyngsat.id,
'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-',
'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
'standard': lyngsat.standard.name if lyngsat.standard else '-',
'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-',
'fec': lyngsat.fec or '-',
'channel_info': lyngsat.channel_info or '-',
'last_update': last_update_str,
'url': lyngsat.url or None,
}
return JsonResponse(data)
except LyngSat.DoesNotExist:
return JsonResponse({'error': 'Источник LyngSat не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView): class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/process_kubsat.html" template_name = "mainapp/process_kubsat.html"
form_class = NewEventForm form_class = NewEventForm
@@ -474,6 +578,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
@@ -487,6 +592,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
@@ -763,6 +869,9 @@ class ObjItemListView(LoginRequiredMixin, View):
comment = obj.geo_obj.comment or "-" comment = obj.geo_obj.comment or "-"
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-" is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
# Check if LyngSat source is linked
source_type = "ТВ" if obj.lyngsat_source else "-"
processed_objects.append( processed_objects.append(
{ {
"id": obj.id, "id": obj.id,
@@ -785,6 +894,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"updated_by": obj.updated_by if obj.updated_by else "-", "updated_by": obj.updated_by if obj.updated_by else "-",
"comment": comment, "comment": comment,
"is_average": is_average, "is_average": is_average,
"source_type": source_type,
"standard": standard_name, "standard": standard_name,
"obj": obj, "obj": obj,
} }