@@ -639,6 +722,7 @@ document.addEventListener('DOMContentLoaded', function() {
setupRadioLikeCheckboxes('has_coords_kupsat');
setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference');
+ setupRadioLikeCheckboxes('has_lyngsat');
// Update filter counter on page load
updateFilterCounter();
@@ -686,7 +770,7 @@ function showSourceDetails(sourceId) {
modal.show();
// Fetch data from API
- fetch(`/api/source/${sourceId}/objitems/`)
+ fetch('/api/source/' + sourceId + '/objitems/')
.then(response => {
if (!response.ok) {
if (response.status === 404) {
@@ -712,28 +796,70 @@ function showSourceDetails(sourceId) {
data.objitems.forEach(objitem => {
const row = document.createElement('tr');
- row.innerHTML = `
-
-
-
- ${objitem.id}
- ${objitem.name}
- ${objitem.satellite_name}
- ${objitem.frequency}
- ${objitem.freq_range}
- ${objitem.polarization}
- ${objitem.bod_velocity}
- ${objitem.modulation}
- ${objitem.snr}
- ${objitem.geo_timestamp}
- ${objitem.geo_location}
- ${objitem.geo_coords}
- `;
+
+ // Build transponder cell
+ let transponderCell = '-';
+ if (objitem.has_transponder) {
+ transponderCell = '' +
+ ' ' + objitem.transponder_info +
+ ' ';
+ }
+
+ // Build LyngSat cell
+ let lyngsatCell = '-';
+ if (objitem.has_lyngsat) {
+ lyngsatCell = '' +
+ ' ТВ' +
+ ' ';
+ }
+
+ // Build Sigma cell
+ let sigmaCell = '-';
+ if (objitem.has_sigma) {
+ sigmaCell = '' +
+ ' ' + objitem.sigma_info +
+ ' ';
+ }
+
+ row.innerHTML = '' +
+ ' ' +
+ ' ' +
+ '' + objitem.id + ' ' +
+ '' + objitem.name + ' ' +
+ '' + objitem.satellite_name + ' ' +
+ '' + transponderCell + ' ' +
+ '' + objitem.frequency + ' ' +
+ '' + objitem.freq_range + ' ' +
+ '' + objitem.polarization + ' ' +
+ '' + objitem.bod_velocity + ' ' +
+ '' + objitem.modulation + ' ' +
+ '' + objitem.snr + ' ' +
+ '' + objitem.geo_timestamp + ' ' +
+ '' + objitem.geo_location + ' ' +
+ '' + objitem.geo_coords + ' ' +
+ '' + objitem.updated_at + ' ' +
+ '' + objitem.updated_by + ' ' +
+ '' + objitem.created_at + ' ' +
+ '' + objitem.created_by + ' ' +
+ '' + objitem.comment + ' ' +
+ '' + objitem.is_average + ' ' +
+ '' + objitem.standard + ' ' +
+ '' + lyngsatCell + ' ' +
+ '' + sigmaCell + ' ' +
+ '' + objitem.mirrors + ' ';
tbody.appendChild(row);
});
// Setup modal select-all checkbox
setupModalSelectAll();
+
+ // Initialize column visibility
+ initModalColumnVisibility();
} else {
// Show no data message
document.getElementById('modalNoData').style.display = 'block';
@@ -776,5 +902,196 @@ function setupModalSelectAll() {
});
}
}
+
+// Function to toggle modal column visibility
+function toggleModalColumn(checkbox) {
+ const columnIndex = parseInt(checkbox.getAttribute('data-column'));
+ const modal = document.getElementById('sourceDetailsModal');
+ const table = modal.querySelector('.table');
+ if (!table) return;
+
+ 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 to toggle all modal columns
+function toggleAllModalColumns(selectAllCheckbox) {
+ const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
+ columnCheckboxes.forEach(checkbox => {
+ checkbox.checked = selectAllCheckbox.checked;
+ toggleModalColumn(checkbox);
+ });
+}
+
+// Initialize modal column visibility
+function initModalColumnVisibility() {
+ // Hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22)
+ const columnsToHide = [16, 17, 18, 19, 20, 22];
+ columnsToHide.forEach(columnIndex => {
+ const checkbox = document.querySelector('.modal-column-toggle[data-column="' + columnIndex + '"]');
+ if (checkbox && !checkbox.checked) {
+ toggleModalColumn(checkbox);
+ }
+ });
+}
+
+// Function to show LyngSat modal
+function showLyngsatModal(lyngsatId) {
+ const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
+ modal.show();
+
+ const modalBody = document.getElementById('lyngsatModalBody');
+ modalBody.innerHTML = '';
+
+ fetch('/api/lyngsat/' + lyngsatId + '/')
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Ошибка загрузки данных');
+ }
+ return response.json();
+ })
+ .then(data => {
+ let html = '' +
+ '
' +
+ '' +
+ '
' +
+ 'Спутник: ' + data.satellite + ' ' +
+ 'Частота: ' + data.frequency + ' МГц ' +
+ 'Поляризация: ' + data.polarization + ' ' +
+ 'Канал: ' + data.channel_info + ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ 'Модуляция: ' + data.modulation + ' ' +
+ 'Стандарт: ' + data.standard + ' ' +
+ 'Сим. скорость: ' + data.sym_velocity + ' БОД ' +
+ 'FEC: ' + data.fec + ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
Последнее обновление: ' + data.last_update + '
' +
+ '
';
+ modalBody.innerHTML = html;
+ })
+ .catch(error => {
+ modalBody.innerHTML = '' +
+ ' ' + error.message + '
';
+ });
+}
+
+// Function to show transponder modal
+function showTransponderModal(transponderId) {
+ const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
+ modal.show();
+
+ const modalBody = document.getElementById('transponderModalBody');
+ modalBody.innerHTML = '';
+
+ fetch('/api/transponder/' + transponderId + '/')
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Ошибка загрузки данных транспондера');
+ }
+ return response.json();
+ })
+ .then(data => {
+ let html = '' +
+ '
' +
+ '' +
+ '
' +
+ 'Название: ' + (data.name || '-') + ' ' +
+ 'Спутник: ' + data.satellite + ' ' +
+ 'Зона покрытия: ' + (data.zone_name || '-') + ' ' +
+ 'Поляризация: ' + data.polarization + ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ 'Downlink: ' + data.downlink + ' МГц ' +
+ 'Uplink: ' + (data.uplink || '-') + (data.uplink ? ' МГц' : '') + ' ' +
+ 'Полоса: ' + data.frequency_range + ' МГц ' +
+ 'Перенос: ' + (data.transfer || '-') + (data.transfer ? ' МГц' : '') + ' ' +
+ 'ОСШ: ' + (data.snr || '-') + (data.snr ? ' дБ' : '') + ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
Дата создания: ' + (data.created_at || '-') + '
' +
+ '
Создан пользователем: ' + (data.created_by || '-') + '
' +
+ '
';
+ modalBody.innerHTML = html;
+ })
+ .catch(error => {
+ modalBody.innerHTML = '' +
+ ' ' + error.message + '
';
+ });
+}
+
+
+
+
+
+
+
+
+{% include 'mainapp/components/_sigma_parameter_modal.html' %}
+
{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/unlink_lyngsat_confirm.html b/dbapp/mainapp/templates/mainapp/unlink_lyngsat_confirm.html
new file mode 100644
index 0000000..3719688
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/unlink_lyngsat_confirm.html
@@ -0,0 +1,79 @@
+{% extends 'mainapp/base.html' %}
+
+{% block title %}Подтверждение отвязки LyngSat{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+ Внимание!
+
+
+ Вы собираетесь отвязать все источники LyngSat от объектов.
+
+
+
+
+
Информация:
+
+
+ Объектов с привязанными источниками LyngSat:
+ {{ linked_count }}
+
+
+
+
+ {% if linked_count > 0 %}
+
+
+
+ После отвязки все объекты перестанут отображаться как "ТВ" источники.
+ Вы сможете заново привязать источники через форму привязки LyngSat.
+
+
+
+
+ {% else %}
+
+
+
+ Нет объектов с привязанными источниками LyngSat. Отвязка не требуется.
+
+
+
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index d8b28b1..21dbf11 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -39,6 +39,7 @@ from .views import (
TransponderListView,
TransponderCreateView,
TransponderUpdateView,
+ UnlinkAllLyngsatSourcesView,
UploadVchLoadView,
custom_logout,
)
@@ -85,5 +86,6 @@ urlpatterns = [
path('lyngsat-task-status//', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('api/lyngsat-task-status//', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
+ path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
path('logout/', custom_logout, name='logout'),
]
\ No newline at end of file
diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py
index e330b7e..e049016 100644
--- a/dbapp/mainapp/views/__init__.py
+++ b/dbapp/mainapp/views/__init__.py
@@ -30,6 +30,7 @@ from .lyngsat import (
FillLyngsatDataView,
LyngsatTaskStatusView,
ClearLyngsatCacheView,
+ UnlinkAllLyngsatSourcesView,
)
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
from .transponder import (
@@ -78,6 +79,7 @@ __all__ = [
'FillLyngsatDataView',
'LyngsatTaskStatusView',
'ClearLyngsatCacheView',
+ 'UnlinkAllLyngsatSourcesView',
# Source
'SourceListView',
'SourceUpdateView',
diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py
index 4dc4f3c..d067230 100644
--- a/dbapp/mainapp/views/api.py
+++ b/dbapp/mainapp/views/api.py
@@ -186,7 +186,13 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
- 'source_objitems__geo_obj'
+ 'source_objitems__parameter_obj__standard',
+ 'source_objitems__geo_obj',
+ 'source_objitems__geo_obj__mirrors',
+ 'source_objitems__lyngsat_source',
+ 'source_objitems__transponder',
+ 'source_objitems__created_by__user',
+ 'source_objitems__updated_by__user'
).get(id=source_id)
# Get all related ObjItems, sorted by created_at
@@ -202,9 +208,12 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
polarization = '-'
bod_velocity = '-'
modulation = '-'
+ standard = '-'
snr = '-'
+ parameter_id = None
if param:
+ parameter_id = param.id
if hasattr(param, 'id_satellite') and param.id_satellite:
satellite_name = param.id_satellite.name
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
@@ -214,6 +223,8 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
if hasattr(param, 'modulation') and param.modulation:
modulation = param.modulation.name
+ if hasattr(param, 'standard') and param.standard:
+ standard = param.standard.name
snr = f"{param.snr:.0f}" if param.snr is not None else '-'
# Get geo data
@@ -235,6 +246,56 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}"
+ # Get created/updated info
+ created_at = '-'
+ if objitem.created_at:
+ local_time = timezone.localtime(objitem.created_at)
+ created_at = local_time.strftime("%d.%m.%Y %H:%M")
+
+ updated_at = '-'
+ if objitem.updated_at:
+ local_time = timezone.localtime(objitem.updated_at)
+ updated_at = local_time.strftime("%d.%m.%Y %H:%M")
+
+ created_by = str(objitem.created_by) if objitem.created_by else '-'
+ updated_by = str(objitem.updated_by) if objitem.updated_by else '-'
+
+ # Check for LyngSat
+ has_lyngsat = hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source is not None
+ lyngsat_id = objitem.lyngsat_source.id if has_lyngsat else None
+
+ # Check for Transponder
+ has_transponder = hasattr(objitem, 'transponder') and objitem.transponder is not None
+ transponder_id = objitem.transponder.id if has_transponder else None
+ transponder_info = '-'
+ if has_transponder:
+ try:
+ downlink = objitem.transponder.downlink if objitem.transponder.downlink else '-'
+ freq_range_t = objitem.transponder.frequency_range if objitem.transponder.frequency_range else '-'
+ transponder_info = f"{downlink}:{freq_range_t}"
+ except Exception:
+ transponder_info = '-'
+
+ # Check for Sigma
+ has_sigma = False
+ sigma_info = '-'
+ if param and hasattr(param, 'sigma_parameter'):
+ sigma_count = param.sigma_parameter.count()
+ if sigma_count > 0:
+ has_sigma = True
+ sigma_info = f"{sigma_count}"
+
+ # Get comment, is_average, and mirrors from geo_obj
+ comment = '-'
+ is_average = '-'
+ mirrors = '-'
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
+ comment = objitem.geo_obj.comment or '-'
+ is_average = 'Да' if objitem.geo_obj.is_average else 'Нет'
+ # Get mirrors list
+ mirrors_list = list(objitem.geo_obj.mirrors.values_list('name', flat=True))
+ mirrors = ', '.join(mirrors_list) if mirrors_list else '-'
+
objitems_data.append({
'id': objitem.id,
'name': objitem.name or '-',
@@ -244,10 +305,26 @@ class SourceObjItemsAPIView(LoginRequiredMixin, View):
'polarization': polarization,
'bod_velocity': bod_velocity,
'modulation': modulation,
+ 'standard': standard,
'snr': snr,
'geo_timestamp': geo_timestamp,
'geo_location': geo_location,
- 'geo_coords': geo_coords
+ 'geo_coords': geo_coords,
+ 'created_at': created_at,
+ 'updated_at': updated_at,
+ 'created_by': created_by,
+ 'updated_by': updated_by,
+ 'comment': comment,
+ 'is_average': is_average,
+ 'has_lyngsat': has_lyngsat,
+ 'lyngsat_id': lyngsat_id,
+ 'has_transponder': has_transponder,
+ 'transponder_id': transponder_id,
+ 'transponder_info': transponder_info,
+ 'has_sigma': has_sigma,
+ 'sigma_info': sigma_info,
+ 'parameter_id': parameter_id,
+ 'mirrors': mirrors,
})
return JsonResponse({
diff --git a/dbapp/mainapp/views/lyngsat.py b/dbapp/mainapp/views/lyngsat.py
index 938a63a..3eb2bda 100644
--- a/dbapp/mainapp/views/lyngsat.py
+++ b/dbapp/mainapp/views/lyngsat.py
@@ -46,22 +46,30 @@ class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
param = objitem.parameter_obj
- # Round object frequency
+ # Round object frequency to 1 decimal place
if param.frequency:
- rounded_freq = round(param.frequency, 0) # Round to integer
+ rounded_freq = round(param.frequency, 1) # Round to 1 decimal place
# Find matching LyngSat source
- # Compare by rounded frequency and polarization
+ # Compare by rounded frequency (with tolerance) and polarization
+ # LyngSat frequencies are also rounded to 1 decimal place for comparison
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')
+ polarization=param.polarization
+ ).select_related('id_satellite', 'polarization')
- if lyngsat_sources.exists():
- # Take first matching source
- objitem.lyngsat_source = lyngsat_sources.first()
+ # Filter by rounded frequency with tolerance
+ matching_sources = []
+ for lyngsat in lyngsat_sources:
+ if lyngsat.frequency:
+ rounded_lyngsat_freq = round(lyngsat.frequency, 1)
+ if abs(rounded_lyngsat_freq - rounded_freq) <= frequency_tolerance:
+ matching_sources.append(lyngsat)
+
+ if matching_sources:
+ # Take first matching source (sorted by frequency difference)
+ matching_sources.sort(key=lambda x: abs(round(x.frequency, 1) - rounded_freq))
+ objitem.lyngsat_source = matching_sources[0]
objitem.save(update_fields=['lyngsat_source'])
linked_count += 1
@@ -159,3 +167,35 @@ class ClearLyngsatCacheView(LoginRequiredMixin, View):
def get(self, request):
"""Cache management page."""
return render(request, 'mainapp/clear_lyngsat_cache.html')
+
+
+class UnlinkAllLyngsatSourcesView(LoginRequiredMixin, View):
+ """View for unlinking all LyngSat sources from ObjItems."""
+
+ def post(self, request):
+ """Unlink all LyngSat sources."""
+ try:
+ # Count objects with LyngSat sources before unlinking
+ linked_count = ObjItem.objects.filter(lyngsat_source__isnull=False).count()
+
+ # Unlink all LyngSat sources
+ ObjItem.objects.filter(lyngsat_source__isnull=False).update(lyngsat_source=None)
+
+ messages.success(
+ request,
+ f"Успешно отвязано {linked_count} объектов от источников LyngSat"
+ )
+ except Exception as e:
+ messages.error(request, f"Ошибка при отвязке источников: {str(e)}")
+
+ return redirect('mainapp:actions')
+
+ def get(self, request):
+ """Show confirmation page."""
+ # Count objects with LyngSat sources
+ linked_count = ObjItem.objects.filter(lyngsat_source__isnull=False).count()
+
+ context = {
+ 'linked_count': linked_count
+ }
+ return render(request, 'mainapp/unlink_lyngsat_confirm.html', context)
diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py
index cbc876c..14f77fc 100644
--- a/dbapp/mainapp/views/source.py
+++ b/dbapp/mainapp/views/source.py
@@ -35,6 +35,7 @@ class SourceListView(LoginRequiredMixin, View):
has_coords_kupsat = request.GET.get("has_coords_kupsat")
has_coords_valid = request.GET.get("has_coords_valid")
has_coords_reference = request.GET.get("has_coords_reference")
+ has_lyngsat = request.GET.get("has_lyngsat")
objitem_count_min = request.GET.get("objitem_count_min", "").strip()
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
date_from = request.GET.get("date_from", "").strip()
@@ -86,6 +87,14 @@ class SourceListView(LoginRequiredMixin, View):
elif has_coords_reference == "0":
sources = sources.filter(coords_reference__isnull=True)
+ # Filter by LyngSat presence
+ if has_lyngsat == "1":
+ sources = sources.filter(source_objitems__lyngsat_source__isnull=False).distinct()
+ elif has_lyngsat == "0":
+ sources = sources.filter(
+ ~Q(source_objitems__lyngsat_source__isnull=False)
+ ).distinct()
+
# Filter by ObjItem count
if objitem_count_min:
try:
@@ -155,6 +164,8 @@ class SourceListView(LoginRequiredMixin, View):
# Prepare data for display
processed_sources = []
+ has_any_lyngsat = False # Track if any source has LyngSat data
+
for source in page_obj:
# Format coordinates
def format_coords(point):
@@ -174,12 +185,21 @@ class SourceListView(LoginRequiredMixin, View):
# Get count of related ObjItems
objitem_count = source.objitem_count
- # Get satellites for this source
+ # Get satellites for this source and check for LyngSat
satellite_names = set()
+ has_lyngsat = False
+ lyngsat_id = None
+
for objitem in source.source_objitems.all():
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite:
satellite_names.add(objitem.parameter_obj.id_satellite.name)
+
+ # Check if any objitem has LyngSat
+ if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source:
+ has_lyngsat = True
+ lyngsat_id = objitem.lyngsat_source.id
+ has_any_lyngsat = True
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
@@ -193,6 +213,8 @@ class SourceListView(LoginRequiredMixin, View):
'satellite': satellite_str,
'created_at': source.created_at,
'updated_at': source.updated_at,
+ 'has_lyngsat': has_lyngsat,
+ 'lyngsat_id': lyngsat_id,
})
# Prepare context for template
@@ -207,6 +229,8 @@ class SourceListView(LoginRequiredMixin, View):
'has_coords_kupsat': has_coords_kupsat,
'has_coords_valid': has_coords_valid,
'has_coords_reference': has_coords_reference,
+ 'has_lyngsat': has_lyngsat,
+ 'has_any_lyngsat': has_any_lyngsat,
'objitem_count_min': objitem_count_min,
'objitem_count_max': objitem_count_max,
'date_from': date_from,