Сделал вкладку спутников
This commit is contained in:
@@ -549,7 +549,7 @@ class KubsatFilterForm(forms.Form):
|
||||
"""Форма фильтров для страницы Кубсат"""
|
||||
|
||||
satellites = forms.ModelMultipleChoiceField(
|
||||
queryset=Satellite.objects.all().order_by('name'),
|
||||
queryset=None, # Будет установлен в __init__
|
||||
label='Спутники',
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'})
|
||||
@@ -617,7 +617,7 @@ class KubsatFilterForm(forms.Form):
|
||||
|
||||
objitem_count = forms.ChoiceField(
|
||||
choices=[('', 'Все'), ('1', '1'), ('2+', '2 и более')],
|
||||
label='Количество привязанных ObjItem',
|
||||
label='Количество привязанных точек ГЛ',
|
||||
required=False,
|
||||
widget=forms.RadioSelect()
|
||||
)
|
||||
@@ -658,7 +658,15 @@ class KubsatFilterForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
from mainapp.models import Band, ObjectInfo
|
||||
from mainapp.models import Band, ObjectInfo, Satellite, ObjItem
|
||||
from django.db.models import Exists, OuterRef
|
||||
|
||||
# Фильтруем спутники: только те, у которых есть источники с точками
|
||||
satellites_with_sources = Satellite.objects.filter(
|
||||
parameters__objitem__source__isnull=False
|
||||
).distinct().order_by('name')
|
||||
|
||||
self.fields['satellites'].queryset = satellites_with_sources
|
||||
self.fields['band'].queryset = Band.objects.all().order_by('name')
|
||||
self.fields['object_type'].queryset = ObjectInfo.objects.all().order_by('name')
|
||||
|
||||
@@ -768,3 +776,103 @@ class TransponderForm(forms.ModelForm):
|
||||
cleaned_data['polarization'] = self.instance.polarization
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class SatelliteForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования спутников.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Satellite
|
||||
fields = [
|
||||
'name',
|
||||
'norad',
|
||||
'band',
|
||||
'undersat_point',
|
||||
'url',
|
||||
'comment',
|
||||
'launch_date',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите название спутника',
|
||||
'required': True
|
||||
}),
|
||||
'norad': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите NORAD ID'
|
||||
}),
|
||||
'band': forms.SelectMultiple(attrs={
|
||||
'class': 'form-select',
|
||||
'size': '5'
|
||||
}),
|
||||
'undersat_point': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.01',
|
||||
'placeholder': 'Введите подспутниковую точку в градусах'
|
||||
}),
|
||||
'url': forms.URLInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'https://example.com'
|
||||
}),
|
||||
'comment': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 3,
|
||||
'placeholder': 'Введите комментарий'
|
||||
}),
|
||||
'launch_date': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'name': 'Название спутника',
|
||||
'norad': 'NORAD ID',
|
||||
'band': 'Диапазоны работы',
|
||||
'undersat_point': 'Подспутниковая точка (градусы)',
|
||||
'url': 'Ссылка на источник',
|
||||
'comment': 'Комментарий',
|
||||
'launch_date': 'Дата запуска',
|
||||
}
|
||||
help_texts = {
|
||||
'name': 'Уникальное название спутника',
|
||||
'norad': 'Идентификатор NORAD для отслеживания спутника',
|
||||
'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)',
|
||||
'undersat_point': 'Восточное полушарие с +, западное с -',
|
||||
'url': 'Ссылка на сайт, где можно проверить информацию',
|
||||
'launch_date': 'Дата запуска спутника',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
from mainapp.models import Band
|
||||
|
||||
# Загружаем choices для select полей
|
||||
self.fields['band'].queryset = Band.objects.all().order_by('name')
|
||||
|
||||
# Делаем name обязательным
|
||||
self.fields['name'].required = True
|
||||
|
||||
def clean_name(self):
|
||||
"""Валидация поля name."""
|
||||
name = self.cleaned_data.get('name')
|
||||
|
||||
if name:
|
||||
# Удаляем лишние пробелы
|
||||
name = name.strip()
|
||||
|
||||
# Проверяем что после удаления пробелов что-то осталось
|
||||
if not name:
|
||||
raise forms.ValidationError('Название не может состоять только из пробелов')
|
||||
|
||||
# Проверяем уникальность (исключая текущий объект при редактировании)
|
||||
qs = Satellite.objects.filter(name=name)
|
||||
if self.instance and self.instance.pk:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
|
||||
if qs.exists():
|
||||
raise forms.ValidationError('Спутник с таким названием уже существует')
|
||||
|
||||
return name
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'lyngsatapp:lyngsat_list' %}">LyngSat</a>
|
||||
<a class="nav-link" href="{% url 'mainapp:satellite_list' %}">Спутники</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'lyngsatapp:lyngsat_list' %}">Справочные данные</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
<!-- Успех 1 (фиктивный) -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">{{ form.success_1.label }} (не рабльает)</label>
|
||||
<label class="form-label">{{ form.success_1.label }} (не работает)</label>
|
||||
<div>
|
||||
{% for radio in form.success_1 %}
|
||||
<div class="form-check">
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Подтверждение удаления спутников{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<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">
|
||||
<strong>Внимание!</strong> Вы собираетесь удалить <strong>{{ total_satellites }}</strong> спутник(ов).
|
||||
Это действие необратимо!
|
||||
</div>
|
||||
|
||||
<h5>Сводная информация:</h5>
|
||||
<ul>
|
||||
<li>Всего спутников к удалению: <strong>{{ total_satellites }}</strong></li>
|
||||
<li>Связанных транспондеров: <strong>{{ total_transponders }}</strong></li>
|
||||
</ul>
|
||||
|
||||
<h5 class="mt-4">Список спутников:</h5>
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>NORAD ID</th>
|
||||
<th>Транспондеры</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for satellite in satellites_info %}
|
||||
<tr>
|
||||
<td>{{ satellite.id }}</td>
|
||||
<td>{{ satellite.name }}</td>
|
||||
<td>{{ satellite.norad }}</td>
|
||||
<td>{{ satellite.transponder_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-4">
|
||||
<strong>Предупреждение:</strong> При удалении спутников будут также удалены все связанные с ними данные.
|
||||
</div>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
<a href="{% url 'mainapp:satellite_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('deleteForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('{% url "mainapp:delete_selected_satellites" %}', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': formData.get('csrfmiddlewaretoken')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = '{% url "mainapp:satellite_list" %}';
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
481
dbapp/mainapp/templates/mainapp/satellite_form.html
Normal file
481
dbapp/mainapp/templates/mainapp/satellite_form.html
Normal file
@@ -0,0 +1,481 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.frequency-plan {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.frequency-chart-container {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#frequencyCanvas {
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.transponder-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>{{ title }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.name.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.name.help_text %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.norad.id_for_label }}" class="form-label">
|
||||
{{ form.norad.label }}
|
||||
</label>
|
||||
{{ form.norad }}
|
||||
{% if form.norad.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.norad.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.norad.help_text %}
|
||||
<div class="form-text">{{ form.norad.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.band.id_for_label }}" class="form-label">
|
||||
{{ form.band.label }}
|
||||
</label>
|
||||
{{ form.band }}
|
||||
{% if form.band.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.band.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.band.help_text %}
|
||||
<div class="form-text">{{ form.band.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.undersat_point.id_for_label }}" class="form-label">
|
||||
{{ form.undersat_point.label }}
|
||||
</label>
|
||||
{{ form.undersat_point }}
|
||||
{% if form.undersat_point.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.undersat_point.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.undersat_point.help_text %}
|
||||
<div class="form-text">{{ form.undersat_point.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.launch_date.id_for_label }}" class="form-label">
|
||||
{{ form.launch_date.label }}
|
||||
</label>
|
||||
{{ form.launch_date }}
|
||||
{% if form.launch_date.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.launch_date.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.launch_date.help_text %}
|
||||
<div class="form-text">{{ form.launch_date.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.url.id_for_label }}" class="form-label">
|
||||
{{ form.url.label }}
|
||||
</label>
|
||||
{{ form.url }}
|
||||
{% if form.url.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.url.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.url.help_text %}
|
||||
<div class="form-text">{{ form.url.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.comment.id_for_label }}" class="form-label">
|
||||
{{ form.comment.label }}
|
||||
</label>
|
||||
{{ form.comment }}
|
||||
{% if form.comment.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.comment.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.comment.help_text %}
|
||||
<div class="form-text">{{ form.comment.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
<a href="{% url 'mainapp:satellite_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if action == 'update' and transponders %}
|
||||
<!-- Frequency Plan Visualization -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4>Частотный план</h4>
|
||||
<p class="text-muted">Визуализация транспондеров спутника по частотам (Downlink). Наведите курсор на транспондер для подробной информации.</p>
|
||||
|
||||
<div class="frequency-plan">
|
||||
<div class="frequency-chart-container">
|
||||
<canvas id="frequencyCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #0d6efd;"></div>
|
||||
<span>H - Горизонтальная</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #198754;"></div>
|
||||
<span>V - Вертикальная</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #dc3545;"></div>
|
||||
<span>L - Левая круговая</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #ffc107;"></div>
|
||||
<span>R - Правая круговая</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: #6c757d;"></div>
|
||||
<span>Другая</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<p><strong>Всего транспондеров:</strong> {{ transponder_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transponder-tooltip" id="transponderTooltip"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if action == 'update' and transponders %}
|
||||
<script>
|
||||
// Transponder data from Django
|
||||
const transponders = {{ transponders|safe }};
|
||||
|
||||
// Color mapping for polarizations
|
||||
const polarizationColors = {
|
||||
'H': '#0d6efd',
|
||||
'V': '#198754',
|
||||
'L': '#dc3545',
|
||||
'R': '#ffc107',
|
||||
'default': '#6c757d'
|
||||
};
|
||||
|
||||
let canvas, ctx, tooltip;
|
||||
let hoveredTransponder = null;
|
||||
|
||||
function getColor(polarization) {
|
||||
return polarizationColors[polarization] || polarizationColors['default'];
|
||||
}
|
||||
|
||||
function renderFrequencyPlan() {
|
||||
if (!transponders || transponders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas = document.getElementById('frequencyCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
tooltip = document.getElementById('transponderTooltip');
|
||||
|
||||
// Find min and max frequencies
|
||||
let minFreq = Infinity;
|
||||
let maxFreq = -Infinity;
|
||||
|
||||
transponders.forEach(t => {
|
||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||
minFreq = Math.min(minFreq, startFreq);
|
||||
maxFreq = Math.max(maxFreq, endFreq);
|
||||
});
|
||||
|
||||
// Add padding (5%)
|
||||
const padding = (maxFreq - minFreq) * 0.05;
|
||||
minFreq -= padding;
|
||||
maxFreq += padding;
|
||||
|
||||
const freqRange = maxFreq - minFreq;
|
||||
|
||||
// Set canvas size
|
||||
const container = canvas.parentElement;
|
||||
const canvasWidth = Math.max(container.clientWidth - 40, 800);
|
||||
const rowHeight = 50;
|
||||
const topMargin = 40;
|
||||
const bottomMargin = 60;
|
||||
|
||||
// Group transponders by polarization to stack them
|
||||
const polarizationGroups = {};
|
||||
transponders.forEach(t => {
|
||||
const pol = t.polarization || 'default';
|
||||
if (!polarizationGroups[pol]) {
|
||||
polarizationGroups[pol] = [];
|
||||
}
|
||||
polarizationGroups[pol].push(t);
|
||||
});
|
||||
|
||||
const numRows = Object.keys(polarizationGroups).length;
|
||||
const canvasHeight = topMargin + (numRows * rowHeight) + bottomMargin;
|
||||
|
||||
// Set canvas dimensions (use device pixel ratio for sharp rendering)
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = canvasWidth * dpr;
|
||||
canvas.height = canvasHeight * dpr;
|
||||
canvas.style.width = canvasWidth + 'px';
|
||||
canvas.style.height = canvasHeight + 'px';
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// Draw frequency axis
|
||||
ctx.strokeStyle = '#dee2e6';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, topMargin);
|
||||
ctx.lineTo(canvasWidth, topMargin);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw frequency labels
|
||||
ctx.fillStyle = '#6c757d';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const numLabels = 10;
|
||||
for (let i = 0; i <= numLabels; i++) {
|
||||
const freq = minFreq + (freqRange * i / numLabels);
|
||||
const x = (canvasWidth * i / numLabels);
|
||||
|
||||
// Draw tick
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, topMargin);
|
||||
ctx.lineTo(x, topMargin - 5);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw label
|
||||
ctx.fillText(freq.toFixed(1), x, topMargin - 10);
|
||||
}
|
||||
|
||||
// Draw "МГц" label
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText('МГц', canvasWidth, topMargin - 25);
|
||||
|
||||
// Store transponder positions for hover detection
|
||||
const transponderRects = [];
|
||||
|
||||
// Draw transponders
|
||||
let yOffset = topMargin + 10;
|
||||
|
||||
Object.keys(polarizationGroups).forEach((pol, groupIndex) => {
|
||||
const group = polarizationGroups[pol];
|
||||
const color = getColor(pol);
|
||||
|
||||
// Draw polarization label
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`${pol}:`, 5, yOffset + 20);
|
||||
|
||||
group.forEach(t => {
|
||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||
|
||||
const x = ((startFreq - minFreq) / freqRange) * canvasWidth;
|
||||
const width = ((endFreq - startFreq) / freqRange) * canvasWidth;
|
||||
const y = yOffset;
|
||||
const height = 30;
|
||||
|
||||
// Draw transponder bar
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, width, height);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
|
||||
// Draw transponder name if there's enough space
|
||||
if (width > 50) {
|
||||
ctx.fillStyle = pol === 'R' ? '#000' : '#fff';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t.name, x + width / 2, y + height / 2 + 4);
|
||||
}
|
||||
|
||||
// Store for hover detection
|
||||
transponderRects.push({
|
||||
x, y, width, height,
|
||||
data: t
|
||||
});
|
||||
});
|
||||
|
||||
yOffset += rowHeight;
|
||||
});
|
||||
|
||||
// Add mouse move event for tooltip
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
hoveredTransponder = null;
|
||||
|
||||
for (const tr of transponderRects) {
|
||||
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
||||
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
||||
hoveredTransponder = tr.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hoveredTransponder) {
|
||||
const startFreq = hoveredTransponder.downlink - (hoveredTransponder.frequency_range / 2);
|
||||
const endFreq = hoveredTransponder.downlink + (hoveredTransponder.frequency_range / 2);
|
||||
|
||||
tooltip.innerHTML = `<strong>${hoveredTransponder.name}</strong>
|
||||
Downlink: ${hoveredTransponder.downlink.toFixed(3)} МГц
|
||||
Полоса: ${hoveredTransponder.frequency_range.toFixed(3)} МГц
|
||||
Диапазон: ${startFreq.toFixed(3)} - ${endFreq.toFixed(3)} МГц
|
||||
Поляризация: ${hoveredTransponder.polarization}
|
||||
Зона: ${hoveredTransponder.zone_name}`;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = (e.pageX + 15) + 'px';
|
||||
tooltip.style.top = (e.pageY + 15) + 'px';
|
||||
} else {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
tooltip.style.display = 'none';
|
||||
hoveredTransponder = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Render on page load
|
||||
document.addEventListener('DOMContentLoaded', renderFrequencyPlan);
|
||||
|
||||
// Re-render on window resize
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(renderFrequencyPlan, 250);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
509
dbapp/mainapp/templates/mainapp/satellite_list.html
Normal file
509
dbapp/mainapp/templates/mainapp/satellite_list.html
Normal file
@@ -0,0 +1,509 @@
|
||||
{% 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:satellite_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="deleteSelectedSatellites()">
|
||||
<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">
|
||||
<!-- Band 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('band_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('band_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="band_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for band in bands %}
|
||||
<option value="{{ band.id }}" {% if band.id in selected_bands %}selected{% endif %}>
|
||||
{{ band.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- NORAD ID Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">NORAD ID:</label>
|
||||
<input type="number" name="norad_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ norad_min|default:'' }}">
|
||||
<input type="number" name="norad_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ norad_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Undersat Point Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Подспутниковая точка, градусы:</label>
|
||||
<input type="number" step="0.01" name="undersat_point_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ undersat_point_min|default:'' }}">
|
||||
<input type="number" step="0.01" name="undersat_point_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ undersat_point_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Launch Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата запуска:</label>
|
||||
<input type="date" name="launch_date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ launch_date_from|default:'' }}">
|
||||
<input type="date" name="launch_date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ launch_date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Creation Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<input type="date" name="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="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 table-bordered" 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: 150px;">
|
||||
<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: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none">
|
||||
NORAD ID
|
||||
{% if sort == 'norad' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-norad' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">Диапазоны</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('undersat_point')" class="text-white text-decoration-none">
|
||||
Подспутниковая точка
|
||||
{% if sort == 'undersat_point' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-undersat_point' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('launch_date')" class="text-white text-decoration-none">
|
||||
Дата запуска
|
||||
{% if sort == 'launch_date' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-launch_date' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 150px;">Ссылка</th>
|
||||
<th scope="col" class="text-center" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('transponder_count')" class="text-white text-decoration-none">
|
||||
Транспондеры
|
||||
{% if sort == 'transponder_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-transponder_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 satellite in processed_satellites %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ satellite.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ satellite.id }}</td>
|
||||
<td>{{ satellite.name }}</td>
|
||||
<td>{{ satellite.norad }}</td>
|
||||
<td>{{ satellite.bands }}</td>
|
||||
<td>{{ satellite.undersat_point }}</td>
|
||||
<td>{{ satellite.launch_date }}</td>
|
||||
<td>
|
||||
{% if satellite.url != '-' %}
|
||||
<a href="{{ satellite.url }}" target="_blank" rel="noopener noreferrer">
|
||||
<i class="bi bi-link-45deg"></i> Ссылка
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">{{ satellite.transponder_count }}</td>
|
||||
<td>{{ satellite.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ satellite.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:satellite_update' satellite.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="12" 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 satellites
|
||||
function deleteSelectedSatellites() {
|
||||
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_satellites" %}' + '?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 === 'band_id') {
|
||||
continue;
|
||||
}
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count selected options in multi-select fields
|
||||
const bandSelect = document.querySelector('select[name="band_id"]');
|
||||
if (bandSelect) {
|
||||
const selectedOptions = Array.from(bandSelect.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 %}
|
||||
@@ -11,6 +11,7 @@ from .views import (
|
||||
DeleteSelectedObjectsView,
|
||||
DeleteSelectedSourcesView,
|
||||
DeleteSelectedTranspondersView,
|
||||
DeleteSelectedSatellitesView,
|
||||
FillLyngsatDataView,
|
||||
GeoPointsAPIView,
|
||||
GetLocationsView,
|
||||
@@ -31,6 +32,9 @@ from .views import (
|
||||
ObjItemUpdateView,
|
||||
ProcessKubsatView,
|
||||
SatelliteDataAPIView,
|
||||
SatelliteListView,
|
||||
SatelliteCreateView,
|
||||
SatelliteUpdateView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
@@ -68,6 +72,10 @@ urlpatterns = [
|
||||
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('satellites/', SatelliteListView.as_view(), name='satellite_list'),
|
||||
path('satellite/create/', SatelliteCreateView.as_view(), name='satellite_create'),
|
||||
path('satellite/<int:pk>/edit/', SatelliteUpdateView.as_view(), name='satellite_update'),
|
||||
path('delete-selected-satellites/', DeleteSelectedSatellitesView.as_view(), name='delete_selected_satellites'),
|
||||
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'),
|
||||
|
||||
@@ -41,6 +41,12 @@ from .transponder import (
|
||||
TransponderUpdateView,
|
||||
DeleteSelectedTranspondersView,
|
||||
)
|
||||
from .satellite import (
|
||||
SatelliteListView,
|
||||
SatelliteCreateView,
|
||||
SatelliteUpdateView,
|
||||
DeleteSelectedSatellitesView,
|
||||
)
|
||||
from .map import (
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
@@ -99,6 +105,11 @@ __all__ = [
|
||||
'TransponderCreateView',
|
||||
'TransponderUpdateView',
|
||||
'DeleteSelectedTranspondersView',
|
||||
# Satellite
|
||||
'SatelliteListView',
|
||||
'SatelliteCreateView',
|
||||
'SatelliteUpdateView',
|
||||
'DeleteSelectedSatellitesView',
|
||||
# Map
|
||||
'ShowMapView',
|
||||
'ShowSelectedObjectsMapView',
|
||||
|
||||
353
dbapp/mainapp/views/satellite.py
Normal file
353
dbapp/mainapp/views/satellite.py
Normal file
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Satellite 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 ..forms import SatelliteForm
|
||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||
from ..models import Satellite, Band
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
class SatelliteListView(LoginRequiredMixin, View):
|
||||
"""View for displaying a list of satellites 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 name)
|
||||
sort_param = request.GET.get("sort", "name")
|
||||
|
||||
# Get filter parameters
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
selected_bands = request.GET.getlist("band_id")
|
||||
norad_min = request.GET.get("norad_min", "").strip()
|
||||
norad_max = request.GET.get("norad_max", "").strip()
|
||||
undersat_point_min = request.GET.get("undersat_point_min", "").strip()
|
||||
undersat_point_max = request.GET.get("undersat_point_max", "").strip()
|
||||
launch_date_from = request.GET.get("launch_date_from", "").strip()
|
||||
launch_date_to = request.GET.get("launch_date_to", "").strip()
|
||||
date_from = request.GET.get("date_from", "").strip()
|
||||
date_to = request.GET.get("date_to", "").strip()
|
||||
|
||||
# Get all bands for filters
|
||||
bands = Band.objects.all().order_by("name")
|
||||
|
||||
# Get all satellites with query optimization
|
||||
satellites = Satellite.objects.prefetch_related(
|
||||
'band',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).annotate(
|
||||
transponder_count=Count('tran_satellite', distinct=True)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
# Filter by bands
|
||||
if selected_bands:
|
||||
satellites = satellites.filter(band__id__in=selected_bands).distinct()
|
||||
|
||||
# Filter by NORAD ID
|
||||
if norad_min:
|
||||
try:
|
||||
min_val = int(norad_min)
|
||||
satellites = satellites.filter(norad__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if norad_max:
|
||||
try:
|
||||
max_val = int(norad_max)
|
||||
satellites = satellites.filter(norad__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by undersat point
|
||||
if undersat_point_min:
|
||||
try:
|
||||
min_val = float(undersat_point_min)
|
||||
satellites = satellites.filter(undersat_point__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if undersat_point_max:
|
||||
try:
|
||||
max_val = float(undersat_point_max)
|
||||
satellites = satellites.filter(undersat_point__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by launch date range
|
||||
if launch_date_from:
|
||||
try:
|
||||
date_from_obj = datetime.strptime(launch_date_from, "%Y-%m-%d").date()
|
||||
satellites = satellites.filter(launch_date__gte=date_from_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if launch_date_to:
|
||||
try:
|
||||
from datetime import timedelta
|
||||
date_to_obj = datetime.strptime(launch_date_to, "%Y-%m-%d").date()
|
||||
# Add one day to include entire end date
|
||||
date_to_obj = date_to_obj + timedelta(days=1)
|
||||
satellites = satellites.filter(launch_date__lt=date_to_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Filter by creation date range
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||
satellites = satellites.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)
|
||||
satellites = satellites.filter(created_at__lt=date_to_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Search by name
|
||||
if search_query:
|
||||
satellites = satellites.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(comment__icontains=search_query)
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
valid_sort_fields = {
|
||||
"id": "id",
|
||||
"-id": "-id",
|
||||
"name": "name",
|
||||
"-name": "-name",
|
||||
"norad": "norad",
|
||||
"-norad": "-norad",
|
||||
"undersat_point": "undersat_point",
|
||||
"-undersat_point": "-undersat_point",
|
||||
"launch_date": "launch_date",
|
||||
"-launch_date": "-launch_date",
|
||||
"created_at": "created_at",
|
||||
"-created_at": "-created_at",
|
||||
"updated_at": "updated_at",
|
||||
"-updated_at": "-updated_at",
|
||||
"transponder_count": "transponder_count",
|
||||
"-transponder_count": "-transponder_count",
|
||||
}
|
||||
|
||||
if sort_param in valid_sort_fields:
|
||||
satellites = satellites.order_by(valid_sort_fields[sort_param])
|
||||
|
||||
# Create paginator
|
||||
paginator = Paginator(satellites, items_per_page)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Prepare data for display
|
||||
processed_satellites = []
|
||||
for satellite in page_obj:
|
||||
# Get band names
|
||||
band_names = [band.name for band in satellite.band.all()]
|
||||
|
||||
processed_satellites.append({
|
||||
'id': satellite.id,
|
||||
'name': satellite.name or "-",
|
||||
'norad': satellite.norad if satellite.norad else "-",
|
||||
'bands': ", ".join(band_names) if band_names else "-",
|
||||
'undersat_point': f"{satellite.undersat_point:.2f}" if satellite.undersat_point is not None else "-",
|
||||
'launch_date': satellite.launch_date.strftime("%d.%m.%Y") if satellite.launch_date else "-",
|
||||
'url': satellite.url or "-",
|
||||
'comment': satellite.comment or "-",
|
||||
'transponder_count': satellite.transponder_count,
|
||||
'created_at': satellite.created_at,
|
||||
'updated_at': satellite.updated_at,
|
||||
'created_by': satellite.created_by if satellite.created_by else "-",
|
||||
'updated_by': satellite.updated_by if satellite.updated_by else "-",
|
||||
})
|
||||
|
||||
# Prepare context for template
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'processed_satellites': processed_satellites,
|
||||
'items_per_page': items_per_page,
|
||||
'available_items_per_page': [50, 100, 500, 1000],
|
||||
'sort': sort_param,
|
||||
'search_query': search_query,
|
||||
'bands': bands,
|
||||
'selected_bands': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_bands
|
||||
if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'norad_min': norad_min,
|
||||
'norad_max': norad_max,
|
||||
'undersat_point_min': undersat_point_min,
|
||||
'undersat_point_max': undersat_point_max,
|
||||
'launch_date_from': launch_date_from,
|
||||
'launch_date_to': launch_date_to,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, "mainapp/satellite_list.html", context)
|
||||
|
||||
|
||||
class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
"""View for creating a new satellite."""
|
||||
|
||||
model = Satellite
|
||||
form_class = SatelliteForm
|
||||
template_name = "mainapp/satellite_form.html"
|
||||
success_url = reverse_lazy("mainapp:satellite_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 SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
"""View for updating an existing satellite."""
|
||||
|
||||
model = Satellite
|
||||
form_class = SatelliteForm
|
||||
template_name = "mainapp/satellite_form.html"
|
||||
success_url = reverse_lazy("mainapp:satellite_list")
|
||||
success_message = "Спутник успешно обновлен!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
import json
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['action'] = 'update'
|
||||
context['title'] = f'Редактирование спутника: {self.object.name}'
|
||||
|
||||
# Get transponders for this satellite for frequency plan
|
||||
from mapsapp.models import Transponders
|
||||
transponders = Transponders.objects.filter(
|
||||
sat_id=self.object
|
||||
).select_related('polarization').order_by('downlink')
|
||||
|
||||
# Prepare transponder data for frequency plan visualization
|
||||
transponder_data = []
|
||||
for t in transponders:
|
||||
if t.downlink and t.frequency_range:
|
||||
transponder_data.append({
|
||||
'id': t.id,
|
||||
'name': t.name or f"TP-{t.id}",
|
||||
'downlink': float(t.downlink),
|
||||
'frequency_range': float(t.frequency_range),
|
||||
'polarization': t.polarization.name if t.polarization else '-',
|
||||
'zone_name': t.zone_name or '-',
|
||||
})
|
||||
|
||||
context['transponders'] = json.dumps(transponder_data)
|
||||
context['transponder_count'] = len(transponder_data)
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.updated_by = self.request.user.customuser
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteSelectedSatellitesView(RoleRequiredMixin, View):
|
||||
"""View for deleting multiple selected satellites with confirmation."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about satellites to be deleted."""
|
||||
ids = request.GET.get("ids", "")
|
||||
if not ids:
|
||||
messages.error(request, "Не выбраны спутники для удаления")
|
||||
return redirect('mainapp:satellite_list')
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
satellites = Satellite.objects.filter(id__in=id_list).prefetch_related(
|
||||
'band'
|
||||
).annotate(
|
||||
transponder_count=Count('tran_satellite', distinct=True)
|
||||
)
|
||||
|
||||
# Prepare detailed information about satellites
|
||||
satellites_info = []
|
||||
total_transponders = 0
|
||||
|
||||
for satellite in satellites:
|
||||
transponder_count = satellite.transponder_count
|
||||
total_transponders += transponder_count
|
||||
|
||||
satellites_info.append({
|
||||
'id': satellite.id,
|
||||
'name': satellite.name or "-",
|
||||
'norad': satellite.norad if satellite.norad else "-",
|
||||
'transponder_count': transponder_count,
|
||||
})
|
||||
|
||||
context = {
|
||||
'satellites_info': satellites_info,
|
||||
'total_satellites': len(satellites_info),
|
||||
'total_transponders': total_transponders,
|
||||
'ids': ids,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/satellite_bulk_delete_confirm.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при подготовке удаления: {str(e)}')
|
||||
return redirect('mainapp:satellite_list')
|
||||
|
||||
def post(self, request):
|
||||
"""Actually delete the selected satellites."""
|
||||
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
|
||||
satellites = Satellite.objects.filter(id__in=id_list)
|
||||
deleted_count = satellites.count()
|
||||
|
||||
# Delete satellites (cascade will handle related objects)
|
||||
satellites.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