Виджет с усреднёнными точками на карте
This commit is contained in:
@@ -14,6 +14,9 @@ from .models import (
|
||||
)
|
||||
from .widgets import CheckboxSelectMultipleWidget
|
||||
|
||||
# Import from mapsapp to avoid circular import issues
|
||||
from mapsapp.models import Transponders
|
||||
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
@@ -530,3 +533,111 @@ class SourceForm(forms.ModelForm):
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
|
||||
class TransponderForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования транспондеров.
|
||||
|
||||
При редактировании только name, zone_name и snr доступны для изменения.
|
||||
Остальные поля только для чтения.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Transponders
|
||||
fields = [
|
||||
'name',
|
||||
'sat_id',
|
||||
'downlink',
|
||||
'uplink',
|
||||
'frequency_range',
|
||||
'zone_name',
|
||||
'polarization',
|
||||
'snr',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите название транспондера'
|
||||
}),
|
||||
'sat_id': forms.Select(attrs={'class': 'form-select'}),
|
||||
'downlink': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.001',
|
||||
'placeholder': 'Введите частоту downlink в МГц'
|
||||
}),
|
||||
'uplink': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.001',
|
||||
'placeholder': 'Введите частоту uplink в МГц'
|
||||
}),
|
||||
'frequency_range': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.001',
|
||||
'placeholder': 'Введите полосу частот в МГц'
|
||||
}),
|
||||
'zone_name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите название зоны покрытия'
|
||||
}),
|
||||
'polarization': forms.Select(attrs={'class': 'form-select'}),
|
||||
'snr': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.1',
|
||||
'placeholder': 'Введите ОСШ в дБ'
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'name': 'Название транспондера',
|
||||
'sat_id': 'Спутник',
|
||||
'downlink': 'Downlink (МГц)',
|
||||
'uplink': 'Uplink (МГц)',
|
||||
'frequency_range': 'Полоса частот (МГц)',
|
||||
'zone_name': 'Название зоны покрытия',
|
||||
'polarization': 'Поляризация',
|
||||
'snr': 'ОСШ (дБ)',
|
||||
}
|
||||
help_texts = {
|
||||
'downlink': 'Частота downlink в МГц',
|
||||
'uplink': 'Частота uplink в МГц',
|
||||
'frequency_range': 'Полоса частот в МГц',
|
||||
'snr': 'Отношение сигнал/шум в децибелах',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Загружаем choices для select полей
|
||||
self.fields['sat_id'].queryset = Satellite.objects.all().order_by('name')
|
||||
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
|
||||
|
||||
# Если это форма редактирования (instance существует), делаем поля readonly
|
||||
if self.instance and self.instance.pk:
|
||||
# Поля только для чтения при редактировании
|
||||
readonly_fields = ['sat_id', 'downlink', 'uplink', 'frequency_range', 'polarization']
|
||||
for field_name in readonly_fields:
|
||||
self.fields[field_name].widget.attrs['readonly'] = True
|
||||
self.fields[field_name].widget.attrs['disabled'] = True
|
||||
self.fields[field_name].required = False
|
||||
else:
|
||||
# При создании все поля обязательны кроме name, zone_name и snr
|
||||
self.fields['sat_id'].required = True
|
||||
self.fields['downlink'].required = True
|
||||
self.fields['name'].required = False
|
||||
self.fields['zone_name'].required = False
|
||||
self.fields['snr'].required = False
|
||||
|
||||
def clean(self):
|
||||
"""Дополнительная валидация формы."""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# При редактировании восстанавливаем значения readonly полей из instance
|
||||
if self.instance and self.instance.pk:
|
||||
cleaned_data['sat_id'] = self.instance.sat_id
|
||||
cleaned_data['downlink'] = self.instance.downlink
|
||||
cleaned_data['uplink'] = self.instance.uplink
|
||||
cleaned_data['frequency_range'] = self.instance.frequency_range
|
||||
cleaned_data['polarization'] = self.instance.polarization
|
||||
|
||||
return cleaned_data
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||
</li>
|
||||
|
||||
237
dbapp/mainapp/templates/mainapp/source_averaging_map.html
Normal file
237
dbapp/mainapp/templates/mainapp/source_averaging_map.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Визуализация усреднения источника #{{ source_id }}{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.legend {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
font-size: 11px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.legend h6 {
|
||||
font-size: 13px;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.legend-item {
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.legend-marker {
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
margin-right: 6px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-info {
|
||||
background: #f8f9fa;
|
||||
padding: 6px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 10);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Цвета для маркеров
|
||||
var markerColors = {
|
||||
'red': 'red',
|
||||
'orange': 'orange',
|
||||
'blue': 'blue',
|
||||
'green': 'green',
|
||||
'purple': 'violet',
|
||||
'cyan': 'yellow',
|
||||
'violet': 'violet'
|
||||
};
|
||||
|
||||
var getColorIcon = function(color) {
|
||||
var iconColor = markerColors[color] || color;
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + iconColor + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var overlays = [];
|
||||
|
||||
// Создаём слои для каждой группы
|
||||
{% for group in groups %}
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = '{{ group.color }}';
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var popupContent = '';
|
||||
{% if point_data.name %}
|
||||
popupContent += '<strong>{{ point_data.name|escapejs }}</strong><br>';
|
||||
{% endif %}
|
||||
{% if point_data.frequency %}
|
||||
popupContent += 'Частота: {{ point_data.frequency|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.objitem_id %}
|
||||
popupContent += 'ObjItem ID: {{ point_data.objitem_id }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.step %}
|
||||
popupContent += '{{ point_data.step|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.distance %}
|
||||
popupContent += 'Расстояние: {{ point_data.distance|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.source_id %}
|
||||
popupContent += '{{ point_data.source_id|escapejs }}<br>';
|
||||
{% endif %}
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(popupContent || 'Точка {{ forloop.counter }}');
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
var label = "{{ forloop.counter }}";
|
||||
{% if point_data.name %}
|
||||
label += " - {{ point_data.name|escapejs|truncatechars:30 }}";
|
||||
{% elif point_data.step %}
|
||||
label += " - {{ point_data.step|escapejs }}";
|
||||
{% elif point_data.source_id %}
|
||||
label += " - {{ point_data.source_id|escapejs }}";
|
||||
{% endif %}
|
||||
|
||||
subgroup.push({
|
||||
label: label,
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: groupName + ' ({{ group.points|length }})',
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Добавляем легенду в левый нижний угол
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = '<h6>Визуализация усреднения</h6>';
|
||||
div.innerHTML += '<div class="legend-info">';
|
||||
div.innerHTML += 'Источник: #{{ source_id }}<br>';
|
||||
div.innerHTML += 'Всего точек: {{ total_points }}<br>';
|
||||
div.innerHTML += 'Шагов усреднения: {{ total_steps }}';
|
||||
div.innerHTML += '</div>';
|
||||
|
||||
{% for group in groups %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}${markerColors['{{ group.color }}'] || '{{ group.color }}'}.png');"></div>
|
||||
<span>{{ group.name|escapejs }} ({{ group.points|length }})</span>
|
||||
</div>
|
||||
`;
|
||||
{% endfor %}
|
||||
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
137
dbapp/mainapp/templates/mainapp/source_bulk_delete_confirm.html
Normal file
137
dbapp/mainapp/templates/mainapp/source_bulk_delete_confirm.html
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Подтверждение удаления источников{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i> Подтверждение удаления источников
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="bi bi-exclamation-circle"></i> Внимание!
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Вы собираетесь удалить <strong>{{ total_sources }}</strong> источник(ов).
|
||||
Это действие также удалит <strong>{{ total_objitems }}</strong> связанных точек.
|
||||
</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<strong>Это действие необратимо!</strong> Все данные будут безвозвратно удалены.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 15%;">ID источника</th>
|
||||
<th class="text-center" style="width: 20%;">Кол-во точек</th>
|
||||
<th style="width: 65%;">Спутники</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for source in sources_info %}
|
||||
<tr>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary">{{ source.objitem_count }}</span>
|
||||
</td>
|
||||
<td>{{ source.satellites }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4" role="alert">
|
||||
<h6 class="alert-heading">Что будет удалено:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><strong>{{ total_sources }}</strong> источник(ов)</li>
|
||||
<li><strong>{{ total_objitems }}</strong> точек ГЛ</li>
|
||||
<li>Все связанные геолокационные данные</li>
|
||||
<li>Все связанные параметры</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('deleteForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('confirmDeleteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Удаление...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
fetch('{% url "mainapp:delete_selected_sources" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
window.location.href = '{% url "mainapp:home" %}';
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении источников');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -67,6 +67,12 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedSources()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
@@ -100,6 +106,24 @@
|
||||
</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>
|
||||
|
||||
<!-- Coordinates Average Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Усредненные координаты:</label>
|
||||
@@ -168,9 +192,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ObjItem Count Filter -->
|
||||
<!-- Point Count Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество ObjItem:</label>
|
||||
<label class="form-label">Количество точек:</label>
|
||||
<input type="number" name="objitem_count_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ objitem_count_min|default:'' }}">
|
||||
<input type="number" name="objitem_count_max" class="form-control form-control-sm"
|
||||
@@ -217,13 +241,14 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">Спутник</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" class="text-center" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||
Кол-во ObjItem
|
||||
Кол-во точек
|
||||
{% if sort == 'objitem_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-objitem_count' %}
|
||||
@@ -262,6 +287,7 @@
|
||||
value="{{ source.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td>{{ source.satellite }}</td>
|
||||
<td>{{ source.coords_average }}</td>
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
<td>{{ source.coords_valid }}</td>
|
||||
@@ -285,6 +311,19 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if source.objitem_count > 1 %}
|
||||
<a href="{% url 'mainapp:show_source_averaging_map' source.id %}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-info"
|
||||
title="Визуализация усреднения">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно точек для усреднения">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})"
|
||||
title="Показать детали">
|
||||
@@ -307,7 +346,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -335,7 +374,7 @@
|
||||
</div>
|
||||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||
<div id="modalContent" style="display: none;">
|
||||
<h6>Связанные объекты (<span id="objitemCount">0</span>):</h6>
|
||||
<h6>Связанные точки (<span id="objitemCount">0</span>):</h6>
|
||||
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-light sticky-top">
|
||||
@@ -364,7 +403,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
|
||||
Нет связанных объектов
|
||||
Нет связанных точек
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -427,6 +466,27 @@ function showSelectedOnMap() {
|
||||
window.open(url, '_blank'); // Open in a new tab
|
||||
}
|
||||
|
||||
// Function to delete selected sources
|
||||
function deleteSelectedSources() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Redirect to confirmation page
|
||||
const url = '{% url "mainapp:delete_selected_sources" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
@@ -501,6 +561,16 @@ function setupRadioLikeCheckboxes(name) {
|
||||
});
|
||||
}
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
function selectAllOptions(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
@@ -510,6 +580,19 @@ function updateFilterCounter() {
|
||||
// Count non-empty form fields
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
// For multi-select fields, skip counting individual selections
|
||||
if (key === 'satellite_id') {
|
||||
continue;
|
||||
}
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count selected options in satellite multi-select field
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (selectedOptions.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +652,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const selectFields = form.querySelectorAll('select');
|
||||
selectFields.forEach(select => {
|
||||
select.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxFields.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFilterCounter);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Подтверждение удаления транспондеров{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i> Подтверждение удаления транспондеров
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="bi bi-exclamation-circle"></i> Внимание!
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Вы собираетесь удалить <strong>{{ total_transponders }}</strong> транспондер(ов).
|
||||
Это действие также удалит <strong>{{ total_objitems }}</strong> связанных точек.
|
||||
</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<strong>Это действие необратимо!</strong> Все данные будут безвозвратно удалены.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 10%;">ID</th>
|
||||
<th style="width: 20%;">Название</th>
|
||||
<th style="width: 20%;">Спутник</th>
|
||||
<th class="text-center" style="width: 15%;">Downlink, МГц</th>
|
||||
<th class="text-center" style="width: 15%;">Полоса, МГц</th>
|
||||
<th class="text-center" style="width: 20%;">Кол-во точек</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transponder in transponders_info %}
|
||||
<tr>
|
||||
<td class="text-center">{{ transponder.id }}</td>
|
||||
<td>{{ transponder.name }}</td>
|
||||
<td>{{ transponder.satellite }}</td>
|
||||
<td class="text-center">{{ transponder.downlink }}</td>
|
||||
<td class="text-center">{{ transponder.frequency_range }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary">{{ transponder.objitem_count }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4" role="alert">
|
||||
<h6 class="alert-heading">Что будет удалено:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><strong>{{ total_transponders }}</strong> транспондер(ов)</li>
|
||||
<li><strong>{{ total_objitems }}</strong> точек ГЛ</li>
|
||||
<li>Все связанные геолокационные данные</li>
|
||||
<li>Все связанные параметры</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'mainapp:transponder_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('deleteForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('confirmDeleteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Удаление...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
fetch('{% url "mainapp:delete_selected_transponders" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
window.location.href = '{% url "mainapp:transponder_list" %}';
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении транспондеров');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
280
dbapp/mainapp/templates/mainapp/transponder_form.html
Normal file
280
dbapp/mainapp/templates/mainapp/transponder_form.html
Normal file
@@ -0,0 +1,280 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if action == 'update' and objitem_count > 0 %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
С этим транспондером связано <strong>{{ objitem_count }}</strong> точек ГЛ.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="transponderForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }}
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.sat_id.id_for_label }}" class="form-label">
|
||||
{{ form.sat_id.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.sat_id }}
|
||||
{% if form.sat_id.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.sat_id.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.zone_name.id_for_label }}" class="form-label">
|
||||
{{ form.zone_name.label }}
|
||||
</label>
|
||||
{{ form.zone_name }}
|
||||
{% if form.zone_name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.zone_name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.zone_name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.zone_name.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.polarization.id_for_label }}" class="form-label">
|
||||
{{ form.polarization.label }}
|
||||
</label>
|
||||
{{ form.polarization }}
|
||||
{% if form.polarization.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.polarization.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Частотные параметры -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Частотные параметры</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.downlink.id_for_label }}" class="form-label">
|
||||
{{ form.downlink.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.downlink }}
|
||||
{% if form.downlink.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.downlink.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.downlink.help_text %}
|
||||
<small class="form-text text-muted">{{ form.downlink.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.uplink.id_for_label }}" class="form-label">
|
||||
{{ form.uplink.label }}
|
||||
</label>
|
||||
{{ form.uplink }}
|
||||
{% if form.uplink.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.uplink.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.uplink.help_text %}
|
||||
<small class="form-text text-muted">{{ form.uplink.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.frequency_range.id_for_label }}" class="form-label">
|
||||
{{ form.frequency_range.label }}
|
||||
</label>
|
||||
{{ form.frequency_range }}
|
||||
{% if form.frequency_range.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.frequency_range.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.frequency_range.help_text %}
|
||||
<small class="form-text text-muted">{{ form.frequency_range.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if action == 'update' and object.transfer %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Перенос (вычисляемое поле)</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.transfer|floatformat:3 }} МГц
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
Автоматически вычисляется как |Downlink - Uplink|
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные параметры -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Дополнительные параметры</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.snr.id_for_label }}" class="form-label">
|
||||
{{ form.snr.label }}
|
||||
</label>
|
||||
{{ form.snr }}
|
||||
{% if form.snr.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.snr.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.snr.help_text %}
|
||||
<small class="form-text text-muted">{{ form.snr.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Метаданные (только при редактировании) -->
|
||||
{% if action == 'update' %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Метаданные</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Создано</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.created_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Создал</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.created_by|default:"-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Обновлено</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.updated_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Обновил</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.updated_by|default:"-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'mainapp:transponder_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Назад к списку
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('transponderForm');
|
||||
|
||||
// Disable readonly fields on submit to prevent them from being sent
|
||||
form.addEventListener('submit', function(e) {
|
||||
const readonlyFields = form.querySelectorAll('[readonly], [disabled]');
|
||||
readonlyFields.forEach(field => {
|
||||
// Don't disable if it's just a display field (div)
|
||||
if (field.tagName === 'SELECT' || field.tagName === 'INPUT') {
|
||||
field.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
577
dbapp/mainapp/templates/mainapp/transponder_list.html
Normal file
577
dbapp/mainapp/templates/mainapp/transponder_list.html
Normal file
@@ -0,0 +1,577 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Список транспондеров{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.table-responsive tr.selected {
|
||||
background-color: #d4edff;
|
||||
}
|
||||
.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Список транспондеров</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<!-- Search bar -->
|
||||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||
<div class="input-group">
|
||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..."
|
||||
value="{{ search_query|default:'' }}">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick="performSearch()">Найти</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="clearSearch()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items per page select -->
|
||||
<div>
|
||||
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||||
<select name="items_per_page" id="items-per-page"
|
||||
class="form-select form-select-sm d-inline-block" style="width: auto;"
|
||||
onchange="updateItemsPerPage()">
|
||||
{% for option in available_items_per_page %}
|
||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||
{{ option }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedTransponders()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<i class="bi bi-funnel"></i> Фильтры
|
||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="ms-auto">
|
||||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offcanvas Filter Panel -->
|
||||
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||||
</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>
|
||||
|
||||
<!-- Polarization 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('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 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>
|
||||
|
||||
<!-- Downlink Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Downlink, МГц:</label>
|
||||
<input type="number" step="0.001" name="downlink_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ downlink_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="downlink_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ downlink_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Uplink Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Uplink, МГц:</label>
|
||||
<input type="number" step="0.001" name="uplink_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ uplink_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="uplink_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ uplink_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Frequency Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_range_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ freq_range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_range_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ freq_range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ, дБ:</label>
|
||||
<input type="number" step="0.1" name="snr_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.1" name="snr_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<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" class="text-center" style="min-width: 60px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||
ID
|
||||
{% if sort == 'id' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-id' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('name')" class="text-white text-decoration-none">
|
||||
Название
|
||||
{% if sort == 'name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('sat_id__name')" class="text-white text-decoration-none">
|
||||
Спутник
|
||||
{% if sort == 'sat_id__name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-sat_id__name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('downlink')" class="text-white text-decoration-none">
|
||||
Downlink, МГц
|
||||
{% if sort == 'downlink' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-downlink' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('uplink')" class="text-white text-decoration-none">
|
||||
Uplink, МГц
|
||||
{% if sort == 'uplink' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-uplink' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('frequency_range')" class="text-white text-decoration-none">
|
||||
Полоса, МГц
|
||||
{% if sort == 'frequency_range' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-frequency_range' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">Перенос, МГц</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('zone_name')" class="text-white text-decoration-none">
|
||||
Зона покрытия
|
||||
{% if sort == 'zone_name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-zone_name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('polarization__name')" class="text-white text-decoration-none">
|
||||
Поляризация
|
||||
{% if sort == 'polarization__name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-polarization__name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('snr')" class="text-white text-decoration-none">
|
||||
ОСШ, дБ
|
||||
{% if sort == 'snr' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-snr' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||
Кол-во точек
|
||||
{% if sort == 'objitem_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-objitem_count' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('created_at')" class="text-white text-decoration-none">
|
||||
Создано
|
||||
{% if sort == 'created_at' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-created_at' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('updated_at')" class="text-white text-decoration-none">
|
||||
Обновлено
|
||||
{% if sort == 'updated_at' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-updated_at' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transponder in processed_transponders %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ transponder.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ transponder.id }}</td>
|
||||
<td>{{ transponder.name }}</td>
|
||||
<td>{{ transponder.satellite }}</td>
|
||||
<td>{{ transponder.downlink }}</td>
|
||||
<td>{{ transponder.uplink }}</td>
|
||||
<td>{{ transponder.frequency_range }}</td>
|
||||
<td>{{ transponder.transfer }}</td>
|
||||
<td>{{ transponder.zone_name }}</td>
|
||||
<td>{{ transponder.polarization }}</td>
|
||||
<td>{{ transponder.snr }}</td>
|
||||
<td class="text-center">{{ transponder.objitem_count }}</td>
|
||||
<td>{{ transponder.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ transponder.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-center">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:transponder_update' transponder.id %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать транспондер">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="15" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
updateRowHighlight(checkboxes[i]);
|
||||
}
|
||||
} else {
|
||||
updateRowHighlight(e.target);
|
||||
}
|
||||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||
}
|
||||
|
||||
// Function to delete selected transponders
|
||||
function deleteSelectedTransponders() {
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один транспондер для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
const url = '{% url "mainapp:delete_selected_transponders" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (searchValue) {
|
||||
urlParams.set('search', searchValue);
|
||||
} else {
|
||||
urlParams.delete('search');
|
||||
}
|
||||
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
document.getElementById('toolbar-search').value = '';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.delete('search');
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Items per page functionality
|
||||
function updateItemsPerPage() {
|
||||
const itemsPerPage = document.getElementById('items-per-page').value;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set('items_per_page', itemsPerPage);
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Sorting functionality
|
||||
function updateSort(field) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentSort = urlParams.get('sort');
|
||||
|
||||
let newSort;
|
||||
if (currentSort === field) {
|
||||
newSort = '-' + field;
|
||||
} else if (currentSort === '-' + field) {
|
||||
newSort = field;
|
||||
} else {
|
||||
newSort = field;
|
||||
}
|
||||
|
||||
urlParams.set('sort', newSort);
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
function selectAllOptions(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const formData = new FormData(form);
|
||||
let filterCount = 0;
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
if (key === 'satellite_id' || key === 'polarization') {
|
||||
continue;
|
||||
}
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count selected options in multi-select fields
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (selectedOptions.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const polarizationSelect = document.querySelector('select[name="polarization"]');
|
||||
if (polarizationSelect) {
|
||||
const selectedOptions = Array.from(polarizationSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (selectedOptions.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const counterElement = document.getElementById('filterCounter');
|
||||
if (counterElement) {
|
||||
if (filterCount > 0) {
|
||||
counterElement.textContent = filterCount;
|
||||
counterElement.style.display = 'inline';
|
||||
} else {
|
||||
counterElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
});
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
|
||||
checkbox.addEventListener('click', handleCheckboxClick);
|
||||
});
|
||||
}
|
||||
|
||||
updateFilterCounter();
|
||||
|
||||
const form = document.getElementById('filter-form');
|
||||
if (form) {
|
||||
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||
inputFields.forEach(input => {
|
||||
input.addEventListener('input', updateFilterCounter);
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const selectFields = form.querySelectorAll('select');
|
||||
selectFields.forEach(select => {
|
||||
select.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
}
|
||||
|
||||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -8,6 +8,8 @@ from .views import (
|
||||
ClusterTestView,
|
||||
ClearLyngsatCacheView,
|
||||
DeleteSelectedObjectsView,
|
||||
DeleteSelectedSourcesView,
|
||||
DeleteSelectedTranspondersView,
|
||||
FillLyngsatDataView,
|
||||
GetLocationsView,
|
||||
LinkLyngsatSourcesView,
|
||||
@@ -27,12 +29,16 @@ from .views import (
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
ShowSourceAveragingStepsMapView,
|
||||
SourceListView,
|
||||
SourceUpdateView,
|
||||
SourceDeleteView,
|
||||
SourceObjItemsAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
TransponderDataAPIView,
|
||||
TransponderListView,
|
||||
TransponderCreateView,
|
||||
TransponderUpdateView,
|
||||
UploadVchLoadView,
|
||||
custom_logout,
|
||||
)
|
||||
@@ -43,7 +49,12 @@ urlpatterns = [
|
||||
path('', SourceListView.as_view(), name='home'),
|
||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||
path('transponders/', TransponderListView.as_view(), name='transponder_list'),
|
||||
path('transponder/create/', TransponderCreateView.as_view(), name='transponder_create'),
|
||||
path('transponder/<int:pk>/edit/', TransponderUpdateView.as_view(), name='transponder_update'),
|
||||
path('delete-selected-transponders/', DeleteSelectedTranspondersView.as_view(), name='delete_selected_transponders'),
|
||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
||||
@@ -54,6 +65,7 @@ urlpatterns = [
|
||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
||||
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
||||
path('show-source-averaging-map/<int:source_id>/', ShowSourceAveragingStepsMapView.as_view(), name='show_source_averaging_map'),
|
||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||
|
||||
@@ -31,12 +31,19 @@ from .lyngsat import (
|
||||
LyngsatTaskStatusView,
|
||||
ClearLyngsatCacheView,
|
||||
)
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
|
||||
from .transponder import (
|
||||
TransponderListView,
|
||||
TransponderCreateView,
|
||||
TransponderUpdateView,
|
||||
DeleteSelectedTranspondersView,
|
||||
)
|
||||
from .map import (
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
ShowSourceAveragingStepsMapView,
|
||||
ClusterTestView,
|
||||
)
|
||||
|
||||
@@ -75,10 +82,17 @@ __all__ = [
|
||||
'SourceListView',
|
||||
'SourceUpdateView',
|
||||
'SourceDeleteView',
|
||||
'DeleteSelectedSourcesView',
|
||||
# Transponder
|
||||
'TransponderListView',
|
||||
'TransponderCreateView',
|
||||
'TransponderUpdateView',
|
||||
'DeleteSelectedTranspondersView',
|
||||
# Map
|
||||
'ShowMapView',
|
||||
'ShowSelectedObjectsMapView',
|
||||
'ShowSourcesMapView',
|
||||
'ShowSourceWithPointsMapView',
|
||||
'ShowSourceAveragingStepsMapView',
|
||||
'ClusterTestView',
|
||||
]
|
||||
|
||||
@@ -255,6 +255,158 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/source_with_points_map.html", context)
|
||||
|
||||
|
||||
class ShowSourceAveragingStepsMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying source averaging steps visualization."""
|
||||
|
||||
def get(self, request, source_id):
|
||||
from ..models import Source
|
||||
from ..utils import calculate_mean_coords, RANGE_DISTANCE
|
||||
|
||||
try:
|
||||
source = Source.objects.prefetch_related(
|
||||
"source_objitems",
|
||||
"source_objitems__parameter_obj",
|
||||
"source_objitems__geo_obj",
|
||||
).get(id=source_id)
|
||||
except Source.DoesNotExist:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
# Получаем все ObjItem, отсортированные по ID (порядок добавления)
|
||||
objitems = source.source_objitems.select_related(
|
||||
"parameter_obj", "geo_obj"
|
||||
).order_by("id")
|
||||
|
||||
# Собираем координаты всех точек
|
||||
original_points = []
|
||||
for obj in objitems:
|
||||
if (
|
||||
not hasattr(obj, "geo_obj")
|
||||
or not obj.geo_obj
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
|
||||
original_points.append(
|
||||
{
|
||||
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||
"name": obj.name,
|
||||
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||
"objitem_id": obj.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Воспроизводим алгоритм усреднения
|
||||
averaging_steps = []
|
||||
|
||||
if original_points:
|
||||
# Первая точка становится начальным средним
|
||||
current_avg = original_points[0]["point"]
|
||||
|
||||
# Обрабатываем остальные точки
|
||||
for i, point_data in enumerate(original_points[1:], start=1):
|
||||
current_coord = point_data["point"]
|
||||
|
||||
# Вычисляем новое среднее и расстояние
|
||||
new_avg, distance = calculate_mean_coords(current_avg, current_coord)
|
||||
|
||||
# Сохраняем шаг усреднения
|
||||
averaging_steps.append(
|
||||
{
|
||||
"point": new_avg,
|
||||
"step": i,
|
||||
"distance": round(distance, 2),
|
||||
"within_range": distance <= RANGE_DISTANCE,
|
||||
}
|
||||
)
|
||||
|
||||
# Обновляем текущее среднее
|
||||
current_avg = new_avg
|
||||
|
||||
# Формируем группы для отображения на карте
|
||||
groups = []
|
||||
|
||||
# 1. Исходные точки ObjItem (красный)
|
||||
if original_points:
|
||||
groups.append(
|
||||
{
|
||||
"name": "Исходные точки ГЛ",
|
||||
"points": original_points,
|
||||
"color": "red",
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Промежуточные точки усреднения (оранжевый)
|
||||
if averaging_steps:
|
||||
intermediate_points = [
|
||||
{
|
||||
"point": step["point"],
|
||||
"step": f"Шаг {step['step']}",
|
||||
"distance": f"{step['distance']} км",
|
||||
}
|
||||
for step in averaging_steps[:-1] # Все кроме последней
|
||||
]
|
||||
if intermediate_points:
|
||||
groups.append(
|
||||
{
|
||||
"name": "Промежуточные шаги усреднения",
|
||||
"points": intermediate_points,
|
||||
"color": "orange",
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Финальная усредненная точка (синий)
|
||||
if averaging_steps:
|
||||
final_step = averaging_steps[-1]
|
||||
groups.append(
|
||||
{
|
||||
"name": "Финальная усредненная координата",
|
||||
"points": [
|
||||
{
|
||||
"point": final_step["point"],
|
||||
"step": f"Шаг {final_step['step']} (финальный)",
|
||||
"distance": f"{final_step['distance']} км",
|
||||
}
|
||||
],
|
||||
"color": "blue",
|
||||
}
|
||||
)
|
||||
|
||||
# 4. Координаты источника для сравнения (если есть)
|
||||
source_coord_types = [
|
||||
("coords_average", "Сохраненные усредненные координаты", "green"),
|
||||
("coords_kupsat", "Координаты Кубсата", "purple"),
|
||||
("coords_valid", "Координаты оперативников", "cyan"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
for coord_field, label, color in source_coord_types:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": [
|
||||
{
|
||||
"point": (coords.x, coords.y),
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
],
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
"source_id": source_id,
|
||||
"total_points": len(original_points),
|
||||
"total_steps": len(averaging_steps),
|
||||
}
|
||||
return render(request, "mainapp/source_averaging_map.html", context)
|
||||
|
||||
|
||||
class ClusterTestView(LoginRequiredMixin, View):
|
||||
"""Test view for clustering functionality."""
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
)
|
||||
|
||||
selected_sat_id = request.GET.get("satellite_id")
|
||||
|
||||
# If no satellite is selected and no filters are applied, select the first satellite
|
||||
if not selected_sat_id and not request.GET.getlist("satellite_id"):
|
||||
first_satellite = satellites.first()
|
||||
if first_satellite:
|
||||
selected_sat_id = str(first_satellite.id)
|
||||
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
sort_param = request.GET.get("sort", "")
|
||||
|
||||
@@ -450,12 +457,14 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"bod_max": bod_max,
|
||||
"search_query": search_query,
|
||||
"selected_modulations": [
|
||||
int(x) for x in selected_modulations if x.isdigit()
|
||||
int(x) if isinstance(x, str) else x for x in selected_modulations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
"selected_polarizations": [
|
||||
int(x) for x in selected_polarizations if x.isdigit()
|
||||
int(x) if isinstance(x, str) else x for x in selected_polarizations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
"selected_satellites": [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
"selected_satellites": [int(x) for x in selected_satellites if x.isdigit()],
|
||||
"has_kupsat": has_kupsat,
|
||||
"has_valid": has_valid,
|
||||
"date_from": date_from,
|
||||
|
||||
@@ -6,13 +6,14 @@ from datetime import datetime
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source
|
||||
from ..models import Source, Satellite
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
@@ -38,6 +39,15 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
|
||||
date_from = request.GET.get("date_from", "").strip()
|
||||
date_to = request.GET.get("date_to", "").strip()
|
||||
selected_satellites = request.GET.getlist("satellite_id")
|
||||
|
||||
# Get all satellites for filter
|
||||
satellites = (
|
||||
Satellite.objects.filter(parameters__objitem__source__isnull=False)
|
||||
.distinct()
|
||||
.only("id", "name")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
# Get all Source objects with query optimization
|
||||
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||
@@ -45,6 +55,7 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
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')
|
||||
@@ -117,6 +128,12 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# If not a number, ignore
|
||||
pass
|
||||
|
||||
# Filter by satellites
|
||||
if selected_satellites:
|
||||
sources = sources.filter(
|
||||
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
|
||||
).distinct()
|
||||
|
||||
# Apply sorting
|
||||
valid_sort_fields = {
|
||||
"id": "id",
|
||||
@@ -157,6 +174,15 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# Get count of related ObjItems
|
||||
objitem_count = source.objitem_count
|
||||
|
||||
# Get satellites for this source
|
||||
satellite_names = set()
|
||||
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)
|
||||
|
||||
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
|
||||
|
||||
processed_sources.append({
|
||||
'id': source.id,
|
||||
'coords_average': coords_average_str,
|
||||
@@ -164,6 +190,7 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'coords_valid': coords_valid_str,
|
||||
'coords_reference': coords_reference_str,
|
||||
'objitem_count': objitem_count,
|
||||
'satellite': satellite_str,
|
||||
'created_at': source.created_at,
|
||||
'updated_at': source.updated_at,
|
||||
})
|
||||
@@ -184,6 +211,10 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'objitem_count_max': objitem_count_max,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'satellites': satellites,
|
||||
'selected_satellites': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
@@ -302,3 +333,89 @@ class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
|
||||
class DeleteSelectedSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for deleting multiple selected sources with confirmation."""
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about sources to be deleted."""
|
||||
ids = request.GET.get("ids", "")
|
||||
if not ids:
|
||||
messages.error(request, "Не выбраны источники для удаления")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
sources = Source.objects.filter(id__in=id_list).prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
'source_objitems__geo_obj'
|
||||
).annotate(
|
||||
objitem_count=Count('source_objitems')
|
||||
)
|
||||
|
||||
# Prepare detailed information about sources
|
||||
sources_info = []
|
||||
total_objitems = 0
|
||||
|
||||
for source in sources:
|
||||
# Get satellites for this source
|
||||
satellite_names = set()
|
||||
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)
|
||||
|
||||
objitem_count = source.objitem_count
|
||||
total_objitems += objitem_count
|
||||
|
||||
sources_info.append({
|
||||
'id': source.id,
|
||||
'objitem_count': objitem_count,
|
||||
'satellites': ", ".join(sorted(satellite_names)) if satellite_names else "-",
|
||||
})
|
||||
|
||||
context = {
|
||||
'sources_info': sources_info,
|
||||
'total_sources': len(sources_info),
|
||||
'total_objitems': total_objitems,
|
||||
'ids': ids,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_bulk_delete_confirm.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при подготовке удаления: {str(e)}')
|
||||
return redirect('mainapp:home')
|
||||
|
||||
def post(self, request):
|
||||
"""Actually delete the selected sources."""
|
||||
ids = request.POST.get("ids", "")
|
||||
if not ids:
|
||||
return JsonResponse({"error": "Нет ID для удаления"}, status=400)
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
|
||||
# Get count before deletion
|
||||
sources = Source.objects.filter(id__in=id_list)
|
||||
deleted_sources_count = sources.count()
|
||||
|
||||
# Delete sources (cascade will delete related objitems)
|
||||
sources.delete()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Успешно удалено источников: {deleted_sources_count}'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"message": f"Успешно удалено источников: {deleted_sources_count}",
|
||||
"deleted_count": deleted_sources_count,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
|
||||
|
||||
374
dbapp/mainapp/views/transponder.py
Normal file
374
dbapp/mainapp/views/transponder.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Transponder CRUD operations and related views.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
|
||||
from mapsapp.models import Transponders
|
||||
from ..forms import TransponderForm
|
||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||
from ..models import Satellite, Polarization
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
class TransponderListView(LoginRequiredMixin, View):
|
||||
"""View for displaying a list of transponders with filtering and pagination."""
|
||||
|
||||
def get(self, request):
|
||||
# Get pagination parameters
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
|
||||
# Get sorting parameters (default to satellite and downlink)
|
||||
sort_param = request.GET.get("sort", "sat_id__name")
|
||||
|
||||
# Get filter parameters
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
selected_satellites = request.GET.getlist("satellite_id")
|
||||
selected_polarizations = request.GET.getlist("polarization")
|
||||
downlink_min = request.GET.get("downlink_min", "").strip()
|
||||
downlink_max = request.GET.get("downlink_max", "").strip()
|
||||
uplink_min = request.GET.get("uplink_min", "").strip()
|
||||
uplink_max = request.GET.get("uplink_max", "").strip()
|
||||
freq_range_min = request.GET.get("freq_range_min", "").strip()
|
||||
freq_range_max = request.GET.get("freq_range_max", "").strip()
|
||||
snr_min = request.GET.get("snr_min", "").strip()
|
||||
snr_max = request.GET.get("snr_max", "").strip()
|
||||
date_from = request.GET.get("date_from", "").strip()
|
||||
date_to = request.GET.get("date_to", "").strip()
|
||||
|
||||
# Get all satellites and polarizations for filters
|
||||
satellites = Satellite.objects.filter(
|
||||
tran_satellite__isnull=False
|
||||
).distinct().only("id", "name").order_by("name")
|
||||
|
||||
polarizations = Polarization.objects.all().order_by("name")
|
||||
|
||||
# Get all transponders with query optimization
|
||||
transponders = Transponders.objects.select_related(
|
||||
'sat_id',
|
||||
'polarization',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).annotate(
|
||||
objitem_count=Count('transponder_objitems')
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
# Filter by satellites
|
||||
if selected_satellites:
|
||||
transponders = transponders.filter(sat_id_id__in=selected_satellites)
|
||||
|
||||
# Filter by polarizations
|
||||
if selected_polarizations:
|
||||
transponders = transponders.filter(polarization_id__in=selected_polarizations)
|
||||
|
||||
# Filter by downlink frequency
|
||||
if downlink_min:
|
||||
try:
|
||||
min_val = float(downlink_min)
|
||||
transponders = transponders.filter(downlink__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if downlink_max:
|
||||
try:
|
||||
max_val = float(downlink_max)
|
||||
transponders = transponders.filter(downlink__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by uplink frequency
|
||||
if uplink_min:
|
||||
try:
|
||||
min_val = float(uplink_min)
|
||||
transponders = transponders.filter(uplink__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if uplink_max:
|
||||
try:
|
||||
max_val = float(uplink_max)
|
||||
transponders = transponders.filter(uplink__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by frequency range
|
||||
if freq_range_min:
|
||||
try:
|
||||
min_val = float(freq_range_min)
|
||||
transponders = transponders.filter(frequency_range__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if freq_range_max:
|
||||
try:
|
||||
max_val = float(freq_range_max)
|
||||
transponders = transponders.filter(frequency_range__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by SNR
|
||||
if snr_min:
|
||||
try:
|
||||
min_val = float(snr_min)
|
||||
transponders = transponders.filter(snr__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if snr_max:
|
||||
try:
|
||||
max_val = float(snr_max)
|
||||
transponders = transponders.filter(snr__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by creation date range
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||
transponders = transponders.filter(created_at__gte=date_from_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
from datetime import timedelta
|
||||
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
|
||||
# Add one day to include entire end date
|
||||
date_to_obj = date_to_obj + timedelta(days=1)
|
||||
transponders = transponders.filter(created_at__lt=date_to_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Search by name or zone name
|
||||
if search_query:
|
||||
transponders = transponders.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(zone_name__icontains=search_query) |
|
||||
Q(sat_id__name__icontains=search_query)
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
valid_sort_fields = {
|
||||
"sat_id__name": "sat_id__name",
|
||||
"-sat_id__name": "-sat_id__name",
|
||||
"name": "name",
|
||||
"-name": "-name",
|
||||
"downlink": "downlink",
|
||||
"-downlink": "-downlink",
|
||||
"uplink": "uplink",
|
||||
"-uplink": "-uplink",
|
||||
"frequency_range": "frequency_range",
|
||||
"-frequency_range": "-frequency_range",
|
||||
"zone_name": "zone_name",
|
||||
"-zone_name": "-zone_name",
|
||||
"polarization__name": "polarization__name",
|
||||
"-polarization__name": "-polarization__name",
|
||||
"snr": "snr",
|
||||
"-snr": "-snr",
|
||||
"created_at": "created_at",
|
||||
"-created_at": "-created_at",
|
||||
"updated_at": "updated_at",
|
||||
"-updated_at": "-updated_at",
|
||||
"objitem_count": "objitem_count",
|
||||
"-objitem_count": "-objitem_count",
|
||||
}
|
||||
|
||||
if sort_param in valid_sort_fields:
|
||||
transponders = transponders.order_by(valid_sort_fields[sort_param])
|
||||
|
||||
# Create paginator
|
||||
paginator = Paginator(transponders, items_per_page)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Prepare data for display
|
||||
processed_transponders = []
|
||||
for transponder in page_obj:
|
||||
processed_transponders.append({
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or "-",
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else "-",
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-",
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else "-",
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",
|
||||
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else "-",
|
||||
'zone_name': transponder.zone_name or "-",
|
||||
'polarization': transponder.polarization.name if transponder.polarization else "-",
|
||||
'snr': f"{transponder.snr:.1f}" if transponder.snr else "-",
|
||||
'objitem_count': transponder.objitem_count,
|
||||
'created_at': transponder.created_at,
|
||||
'updated_at': transponder.updated_at,
|
||||
'created_by': transponder.created_by if transponder.created_by else "-",
|
||||
'updated_by': transponder.updated_by if transponder.updated_by else "-",
|
||||
})
|
||||
|
||||
# Prepare context for template
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'processed_transponders': processed_transponders,
|
||||
'items_per_page': items_per_page,
|
||||
'available_items_per_page': [50, 100, 500, 1000],
|
||||
'sort': sort_param,
|
||||
'search_query': search_query,
|
||||
'satellites': satellites,
|
||||
'polarizations': polarizations,
|
||||
'selected_satellites': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites
|
||||
if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'selected_polarizations': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_polarizations
|
||||
if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'downlink_min': downlink_min,
|
||||
'downlink_max': downlink_max,
|
||||
'uplink_min': uplink_min,
|
||||
'uplink_max': uplink_max,
|
||||
'freq_range_min': freq_range_min,
|
||||
'freq_range_max': freq_range_max,
|
||||
'snr_min': snr_min,
|
||||
'snr_max': snr_max,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, "mainapp/transponder_list.html", context)
|
||||
|
||||
|
||||
class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
"""View for creating a new transponder."""
|
||||
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно создан!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['action'] = 'create'
|
||||
context['title'] = 'Создание транспондера'
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user.customuser
|
||||
form.instance.updated_by = self.request.user.customuser
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
"""View for updating an existing transponder."""
|
||||
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно обновлен!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['action'] = 'update'
|
||||
context['title'] = f'Редактирование транспондера #{self.object.id}'
|
||||
|
||||
# Get related objitems count
|
||||
context['objitem_count'] = self.object.transponder_objitems.count()
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.updated_by = self.request.user.customuser
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteSelectedTranspondersView(RoleRequiredMixin, View):
|
||||
"""View for deleting multiple selected transponders with confirmation."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about transponders to be deleted."""
|
||||
ids = request.GET.get("ids", "")
|
||||
if not ids:
|
||||
messages.error(request, "Не выбраны транспондеры для удаления")
|
||||
return redirect('mainapp:transponder_list')
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
transponders = Transponders.objects.filter(id__in=id_list).select_related(
|
||||
'sat_id',
|
||||
'polarization'
|
||||
).annotate(
|
||||
objitem_count=Count('transponder_objitems')
|
||||
)
|
||||
|
||||
# Prepare detailed information about transponders
|
||||
transponders_info = []
|
||||
total_objitems = 0
|
||||
|
||||
for transponder in transponders:
|
||||
objitem_count = transponder.objitem_count
|
||||
total_objitems += objitem_count
|
||||
|
||||
transponders_info.append({
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or "-",
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else "-",
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-",
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",
|
||||
'objitem_count': objitem_count,
|
||||
})
|
||||
|
||||
context = {
|
||||
'transponders_info': transponders_info,
|
||||
'total_transponders': len(transponders_info),
|
||||
'total_objitems': total_objitems,
|
||||
'ids': ids,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/transponder_bulk_delete_confirm.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при подготовке удаления: {str(e)}')
|
||||
return redirect('mainapp:transponder_list')
|
||||
|
||||
def post(self, request):
|
||||
"""Actually delete the selected transponders."""
|
||||
ids = request.POST.get("ids", "")
|
||||
if not ids:
|
||||
return JsonResponse({"error": "Нет ID для удаления"}, status=400)
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
|
||||
# Get count before deletion
|
||||
transponders = Transponders.objects.filter(id__in=id_list)
|
||||
deleted_count = transponders.count()
|
||||
|
||||
# Delete transponders (cascade will handle related objitems)
|
||||
transponders.delete()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Успешно удалено транспондеров: {deleted_count}'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"message": f"Успешно удалено транспондеров: {deleted_count}",
|
||||
"deleted_count": deleted_count,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
|
||||
Reference in New Issue
Block a user