diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 663ea33..36f5a16 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -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",), + }, + ), + ) diff --git a/dbapp/mainapp/migrations/0016_alter_satellite_international_code_techanalyze.py b/dbapp/mainapp/migrations/0016_alter_satellite_international_code_techanalyze.py new file mode 100644 index 0000000..43f9dcd --- /dev/null +++ b/dbapp/mainapp/migrations/0016_alter_satellite_international_code_techanalyze.py @@ -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'], + }, + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index c4fa522..3c96cde 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -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): """ Модель источника сигнала. diff --git a/dbapp/mainapp/templates/mainapp/objitem_list.html b/dbapp/mainapp/templates/mainapp/objitem_list.html index 69bc067..a12f2ce 100644 --- a/dbapp/mainapp/templates/mainapp/objitem_list.html +++ b/dbapp/mainapp/templates/mainapp/objitem_list.html @@ -49,10 +49,13 @@ Удалить {% endif %} - + + Тех. анализ + diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html index 401db58..82b2251 100644 --- a/dbapp/mainapp/templates/mainapp/source_list.html +++ b/dbapp/mainapp/templates/mainapp/source_list.html @@ -80,7 +80,7 @@ {% endif %} - Ввод данных + Передача точек Excel @@ -94,7 +94,7 @@ Удалить {% endif %} - diff --git a/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html b/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html new file mode 100644 index 0000000..d892f53 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html @@ -0,0 +1,343 @@ +{% extends "mainapp/base.html" %} +{% load static %} + +{% block title %}Тех. анализ - Ввод данных{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+

Тех. анализ - Ввод данных

+ +
+
+
+ + +
+ +
+
+ +
+
+
+
Таблица данных 0
+
+
+ + + + +
+
+ +
+
+
+{% endblock %} + +{% block extra_js %} + + + +{% endblock %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 48b631a..fa980d0 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -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'), ] \ No newline at end of file diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index f3d51e7..2a5cfa3 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -400,6 +400,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_au def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic=False): """ Вспомогательная функция для создания ObjItem из строки DataFrame. + + Теперь ищет дополнительные данные (модуляция, стандарт, символьная скорость) + в таблице TechAnalyze по имени источника и спутнику, если они не указаны в Excel. Args: row: строка DataFrame @@ -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["Символьная скорость, БОД"]) @@ -429,8 +432,42 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic mod_obj, _ = Modulation.objects.get_or_create(name=row["Модуляция"].strip()) 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() @@ -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,9 +855,43 @@ 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 @@ -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,14 +976,28 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False): created_by=user_to_use ) - # Создаем Parameter - Parameter.objects.create( - id_satellite=sat_obj, - polarization=pol_obj, - frequency=row["freq"], - freq_range=row["f_range"], - objitem=obj_item, - ) + # Создаем 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, + frequency=row["freq"], + freq_range=row["f_range"], + objitem=obj_item, + ) # Связываем geo с objitem geo_obj.objitem = obj_item diff --git a/dbapp/mainapp/views/tech_analyze.py b/dbapp/mainapp/views/tech_analyze.py new file mode 100644 index 0000000..05eb4f3 --- /dev/null +++ b/dbapp/mainapp/views/tech_analyze.py @@ -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) diff --git a/dbapp/mapsapp/utils.py b/dbapp/mapsapp/utils.py index 390e643..69de76e 100644 --- a/dbapp/mapsapp/utils.py +++ b/dbapp/mapsapp/utils.py @@ -177,8 +177,8 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None): name=name, defaults={ "norad": int(norad[0]) if norad else -1, - "international_code": intl_code[0], - "undersat_point": sub_sat_point[0] + "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,