Исправил отображения объектов в источниках

This commit is contained in:
2025-11-16 00:16:50 +03:00
parent 9a816e62c2
commit d9cb243388
13 changed files with 1198 additions and 48 deletions

View File

@@ -0,0 +1,428 @@
{% extends 'mainapp/base.html' %}
{% block title %}Источники LyngSat{% 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>Источники LyngSat</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="Поиск по ID..."
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>
<!-- Filter Toggle Button -->
<div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
<!-- Pagination -->
<div class="ms-auto">
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offcanvas Filter Panel -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
</div>
<div class="offcanvas-body">
<form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Polarization Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization_id', false)">Снять</button>
</div>
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for polarization in polarizations %}
<option value="{{ polarization.id }}" {% if polarization.id in selected_polarizations %}selected{% endif %}>
{{ polarization.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Modulation 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('modulation_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation_id', false)">Снять</button>
</div>
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for modulation in modulations %}
<option value="{{ modulation.id }}" {% if modulation.id in selected_modulations %}selected{% endif %}>
{{ modulation.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Standard 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('standard_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('standard_id', false)">Снять</button>
</div>
<select name="standard_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for standard in standards %}
<option value="{{ standard.id }}" {% if standard.id in selected_standards %}selected{% endif %}>
{{ standard.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Символьная скорость, БОД:</label>
<input type="number" step="0.001" name="sym_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ sym_min|default:'' }}">
<input type="number" step="0.001" name="sym_max" class="form-control form-control-sm"
placeholder="До" value="{{ sym_max|default:'' }}">
</div>
<!-- Date Filter -->
<div class="mb-2">
<label class="form-label">Дата обновления:</label>
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ date_from|default:'' }}">
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
placeholder="До" value="{{ date_to|default:'' }}">
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
<!-- Main Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="min-width: 60px;">
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
ID
{% if sort == 'id' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-id' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 120px;">Спутник</th>
<th scope="col" style="min-width: 100px;">
<a href="javascript:void(0)" onclick="updateSort('frequency')" class="text-white text-decoration-none">
Частота, МГц
{% if sort == 'frequency' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-frequency' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Поляризация</th>
<th scope="col" style="min-width: 120px;">
<a href="javascript:void(0)" onclick="updateSort('sym_velocity')" class="text-white text-decoration-none">
Сим. скорость, БОД
{% if sort == 'sym_velocity' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-sym_velocity' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Модуляция</th>
<th scope="col" style="min-width: 100px;">Стандарт</th>
<th scope="col" style="min-width: 80px;">FEC</th>
<th scope="col" style="min-width: 150px;">Описание</th>
<th scope="col" style="min-width: 120px;">
<a href="javascript:void(0)" onclick="updateSort('last_update')" class="text-white text-decoration-none">
Обновлено
{% if sort == 'last_update' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-last_update' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;">Ссылка</th>
</tr>
</thead>
<tbody>
{% for item in lyngsat_items %}
<tr>
<td class="text-center">{{ item.id }}</td>
<td>{{ item.id_satellite.name|default:"-" }}</td>
<td>{{ item.frequency|floatformat:3|default:"-" }}</td>
<td>{{ item.polarization.name|default:"-" }}</td>
<td>{{ item.sym_velocity|floatformat:3|default:"-" }}</td>
<td>{{ item.modulation.name|default:"-" }}</td>
<td>{{ item.standard.name|default:"-" }}</td>
<td>{{ item.fec|default:"-" }}</td>
<td>{{ item.channel_info|default:"-" }}</td>
<td>{{ item.last_update|date:"d.m.Y"|default:"-" }}</td>
<td>
{% if item.url %}
<a href="{{ item.url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="Открыть ссылку">
<i class="bi bi-link-45deg"></i>
</a>
{% else %}
-
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 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();
}
// Handle Enter key in search input
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;
// Count non-empty form fields
for (const [key, value] of formData.entries()) {
if (value && value.trim() !== '') {
// For multi-select fields, skip counting individual selections
if (key === 'satellite_id' || key === 'polarization_id' || key === 'modulation_id' || key === 'standard_id') {
continue;
}
filterCount++;
}
}
// Count selected options in multi-select fields
const multiSelectFields = ['satellite_id', 'polarization_id', 'modulation_id', 'standard_id'];
multiSelectFields.forEach(fieldName => {
const selectElement = document.querySelector(`select[name="${fieldName}"]`);
if (selectElement) {
const selectedOptions = Array.from(selectElement.selectedOptions).filter(opt => opt.selected);
if (selectedOptions.length > 0) {
filterCount++;
}
}
});
// Display the filter counter
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() {
// Update filter counter on page load
updateFilterCounter();
// Add event listeners to form elements to update counter when filters change
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);
});
}
// Update counter when offcanvas is shown
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
});
</script>
{% endblock %}

8
dbapp/lyngsatapp/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name = 'lyngsatapp'
urlpatterns = [
path('', views.LyngSatListView.as_view(), name='lyngsat_list'),
]

View File

@@ -1,3 +1,147 @@
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q
from django.views.generic import ListView
# Create your views here.
from .models import LyngSat
from mainapp.models import Satellite, Polarization, Modulation, Standard
from mainapp.utils import parse_pagination_params
class LyngSatListView(LoginRequiredMixin, ListView):
"""
Представление для отображения списка источников LyngSat с фильтрацией и пагинацией.
"""
model = LyngSat
template_name = 'lyngsatapp/lyngsat_list.html'
context_object_name = 'lyngsat_items'
paginate_by = 50
def get_queryset(self):
"""
Возвращает отфильтрованный и отсортированный queryset.
"""
queryset = LyngSat.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).all()
# Поиск по ID
search_query = self.request.GET.get('search', '').strip()
if search_query:
try:
search_id = int(search_query)
queryset = queryset.filter(id=search_id)
except ValueError:
queryset = queryset.none()
# Фильтр по спутнику
satellite_ids = self.request.GET.getlist('satellite_id')
if satellite_ids:
queryset = queryset.filter(id_satellite_id__in=satellite_ids)
# Фильтр по поляризации
polarization_ids = self.request.GET.getlist('polarization_id')
if polarization_ids:
queryset = queryset.filter(polarization_id__in=polarization_ids)
# Фильтр по модуляции
modulation_ids = self.request.GET.getlist('modulation_id')
if modulation_ids:
queryset = queryset.filter(modulation_id__in=modulation_ids)
# Фильтр по стандарту
standard_ids = self.request.GET.getlist('standard_id')
if standard_ids:
queryset = queryset.filter(standard_id__in=standard_ids)
# Фильтр по частоте
freq_min = self.request.GET.get('freq_min', '').strip()
freq_max = self.request.GET.get('freq_max', '').strip()
if freq_min:
try:
queryset = queryset.filter(frequency__gte=float(freq_min))
except ValueError:
pass
if freq_max:
try:
queryset = queryset.filter(frequency__lte=float(freq_max))
except ValueError:
pass
# Фильтр по символьной скорости
sym_min = self.request.GET.get('sym_min', '').strip()
sym_max = self.request.GET.get('sym_max', '').strip()
if sym_min:
try:
queryset = queryset.filter(sym_velocity__gte=float(sym_min))
except ValueError:
pass
if sym_max:
try:
queryset = queryset.filter(sym_velocity__lte=float(sym_max))
except ValueError:
pass
# Фильтр по дате обновления
date_from = self.request.GET.get('date_from', '').strip()
date_to = self.request.GET.get('date_to', '').strip()
if date_from:
queryset = queryset.filter(last_update__gte=date_from)
if date_to:
queryset = queryset.filter(last_update__lte=date_to)
# Сортировка
sort = self.request.GET.get('sort', '-id')
valid_sort_fields = ['id', '-id', 'frequency', '-frequency', 'sym_velocity', '-sym_velocity', 'last_update', '-last_update']
if sort in valid_sort_fields:
queryset = queryset.order_by(sort)
else:
queryset = queryset.order_by('-id')
return queryset
def get_context_data(self, **kwargs):
"""
Добавляет дополнительный контекст для шаблона.
"""
context = super().get_context_data(**kwargs)
# Параметры пагинации
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
context['items_per_page'] = items_per_page
context['available_items_per_page'] = [25, 50, 100, 200, 500]
# Пагинация
paginator = Paginator(self.get_queryset(), items_per_page)
page_obj = paginator.get_page(page_number)
context['page_obj'] = page_obj
context['lyngsat_items'] = page_obj.object_list
# Параметры поиска и фильтрации
context['search_query'] = self.request.GET.get('search', '')
context['sort'] = self.request.GET.get('sort', '-id')
# Данные для фильтров
context['satellites'] = Satellite.objects.all().order_by('name')
context['polarizations'] = Polarization.objects.all().order_by('name')
context['modulations'] = Modulation.objects.all().order_by('name')
context['standards'] = Standard.objects.all().order_by('name')
# Выбранные фильтры
context['selected_satellites'] = [int(x) for x in self.request.GET.getlist('satellite_id') if x.isdigit()]
context['selected_polarizations'] = [int(x) for x in self.request.GET.getlist('polarization_id') if x.isdigit()]
context['selected_modulations'] = [int(x) for x in self.request.GET.getlist('modulation_id') if x.isdigit()]
context['selected_standards'] = [int(x) for x in self.request.GET.getlist('standard_id') if x.isdigit()]
# Параметры фильтров
context['freq_min'] = self.request.GET.get('freq_min', '')
context['freq_max'] = self.request.GET.get('freq_max', '')
context['sym_min'] = self.request.GET.get('sym_min', '')
context['sym_max'] = self.request.GET.get('sym_max', '')
context['date_from'] = self.request.GET.get('date_from', '')
context['date_to'] = self.request.GET.get('date_to', '')
return context