Поменял теханализ, улучшения по простбам
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<!-- Frequency Plan Modal -->
|
||||
<div class="modal fade" id="frequencyPlanModal" tabindex="-1" aria-labelledby="frequencyPlanModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="frequencyPlanModalLabel">Частотный план</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-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalFrequencyContent" style="display: none;">
|
||||
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;">■</span> Downlink (синий), <span style="color: #fd7e14;">■</span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации и связи с парным каналом.</p>
|
||||
|
||||
<div class="frequency-plan">
|
||||
<div class="chart-controls">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="modalResetZoom">
|
||||
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="frequency-chart-container">
|
||||
<canvas id="modalFrequencyChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<p><strong>Всего транспондеров:</strong> <span id="modalTransponderCount">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalNoData" style="display: none;" class="text-center text-muted py-5">
|
||||
<p>Нет данных о транспондерах для этого спутника</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.frequency-plan {
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.frequency-chart-container {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-controls button {
|
||||
padding: 5px 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
@@ -53,9 +53,9 @@
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
|
||||
<!-- <a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
|
||||
<i class="bi bi-clipboard-data"></i> Тех. анализ
|
||||
</a>
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<!-- Items per page select moved here -->
|
||||
|
||||
@@ -275,11 +275,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
layout: "fitDataStretch",
|
||||
height: "500px",
|
||||
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'.",
|
||||
initialSort: [
|
||||
{column: "frequency", dir: "asc"}
|
||||
],
|
||||
columns: [
|
||||
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2},
|
||||
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
|
||||
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
|
||||
{title: "Частота", field: "frequency", minWidth: 100},
|
||||
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
|
||||
{title: "Модуляция", field: "modulation", minWidth: 90},
|
||||
{title: "Зеркала", field: "mirrors", minWidth: 130},
|
||||
{
|
||||
@@ -624,14 +627,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'ОСШ': group.snr,
|
||||
'Зеркала': group.mirrors,
|
||||
'Усреднённые координаты': group.avg_coordinates,
|
||||
'Тип усреднения': group.avg_type || 'ГК',
|
||||
'Медианное время': group.avg_time || '-',
|
||||
'Кол-во точек': group.valid_points_count,
|
||||
'Интервал': group.interval_label
|
||||
// 'Тип усреднения': group.avg_type || 'ГК',
|
||||
'Время': group.avg_time || '-',
|
||||
'Кол-во точек': group.valid_points_count
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by frequency
|
||||
summaryData.sort((a, b) => {
|
||||
const freqA = parseFloat(a['Частота, МГц']) || 0;
|
||||
const freqB = parseFloat(b['Частота, МГц']) || 0;
|
||||
return freqA - freqB;
|
||||
});
|
||||
|
||||
const allPointsData = [];
|
||||
allSourcesData.forEach(source => {
|
||||
source.groups.forEach(group => {
|
||||
@@ -651,13 +660,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'Координаты точки': point.coordinates,
|
||||
'Усреднённые координаты': group.avg_coordinates,
|
||||
'Расстояние от среднего, км': point.distance_from_avg,
|
||||
'Статус': point.is_outlier ? 'Выброс' : 'OK',
|
||||
'Интервал': group.interval_label
|
||||
'Статус': point.is_outlier ? 'Выброс' : 'OK'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by frequency
|
||||
allPointsData.sort((a, b) => {
|
||||
const freqA = parseFloat(a['Частота, МГц']) || 0;
|
||||
const freqB = parseFloat(b['Частота, МГц']) || 0;
|
||||
return freqA - freqB;
|
||||
});
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(summaryData), "Усреднение");
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(allPointsData), "Все точки");
|
||||
@@ -737,41 +752,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
"tags": {"layers": [], "creator": CREATOR_ID}
|
||||
});
|
||||
|
||||
group.points.forEach(point => {
|
||||
if (point.is_outlier) return;
|
||||
// group.points.forEach(point => {
|
||||
// if (point.is_outlier) return;
|
||||
|
||||
const pointCoord = point.coord_tuple;
|
||||
const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
|
||||
const pointSourceId = generateUUID();
|
||||
// const pointCoord = point.coord_tuple;
|
||||
// const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
|
||||
// const pointSourceId = generateUUID();
|
||||
|
||||
result.push({
|
||||
"tacticObjectType": "source",
|
||||
"captionPosition": "right",
|
||||
"id": pointSourceId,
|
||||
"icon": {"type": "circle", "color": sourceColor},
|
||||
"caption": pointCaption,
|
||||
"name": pointCaption,
|
||||
"customActions": [],
|
||||
"trackBehavior": {},
|
||||
"bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
||||
"bearingBehavior": {},
|
||||
"tags": {"creator": CREATOR_ID}
|
||||
});
|
||||
// result.push({
|
||||
// "tacticObjectType": "source",
|
||||
// "captionPosition": "right",
|
||||
// "id": pointSourceId,
|
||||
// "icon": {"type": "circle", "color": sourceColor},
|
||||
// "caption": pointCaption,
|
||||
// "name": pointCaption,
|
||||
// "customActions": [],
|
||||
// "trackBehavior": {},
|
||||
// "bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
||||
// "bearingBehavior": {},
|
||||
// "tags": {"creator": CREATOR_ID}
|
||||
// });
|
||||
|
||||
result.push({
|
||||
"tacticObjectType": "position",
|
||||
"id": generateUUID(),
|
||||
"parentId": pointSourceId,
|
||||
"timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
||||
"latitude": pointCoord[1],
|
||||
"altitude": 0,
|
||||
"longitude": pointCoord[0],
|
||||
"caption": "",
|
||||
"tooltip": "",
|
||||
"customActions": [],
|
||||
"tags": {"layers": [], "creator": CREATOR_ID}
|
||||
});
|
||||
});
|
||||
// result.push({
|
||||
// "tacticObjectType": "position",
|
||||
// "id": generateUUID(),
|
||||
// "parentId": pointSourceId,
|
||||
// "timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
||||
// "latitude": pointCoord[1],
|
||||
// "altitude": 0,
|
||||
// "longitude": pointCoord[0],
|
||||
// "caption": "",
|
||||
// "tooltip": "",
|
||||
// "customActions": [],
|
||||
// "tags": {"layers": [], "creator": CREATOR_ID}
|
||||
// });
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -303,17 +303,26 @@
|
||||
<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 %}
|
||||
<div class="d-flex gap-1 justify-content-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 %}
|
||||
{% if satellite.transponder_count > 0 %}
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="showFrequencyPlan({{ satellite.id }})"
|
||||
title="Частотный план">
|
||||
<i class="bi bi-bar-chart"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -330,6 +339,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'mainapp/components/_frequency_plan_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
@@ -526,6 +537,607 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
});
|
||||
|
||||
// Frequency Plan Modal functionality
|
||||
let modalCanvas, modalCtx, modalContainer;
|
||||
let modalZoomLevelUL = 1;
|
||||
let modalZoomLevelDL = 1;
|
||||
let modalPanOffsetUL = 0;
|
||||
let modalPanOffsetDL = 0;
|
||||
let modalIsDragging = false;
|
||||
let modalDragStartX = 0;
|
||||
let modalDragStartOffsetUL = 0;
|
||||
let modalDragStartOffsetDL = 0;
|
||||
let modalDragArea = null;
|
||||
let modalHoveredTransponder = null;
|
||||
let modalTransponderRects = [];
|
||||
let modalTranspondersData = [];
|
||||
|
||||
let modalMinFreqUL, modalMaxFreqUL, modalFreqRangeUL;
|
||||
let modalMinFreqDL, modalMaxFreqDL, modalFreqRangeDL;
|
||||
let modalOriginalMinFreqUL, modalOriginalMaxFreqUL, modalOriginalFreqRangeUL;
|
||||
let modalOriginalMinFreqDL, modalOriginalMaxFreqDL, modalOriginalFreqRangeDL;
|
||||
|
||||
let modalUplinkStartY, modalUplinkHeight, modalDownlinkStartY, modalDownlinkHeight;
|
||||
|
||||
function showFrequencyPlan(satelliteId) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('frequencyPlanModal'));
|
||||
|
||||
// Reset modal state
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'block';
|
||||
document.getElementById('modalFrequencyContent').style.display = 'none';
|
||||
document.getElementById('modalNoData').style.display = 'none';
|
||||
|
||||
modal.show();
|
||||
|
||||
// Fetch transponder data
|
||||
fetch(`/api/satellite/${satelliteId}/transponders/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||
|
||||
if (data.transponders && data.transponders.length > 0) {
|
||||
modalTranspondersData = data.transponders;
|
||||
document.getElementById('modalTransponderCount').textContent = data.count;
|
||||
document.getElementById('modalFrequencyContent').style.display = 'block';
|
||||
|
||||
// Initialize chart after modal is shown
|
||||
setTimeout(() => {
|
||||
initializeModalFrequencyChart();
|
||||
}, 100);
|
||||
} else {
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching transponder data:', error);
|
||||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
function initializeModalFrequencyChart() {
|
||||
if (!modalTranspondersData || modalTranspondersData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalCanvas = document.getElementById('modalFrequencyChart');
|
||||
if (!modalCanvas) return;
|
||||
|
||||
modalContainer = modalCanvas.parentElement;
|
||||
modalCtx = modalCanvas.getContext('2d');
|
||||
|
||||
// Calculate frequency ranges
|
||||
modalMinFreqUL = Infinity;
|
||||
modalMaxFreqUL = -Infinity;
|
||||
modalMinFreqDL = Infinity;
|
||||
modalMaxFreqDL = -Infinity;
|
||||
|
||||
modalTranspondersData.forEach(t => {
|
||||
const dlStartFreq = t.downlink - (t.frequency_range / 2);
|
||||
const dlEndFreq = t.downlink + (t.frequency_range / 2);
|
||||
modalMinFreqDL = Math.min(modalMinFreqDL, dlStartFreq);
|
||||
modalMaxFreqDL = Math.max(modalMaxFreqDL, dlEndFreq);
|
||||
|
||||
if (t.uplink) {
|
||||
const ulStartFreq = t.uplink - (t.frequency_range / 2);
|
||||
const ulEndFreq = t.uplink + (t.frequency_range / 2);
|
||||
modalMinFreqUL = Math.min(modalMinFreqUL, ulStartFreq);
|
||||
modalMaxFreqUL = Math.max(modalMaxFreqUL, ulEndFreq);
|
||||
}
|
||||
});
|
||||
|
||||
// Add padding
|
||||
const paddingDL = (modalMaxFreqDL - modalMinFreqDL) * 0.04;
|
||||
modalMinFreqDL -= paddingDL;
|
||||
modalMaxFreqDL += paddingDL;
|
||||
|
||||
if (modalMaxFreqUL !== -Infinity) {
|
||||
const paddingUL = (modalMaxFreqUL - modalMinFreqUL) * 0.04;
|
||||
modalMinFreqUL -= paddingUL;
|
||||
modalMaxFreqUL += paddingUL;
|
||||
}
|
||||
|
||||
// Store original values
|
||||
modalOriginalMinFreqDL = modalMinFreqDL;
|
||||
modalOriginalMaxFreqDL = modalMaxFreqDL;
|
||||
modalOriginalFreqRangeDL = modalMaxFreqDL - modalMinFreqDL;
|
||||
modalFreqRangeDL = modalOriginalFreqRangeDL;
|
||||
|
||||
modalOriginalMinFreqUL = modalMinFreqUL;
|
||||
modalOriginalMaxFreqUL = modalMaxFreqUL;
|
||||
modalOriginalFreqRangeUL = modalMaxFreqUL - modalMinFreqUL;
|
||||
modalFreqRangeUL = modalOriginalFreqRangeUL;
|
||||
|
||||
// Reset zoom and pan
|
||||
modalZoomLevelUL = 1;
|
||||
modalZoomLevelDL = 1;
|
||||
modalPanOffsetUL = 0;
|
||||
modalPanOffsetDL = 0;
|
||||
|
||||
// Setup event listeners
|
||||
modalCanvas.addEventListener('wheel', handleModalWheel, { passive: false });
|
||||
modalCanvas.addEventListener('mousedown', handleModalMouseDown);
|
||||
modalCanvas.addEventListener('mousemove', handleModalMouseMove);
|
||||
modalCanvas.addEventListener('mouseup', handleModalMouseUp);
|
||||
modalCanvas.addEventListener('mouseleave', handleModalMouseLeave);
|
||||
|
||||
renderModalChart();
|
||||
}
|
||||
|
||||
function renderModalChart() {
|
||||
if (!modalCanvas || !modalCtx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = modalContainer.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
modalCanvas.width = width * dpr;
|
||||
modalCanvas.height = height * dpr;
|
||||
modalCanvas.style.width = width + 'px';
|
||||
modalCanvas.style.height = height + 'px';
|
||||
modalCtx.scale(dpr, dpr);
|
||||
|
||||
modalCtx.clearRect(0, 0, width, height);
|
||||
|
||||
const leftMargin = 60;
|
||||
const rightMargin = 20;
|
||||
const topMargin = 60;
|
||||
const middleMargin = 60;
|
||||
const bottomMargin = 40;
|
||||
const chartWidth = width - leftMargin - rightMargin;
|
||||
const availableHeight = height - topMargin - middleMargin - bottomMargin;
|
||||
|
||||
modalUplinkHeight = availableHeight * 0.48;
|
||||
modalDownlinkHeight = availableHeight * 0.48;
|
||||
|
||||
// Group by polarization
|
||||
const polarizationGroups = {};
|
||||
modalTranspondersData.forEach(t => {
|
||||
let pol = t.polarization || '-';
|
||||
pol = pol.charAt(0).toUpperCase();
|
||||
if (!polarizationGroups[pol]) {
|
||||
polarizationGroups[pol] = [];
|
||||
}
|
||||
polarizationGroups[pol].push(t);
|
||||
});
|
||||
|
||||
const polarizations = Object.keys(polarizationGroups);
|
||||
const rowHeightUL = modalUplinkHeight / polarizations.length;
|
||||
const rowHeightDL = modalDownlinkHeight / polarizations.length;
|
||||
|
||||
// Calculate visible ranges
|
||||
const visibleFreqRangeUL = modalFreqRangeUL / modalZoomLevelUL;
|
||||
const centerFreqUL = (modalMinFreqUL + modalMaxFreqUL) / 2;
|
||||
const visibleMinFreqUL = centerFreqUL - visibleFreqRangeUL / 2 + modalPanOffsetUL;
|
||||
const visibleMaxFreqUL = centerFreqUL + visibleFreqRangeUL / 2 + modalPanOffsetUL;
|
||||
|
||||
const visibleFreqRangeDL = modalFreqRangeDL / modalZoomLevelDL;
|
||||
const centerFreqDL = (modalMinFreqDL + modalMaxFreqDL) / 2;
|
||||
const visibleMinFreqDL = centerFreqDL - visibleFreqRangeDL / 2 + modalPanOffsetDL;
|
||||
const visibleMaxFreqDL = centerFreqDL + visibleFreqRangeDL / 2 + modalPanOffsetDL;
|
||||
|
||||
modalUplinkStartY = topMargin;
|
||||
modalDownlinkStartY = topMargin + modalUplinkHeight + middleMargin;
|
||||
|
||||
// Draw UPLINK axis
|
||||
modalCtx.strokeStyle = '#dee2e6';
|
||||
modalCtx.lineWidth = 1;
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(leftMargin, modalUplinkStartY);
|
||||
modalCtx.lineTo(width - rightMargin, modalUplinkStartY);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.fillStyle = '#6c757d';
|
||||
modalCtx.font = '11px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
|
||||
const numTicks = 10;
|
||||
for (let i = 0; i <= numTicks; i++) {
|
||||
const freq = visibleMinFreqUL + (visibleMaxFreqUL - visibleMinFreqUL) * i / numTicks;
|
||||
const x = leftMargin + chartWidth * i / numTicks;
|
||||
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(x, modalUplinkStartY);
|
||||
modalCtx.lineTo(x, modalUplinkStartY - 5);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(x, modalUplinkStartY);
|
||||
modalCtx.lineTo(x, modalUplinkStartY + modalUplinkHeight);
|
||||
modalCtx.stroke();
|
||||
modalCtx.strokeStyle = '#dee2e6';
|
||||
|
||||
modalCtx.fillText(freq.toFixed(1), x, modalUplinkStartY - 10);
|
||||
}
|
||||
|
||||
modalCtx.fillStyle = '#000';
|
||||
modalCtx.font = 'bold 12px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText('Uplink Частота (МГц)', width / 2, modalUplinkStartY - 25);
|
||||
|
||||
// Draw DOWNLINK axis
|
||||
modalCtx.strokeStyle = '#dee2e6';
|
||||
modalCtx.lineWidth = 1;
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(leftMargin, modalDownlinkStartY);
|
||||
modalCtx.lineTo(width - rightMargin, modalDownlinkStartY);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.fillStyle = '#6c757d';
|
||||
modalCtx.font = '11px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
|
||||
for (let i = 0; i <= numTicks; i++) {
|
||||
const freq = visibleMinFreqDL + (visibleMaxFreqDL - visibleMinFreqDL) * i / numTicks;
|
||||
const x = leftMargin + chartWidth * i / numTicks;
|
||||
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(x, modalDownlinkStartY);
|
||||
modalCtx.lineTo(x, modalDownlinkStartY - 5);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(x, modalDownlinkStartY);
|
||||
modalCtx.lineTo(x, modalDownlinkStartY + modalDownlinkHeight);
|
||||
modalCtx.stroke();
|
||||
modalCtx.strokeStyle = '#dee2e6';
|
||||
|
||||
modalCtx.fillText(freq.toFixed(1), x, modalDownlinkStartY - 10);
|
||||
}
|
||||
|
||||
modalCtx.fillStyle = '#000';
|
||||
modalCtx.font = 'bold 12px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText('Downlink Частота (МГц)', width / 2, modalDownlinkStartY - 25);
|
||||
|
||||
modalCtx.save();
|
||||
modalCtx.translate(15, height / 2);
|
||||
modalCtx.rotate(-Math.PI / 2);
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText('Поляризация', 0, 0);
|
||||
modalCtx.restore();
|
||||
|
||||
modalTransponderRects = [];
|
||||
|
||||
// Draw transponders
|
||||
polarizations.forEach((pol, index) => {
|
||||
const group = polarizationGroups[pol];
|
||||
const downlinkColor = '#0d6efd';
|
||||
const uplinkColor = '#fd7e14';
|
||||
|
||||
const uplinkY = modalUplinkStartY + index * rowHeightUL;
|
||||
const uplinkBarHeight = rowHeightUL * 0.8;
|
||||
const uplinkBarY = uplinkY + (rowHeightUL - uplinkBarHeight) / 2;
|
||||
|
||||
const downlinkY = modalDownlinkStartY + index * rowHeightDL;
|
||||
const downlinkBarHeight = rowHeightDL * 0.8;
|
||||
const downlinkBarY = downlinkY + (rowHeightDL - downlinkBarHeight) / 2;
|
||||
|
||||
modalCtx.fillStyle = '#000';
|
||||
modalCtx.font = 'bold 14px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText(pol, leftMargin - 25, uplinkBarY + uplinkBarHeight / 2 + 4);
|
||||
modalCtx.fillText(pol, leftMargin - 25, downlinkBarY + downlinkBarHeight / 2 + 4);
|
||||
|
||||
if (index < polarizations.length - 1) {
|
||||
modalCtx.strokeStyle = '#adb5bd';
|
||||
modalCtx.lineWidth = 1;
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(leftMargin, uplinkY + rowHeightUL);
|
||||
modalCtx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(leftMargin, downlinkY + rowHeightDL);
|
||||
modalCtx.lineTo(width - rightMargin, downlinkY + rowHeightDL);
|
||||
modalCtx.stroke();
|
||||
}
|
||||
|
||||
// Draw uplink transponders
|
||||
group.forEach(t => {
|
||||
if (!t.uplink) return;
|
||||
|
||||
const startFreq = t.uplink - (t.frequency_range / 2);
|
||||
const endFreq = t.uplink + (t.frequency_range / 2);
|
||||
|
||||
if (endFreq < visibleMinFreqUL || startFreq > visibleMaxFreqUL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x1 = leftMargin + ((startFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||
const x2 = leftMargin + ((endFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||
const barWidth = x2 - x1;
|
||||
|
||||
if (barWidth < 1) return;
|
||||
|
||||
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
|
||||
|
||||
modalCtx.fillStyle = uplinkColor;
|
||||
modalCtx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||
|
||||
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||
modalCtx.lineWidth = isHovered ? 3 : 1;
|
||||
modalCtx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||
|
||||
if (barWidth > 40) {
|
||||
modalCtx.fillStyle = '#fff';
|
||||
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||
}
|
||||
|
||||
modalTransponderRects.push({
|
||||
x: x1,
|
||||
y: uplinkBarY,
|
||||
width: barWidth,
|
||||
height: uplinkBarHeight,
|
||||
transponder: t,
|
||||
type: 'uplink',
|
||||
centerX: x1 + barWidth / 2
|
||||
});
|
||||
});
|
||||
|
||||
// Draw downlink transponders
|
||||
group.forEach(t => {
|
||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||
|
||||
if (endFreq < visibleMinFreqDL || startFreq > visibleMaxFreqDL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x1 = leftMargin + ((startFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
|
||||
const x2 = leftMargin + ((endFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
|
||||
const barWidth = x2 - x1;
|
||||
|
||||
if (barWidth < 1) return;
|
||||
|
||||
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
|
||||
|
||||
modalCtx.fillStyle = downlinkColor;
|
||||
modalCtx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||
|
||||
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||
modalCtx.lineWidth = isHovered ? 3 : 1;
|
||||
modalCtx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||
|
||||
if (barWidth > 40) {
|
||||
modalCtx.fillStyle = '#fff';
|
||||
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||
modalCtx.textAlign = 'center';
|
||||
modalCtx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||
}
|
||||
|
||||
modalTransponderRects.push({
|
||||
x: x1,
|
||||
y: downlinkBarY,
|
||||
width: barWidth,
|
||||
height: downlinkBarHeight,
|
||||
transponder: t,
|
||||
type: 'downlink',
|
||||
centerX: x1 + barWidth / 2
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (modalHoveredTransponder) {
|
||||
drawModalConnectionLine(modalHoveredTransponder);
|
||||
drawModalTooltip(modalHoveredTransponder);
|
||||
}
|
||||
}
|
||||
|
||||
function drawModalConnectionLine(rectInfo) {
|
||||
const t = rectInfo.transponder;
|
||||
if (!t.uplink) return;
|
||||
|
||||
const downlinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'downlink');
|
||||
const uplinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'uplink');
|
||||
|
||||
if (!downlinkRect || !uplinkRect) return;
|
||||
|
||||
const x1 = downlinkRect.centerX;
|
||||
const y1 = downlinkRect.y + downlinkRect.height;
|
||||
const x2 = uplinkRect.centerX;
|
||||
const y2 = uplinkRect.y;
|
||||
|
||||
modalCtx.save();
|
||||
modalCtx.strokeStyle = '#ffc107';
|
||||
modalCtx.lineWidth = 2;
|
||||
modalCtx.setLineDash([5, 3]);
|
||||
modalCtx.globalAlpha = 0.8;
|
||||
|
||||
modalCtx.beginPath();
|
||||
modalCtx.moveTo(x1, y1);
|
||||
modalCtx.lineTo(x2, y2);
|
||||
modalCtx.stroke();
|
||||
|
||||
modalCtx.restore();
|
||||
}
|
||||
|
||||
function drawModalTooltip(rectInfo) {
|
||||
const t = rectInfo.transponder;
|
||||
const isUplink = rectInfo.type === 'uplink';
|
||||
const freq = isUplink ? t.uplink : t.downlink;
|
||||
const startFreq = freq - (t.frequency_range / 2);
|
||||
const endFreq = freq + (t.frequency_range / 2);
|
||||
|
||||
const lines = [
|
||||
t.name,
|
||||
'Тип: ' + (isUplink ? 'Uplink' : 'Downlink'),
|
||||
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
|
||||
'Центр: ' + freq.toFixed(3) + ' МГц',
|
||||
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
|
||||
'Поляризация: ' + t.polarization,
|
||||
'Зона: ' + t.zone_name
|
||||
];
|
||||
|
||||
if (isUplink && t.downlink && t.uplink) {
|
||||
const conversion = t.downlink - t.uplink;
|
||||
lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц');
|
||||
}
|
||||
|
||||
modalCtx.font = '12px sans-serif';
|
||||
const padding = 10;
|
||||
const lineHeight = 16;
|
||||
let maxWidth = 0;
|
||||
lines.forEach(line => {
|
||||
const width = modalCtx.measureText(line).width;
|
||||
maxWidth = Math.max(maxWidth, width);
|
||||
});
|
||||
|
||||
const tooltipWidth = maxWidth + padding * 2;
|
||||
const tooltipHeight = lines.length * lineHeight + padding * 2;
|
||||
|
||||
const mouseX = rectInfo._mouseX || modalCanvas.width / 2;
|
||||
const mouseY = rectInfo._mouseY || modalCanvas.height / 2;
|
||||
let tooltipX = mouseX + 15;
|
||||
let tooltipY = mouseY - tooltipHeight - 15;
|
||||
|
||||
if (tooltipX + tooltipWidth > modalCanvas.width) {
|
||||
tooltipX = mouseX - tooltipWidth - 15;
|
||||
}
|
||||
|
||||
if (tooltipY < 0) {
|
||||
tooltipY = mouseY + 15;
|
||||
}
|
||||
|
||||
modalCtx.fillStyle = 'rgba(0, 0, 0, 0.9)';
|
||||
modalCtx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
|
||||
|
||||
modalCtx.fillStyle = '#fff';
|
||||
modalCtx.font = 'bold 12px sans-serif';
|
||||
modalCtx.textAlign = 'left';
|
||||
modalCtx.fillText(lines[0], tooltipX + padding, tooltipY + padding + 12);
|
||||
|
||||
modalCtx.font = '11px sans-serif';
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
modalCtx.fillText(lines[i], tooltipX + padding, tooltipY + padding + 12 + i * lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalWheel(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const rect = modalCanvas.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
const isUplinkArea = mouseY < (modalUplinkStartY + modalUplinkHeight);
|
||||
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
|
||||
if (isUplinkArea) {
|
||||
const newZoom = Math.max(1, Math.min(20, modalZoomLevelUL * delta));
|
||||
if (newZoom !== modalZoomLevelUL) {
|
||||
modalZoomLevelUL = newZoom;
|
||||
|
||||
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
|
||||
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
|
||||
|
||||
renderModalChart();
|
||||
}
|
||||
} else {
|
||||
const newZoom = Math.max(1, Math.min(20, modalZoomLevelDL * delta));
|
||||
if (newZoom !== modalZoomLevelDL) {
|
||||
modalZoomLevelDL = newZoom;
|
||||
|
||||
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
|
||||
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
|
||||
|
||||
renderModalChart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalMouseDown(e) {
|
||||
const rect = modalCanvas.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
modalDragArea = mouseY < (modalUplinkStartY + modalUplinkHeight) ? 'uplink' : 'downlink';
|
||||
|
||||
modalIsDragging = true;
|
||||
modalDragStartX = e.clientX;
|
||||
modalDragStartOffsetUL = modalPanOffsetUL;
|
||||
modalDragStartOffsetDL = modalPanOffsetDL;
|
||||
modalCanvas.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
function handleModalMouseMove(e) {
|
||||
const rect = modalCanvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
if (modalIsDragging) {
|
||||
const dx = e.clientX - modalDragStartX;
|
||||
|
||||
if (modalDragArea === 'uplink') {
|
||||
const freqPerPixel = (modalFreqRangeUL / modalZoomLevelUL) / (rect.width - 80);
|
||||
modalPanOffsetUL = modalDragStartOffsetUL - dx * freqPerPixel;
|
||||
|
||||
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
|
||||
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
|
||||
} else {
|
||||
const freqPerPixel = (modalFreqRangeDL / modalZoomLevelDL) / (rect.width - 80);
|
||||
modalPanOffsetDL = modalDragStartOffsetDL - dx * freqPerPixel;
|
||||
|
||||
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
|
||||
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
|
||||
}
|
||||
|
||||
renderModalChart();
|
||||
} else {
|
||||
let found = null;
|
||||
for (const tr of modalTransponderRects) {
|
||||
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
||||
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
||||
found = tr;
|
||||
found._mouseX = mouseX;
|
||||
found._mouseY = mouseY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found !== modalHoveredTransponder) {
|
||||
modalHoveredTransponder = found;
|
||||
modalCanvas.style.cursor = found ? 'pointer' : 'default';
|
||||
renderModalChart();
|
||||
} else if (found) {
|
||||
found._mouseX = mouseX;
|
||||
found._mouseY = mouseY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalMouseUp() {
|
||||
modalIsDragging = false;
|
||||
modalCanvas.style.cursor = modalHoveredTransponder ? 'pointer' : 'default';
|
||||
}
|
||||
|
||||
function handleModalMouseLeave() {
|
||||
modalIsDragging = false;
|
||||
modalHoveredTransponder = null;
|
||||
modalCanvas.style.cursor = 'default';
|
||||
renderModalChart();
|
||||
}
|
||||
|
||||
function resetModalZoom() {
|
||||
modalZoomLevelUL = 1;
|
||||
modalZoomLevelDL = 1;
|
||||
modalPanOffsetUL = 0;
|
||||
modalPanOffsetDL = 0;
|
||||
renderModalChart();
|
||||
}
|
||||
|
||||
// Setup reset button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const resetBtn = document.getElementById('modalResetZoom');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', resetModalZoom);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -79,9 +79,9 @@
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
||||
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
||||
Передача точек
|
||||
</a>
|
||||
</a> -->
|
||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
||||
<i class="bi bi-file-earmark-excel"></i> Excel
|
||||
</a>
|
||||
@@ -101,6 +101,9 @@
|
||||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||
<i class="bi bi-calculator"></i> Усреднение
|
||||
</a>
|
||||
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
|
||||
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Add to List Button -->
|
||||
@@ -157,7 +160,7 @@
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="12" onchange="toggleColumn(this)"> Создано</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="13" onchange="toggleColumn(this)"> Обновлено</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Дата подтверждения</label></li>
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Последний сигнал</label></li>
|
||||
<!-- <li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Последний сигнал</label></li> -->
|
||||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Действия</label></li>
|
||||
|
||||
</ul>
|
||||
@@ -496,7 +499,7 @@
|
||||
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
|
||||
</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: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -539,7 +542,7 @@
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}</td>
|
||||
<td>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td>
|
||||
<!-- <td>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td> -->
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
{% if source.objitem_count > 0 %}
|
||||
|
||||
@@ -53,6 +53,18 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
|
||||
<div id="saveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto" id="toastTitle">Уведомление</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="toast-body" id="toastBody">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-entry-container">
|
||||
<h2>Тех. анализ - Ввод данных</h2>
|
||||
|
||||
@@ -67,6 +79,11 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8 mb-3 d-flex align-items-end gap-2">
|
||||
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-primary">
|
||||
<i class="bi bi-list"></i> Список данных
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div class="col-md-8 mb-3">
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
@@ -228,16 +245,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to show toast
|
||||
function showToast(title, message, type = 'info') {
|
||||
const toastEl = document.getElementById('saveToast');
|
||||
const toastTitle = document.getElementById('toastTitle');
|
||||
const toastBody = document.getElementById('toastBody');
|
||||
const toastHeader = toastEl.querySelector('.toast-header');
|
||||
|
||||
// Remove previous background classes
|
||||
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'text-white');
|
||||
|
||||
// Add appropriate background class
|
||||
if (type === 'success') {
|
||||
toastHeader.classList.add('bg-success', 'text-white');
|
||||
} else if (type === 'error') {
|
||||
toastHeader.classList.add('bg-danger', 'text-white');
|
||||
} else if (type === 'warning') {
|
||||
toastHeader.classList.add('bg-warning');
|
||||
}
|
||||
|
||||
toastTitle.textContent = title;
|
||||
toastBody.innerHTML = message;
|
||||
|
||||
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
|
||||
toast.show();
|
||||
}
|
||||
|
||||
// Delete selected rows
|
||||
document.getElementById('delete-selected').addEventListener('click', function() {
|
||||
const selectedRows = table.getSelectedRows();
|
||||
if (selectedRows.length === 0) {
|
||||
alert('Выберите строки для удаления');
|
||||
showToast('Внимание', 'Выберите строки для удаления', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Удалить ${selectedRows.length} строк(и)?`)) {
|
||||
selectedRows.forEach(row => row.delete());
|
||||
showToast('Успешно', `Удалено строк: ${selectedRows.length}`, 'success');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -246,21 +290,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const satelliteId = document.getElementById('satellite-select').value;
|
||||
|
||||
if (!satelliteId) {
|
||||
alert('Пожалуйста, выберите спутник');
|
||||
showToast('Внимание', 'Пожалуйста, выберите спутник', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = table.getData();
|
||||
|
||||
if (data.length === 0) {
|
||||
alert('Нет данных для сохранения');
|
||||
showToast('Внимание', 'Нет данных для сохранения', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that all rows have names
|
||||
const emptyNames = data.filter(row => !row.name || row.name.trim() === '');
|
||||
if (emptyNames.length > 0) {
|
||||
alert('Все строки должны иметь имя');
|
||||
showToast('Внимание', 'Все строки должны иметь имя', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,27 +328,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
let message = `Успешно сохранено!\n`;
|
||||
message += `Создано: ${result.created}\n`;
|
||||
message += `Обновлено: ${result.updated}\n`;
|
||||
let message = `<strong>Успешно сохранено!</strong><br>`;
|
||||
message += `Создано: ${result.created}<br>`;
|
||||
message += `Обновлено: ${result.updated}<br>`;
|
||||
message += `Всего: ${result.total}`;
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
message += `\n\nОшибки:\n${result.errors.join('\n')}`;
|
||||
message += `<br><br><strong>Ошибки:</strong><br>${result.errors.join('<br>')}`;
|
||||
}
|
||||
|
||||
alert(message);
|
||||
showToast('Сохранение завершено', message, 'success');
|
||||
|
||||
// Clear table after successful save
|
||||
if (!result.errors || result.errors.length === 0) {
|
||||
table.clearData();
|
||||
}
|
||||
} else {
|
||||
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
|
||||
showToast('Ошибка', result.error || 'Неизвестная ошибка', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при сохранении данных');
|
||||
showToast('Ошибка', 'Произошла ошибка при сохранении данных', 'error');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
|
||||
518
dbapp/mainapp/templates/mainapp/tech_analyze_list.html
Normal file
518
dbapp/mainapp/templates/mainapp/tech_analyze_list.html
Normal file
@@ -0,0 +1,518 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Тех. анализ - Список{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
#tech-analyze-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
#tech-analyze-table .tabulator-header {
|
||||
font-size: 12px;
|
||||
}
|
||||
#tech-analyze-table .tabulator-cell {
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
#tech-analyze-table .tabulator-row {
|
||||
min-height: 40px;
|
||||
}
|
||||
</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 или имени...">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-success btn-sm" title="Ввод данных">
|
||||
<i class="bi bi-plus-circle"></i> Ввод данных
|
||||
</a>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить выбранные" onclick="deleteSelected()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
|
||||
<i class="bi bi-link-45deg"></i> Привязать к точкам
|
||||
</button>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- 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">
|
||||
<div id="tech-analyze-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link to Points Modal -->
|
||||
<div class="modal fade" id="linkToPointsModal" tabindex="-1" aria-labelledby="linkToPointsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="linkToPointsModalLabel">
|
||||
<i class="bi bi-link-45deg"></i> Привязать к существующим точкам
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Будут обновлены только точки с отсутствующими данными (модуляция "-", символьная скорость -1 или 0, стандарт "-").
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="linkSatelliteSelect" class="form-label">Выберите спутник <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="linkSatelliteSelect" required>
|
||||
<option value="">Выберите спутник</option>
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="linkResultMessage" class="alert" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmLinkBtn" onclick="confirmLink(event)">
|
||||
<i class="bi bi-check-circle"></i> Привязать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the satellite modal component -->
|
||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'tabulator/js/tabulator.min.js' %}"></script>
|
||||
<script>
|
||||
// Helper function to get CSRF token
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// Initialize Tabulator
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const ajaxParams = {};
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (ajaxParams[key]) {
|
||||
if (!Array.isArray(ajaxParams[key])) {
|
||||
ajaxParams[key] = [ajaxParams[key]];
|
||||
}
|
||||
ajaxParams[key].push(value);
|
||||
} else {
|
||||
ajaxParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const table = new Tabulator("#tech-analyze-table", {
|
||||
ajaxURL: "{% url 'mainapp:tech_analyze_api' %}",
|
||||
ajaxParams: ajaxParams,
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: {{ items_per_page }},
|
||||
paginationSizeSelector: [25, 50, 100, 200, 500],
|
||||
layout: "fitDataStretch",
|
||||
height: "70vh",
|
||||
placeholder: "Нет данных для отображения",
|
||||
rowHeight: null, // Автоматическая высота строк
|
||||
columns: [
|
||||
{
|
||||
formatter: "rowSelection",
|
||||
titleFormatter: "rowSelection",
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
width: 40,
|
||||
cellClick: function(e, cell) {
|
||||
cell.getRow().toggleSelect();
|
||||
}
|
||||
},
|
||||
{title: "ID", field: "id", width: 80, hozAlign: "center"},
|
||||
{
|
||||
title: "Имя",
|
||||
field: "name",
|
||||
minWidth: 250,
|
||||
widthGrow: 3,
|
||||
formatter: function(cell) {
|
||||
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
|
||||
(cell.getValue() || '-') + '</div>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Спутник",
|
||||
field: "satellite_name",
|
||||
minWidth: 120,
|
||||
widthGrow: 1,
|
||||
formatter: function(cell) {
|
||||
const data = cell.getData();
|
||||
if (data.satellite_id) {
|
||||
return '<a href="#" class="text-decoration-underline" onclick="showSatelliteModal(' + data.satellite_id + '); return false;">' +
|
||||
(data.satellite_name || '-') + '</a>';
|
||||
}
|
||||
return data.satellite_name || '-';
|
||||
}
|
||||
},
|
||||
{title: "Частота, МГц", field: "frequency", width: 120, hozAlign: "right", formatter: function(cell) {
|
||||
const val = cell.getValue();
|
||||
return val && val !== 0 ? val.toFixed(3) : '-';
|
||||
}},
|
||||
{title: "Полоса, МГц", field: "freq_range", width: 120, hozAlign: "right", formatter: function(cell) {
|
||||
const val = cell.getValue();
|
||||
return val && val !== 0 ? val.toFixed(3) : '-';
|
||||
}},
|
||||
{title: "Сим. скорость, БОД", field: "bod_velocity", width: 150, hozAlign: "right", formatter: function(cell) {
|
||||
const val = cell.getValue();
|
||||
return val && val !== 0 ? val.toFixed(0) : '-';
|
||||
}},
|
||||
{title: "Поляризация", field: "polarization_name", width: 120},
|
||||
{title: "Модуляция", field: "modulation_name", width: 120},
|
||||
{title: "Стандарт", field: "standard_name", width: 120},
|
||||
{
|
||||
title: "Примечание",
|
||||
field: "note",
|
||||
minWidth: 150,
|
||||
widthGrow: 2,
|
||||
formatter: function(cell) {
|
||||
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
|
||||
(cell.getValue() || '-') + '</div>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Создано",
|
||||
field: "created_at",
|
||||
width: 140,
|
||||
formatter: function(cell) {
|
||||
const val = cell.getValue();
|
||||
if (!val) return '-';
|
||||
try {
|
||||
const date = new Date(val);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Обновлено",
|
||||
field: "updated_at",
|
||||
width: 140,
|
||||
formatter: function(cell) {
|
||||
const val = cell.getValue();
|
||||
if (!val) return '-';
|
||||
try {
|
||||
const date = new Date(val);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
// 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');
|
||||
let filterCount = 0;
|
||||
|
||||
// Count selected satellites
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(function(opt) { return 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete selected items
|
||||
function deleteSelected() {
|
||||
const selectedRows = table.getSelectedRows();
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы одну запись для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Удалить ' + selectedRows.length + ' записей?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = selectedRows.map(function(row) { return row.getData().id; });
|
||||
|
||||
fetch('{% url "mainapp:tech_analyze_delete" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ids: selectedIds
|
||||
})
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
table.replaceData();
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении записей');
|
||||
});
|
||||
}
|
||||
|
||||
// Show link modal
|
||||
function showLinkModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('linkToPointsModal'));
|
||||
document.getElementById('linkResultMessage').style.display = 'none';
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Confirm link
|
||||
function confirmLink(event) {
|
||||
const satelliteId = document.getElementById('linkSatelliteSelect').value;
|
||||
const resultDiv = document.getElementById('linkResultMessage');
|
||||
|
||||
if (!satelliteId) {
|
||||
resultDiv.className = 'alert alert-warning';
|
||||
resultDiv.textContent = 'Пожалуйста, выберите спутник';
|
||||
resultDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const btn = document.getElementById('confirmLinkBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
fetch('{% url "mainapp:tech_analyze_link_existing" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
satellite_id: satelliteId
|
||||
})
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = '<strong>Привязка завершена!</strong><br>' +
|
||||
'Обновлено точек: ' + data.updated + '<br>' +
|
||||
'Пропущено: ' + data.skipped + '<br>' +
|
||||
'Всего обработано: ' + data.total;
|
||||
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
resultDiv.innerHTML += '<br><br><strong>Ошибки:</strong><br>' + data.errors.join('<br>');
|
||||
}
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.textContent = 'Ошибка: ' + (data.error || 'Неизвестная ошибка');
|
||||
}
|
||||
resultDiv.style.display = 'block';
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Error:', error);
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.textContent = 'Произошла ошибка при привязке точек';
|
||||
resultDiv.style.display = 'block';
|
||||
})
|
||||
.finally(function() {
|
||||
// Re-enable button
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check-circle"></i> Привязать';
|
||||
});
|
||||
}
|
||||
|
||||
// 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 selectFields = form.querySelectorAll('select');
|
||||
selectFields.forEach(function(select) {
|
||||
select.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
}
|
||||
|
||||
// Update counter when offcanvas is shown
|
||||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
|
||||
// Set search value from URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const searchQuery = urlParams.get('search');
|
||||
if (searchQuery) {
|
||||
document.getElementById('toolbar-search').value = searchQuery;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -36,6 +36,7 @@ from .views import (
|
||||
ObjItemUpdateView,
|
||||
ProcessKubsatView,
|
||||
SatelliteDataAPIView,
|
||||
SatelliteTranspondersAPIView,
|
||||
SatelliteListView,
|
||||
SatelliteCreateView,
|
||||
SatelliteUpdateView,
|
||||
@@ -60,7 +61,14 @@ from .views import (
|
||||
custom_logout,
|
||||
)
|
||||
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
||||
from .views.tech_analyze import tech_analyze_entry, tech_analyze_save
|
||||
from .views.tech_analyze import (
|
||||
TechAnalyzeEntryView,
|
||||
TechAnalyzeSaveView,
|
||||
LinkExistingPointsView,
|
||||
TechAnalyzeListView,
|
||||
TechAnalyzeDeleteView,
|
||||
TechAnalyzeAPIView,
|
||||
)
|
||||
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
||||
|
||||
app_name = 'mainapp'
|
||||
@@ -108,6 +116,7 @@ urlpatterns = [
|
||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
||||
path('api/satellite/<int:satellite_id>/transponders/', SatelliteTranspondersAPIView.as_view(), name='satellite_transponders_api'),
|
||||
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
||||
path('api/multi-sources-playback/', MultiSourcesPlaybackDataAPIView.as_view(), name='multi_sources_playback_api'),
|
||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
@@ -128,8 +137,12 @@ urlpatterns = [
|
||||
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
|
||||
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
|
||||
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
|
||||
path('tech-analyze/', tech_analyze_entry, name='tech_analyze_entry'),
|
||||
path('tech-analyze/save/', tech_analyze_save, name='tech_analyze_save'),
|
||||
path('tech-analyze/', TechAnalyzeEntryView.as_view(), name='tech_analyze_entry'),
|
||||
path('tech-analyze/list/', TechAnalyzeListView.as_view(), name='tech_analyze_list'),
|
||||
path('tech-analyze/save/', TechAnalyzeSaveView.as_view(), name='tech_analyze_save'),
|
||||
path('tech-analyze/delete/', TechAnalyzeDeleteView.as_view(), name='tech_analyze_delete'),
|
||||
path('tech-analyze/link-existing/', LinkExistingPointsView.as_view(), name='tech_analyze_link_existing'),
|
||||
path('api/tech-analyze/', TechAnalyzeAPIView.as_view(), name='tech_analyze_api'),
|
||||
path('points-averaging/', PointsAveragingView.as_view(), name='points_averaging'),
|
||||
path('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'),
|
||||
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
||||
|
||||
@@ -22,6 +22,7 @@ from .api import (
|
||||
GetLocationsView,
|
||||
LyngsatDataAPIView,
|
||||
SatelliteDataAPIView,
|
||||
SatelliteTranspondersAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
SourceObjItemsAPIView,
|
||||
LyngsatTaskStatusAPIView,
|
||||
@@ -96,6 +97,7 @@ __all__ = [
|
||||
'GetLocationsView',
|
||||
'LyngsatDataAPIView',
|
||||
'SatelliteDataAPIView',
|
||||
'SatelliteTranspondersAPIView',
|
||||
'SigmaParameterDataAPIView',
|
||||
'SourceObjItemsAPIView',
|
||||
'LyngsatTaskStatusAPIView',
|
||||
|
||||
@@ -723,3 +723,43 @@ class MultiSourcesPlaybackDataAPIView(LoginRequiredMixin, View):
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
|
||||
class SatelliteTranspondersAPIView(LoginRequiredMixin, View):
|
||||
"""API endpoint for getting transponders for a satellite."""
|
||||
|
||||
def get(self, request, satellite_id):
|
||||
from mapsapp.models import Transponders
|
||||
|
||||
try:
|
||||
transponders = Transponders.objects.filter(
|
||||
sat_id=satellite_id
|
||||
).select_related('polarization').order_by('downlink')
|
||||
|
||||
if not transponders.exists():
|
||||
return JsonResponse({
|
||||
'satellite_id': satellite_id,
|
||||
'transponders': [],
|
||||
'count': 0
|
||||
})
|
||||
|
||||
transponders_data = []
|
||||
for t in transponders:
|
||||
transponders_data.append({
|
||||
'id': t.id,
|
||||
'name': t.name or '-',
|
||||
'downlink': float(t.downlink) if t.downlink else 0,
|
||||
'uplink': float(t.uplink) if t.uplink else None,
|
||||
'frequency_range': float(t.frequency_range) if t.frequency_range else 0,
|
||||
'polarization': t.polarization.name if t.polarization else '-',
|
||||
'zone_name': t.zone_name or '-',
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'satellite_id': satellite_id,
|
||||
'transponders': transponders_data,
|
||||
'count': len(transponders_data)
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.db import transaction
|
||||
import json
|
||||
|
||||
from ..models import (
|
||||
@@ -11,157 +15,415 @@ from ..models import (
|
||||
Polarization,
|
||||
Modulation,
|
||||
Standard,
|
||||
ObjItem,
|
||||
Parameter,
|
||||
)
|
||||
from ..mixins import RoleRequiredMixin
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
@login_required
|
||||
def tech_analyze_entry(request):
|
||||
class TechAnalyzeEntryView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Представление для ввода данных технического анализа.
|
||||
"""
|
||||
satellites = Satellite.objects.all().order_by('name')
|
||||
|
||||
context = {
|
||||
'satellites': satellites,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
||||
def get(self, request):
|
||||
satellites = Satellite.objects.all().order_by('name')
|
||||
|
||||
context = {
|
||||
'satellites': satellites,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def tech_analyze_save(request):
|
||||
class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для сохранения данных технического анализа.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
satellite_id = data.get('satellite_id')
|
||||
rows = data.get('rows', [])
|
||||
|
||||
if not satellite_id:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не выбран спутник'
|
||||
}, status=400)
|
||||
|
||||
if not rows:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Нет данных для сохранения'
|
||||
}, status=400)
|
||||
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
satellite = Satellite.objects.get(id=satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
data = json.loads(request.body)
|
||||
satellite_id = data.get('satellite_id')
|
||||
rows = data.get('rows', [])
|
||||
|
||||
if not satellite_id:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не выбран спутник'
|
||||
}, status=400)
|
||||
|
||||
if not rows:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Нет данных для сохранения'
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
satellite = Satellite.objects.get(id=satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Спутник не найден'
|
||||
}, status=404)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
with transaction.atomic():
|
||||
for idx, row in enumerate(rows, start=1):
|
||||
try:
|
||||
name = row.get('name', '').strip()
|
||||
if not name:
|
||||
errors.append(f"Строка {idx}: отсутствует имя")
|
||||
continue
|
||||
|
||||
# Обработка поляризации
|
||||
polarization_name = row.get('polarization', '').strip() or '-'
|
||||
polarization, _ = Polarization.objects.get_or_create(name=polarization_name)
|
||||
|
||||
# Обработка модуляции
|
||||
modulation_name = row.get('modulation', '').strip() or '-'
|
||||
modulation, _ = Modulation.objects.get_or_create(name=modulation_name)
|
||||
|
||||
# Обработка стандарта
|
||||
standard_name = row.get('standard', '').strip()
|
||||
if standard_name.lower() == 'unknown':
|
||||
standard_name = '-'
|
||||
if not standard_name:
|
||||
standard_name = '-'
|
||||
standard, _ = Standard.objects.get_or_create(name=standard_name)
|
||||
|
||||
# Обработка числовых полей
|
||||
frequency = row.get('frequency')
|
||||
if frequency:
|
||||
try:
|
||||
frequency = float(str(frequency).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
frequency = 0
|
||||
else:
|
||||
frequency = 0
|
||||
|
||||
freq_range = row.get('freq_range')
|
||||
if freq_range:
|
||||
try:
|
||||
freq_range = float(str(freq_range).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
freq_range = 0
|
||||
else:
|
||||
freq_range = 0
|
||||
|
||||
bod_velocity = row.get('bod_velocity')
|
||||
if bod_velocity:
|
||||
try:
|
||||
bod_velocity = float(str(bod_velocity).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
bod_velocity = 0
|
||||
else:
|
||||
bod_velocity = 0
|
||||
|
||||
note = row.get('note', '').strip()
|
||||
|
||||
# Создание или обновление записи
|
||||
tech_analyze, created = TechAnalyze.objects.update_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
'satellite': satellite,
|
||||
'polarization': polarization,
|
||||
'frequency': frequency,
|
||||
'freq_range': freq_range,
|
||||
'bod_velocity': bod_velocity,
|
||||
'modulation': modulation,
|
||||
'standard': standard,
|
||||
'note': note,
|
||||
'updated_by': request.user.customuser if hasattr(request.user, 'customuser') else None,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
tech_analyze.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
||||
tech_analyze.save()
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Строка {idx}: {str(e)}")
|
||||
|
||||
response_data = {
|
||||
'success': True,
|
||||
'created': created_count,
|
||||
'updated': updated_count,
|
||||
'total': created_count + updated_count,
|
||||
}
|
||||
|
||||
if errors:
|
||||
response_data['errors'] = errors
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Спутник не найден'
|
||||
}, status=404)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
with transaction.atomic():
|
||||
for idx, row in enumerate(rows, start=1):
|
||||
try:
|
||||
name = row.get('name', '').strip()
|
||||
if not name:
|
||||
errors.append(f"Строка {idx}: отсутствует имя")
|
||||
continue
|
||||
|
||||
# Обработка поляризации
|
||||
polarization_name = row.get('polarization', '').strip() or '-'
|
||||
polarization, _ = Polarization.objects.get_or_create(name=polarization_name)
|
||||
|
||||
# Обработка модуляции
|
||||
modulation_name = row.get('modulation', '').strip() or '-'
|
||||
modulation, _ = Modulation.objects.get_or_create(name=modulation_name)
|
||||
|
||||
# Обработка стандарта
|
||||
standard_name = row.get('standard', '').strip()
|
||||
if standard_name.lower() == 'unknown':
|
||||
standard_name = '-'
|
||||
if not standard_name:
|
||||
standard_name = '-'
|
||||
standard, _ = Standard.objects.get_or_create(name=standard_name)
|
||||
|
||||
# Обработка числовых полей
|
||||
frequency = row.get('frequency')
|
||||
if frequency:
|
||||
try:
|
||||
frequency = float(str(frequency).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
frequency = 0
|
||||
else:
|
||||
frequency = 0
|
||||
|
||||
freq_range = row.get('freq_range')
|
||||
if freq_range:
|
||||
try:
|
||||
freq_range = float(str(freq_range).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
freq_range = 0
|
||||
else:
|
||||
freq_range = 0
|
||||
|
||||
bod_velocity = row.get('bod_velocity')
|
||||
if bod_velocity:
|
||||
try:
|
||||
bod_velocity = float(str(bod_velocity).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
bod_velocity = 0
|
||||
else:
|
||||
bod_velocity = 0
|
||||
|
||||
note = row.get('note', '').strip()
|
||||
|
||||
# Создание или обновление записи
|
||||
tech_analyze, created = TechAnalyze.objects.update_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
'satellite': satellite,
|
||||
'polarization': polarization,
|
||||
'frequency': frequency,
|
||||
'freq_range': freq_range,
|
||||
'bod_velocity': bod_velocity,
|
||||
'modulation': modulation,
|
||||
'standard': standard,
|
||||
'note': note,
|
||||
'updated_by': request.user.customuser if hasattr(request.user, 'customuser') else None,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
tech_analyze.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
||||
tech_analyze.save()
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
|
||||
class LinkExistingPointsView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для привязки существующих точек к данным теханализа.
|
||||
|
||||
Алгоритм:
|
||||
1. Получить все ObjItem для выбранного спутника
|
||||
2. Для каждого ObjItem:
|
||||
- Извлечь имя источника
|
||||
- Найти соответствующую запись TechAnalyze по имени и спутнику
|
||||
- Если найдена и данные отсутствуют в Parameter:
|
||||
* Обновить модуляцию (если "-")
|
||||
* Обновить символьную скорость (если -1.0 или None)
|
||||
* Обновить стандарт (если "-")
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
satellite_id = data.get('satellite_id')
|
||||
|
||||
if not satellite_id:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не выбран спутник'
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
satellite = Satellite.objects.get(id=satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Спутник не найден'
|
||||
}, status=404)
|
||||
|
||||
# Получаем все ObjItem для данного спутника
|
||||
objitems = ObjItem.objects.filter(
|
||||
parameter_obj__id_satellite=satellite
|
||||
).select_related('parameter_obj', 'parameter_obj__modulation', 'parameter_obj__standard')
|
||||
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
errors = []
|
||||
|
||||
with transaction.atomic():
|
||||
for objitem in objitems:
|
||||
try:
|
||||
if not objitem.parameter_obj:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Строка {idx}: {str(e)}")
|
||||
parameter = objitem.parameter_obj
|
||||
source_name = objitem.name
|
||||
|
||||
# Проверяем, нужно ли обновлять данные
|
||||
needs_update = (
|
||||
(parameter.modulation and parameter.modulation.name == "-") or
|
||||
parameter.bod_velocity is None or
|
||||
parameter.bod_velocity == -1.0 or
|
||||
parameter.bod_velocity == 0 or
|
||||
(parameter.standard and parameter.standard.name == "-")
|
||||
)
|
||||
|
||||
if not needs_update:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Ищем данные в TechAnalyze по имени и спутнику
|
||||
tech_analyze = TechAnalyze.objects.filter(
|
||||
name=source_name,
|
||||
satellite=satellite
|
||||
).select_related('modulation', 'standard').first()
|
||||
|
||||
if not tech_analyze:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Обновляем данные
|
||||
updated = False
|
||||
|
||||
# Обновляем модуляцию
|
||||
if parameter.modulation and parameter.modulation.name == "-" and tech_analyze.modulation:
|
||||
parameter.modulation = tech_analyze.modulation
|
||||
updated = True
|
||||
|
||||
# Обновляем символьную скорость
|
||||
if (parameter.bod_velocity is None or parameter.bod_velocity == -1.0 or parameter.bod_velocity == 0) and \
|
||||
tech_analyze.bod_velocity and tech_analyze.bod_velocity > 0:
|
||||
parameter.bod_velocity = tech_analyze.bod_velocity
|
||||
updated = True
|
||||
|
||||
# Обновляем стандарт
|
||||
if parameter.standard and parameter.standard.name == "-" and tech_analyze.standard:
|
||||
parameter.standard = tech_analyze.standard
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
parameter.save()
|
||||
updated_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"ObjItem {objitem.id}: {str(e)}")
|
||||
|
||||
response_data = {
|
||||
'success': True,
|
||||
'updated': updated_count,
|
||||
'skipped': skipped_count,
|
||||
'total': objitems.count(),
|
||||
}
|
||||
|
||||
if errors:
|
||||
response_data['errors'] = errors
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
|
||||
class TechAnalyzeListView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Представление для отображения списка данных технического анализа.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
# Получаем список спутников для фильтра
|
||||
satellites = Satellite.objects.all().order_by('name')
|
||||
|
||||
response_data = {
|
||||
'success': True,
|
||||
'created': created_count,
|
||||
'updated': updated_count,
|
||||
'total': created_count + updated_count,
|
||||
# Получаем параметры из URL для передачи в шаблон
|
||||
search_query = request.GET.get('search', '').strip()
|
||||
satellite_ids = request.GET.getlist('satellite_id')
|
||||
items_per_page = int(request.GET.get('items_per_page', 50))
|
||||
|
||||
context = {
|
||||
'satellites': satellites,
|
||||
'selected_satellites': [int(sid) for sid in satellite_ids if sid],
|
||||
'search_query': search_query,
|
||||
'items_per_page': items_per_page,
|
||||
'available_items_per_page': [25, 50, 100, 200, 500],
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
if errors:
|
||||
response_data['errors'] = errors
|
||||
return render(request, 'mainapp/tech_analyze_list.html', context)
|
||||
|
||||
|
||||
class TechAnalyzeDeleteView(LoginRequiredMixin, RoleRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для удаления выбранных записей теханализа.
|
||||
"""
|
||||
allowed_roles = ['admin', 'moderator']
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
ids = data.get('ids', [])
|
||||
|
||||
if not ids:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не выбраны записи для удаления'
|
||||
}, status=400)
|
||||
|
||||
# Удаляем записи
|
||||
deleted_count, _ = TechAnalyze.objects.filter(id__in=ids).delete()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'deleted': deleted_count,
|
||||
'message': f'Удалено записей: {deleted_count}'
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
|
||||
class TechAnalyzeAPIView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для получения данных теханализа в формате для Tabulator.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
# Получаем параметры фильтрации
|
||||
search_query = request.GET.get('search', '').strip()
|
||||
satellite_ids = request.GET.getlist('satellite_id')
|
||||
|
||||
return JsonResponse(response_data)
|
||||
# Получаем параметры пагинации от Tabulator
|
||||
page = int(request.GET.get('page', 1))
|
||||
size = int(request.GET.get('size', 50))
|
||||
|
||||
# Базовый queryset
|
||||
tech_analyzes = TechAnalyze.objects.select_related(
|
||||
'satellite', 'polarization', 'modulation', 'standard', 'created_by', 'updated_by'
|
||||
).order_by('-created_at')
|
||||
|
||||
# Применяем фильтры
|
||||
if search_query:
|
||||
tech_analyzes = tech_analyzes.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(id__icontains=search_query)
|
||||
)
|
||||
|
||||
if satellite_ids:
|
||||
tech_analyzes = tech_analyzes.filter(satellite_id__in=satellite_ids)
|
||||
|
||||
# Пагинация
|
||||
paginator = Paginator(tech_analyzes, size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# Формируем данные для Tabulator
|
||||
results = []
|
||||
for item in page_obj:
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name or '',
|
||||
'satellite_id': item.satellite.id if item.satellite else None,
|
||||
'satellite_name': item.satellite.name if item.satellite else '-',
|
||||
'frequency': float(item.frequency) if item.frequency else 0,
|
||||
'freq_range': float(item.freq_range) if item.freq_range else 0,
|
||||
'bod_velocity': float(item.bod_velocity) if item.bod_velocity else 0,
|
||||
'polarization_name': item.polarization.name if item.polarization else '-',
|
||||
'modulation_name': item.modulation.name if item.modulation else '-',
|
||||
'standard_name': item.standard.name if item.standard else '-',
|
||||
'note': item.note or '',
|
||||
'created_at': item.created_at.isoformat() if item.created_at else None,
|
||||
'updated_at': item.updated_at.isoformat() if item.updated_at else None,
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
'last_page': paginator.num_pages,
|
||||
'data': results,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user