Добавил разделение по исчтоника и поправил функцию импорта из Excel csv
This commit is contained in:
453
dbapp/mainapp/templates/mainapp/source_list.html
Normal file
453
dbapp/mainapp/templates/mainapp/source_list.html
Normal file
@@ -0,0 +1,453 @@
|
||||
{% 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="Поиск по 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> Фильтры
|
||||
</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">
|
||||
<!-- Coordinates Average Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Усредненные координаты:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_1"
|
||||
value="1" {% if has_coords_average == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_average_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_0"
|
||||
value="0" {% if has_coords_average == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_average_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kubsat Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты Кубсата:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_1"
|
||||
value="1" {% if has_coords_kupsat == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_kupsat_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_0"
|
||||
value="0" {% if has_coords_kupsat == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_kupsat_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valid Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты оперативников:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_1"
|
||||
value="1" {% if has_coords_valid == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_valid_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_0"
|
||||
value="0" {% if has_coords_valid == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_valid_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты справочные:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_1"
|
||||
value="1" {% if has_coords_reference == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_reference_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_0"
|
||||
value="0" {% if has_coords_reference == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_coords_reference_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ObjItem Count Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество ObjItem:</label>
|
||||
<input type="number" name="objitem_count_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ objitem_count_min|default:'' }}">
|
||||
<input type="number" name="objitem_count_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ objitem_count_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: 150px;">Усредненные координаты</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты оперативников</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||
Кол-во ObjItem
|
||||
{% if sort == 'objitem_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-objitem_count' %}
|
||||
<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 source in processed_sources %}
|
||||
<tr>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td>{{ source.coords_average }}</td>
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
<td>{{ source.coords_valid }}</td>
|
||||
<td>{{ source.coords_reference }}</td>
|
||||
<td class="text-center">{{ source.objitem_count }}</td>
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})">
|
||||
<i class="bi bi-eye"></i> Показать
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Source Details -->
|
||||
<div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника #<span id="modalSourceId"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="modalLoadingSpinner" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||
<div id="modalContent" style="display: none;">
|
||||
<h6>Связанные объекты (<span id="objitemCount">0</span>):</h6>
|
||||
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Спутник</th>
|
||||
<th>Частота, МГц</th>
|
||||
<th>Полоса, МГц</th>
|
||||
<th>Поляризация</th>
|
||||
<th>Сим. скорость, БОД</th>
|
||||
<th>Модуляция</th>
|
||||
<th>ОСШ</th>
|
||||
<th>Время ГЛ</th>
|
||||
<th>Местоположение</th>
|
||||
<th>Координаты ГЛ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="objitemTableBody">
|
||||
<!-- Data will be loaded here via AJAX -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
|
||||
Нет связанных объектов
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</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();
|
||||
}
|
||||
|
||||
// Show source details in modal
|
||||
function showSourceDetails(sourceId) {
|
||||
// Update modal title
|
||||
document.getElementById('modalSourceId').textContent = sourceId;
|
||||
|
||||
// Show loading spinner, hide content and error
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'block';
|
||||
document.getElementById('modalContent').style.display = 'none';
|
||||
document.getElementById('modalNoData').style.display = 'none';
|
||||
document.getElementById('modalErrorMessage').style.display = 'none';
|
||||
|
||||
// Open modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
|
||||
modal.show();
|
||||
|
||||
// Fetch data from API
|
||||
fetch(`/api/source/${sourceId}/objitems/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Источник не найден');
|
||||
} else {
|
||||
throw new Error('Ошибка при загрузке данных');
|
||||
}
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Hide loading spinner
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||
|
||||
if (data.objitems && data.objitems.length > 0) {
|
||||
// Show content
|
||||
document.getElementById('modalContent').style.display = 'block';
|
||||
document.getElementById('objitemCount').textContent = data.objitems.length;
|
||||
|
||||
// Populate table
|
||||
const tbody = document.getElementById('objitemTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.objitems.forEach(objitem => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${objitem.name}</td>
|
||||
<td>${objitem.satellite_name}</td>
|
||||
<td>${objitem.frequency}</td>
|
||||
<td>${objitem.freq_range}</td>
|
||||
<td>${objitem.polarization}</td>
|
||||
<td>${objitem.bod_velocity}</td>
|
||||
<td>${objitem.modulation}</td>
|
||||
<td>${objitem.snr}</td>
|
||||
<td>${objitem.geo_timestamp}</td>
|
||||
<td>${objitem.geo_location}</td>
|
||||
<td>${objitem.geo_coords}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
// Show no data message
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide loading spinner
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||
|
||||
// Show error message
|
||||
const errorDiv = document.getElementById('modalErrorMessage');
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user