Compare commits

...

3 Commits

13 changed files with 928 additions and 22 deletions

View File

@@ -34,6 +34,7 @@ from .models import (
CustomUser,
Band,
Source,
TechAnalyze,
)
from .filters import (
GeoKupDistanceFilter,
@@ -1084,3 +1085,79 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
)
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(
max_length=20,
max_length=80,
unique=True,
verbose_name="Стандарт",
db_index=True,
@@ -475,6 +475,123 @@ class ObjItemManager(models.Manager):
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):
"""
Модель источника сигнала.

View File

@@ -183,7 +183,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Search for ObjItem data
async function searchObjItemData(objectName, satelliteId) {
async function searchObjItemData(objectName, satelliteId, latitude, longitude) {
try {
const params = new URLSearchParams({
name: objectName,
@@ -193,6 +193,11 @@ document.addEventListener('DOMContentLoaded', function() {
params.append('satellite_id', satelliteId);
}
if (latitude && longitude) {
params.append('latitude', latitude);
params.append('longitude', longitude);
}
const response = await fetch(`/api/search-objitem/?${params.toString()}`);
const data = await response.json();
@@ -232,7 +237,24 @@ document.addEventListener('DOMContentLoaded', function() {
// Search for ObjItem data
const satelliteId = satelliteSelect.value;
const objItemData = await searchObjItemData(parsedData.object_name, satelliteId);
// Extract latitude and longitude from coordinates
let latitude = null;
let longitude = null;
if (parsedData.coordinates && parsedData.coordinates !== '-') {
const coordParts = parsedData.coordinates.split(',').map(c => c.trim());
if (coordParts.length === 2) {
latitude = coordParts[0];
longitude = coordParts[1];
}
}
const objItemData = await searchObjItemData(
parsedData.object_name,
satelliteId,
latitude,
longitude
);
// Show warning if object not found
if (!objItemData.found) {

View File

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

View File

@@ -80,7 +80,7 @@
</a>
{% endif %}
<a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
<i class="bi bi-keyboard"></i> Ввод данных
Передача точек
</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
@@ -94,7 +94,7 @@
<i class="bi bi-trash"></i> Удалить
</button>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
<button type="button" class="btn btn-primary btn-sm" title="Показать на карте"
onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</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,
)
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
from .views.tech_analyze import tech_analyze_entry, tech_analyze_save
app_name = 'mainapp'
@@ -126,5 +127,7 @@ 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('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.
Теперь ищет дополнительные данные (модуляция, стандарт, символьная скорость)
в таблице TechAnalyze по имени источника и спутнику, если они не указаны в Excel.
Args:
row: строка DataFrame
sat: объект Satellite
@@ -420,7 +423,7 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
except KeyError:
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
# Обработка ВЧ параметров
# Обработка ВЧ параметров из Excel
freq = remove_str(row["Частота, МГц"])
freq_line = 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:
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["ОСШ"])
# Обработка стандарта (если есть в 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()
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
)
# Создаем Parameter
# Создаем Parameter (с данными из TechAnalyze если они были найдены)
Parameter.objects.create(
id_satellite=sat,
polarization=polarization_obj,
@@ -519,6 +556,7 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
bod_velocity=v,
modulation=mod_obj,
snr=snr,
standard=standard_obj,
objitem=obj_item,
)
@@ -817,10 +855,44 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
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):
"""
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
Теперь ищет дополнительные данные (модуляция, стандарт, символьная скорость)
в таблице TechAnalyze по имени источника и спутнику.
Args:
row: строка DataFrame
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"]}
)
# Ищем данные в TechAnalyze
tech_data = _find_tech_analyze_data(row["obj"], sat_obj)
# Обработка зеркал - теперь это спутники
mirror_names = []
if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-":
@@ -901,7 +976,21 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
created_by=user_to_use
)
# Создаем Parameter
# Создаем Parameter с данными из TechAnalyze (если найдены)
if tech_data:
# Используем данные из TechAnalyze
Parameter.objects.create(
id_satellite=sat_obj,
polarization=pol_obj,
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,

View File

@@ -35,13 +35,18 @@ class DataEntryView(LoginRequiredMixin, View):
class SearchObjItemAPIView(LoginRequiredMixin, View):
"""
API endpoint for searching ObjItem by name.
Returns first matching ObjItem with all required data.
API endpoint for searching ObjItem by name and coordinates.
Returns closest matching ObjItem with all required data.
"""
def get(self, request):
from django.contrib.gis.geos import Point
from django.contrib.gis.db.models.functions import Distance
name = request.GET.get('name', '').strip()
satellite_id = request.GET.get('satellite_id', '').strip()
latitude = request.GET.get('latitude', '').strip()
longitude = request.GET.get('longitude', '').strip()
if not name:
return JsonResponse({'error': 'Name parameter is required'}, status=400)
@@ -57,8 +62,11 @@ class SearchObjItemAPIView(LoginRequiredMixin, View):
except (ValueError, TypeError):
pass
# Search for ObjItem
objitem = ObjItem.objects.filter(query).select_related(
# Filter ObjItems with geo data
query &= Q(geo_obj__coords__isnull=False)
# Get queryset
objitems = ObjItem.objects.filter(query).select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
@@ -67,7 +75,25 @@ class SearchObjItemAPIView(LoginRequiredMixin, View):
'geo_obj'
).prefetch_related(
'geo_obj__mirrors'
).first()
)
# If coordinates provided, find closest point
if latitude and longitude:
try:
lat = float(latitude.replace(',', '.'))
lon = float(longitude.replace(',', '.'))
point = Point(lon, lat, srid=4326)
# Order by distance and get closest
objitem = objitems.annotate(
distance=Distance('geo_obj__coords', point)
).order_by('distance').first()
except (ValueError, TypeError):
# If coordinate parsing fails, just get first match
objitem = objitems.first()
else:
# No coordinates provided, get first match
objitem = objitems.first()
if not objitem:
return JsonResponse({'found': False})

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

@@ -141,6 +141,8 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
sub_sat_point = sat.xpath('.//ns:subSatellitePoint/text()', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
@@ -174,7 +176,9 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
"norad": int(norad[0]) if norad else -1,
"international_code": intl_code[0] if intl_code else "",
"undersat_point": sub_sat_point[0 if sub_sat_point else ""]
})
trans_obj, created = Transponders.objects.get_or_create(
polarization=pol_obj,

View File

@@ -57,6 +57,17 @@ services:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static_volume:/usr/share/nginx/html/static
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
restart: unless-stopped
ports:
- "8191:8191"
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=none
volumes:
pgdata:
static_volume: