Сделал вкладку спутников
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2>Источники LyngSat</h2>
|
<h2>Данные по ИРИ с ресурса LyngSat</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ class KubsatFilterForm(forms.Form):
|
|||||||
"""Форма фильтров для страницы Кубсат"""
|
"""Форма фильтров для страницы Кубсат"""
|
||||||
|
|
||||||
satellites = forms.ModelMultipleChoiceField(
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
queryset=Satellite.objects.all().order_by('name'),
|
queryset=None, # Будет установлен в __init__
|
||||||
label='Спутники',
|
label='Спутники',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'})
|
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'})
|
||||||
@@ -617,7 +617,7 @@ class KubsatFilterForm(forms.Form):
|
|||||||
|
|
||||||
objitem_count = forms.ChoiceField(
|
objitem_count = forms.ChoiceField(
|
||||||
choices=[('', 'Все'), ('1', '1'), ('2+', '2 и более')],
|
choices=[('', 'Все'), ('1', '1'), ('2+', '2 и более')],
|
||||||
label='Количество привязанных ObjItem',
|
label='Количество привязанных точек ГЛ',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.RadioSelect()
|
widget=forms.RadioSelect()
|
||||||
)
|
)
|
||||||
@@ -658,7 +658,15 @@ class KubsatFilterForm(forms.Form):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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['band'].queryset = Band.objects.all().order_by('name')
|
||||||
self.fields['object_type'].queryset = ObjectInfo.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
|
cleaned_data['polarization'] = self.instance.polarization
|
||||||
|
|
||||||
return cleaned_data
|
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>
|
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
<!-- Успех 1 (фиктивный) -->
|
<!-- Успех 1 (фиктивный) -->
|
||||||
<div class="col-md-3 mb-3">
|
<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>
|
<div>
|
||||||
{% for radio in form.success_1 %}
|
{% for radio in form.success_1 %}
|
||||||
<div class="form-check">
|
<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,
|
DeleteSelectedObjectsView,
|
||||||
DeleteSelectedSourcesView,
|
DeleteSelectedSourcesView,
|
||||||
DeleteSelectedTranspondersView,
|
DeleteSelectedTranspondersView,
|
||||||
|
DeleteSelectedSatellitesView,
|
||||||
FillLyngsatDataView,
|
FillLyngsatDataView,
|
||||||
GeoPointsAPIView,
|
GeoPointsAPIView,
|
||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
@@ -31,6 +32,9 @@ from .views import (
|
|||||||
ObjItemUpdateView,
|
ObjItemUpdateView,
|
||||||
ProcessKubsatView,
|
ProcessKubsatView,
|
||||||
SatelliteDataAPIView,
|
SatelliteDataAPIView,
|
||||||
|
SatelliteListView,
|
||||||
|
SatelliteCreateView,
|
||||||
|
SatelliteUpdateView,
|
||||||
ShowMapView,
|
ShowMapView,
|
||||||
ShowSelectedObjectsMapView,
|
ShowSelectedObjectsMapView,
|
||||||
ShowSourcesMapView,
|
ShowSourcesMapView,
|
||||||
@@ -68,6 +72,10 @@ urlpatterns = [
|
|||||||
path('transponder/create/', TransponderCreateView.as_view(), name='transponder_create'),
|
path('transponder/create/', TransponderCreateView.as_view(), name='transponder_create'),
|
||||||
path('transponder/<int:pk>/edit/', TransponderUpdateView.as_view(), name='transponder_update'),
|
path('transponder/<int:pk>/edit/', TransponderUpdateView.as_view(), name='transponder_update'),
|
||||||
path('delete-selected-transponders/', DeleteSelectedTranspondersView.as_view(), name='delete_selected_transponders'),
|
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('actions/', ActionsPageView.as_view(), name='actions'),
|
||||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||||
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ from .transponder import (
|
|||||||
TransponderUpdateView,
|
TransponderUpdateView,
|
||||||
DeleteSelectedTranspondersView,
|
DeleteSelectedTranspondersView,
|
||||||
)
|
)
|
||||||
|
from .satellite import (
|
||||||
|
SatelliteListView,
|
||||||
|
SatelliteCreateView,
|
||||||
|
SatelliteUpdateView,
|
||||||
|
DeleteSelectedSatellitesView,
|
||||||
|
)
|
||||||
from .map import (
|
from .map import (
|
||||||
ShowMapView,
|
ShowMapView,
|
||||||
ShowSelectedObjectsMapView,
|
ShowSelectedObjectsMapView,
|
||||||
@@ -99,6 +105,11 @@ __all__ = [
|
|||||||
'TransponderCreateView',
|
'TransponderCreateView',
|
||||||
'TransponderUpdateView',
|
'TransponderUpdateView',
|
||||||
'DeleteSelectedTranspondersView',
|
'DeleteSelectedTranspondersView',
|
||||||
|
# Satellite
|
||||||
|
'SatelliteListView',
|
||||||
|
'SatelliteCreateView',
|
||||||
|
'SatelliteUpdateView',
|
||||||
|
'DeleteSelectedSatellitesView',
|
||||||
# Map
|
# Map
|
||||||
'ShowMapView',
|
'ShowMapView',
|
||||||
'ShowSelectedObjectsMapView',
|
'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