Добавил теханализ

This commit is contained in:
2025-11-27 11:36:00 +03:00
parent efb99ea8d5
commit 810d3a8f7f
10 changed files with 859 additions and 16 deletions

View File

@@ -34,6 +34,7 @@ from .models import (
CustomUser, CustomUser,
Band, Band,
Source, Source,
TechAnalyze,
) )
from .filters import ( from .filters import (
GeoKupDistanceFilter, GeoKupDistanceFilter,
@@ -1084,3 +1085,79 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
) )
autocomplete_fields = ("info",) autocomplete_fields = ("info",)
@admin.register(TechAnalyze)
class TechAnalyzeAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""Админ-панель для модели TechAnalyze."""
list_display = (
"name",
"satellite",
"frequency",
"freq_range",
"polarization",
"bod_velocity",
"modulation",
"standard",
"created_at",
"updated_at",
)
list_display_links = ("name",)
list_select_related = (
"satellite",
"polarization",
"modulation",
"standard",
"created_by__user",
"updated_by__user",
)
list_filter = (
("satellite", MultiSelectRelatedDropdownFilter),
("polarization", MultiSelectRelatedDropdownFilter),
("modulation", MultiSelectRelatedDropdownFilter),
("standard", MultiSelectRelatedDropdownFilter),
("frequency", NumericRangeFilterBuilder()),
("freq_range", NumericRangeFilterBuilder()),
("created_at", DateRangeQuickSelectListFilterBuilder()),
("updated_at", DateRangeQuickSelectListFilterBuilder()),
)
search_fields = (
"name",
"satellite__name",
"frequency",
"note",
)
ordering = ("-created_at",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
autocomplete_fields = ("satellite", "polarization", "modulation", "standard")
fieldsets = (
(
"Основная информация",
{"fields": ("name", "satellite", "note")},
),
(
"Технические параметры",
{
"fields": (
"frequency",
"freq_range",
"polarization",
"bod_velocity",
"modulation",
"standard",
)
},
),
(
"Метаданные",
{
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
"classes": ("collapse",),
},
),
)

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.2.7 on 2025-11-27 07:10
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0015_add_international_code_to_satellite'),
]
operations = [
migrations.AlterField(
model_name='satellite',
name='international_code',
field=models.CharField(blank=True, help_text='Международный идентификатор спутника (например, 2011-074A)', max_length=50, null=True, verbose_name='Международный код'),
),
migrations.CreateModel(
name='TechAnalyze',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, help_text='Уникальное название для технического анализа', max_length=255, unique=True, verbose_name='Имя')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')),
('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц')),
('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость', null=True, verbose_name='Символьная скорость, БОД')),
('note', models.TextField(blank=True, help_text='Дополнительные примечания', null=True, verbose_name='Примечание')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tech_analyze_created', to='mainapp.customuser', verbose_name='Создан пользователем')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tech_analyze_modulations', to='mainapp.modulation', verbose_name='Модуляция')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tech_analyze_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('satellite', models.ForeignKey(help_text='Спутник, к которому относится анализ', on_delete=django.db.models.deletion.PROTECT, related_name='tech_analyzes', to='mainapp.satellite', verbose_name='Спутник')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tech_analyze_standards', to='mainapp.standard', verbose_name='Стандарт')),
('updated_by', models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tech_analyze_updated', to='mainapp.customuser', verbose_name='Изменен пользователем')),
],
options={
'verbose_name': 'Тех. анализ',
'verbose_name_plural': 'Тех. анализы',
'ordering': ['-created_at'],
},
),
]

View File

@@ -290,7 +290,7 @@ class Standard(models.Model):
# Основные поля # Основные поля
name = models.CharField( name = models.CharField(
max_length=20, max_length=80,
unique=True, unique=True,
verbose_name="Стандарт", verbose_name="Стандарт",
db_index=True, db_index=True,
@@ -475,6 +475,123 @@ class ObjItemManager(models.Manager):
return self.get_queryset().by_user(user) return self.get_queryset().by_user(user)
class TechAnalyze(models.Model):
"""
Модель технического анализа сигнала.
Хранит информацию о технических параметрах сигнала для анализа.
"""
# Основные поля
name = models.CharField(
max_length=255,
unique=True,
verbose_name="Имя",
db_index=True,
help_text="Уникальное название для технического анализа",
)
satellite = models.ForeignKey(
Satellite,
on_delete=models.PROTECT,
related_name="tech_analyzes",
verbose_name="Спутник",
help_text="Спутник, к которому относится анализ",
)
polarization = models.ForeignKey(
Polarization,
default=get_default_polarization,
on_delete=models.SET_DEFAULT,
related_name="tech_analyze_polarizations",
null=True,
blank=True,
verbose_name="Поляризация",
)
frequency = models.FloatField(
default=0,
null=True,
blank=True,
verbose_name="Частота, МГц",
db_index=True,
help_text="Центральная частота сигнала",
)
freq_range = models.FloatField(
default=0,
null=True,
blank=True,
verbose_name="Полоса частот, МГц",
help_text="Полоса частот сигнала",
)
bod_velocity = models.FloatField(
default=0,
null=True,
blank=True,
verbose_name="Символьная скорость, БОД",
help_text="Символьная скорость",
)
modulation = models.ForeignKey(
Modulation,
default=get_default_modulation,
on_delete=models.SET_DEFAULT,
related_name="tech_analyze_modulations",
null=True,
blank=True,
verbose_name="Модуляция",
)
standard = models.ForeignKey(
Standard,
default=get_default_standard,
on_delete=models.SET_DEFAULT,
related_name="tech_analyze_standards",
null=True,
blank=True,
verbose_name="Стандарт",
)
note = models.TextField(
null=True,
blank=True,
verbose_name="Примечание",
help_text="Дополнительные примечания",
)
# Метаданные
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания",
help_text="Дата и время создания записи",
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="tech_analyze_created",
null=True,
blank=True,
verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись",
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения",
)
updated_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="tech_analyze_updated",
null=True,
blank=True,
verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись",
)
def __str__(self):
return f"{self.name} ({self.satellite.name if self.satellite else '-'})"
class Meta:
verbose_name = "Тех. анализ"
verbose_name_plural = "Тех. анализы"
ordering = ["-created_at"]
class Source(models.Model): class Source(models.Model):
""" """
Модель источника сигнала. Модель источника сигнала.

View File

@@ -49,10 +49,13 @@
<i class="bi bi-trash"></i> Удалить <i class="bi bi-trash"></i> Удалить
</button> </button>
{% endif %} {% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" <button type="button" class="btn btn-primary btn-sm" title="Показать на карте"
onclick="showSelectedOnMap()"> onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта <i class="bi bi-map"></i> Карта
</button> </button>
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
<i class="bi bi-clipboard-data"></i> Тех. анализ
</a>
</div> </div>
<!-- Items per page select moved here --> <!-- Items per page select moved here -->

View File

@@ -80,7 +80,7 @@
</a> </a>
{% endif %} {% 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="Ввод данных точек спутников">
<i class="bi bi-keyboard"></i> Ввод данных Передача точек
</a> </a>
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel"> <a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
<i class="bi bi-file-earmark-excel"></i> Excel <i class="bi bi-file-earmark-excel"></i> Excel
@@ -94,7 +94,7 @@
<i class="bi bi-trash"></i> Удалить <i class="bi bi-trash"></i> Удалить
</button> </button>
{% endif %} {% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" <button type="button" class="btn btn-primary btn-sm" title="Показать на карте"
onclick="showSelectedOnMap()"> onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта <i class="bi bi-map"></i> Карта
</button> </button>

View File

@@ -0,0 +1,343 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Тех. анализ - Ввод данных{% endblock %}
{% block extra_css %}
<link href="https://unpkg.com/tabulator-tables@6.2.5/dist/css/tabulator_bootstrap5.min.css" rel="stylesheet">
<style>
.data-entry-container {
padding: 20px;
}
.form-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.table-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#tech-analyze-table {
margin-top: 20px;
font-size: 12px;
}
#tech-analyze-table .tabulator-header {
font-size: 12px;
white-space: normal;
word-wrap: break-word;
}
#tech-analyze-table .tabulator-header .tabulator-col {
white-space: normal;
word-wrap: break-word;
height: auto;
min-height: 40px;
}
#tech-analyze-table .tabulator-header .tabulator-col-content {
white-space: normal;
word-wrap: break-word;
padding: 6px 4px;
}
#tech-analyze-table .tabulator-cell {
font-size: 12px;
padding: 6px 4px;
}
.btn-group-custom {
margin-top: 15px;
}
</style>
{% endblock %}
{% block content %}
<div class="data-entry-container">
<h2>Тех. анализ - Ввод данных</h2>
<div class="form-section">
<div class="row">
<div class="col-md-4 mb-3">
<label for="satellite-select" class="form-label">Спутник <span class="text-danger">*</span></label>
<select id="satellite-select" class="form-select">
<option value="">Выберите спутник</option>
{% for satellite in satellites %}
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
{% endfor %}
</select>
</div>
<!-- <div class="col-md-8 mb-3">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i>
<strong>Инструкция:</strong>
<ul class="mb-0 mt-2" style="font-size: 0.9em;">
<li><strong>Порядок столбцов в Excel:</strong> Имя, Частота МГц, Полоса МГц, Сим. скорость БОД, Модуляция, Стандарт, Примечание</li>
<li><strong>Поляризация извлекается автоматически</strong> из имени (например: "Сигнал 11500 МГц L" → "Левая")</li>
<li>Поддерживаемые буквы: L=Левая, R=Правая, H=Горизонтальная, V=Вертикальная</li>
<li>Скопируйте данные из Excel и вставьте в таблицу (Ctrl+V)</li>
<li>Используйте стрелки, Tab, Enter для навигации и редактирования</li>
</ul>
</div>
</div> -->
</div>
</div>
<div class="table-section">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5>Таблица данных <span id="row-count" class="badge bg-primary">0</span></h5>
</div>
<div class="btn-group-custom">
<button id="add-row" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Добавить строку
</button>
<button id="delete-selected" class="btn btn-warning ms-2">
<i class="bi bi-trash"></i> Удалить выбранные
</button>
<button id="save-data" class="btn btn-success ms-2">
<i class="bi bi-save"></i> Сохранить
</button>
<button id="clear-table" class="btn btn-danger ms-2">
<i class="bi bi-x-circle"></i> Очистить таблицу
</button>
</div>
</div>
<div id="tech-analyze-table"></div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/tabulator-tables@6.2.5/dist/js/tabulator.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Tabulator
const table = new Tabulator("#tech-analyze-table", {
layout: "fitDataStretch",
height: "500px",
placeholder: "Нет данных. Скопируйте данные из Excel и вставьте в таблицу (Ctrl+V).",
headerWordWrap: true,
clipboard: true,
clipboardPasteAction: "replace",
clipboardPasteParser: function(clipboard) {
// Парсим данные из буфера обмена
const rows = clipboard.split('\n').filter(row => row.trim() !== '');
const data = [];
// Функция для извлечения поляризации из имени
function extractPolarization(name) {
if (!name) return '';
// Маппинг букв на полные названия
const polarizationMap = {
'L': 'Левая',
'R': 'Правая',
'H': 'Горизонтальная',
'V': 'Вертикальная'
};
// Ищем паттерн "МГц X" где X - буква поляризации
const match = name.match(/МГц\s+([LRHV])/i);
if (match) {
const letter = match[1].toUpperCase();
return polarizationMap[letter] || '';
}
// Альтернативный паттерн: просто последняя буква L/R/H/V
const lastChar = name.trim().slice(-1).toUpperCase();
if (polarizationMap[lastChar]) {
return polarizationMap[lastChar];
}
return '';
}
rows.forEach(row => {
// Разделяем по табуляции (стандартный разделитель Excel)
const cells = row.split('\t');
const name = cells[0] || '';
const polarization = extractPolarization(name);
// Создаем объект с правильными полями (новый порядок без поляризации в начале)
const rowData = {
name: name,
frequency: cells[1] || '',
freq_range: cells[2] || '',
bod_velocity: cells[3] || '',
modulation: cells[4] || '',
standard: cells[5] || '',
note: cells[6] || '',
polarization: polarization // Автоматически извлеченная поляризация
};
data.push(rowData);
});
return data;
},
columns: [
{
formatter: "rowSelection",
titleFormatter: "rowSelection",
hozAlign: "center",
headerSort: false,
width: 40,
clipboard: false,
cellClick: function(e, cell) {
cell.getRow().toggleSelect();
}
},
{title: "Имя", field: "name", minWidth: 150, widthGrow: 2, editor: "input"},
{title: "Частота, МГц", field: "frequency", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 150, widthGrow: 1.5, editor: "input"},
{title: "Вид модуляции", field: "modulation", minWidth: 120, widthGrow: 1.2, editor: "input"},
{title: "Стандарт", field: "standard", minWidth: 100, widthGrow: 1, editor: "input"},
{title: "Примечание", field: "note", minWidth: 150, widthGrow: 2, editor: "input"},
{title: "Поляризация", field: "polarization", minWidth: 100, widthGrow: 1, editor: "input"},
],
data: [],
});
// Update row count
function updateRowCount() {
const count = table.getDataCount();
document.getElementById('row-count').textContent = count;
}
// Listen to table events
table.on("rowAdded", updateRowCount);
table.on("dataChanged", updateRowCount);
table.on("rowDeleted", updateRowCount);
// Add row button
document.getElementById('add-row').addEventListener('click', function() {
table.addRow({
name: '',
frequency: '',
freq_range: '',
bod_velocity: '',
modulation: '',
standard: '',
note: '',
polarization: ''
});
});
// Delete selected rows
document.getElementById('delete-selected').addEventListener('click', function() {
const selectedRows = table.getSelectedRows();
if (selectedRows.length === 0) {
alert('Выберите строки для удаления');
return;
}
if (confirm(`Удалить ${selectedRows.length} строк(и)?`)) {
selectedRows.forEach(row => row.delete());
}
});
// Save data
document.getElementById('save-data').addEventListener('click', async function() {
const satelliteId = document.getElementById('satellite-select').value;
if (!satelliteId) {
alert('Пожалуйста, выберите спутник');
return;
}
const data = table.getData();
if (data.length === 0) {
alert('Нет данных для сохранения');
return;
}
// Validate that all rows have names
const emptyNames = data.filter(row => !row.name || row.name.trim() === '');
if (emptyNames.length > 0) {
alert('Все строки должны иметь имя');
return;
}
// Disable button while saving
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Сохранение...';
try {
const response = await fetch('/tech-analyze/save/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
satellite_id: satelliteId,
rows: data
})
});
const result = await response.json();
if (result.success) {
let message = `Успешно сохранено!\n`;
message += `Создано: ${result.created}\n`;
message += `Обновлено: ${result.updated}\n`;
message += `Всего: ${result.total}`;
if (result.errors && result.errors.length > 0) {
message += `\n\nОшибки:\n${result.errors.join('\n')}`;
}
alert(message);
// Clear table after successful save
if (!result.errors || result.errors.length === 0) {
table.clearData();
}
} else {
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Error:', error);
alert('Произошла ошибка при сохранении данных');
} finally {
// Re-enable button
this.disabled = false;
this.innerHTML = '<i class="bi bi-save"></i> Сохранить';
}
});
// Clear table
document.getElementById('clear-table').addEventListener('click', function() {
if (confirm('Вы уверены, что хотите очистить таблицу?')) {
table.clearData();
updateRowCount();
}
});
// 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 row count
updateRowCount();
});
</script>
{% endblock %}

View File

@@ -60,6 +60,7 @@ from .views import (
custom_logout, custom_logout,
) )
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
from .views.tech_analyze import tech_analyze_entry, tech_analyze_save
app_name = 'mainapp' app_name = 'mainapp'
@@ -126,5 +127,7 @@ urlpatterns = [
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'), path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
path('data-entry/', DataEntryView.as_view(), name='data_entry'), path('data-entry/', DataEntryView.as_view(), name='data_entry'),
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'), 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('logout/', custom_logout, name='logout'), path('logout/', custom_logout, name='logout'),
] ]

View File

@@ -401,6 +401,9 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
""" """
Вспомогательная функция для создания ObjItem из строки DataFrame. Вспомогательная функция для создания ObjItem из строки DataFrame.
Теперь ищет дополнительные данные (модуляция, стандарт, символьная скорость)
в таблице TechAnalyze по имени источника и спутнику, если они не указаны в Excel.
Args: Args:
row: строка DataFrame row: строка DataFrame
sat: объект Satellite sat: объект Satellite
@@ -420,7 +423,7 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
except KeyError: except KeyError:
polarization_obj, _ = Polarization.objects.get_or_create(name="-") polarization_obj, _ = Polarization.objects.get_or_create(name="-")
# Обработка ВЧ параметров # Обработка ВЧ параметров из Excel
freq = remove_str(row["Частота, МГц"]) freq = remove_str(row["Частота, МГц"])
freq_line = remove_str(row["Полоса, МГц"]) freq_line = remove_str(row["Полоса, МГц"])
v = remove_str(row["Символьная скорость, БОД"]) v = remove_str(row["Символьная скорость, БОД"])
@@ -430,8 +433,42 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
except AttributeError: except AttributeError:
mod_obj, _ = Modulation.objects.get_or_create(name="-") mod_obj, _ = Modulation.objects.get_or_create(name="-")
# Ищем данные в TechAnalyze (если не указаны в Excel или указаны как "-")
source_name = row["Объект наблюдения"]
tech_data = None
# Проверяем, нужно ли искать данные в TechAnalyze
# (если модуляция "-" или символьная скорость не указана)
if mod_obj.name == "-" or v == -1.0:
tech_data = _find_tech_analyze_data(source_name, sat)
# Если нашли данные в TechAnalyze, используем их
if tech_data:
if mod_obj.name == "-":
mod_obj = tech_data['modulation']
if v == -1.0:
v = tech_data['bod_velocity']
snr = remove_str(row["ОСШ"]) snr = remove_str(row["ОСШ"])
# Обработка стандарта (если есть в Excel или из TechAnalyze)
try:
standard_name = row.get("Стандарт", "-")
if pd.isna(standard_name) or standard_name == "-":
# Если стандарт не указан в Excel, пытаемся взять из TechAnalyze
if tech_data and tech_data['standard']:
standard_obj = tech_data['standard']
else:
standard_obj, _ = Standard.objects.get_or_create(name="-")
else:
standard_obj, _ = Standard.objects.get_or_create(name=standard_name.strip())
except (KeyError, AttributeError):
# Если столбца "Стандарт" нет, пытаемся взять из TechAnalyze
if tech_data and tech_data['standard']:
standard_obj = tech_data['standard']
else:
standard_obj, _ = Standard.objects.get_or_create(name="-")
# Обработка времени # Обработка времени
date = row["Дата"].date() date = row["Дата"].date()
time_ = row["Время"] time_ = row["Время"]
@@ -510,7 +547,7 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
created_by=user_to_use created_by=user_to_use
) )
# Создаем Parameter # Создаем Parameter (с данными из TechAnalyze если они были найдены)
Parameter.objects.create( Parameter.objects.create(
id_satellite=sat, id_satellite=sat,
polarization=polarization_obj, polarization=polarization_obj,
@@ -519,6 +556,7 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
bod_velocity=v, bod_velocity=v,
modulation=mod_obj, modulation=mod_obj,
snr=snr, snr=snr,
standard=standard_obj,
objitem=obj_item, objitem=obj_item,
) )
@@ -817,10 +855,44 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
return False return False
def _find_tech_analyze_data(name: str, satellite: Satellite):
"""
Ищет данные технического анализа по имени и спутнику.
Args:
name: имя источника
satellite: объект Satellite
Returns:
dict или None: словарь с данными {modulation, standard, bod_velocity} или None
"""
from .models import TechAnalyze
try:
tech_analyze = TechAnalyze.objects.filter(
name=name,
satellite=satellite
).select_related('modulation', 'standard').first()
if tech_analyze:
return {
'modulation': tech_analyze.modulation,
'standard': tech_analyze.standard,
'bod_velocity': tech_analyze.bod_velocity if tech_analyze.bod_velocity else -1.0
}
except Exception as e:
print(f"Ошибка при поиске TechAnalyze для {name}: {e}")
return None
def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False): def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
""" """
Вспомогательная функция для создания ObjItem из строки CSV DataFrame. Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
Теперь ищет дополнительные данные (модуляция, стандарт, символьная скорость)
в таблице TechAnalyze по имени источника и спутнику.
Args: Args:
row: строка DataFrame row: строка DataFrame
source: объект Source для связи (может быть None если is_automatic=True) source: объект Source для связи (может быть None если is_automatic=True)
@@ -845,6 +917,9 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
name=row["sat"], defaults={"norad": row["norad_id"]} name=row["sat"], defaults={"norad": row["norad_id"]}
) )
# Ищем данные в TechAnalyze
tech_data = _find_tech_analyze_data(row["obj"], sat_obj)
# Обработка зеркал - теперь это спутники # Обработка зеркал - теперь это спутники
mirror_names = [] mirror_names = []
if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-": if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-":
@@ -901,14 +976,28 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
created_by=user_to_use created_by=user_to_use
) )
# Создаем Parameter # Создаем Parameter с данными из TechAnalyze (если найдены)
Parameter.objects.create( if tech_data:
id_satellite=sat_obj, # Используем данные из TechAnalyze
polarization=pol_obj, Parameter.objects.create(
frequency=row["freq"], id_satellite=sat_obj,
freq_range=row["f_range"], polarization=pol_obj,
objitem=obj_item, frequency=row["freq"],
) freq_range=row["f_range"],
bod_velocity=tech_data['bod_velocity'],
modulation=tech_data['modulation'],
standard=tech_data['standard'],
objitem=obj_item,
)
else:
# Создаем без дополнительных данных (как раньше)
Parameter.objects.create(
id_satellite=sat_obj,
polarization=pol_obj,
frequency=row["freq"],
freq_range=row["f_range"],
objitem=obj_item,
)
# Связываем geo с objitem # Связываем geo с objitem
geo_obj.objitem = obj_item geo_obj.objitem = obj_item

View File

@@ -0,0 +1,167 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from django.db import transaction
import json
from ..models import (
TechAnalyze,
Satellite,
Polarization,
Modulation,
Standard,
)
@login_required
def tech_analyze_entry(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):
"""
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)
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=400)
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)

View File

@@ -177,8 +177,8 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
name=name, name=name,
defaults={ defaults={
"norad": int(norad[0]) if norad else -1, "norad": int(norad[0]) if norad else -1,
"international_code": intl_code[0], "international_code": intl_code[0] if intl_code else "",
"undersat_point": sub_sat_point[0] "undersat_point": sub_sat_point[0 if sub_sat_point else ""]
}) })
trans_obj, created = Transponders.objects.get_or_create( trans_obj, created = Transponders.objects.get_or_create(
polarization=pol_obj, polarization=pol_obj,