Compare commits
3 Commits
bd39717e86
...
0be829b97b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0be829b97b | |||
| 810d3a8f7f | |||
| efb99ea8d5 |
@@ -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",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
Модель источника сигнала.
|
Модель источника сигнала.
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search for ObjItem data
|
// Search for ObjItem data
|
||||||
async function searchObjItemData(objectName, satelliteId) {
|
async function searchObjItemData(objectName, satelliteId, latitude, longitude) {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
name: objectName,
|
name: objectName,
|
||||||
@@ -193,6 +193,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
params.append('satellite_id', satelliteId);
|
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 response = await fetch(`/api/search-objitem/?${params.toString()}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -232,7 +237,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Search for ObjItem data
|
// Search for ObjItem data
|
||||||
const satelliteId = satelliteSelect.value;
|
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
|
// Show warning if object not found
|
||||||
if (!objItemData.found) {
|
if (!objItemData.found) {
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
343
dbapp/mainapp/templates/mainapp/tech_analyze_entry.html
Normal file
343
dbapp/mainapp/templates/mainapp/tech_analyze_entry.html
Normal 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 %}
|
||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
@@ -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,7 +976,21 @@ 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 (если найдены)
|
||||||
|
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(
|
Parameter.objects.create(
|
||||||
id_satellite=sat_obj,
|
id_satellite=sat_obj,
|
||||||
polarization=pol_obj,
|
polarization=pol_obj,
|
||||||
|
|||||||
@@ -35,13 +35,18 @@ class DataEntryView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
class SearchObjItemAPIView(LoginRequiredMixin, View):
|
class SearchObjItemAPIView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API endpoint for searching ObjItem by name.
|
API endpoint for searching ObjItem by name and coordinates.
|
||||||
Returns first matching ObjItem with all required data.
|
Returns closest matching ObjItem with all required data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
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()
|
name = request.GET.get('name', '').strip()
|
||||||
satellite_id = request.GET.get('satellite_id', '').strip()
|
satellite_id = request.GET.get('satellite_id', '').strip()
|
||||||
|
latitude = request.GET.get('latitude', '').strip()
|
||||||
|
longitude = request.GET.get('longitude', '').strip()
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
return JsonResponse({'error': 'Name parameter is required'}, status=400)
|
return JsonResponse({'error': 'Name parameter is required'}, status=400)
|
||||||
@@ -57,8 +62,11 @@ class SearchObjItemAPIView(LoginRequiredMixin, View):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Search for ObjItem
|
# Filter ObjItems with geo data
|
||||||
objitem = ObjItem.objects.filter(query).select_related(
|
query &= Q(geo_obj__coords__isnull=False)
|
||||||
|
|
||||||
|
# Get queryset
|
||||||
|
objitems = ObjItem.objects.filter(query).select_related(
|
||||||
'parameter_obj',
|
'parameter_obj',
|
||||||
'parameter_obj__id_satellite',
|
'parameter_obj__id_satellite',
|
||||||
'parameter_obj__polarization',
|
'parameter_obj__polarization',
|
||||||
@@ -67,7 +75,25 @@ class SearchObjItemAPIView(LoginRequiredMixin, View):
|
|||||||
'geo_obj'
|
'geo_obj'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'geo_obj__mirrors'
|
'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:
|
if not objitem:
|
||||||
return JsonResponse({'found': False})
|
return JsonResponse({'found': False})
|
||||||
|
|||||||
167
dbapp/mainapp/views/tech_analyze.py
Normal file
167
dbapp/mainapp/views/tech_analyze.py
Normal 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)
|
||||||
@@ -141,6 +141,8 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
continue
|
continue
|
||||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||||
beams = sat.xpath('.//ns:BeamMemo', 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 = {}
|
zones = {}
|
||||||
for zone in beams:
|
for zone in beams:
|
||||||
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
|
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(
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
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] if intl_code else "",
|
||||||
|
"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,
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ services:
|
|||||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
- static_volume:/usr/share/nginx/html/static
|
- 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:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
static_volume:
|
static_volume:
|
||||||
Reference in New Issue
Block a user