Улучшение и добавления
This commit is contained in:
@@ -482,7 +482,6 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"modulation__name",
|
||||
"standard__name",
|
||||
)
|
||||
|
||||
autocomplete_fields = ("mark",)
|
||||
ordering = ("-frequency",)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from django import forms
|
||||
|
||||
# Local imports
|
||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
||||
from .widgets import TagSelectWidget
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
@@ -294,11 +295,21 @@ class ParameterForm(forms.ModelForm):
|
||||
class GeoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Geo
|
||||
fields = ['location', 'comment', 'is_average']
|
||||
fields = ['location', 'comment', 'is_average', 'mirrors']
|
||||
widgets = {
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}),
|
||||
}
|
||||
labels = {
|
||||
'location': 'Местоположение',
|
||||
'comment': 'Комментарий',
|
||||
'is_average': 'Усреднённое',
|
||||
'mirrors': 'Спутники-зеркала, использованные для приёма',
|
||||
}
|
||||
help_texts = {
|
||||
'mirrors': 'Начните вводить название спутника для поиска',
|
||||
}
|
||||
|
||||
class ObjItemForm(forms.ModelForm):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-13 14:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0003_source_coords_average_alter_objitem_source_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='mirrors',
|
||||
field=models.ManyToManyField(blank=True, help_text='Спутники-зеркала, использованные для приема', related_name='geo_mirrors', to='mainapp.satellite', verbose_name='Зеркала'),
|
||||
),
|
||||
]
|
||||
@@ -873,11 +873,11 @@ class Geo(models.Model):
|
||||
|
||||
# Связи
|
||||
mirrors = models.ManyToManyField(
|
||||
Mirror,
|
||||
Satellite,
|
||||
related_name="geo_mirrors",
|
||||
verbose_name="Зеркала",
|
||||
blank=True,
|
||||
help_text="Зеркала антенн, использованные для приема",
|
||||
help_text="Спутники-зеркала, использованные для приема",
|
||||
)
|
||||
objitem = models.OneToOneField(
|
||||
ObjItem,
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -47,6 +47,7 @@
|
||||
<th scope="col">Кем(обн)</th>
|
||||
<th scope="col">Создано</th>
|
||||
<th scope="col">Кем(созд)</th>
|
||||
<th scope="col">Зеркала</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="selected-items-table-body">
|
||||
|
||||
@@ -243,19 +243,36 @@
|
||||
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Нет данных о геолокации</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Зеркала:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.mirrors.all %}
|
||||
{% for mirror in object.geo_obj.mirrors.all %}
|
||||
{{ mirror.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Нет данных о геолокации</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
|
||||
@@ -235,6 +235,12 @@
|
||||
{% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'mainapp/components/_form_field.html' with field=geo_form.mirrors %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -247,7 +253,6 @@
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация карты
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -379,10 +380,11 @@
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.mirrors }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="26" class="text-center py-4">
|
||||
<td colspan="22" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
@@ -718,8 +720,8 @@
|
||||
|
||||
// Initialize column visibility - hide creation columns by default
|
||||
function initColumnVisibility() {
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="12"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="13"]');
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
|
||||
if (creationDateCheckbox) {
|
||||
creationDateCheckbox.checked = false;
|
||||
toggleColumn(creationDateCheckbox);
|
||||
@@ -731,9 +733,9 @@
|
||||
}
|
||||
|
||||
// Hide comment, is_average, and standard columns by default
|
||||
const commentCheckbox = document.querySelector('input[data-column="14"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="16"]');
|
||||
const commentCheckbox = document.querySelector('input[data-column="16"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="18"]');
|
||||
|
||||
if (commentCheckbox) {
|
||||
commentCheckbox.checked = false;
|
||||
@@ -913,10 +915,11 @@
|
||||
geo_coords: row.cells[11].textContent,
|
||||
kupsat_coords: row.cells[12].textContent,
|
||||
valid_coords: row.cells[13].textContent,
|
||||
updated_at: row.cells[17].textContent,
|
||||
updated_by: row.cells[18].textContent,
|
||||
created_at: row.cells[19].textContent,
|
||||
created_by: row.cells[20].textContent
|
||||
updated_at: row.cells[12].textContent,
|
||||
updated_by: row.cells[13].textContent,
|
||||
created_at: row.cells[14].textContent,
|
||||
created_by: row.cells[15].textContent,
|
||||
mirrors: row.cells[21].textContent
|
||||
};
|
||||
|
||||
window.selectedItems.push(rowData);
|
||||
@@ -978,6 +981,7 @@
|
||||
<td>${item.updated_by}</td>
|
||||
<td>${item.created_at}</td>
|
||||
<td>${item.created_by}</td>
|
||||
<td>${item.mirrors}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
|
||||
@@ -164,8 +164,6 @@ class CoordinateProcessingMixinTestCase(TestCase):
|
||||
{
|
||||
"geo_longitude": "37.62",
|
||||
"geo_latitude": "55.75",
|
||||
"kupsat_longitude": "37.63",
|
||||
"kupsat_latitude": "55.76",
|
||||
},
|
||||
)
|
||||
view.request = request
|
||||
@@ -175,5 +173,250 @@ class CoordinateProcessingMixinTestCase(TestCase):
|
||||
|
||||
self.assertIsNotNone(geo_instance.coords)
|
||||
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
|
||||
self.assertIsNotNone(geo_instance.coords_kupsat)
|
||||
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
|
||||
|
||||
|
||||
|
||||
class CSVImportTestCase(TestCase):
|
||||
"""Тесты для функции get_points_from_csv"""
|
||||
|
||||
def setUp(self):
|
||||
"""Подготовка тестовых данных"""
|
||||
from .models import CustomUser, Satellite, Polarization
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Создаем пользователя
|
||||
user = User.objects.create_user(username="testuser", password="12345")
|
||||
self.custom_user = CustomUser.objects.get(user=user)
|
||||
|
||||
# Создаем спутник
|
||||
self.satellite = Satellite.objects.create(name="Test Satellite", norad=12345)
|
||||
|
||||
# Создаем поляризации
|
||||
Polarization.objects.get_or_create(name="Вертикальная")
|
||||
Polarization.objects.get_or_create(name="Горизонтальная")
|
||||
Polarization.objects.get_or_create(name="Правая")
|
||||
Polarization.objects.get_or_create(name="Левая")
|
||||
Polarization.objects.get_or_create(name="-")
|
||||
|
||||
# Создаем спутники-зеркала для тестов
|
||||
Satellite.objects.get_or_create(name="Mirror1 Satellite", norad=11111)
|
||||
Satellite.objects.get_or_create(name="Mirror2 Satellite", norad=22222)
|
||||
Satellite.objects.get_or_create(name="Mirror3 Satellite", norad=33333)
|
||||
|
||||
def test_initial_csv_import(self):
|
||||
"""Тест первичного импорта из CSV файла"""
|
||||
from .utils import get_points_from_csv
|
||||
from .models import Source, ObjItem
|
||||
|
||||
# Тестовые данные CSV - 3 точки в разных местах
|
||||
csv_content = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;Mirror3
|
||||
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;
|
||||
3;Signal3 V;56.8389;60.6057;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
# Выполняем импорт
|
||||
sources_created = get_points_from_csv(csv_content, self.custom_user)
|
||||
|
||||
# Проверяем результаты
|
||||
# Первые две точки близко (Москва), третья далеко (Екатеринбург)
|
||||
# Должно быть создано 2 источника
|
||||
self.assertEqual(sources_created, 2)
|
||||
self.assertEqual(Source.objects.count(), 2)
|
||||
self.assertEqual(ObjItem.objects.count(), 3)
|
||||
|
||||
# Проверяем, что первые две точки привязаны к одному источнику
|
||||
source1 = Source.objects.first()
|
||||
items_in_source1 = ObjItem.objects.filter(source=source1).count()
|
||||
self.assertEqual(items_in_source1, 2)
|
||||
|
||||
def test_csv_import_with_existing_sources(self):
|
||||
"""Тест импорта CSV с существующими источниками"""
|
||||
from .utils import get_points_from_csv
|
||||
from .models import Source, ObjItem
|
||||
|
||||
# Первый импорт - создаем начальные данные
|
||||
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
|
||||
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
sources_created_1 = get_points_from_csv(csv_content_1, self.custom_user)
|
||||
self.assertEqual(sources_created_1, 1)
|
||||
initial_sources_count = Source.objects.count()
|
||||
initial_objitems_count = ObjItem.objects.count()
|
||||
|
||||
# Второй импорт - добавляем новые точки
|
||||
# Точка 3 - близко к существующему источнику (Москва)
|
||||
# Точка 4 - далеко (Екатеринбург) - создаст новый источник
|
||||
csv_content_2 = """3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
|
||||
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
sources_created_2 = get_points_from_csv(csv_content_2, self.custom_user)
|
||||
|
||||
# Проверяем результаты
|
||||
# Должен быть создан 1 новый источник (для точки 4)
|
||||
self.assertEqual(sources_created_2, 1)
|
||||
self.assertEqual(Source.objects.count(), initial_sources_count + 1)
|
||||
self.assertEqual(ObjItem.objects.count(), initial_objitems_count + 2)
|
||||
|
||||
# Проверяем, что точка 3 добавлена к существующему источнику
|
||||
first_source = Source.objects.first()
|
||||
items_in_first_source = ObjItem.objects.filter(source=first_source).count()
|
||||
self.assertEqual(items_in_first_source, 3) # 2 начальных + 1 новая
|
||||
|
||||
def test_csv_import_skip_duplicates(self):
|
||||
"""Тест пропуска дубликатов при импорте CSV"""
|
||||
from .utils import get_points_from_csv
|
||||
from .models import Source, ObjItem
|
||||
|
||||
# Первый импорт
|
||||
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
get_points_from_csv(csv_content_1, self.custom_user)
|
||||
initial_sources_count = Source.objects.count()
|
||||
initial_objitems_count = ObjItem.objects.count()
|
||||
|
||||
# Второй импорт - та же точка (дубликат)
|
||||
csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
sources_created = get_points_from_csv(csv_content_2, self.custom_user)
|
||||
|
||||
# Проверяем, что дубликат пропущен
|
||||
self.assertEqual(sources_created, 0)
|
||||
self.assertEqual(Source.objects.count(), initial_sources_count)
|
||||
self.assertEqual(ObjItem.objects.count(), initial_objitems_count)
|
||||
|
||||
def test_csv_import_mixed_scenario(self):
|
||||
"""Тест смешанного сценария: дубликаты + новые точки + близкие точки"""
|
||||
from .utils import get_points_from_csv
|
||||
from .models import Source, ObjItem
|
||||
|
||||
# Первый импорт - 2 точки в Москве
|
||||
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
|
||||
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
get_points_from_csv(csv_content_1, self.custom_user)
|
||||
|
||||
# Второй импорт:
|
||||
# - Точка 1 (дубликат) - должна быть пропущена
|
||||
# - Точка 3 (близко к Москве) - должна добавиться к существующему источнику
|
||||
# - Точка 4 (Екатеринбург) - должна создать новый источник
|
||||
# - Точка 5 (близко к Екатеринбургу) - должна добавиться к новому источнику
|
||||
csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
|
||||
3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
|
||||
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;
|
||||
5;Signal5 V;56.8391;60.6059;0;01.01.2024 12:20:00;Test Satellite;12345;11580.8;36.0;1;good;Mirror1;Mirror2;"""
|
||||
|
||||
sources_created = get_points_from_csv(csv_content_2, self.custom_user)
|
||||
|
||||
# Проверяем результаты
|
||||
self.assertEqual(sources_created, 1) # Только для Екатеринбурга
|
||||
self.assertEqual(Source.objects.count(), 2) # Москва + Екатеринбург
|
||||
self.assertEqual(ObjItem.objects.count(), 5) # 2 начальных + 3 новых (дубликат пропущен)
|
||||
|
||||
# Проверяем распределение по источникам
|
||||
moscow_source = Source.objects.first()
|
||||
ekb_source = Source.objects.last()
|
||||
|
||||
moscow_items = ObjItem.objects.filter(source=moscow_source).count()
|
||||
ekb_items = ObjItem.objects.filter(source=ekb_source).count()
|
||||
|
||||
self.assertEqual(moscow_items, 3) # 2 начальных + 1 новая
|
||||
self.assertEqual(ekb_items, 2) # 2 новых точки в Екатеринбурге
|
||||
|
||||
|
||||
|
||||
class FindMirrorSatellitesTestCase(TestCase):
|
||||
"""Тесты для функции find_mirror_satellites"""
|
||||
|
||||
def setUp(self):
|
||||
"""Подготовка тестовых данных"""
|
||||
from .models import Satellite
|
||||
|
||||
# Создаем спутники с разными именами
|
||||
Satellite.objects.create(name="Eutelsat 16A", norad=40874)
|
||||
Satellite.objects.create(name="Eutelsat 21B", norad=41591)
|
||||
Satellite.objects.create(name="Astra 4A", norad=41404)
|
||||
Satellite.objects.create(name="Turksat 4A", norad=40361)
|
||||
Satellite.objects.create(name="Express AM6", norad=39508)
|
||||
|
||||
def test_find_exact_match(self):
|
||||
"""Тест поиска спутника по точному совпадению"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites(["Eutelsat 16A"])
|
||||
self.assertEqual(len(mirrors), 1)
|
||||
self.assertEqual(mirrors[0].name, "Eutelsat 16A")
|
||||
|
||||
def test_find_partial_match(self):
|
||||
"""Тест поиска спутника по частичному совпадению"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
# Ищем по части имени "Eutelsat"
|
||||
mirrors = find_mirror_satellites(["eutelsat"])
|
||||
self.assertEqual(len(mirrors), 2)
|
||||
names = [m.name for m in mirrors]
|
||||
self.assertIn("Eutelsat 16A", names)
|
||||
self.assertIn("Eutelsat 21B", names)
|
||||
|
||||
def test_find_case_insensitive(self):
|
||||
"""Тест поиска без учета регистра"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
# Разные варианты регистра
|
||||
mirrors1 = find_mirror_satellites(["ASTRA"])
|
||||
mirrors2 = find_mirror_satellites(["astra"])
|
||||
mirrors3 = find_mirror_satellites(["AsTrA"])
|
||||
|
||||
self.assertEqual(len(mirrors1), 1)
|
||||
self.assertEqual(len(mirrors2), 1)
|
||||
self.assertEqual(len(mirrors3), 1)
|
||||
self.assertEqual(mirrors1[0].name, "Astra 4A")
|
||||
self.assertEqual(mirrors2[0].name, "Astra 4A")
|
||||
self.assertEqual(mirrors3[0].name, "Astra 4A")
|
||||
|
||||
def test_find_multiple_mirrors(self):
|
||||
"""Тест поиска нескольких зеркал"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites(["Eutelsat", "Turksat"])
|
||||
self.assertEqual(len(mirrors), 3) # 2 Eutelsat + 1 Turksat
|
||||
names = [m.name for m in mirrors]
|
||||
self.assertIn("Eutelsat 16A", names)
|
||||
self.assertIn("Eutelsat 21B", names)
|
||||
self.assertIn("Turksat 4A", names)
|
||||
|
||||
def test_find_with_spaces(self):
|
||||
"""Тест поиска с пробелами в начале и конце"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites([" Express "])
|
||||
self.assertEqual(len(mirrors), 1)
|
||||
self.assertEqual(mirrors[0].name, "Express AM6")
|
||||
|
||||
def test_find_empty_list(self):
|
||||
"""Тест с пустым списком"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites([])
|
||||
self.assertEqual(len(mirrors), 0)
|
||||
|
||||
def test_find_with_dash(self):
|
||||
"""Тест с дефисом (должен быть пропущен)"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites(["-"])
|
||||
self.assertEqual(len(mirrors), 0)
|
||||
|
||||
def test_find_no_match(self):
|
||||
"""Тест когда спутник не найден"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
mirrors = find_mirror_satellites(["NonExistentSatellite"])
|
||||
self.assertEqual(len(mirrors), 0)
|
||||
|
||||
def test_find_removes_duplicates(self):
|
||||
"""Тест удаления дубликатов"""
|
||||
from .utils import find_mirror_satellites
|
||||
|
||||
# Ищем один и тот же спутник дважды
|
||||
mirrors = find_mirror_satellites(["Astra", "Astra 4A"])
|
||||
self.assertEqual(len(mirrors), 1)
|
||||
self.assertEqual(mirrors[0].name, "Astra 4A")
|
||||
|
||||
@@ -51,6 +51,45 @@ def get_all_constants():
|
||||
return sats, standards, pols, mirrors, modulations
|
||||
|
||||
|
||||
def find_mirror_satellites(mirror_names: list) -> list:
|
||||
"""
|
||||
Находит спутники, которые соответствуют именам зеркал.
|
||||
|
||||
Алгоритм:
|
||||
1. Для каждого имени зеркала:
|
||||
- Обрезать пробелы и привести к нижнему регистру
|
||||
- Найти все спутники, в имени которых содержится это имя
|
||||
2. Вернуть список найденных спутников
|
||||
|
||||
Args:
|
||||
mirror_names: список имен зеркал
|
||||
|
||||
Returns:
|
||||
list: список объектов Satellite
|
||||
"""
|
||||
found_satellites = []
|
||||
|
||||
for mirror_name in mirror_names:
|
||||
if not mirror_name or mirror_name == "-":
|
||||
continue
|
||||
|
||||
# Обрезаем пробелы и приводим к нижнему регистру
|
||||
mirror_name_clean = mirror_name.strip().lower()
|
||||
|
||||
if not mirror_name_clean:
|
||||
continue
|
||||
|
||||
# Ищем спутники, в имени которых содержится имя зеркала
|
||||
satellites = Satellite.objects.filter(
|
||||
name__icontains=mirror_name_clean
|
||||
)
|
||||
|
||||
found_satellites.extend(satellites)
|
||||
|
||||
# Убираем дубликаты
|
||||
return list(set(found_satellites))
|
||||
|
||||
|
||||
def coords_transform(coords: str):
|
||||
lat_part, lon_part = coords.strip().split()
|
||||
sign_map = {"N": 1, "E": 1, "S": -1, "W": -1}
|
||||
@@ -83,24 +122,31 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
"""
|
||||
Импортирует данные из DataFrame с группировкой близких координат.
|
||||
|
||||
Алгоритм:
|
||||
Улучшенный алгоритм с учетом существующих Source:
|
||||
1. Извлечь все координаты и данные строк из DataFrame
|
||||
2. Создать список необработанных записей (координата + данные строки)
|
||||
3. Пока список не пуст:
|
||||
3. Получить все существующие Source из БД
|
||||
4. Для каждой необработанной записи:
|
||||
a. Найти ближайший существующий Source (расстояние <= 56 км)
|
||||
b. Если найден:
|
||||
- Обновить coords_average этого Source (инкрементально)
|
||||
- Создать ObjItem и связать с этим Source
|
||||
- Удалить запись из списка необработанных
|
||||
5. Пока список необработанных записей не пуст:
|
||||
a. Взять первую запись из списка
|
||||
b. Создать новый Source с coords_average = эта координата
|
||||
c. Создать ObjItem для этой записи и связать с Source
|
||||
d. Удалить запись из списка
|
||||
e. Для каждой оставшейся записи в списке:
|
||||
- Вычислить расстояние от её координаты до coords_average
|
||||
- Если расстояние <= 0.5 градуса:
|
||||
- Если расстояние <= 56 км:
|
||||
* Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
|
||||
new_avg = (coords_average + current_coord) / 2
|
||||
* Обновить coords_average в Source
|
||||
* Создать ObjItem для этой записи и связать с Source
|
||||
* Удалить запись из списка
|
||||
- Иначе: пропустить и проверить следующую запись
|
||||
4. Сохранить все изменения в БД
|
||||
6. Сохранить все изменения в БД
|
||||
|
||||
Важно: Среднее вычисляется инкрементально - каждая новая точка
|
||||
усредняется с текущим средним, а не со всеми точками кластера.
|
||||
@@ -137,23 +183,66 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
source_count = 0
|
||||
added_to_existing_count = 0
|
||||
|
||||
# Шаг 3: Цикл обработки пока список не пуст
|
||||
# Шаг 3: Получить все существующие Source из БД
|
||||
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
|
||||
|
||||
# Шаг 4: Попытка добавить записи к существующим Source
|
||||
records_to_remove = []
|
||||
|
||||
for i, record in enumerate(unprocessed_records):
|
||||
current_coord = record["coord"]
|
||||
|
||||
# Найти ближайший существующий Source
|
||||
closest_source = None
|
||||
min_distance = float('inf')
|
||||
best_new_avg = None
|
||||
|
||||
for source in existing_sources:
|
||||
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
|
||||
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_source = source
|
||||
best_new_avg = new_avg
|
||||
|
||||
# Если найден близкий Source (расстояние <= 56 км)
|
||||
if closest_source and min_distance <= RANGE_DISTANCE:
|
||||
# Обновить coords_average инкрементально
|
||||
closest_source.coords_average = Point(best_new_avg, srid=4326)
|
||||
closest_source.save()
|
||||
|
||||
# Создать ObjItem и связать с существующим Source
|
||||
_create_objitem_from_row(
|
||||
record["row"], sat, closest_source, user_to_use, consts
|
||||
)
|
||||
added_to_existing_count += 1
|
||||
|
||||
# Пометить запись для удаления
|
||||
records_to_remove.append(i)
|
||||
|
||||
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
|
||||
for i in reversed(records_to_remove):
|
||||
unprocessed_records.pop(i)
|
||||
|
||||
# Шаг 5: Цикл обработки оставшихся записей - создание новых Source
|
||||
while unprocessed_records:
|
||||
# Шаг 3a: Взять первую запись из списка
|
||||
# Шаг 5a: Взять первую запись из списка
|
||||
first_record = unprocessed_records.pop(0)
|
||||
first_coord = first_record["coord"]
|
||||
|
||||
# Шаг 3b: Создать новый Source с coords_average = эта координата
|
||||
# Шаг 5b: Создать новый Source с coords_average = эта координата
|
||||
source = Source.objects.create(
|
||||
coords_average=Point(first_coord, srid=4326), created_by=user_to_use
|
||||
)
|
||||
source_count += 1
|
||||
|
||||
# Шаг 3c: Создать ObjItem для этой записи и связать с Source
|
||||
# Шаг 5c: Создать ObjItem для этой записи и связать с Source
|
||||
_create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
|
||||
|
||||
# Шаг 3e: Для каждой оставшейся записи в списке
|
||||
# Шаг 5e: Для каждой оставшейся записи в списке
|
||||
records_to_remove = []
|
||||
|
||||
for i, record in enumerate(unprocessed_records):
|
||||
@@ -180,6 +269,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
for i in reversed(records_to_remove):
|
||||
unprocessed_records.pop(i)
|
||||
|
||||
print(f"Импорт завершен: создано {source_count} новых источников, "
|
||||
f"добавлено {added_to_existing_count} точек к существующим источникам")
|
||||
|
||||
return source_count
|
||||
|
||||
|
||||
@@ -225,26 +317,22 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
time_ = time(0, 0, 0)
|
||||
timestamp = datetime.combine(date, time_)
|
||||
|
||||
# Обработка зеркал
|
||||
current_mirrors = []
|
||||
# Обработка зеркал - теперь это спутники
|
||||
mirror_names = []
|
||||
mirror_1 = row["Зеркало 1"].strip().split("\n")
|
||||
mirror_2 = row["Зеркало 2"].strip().split("\n")
|
||||
|
||||
if len(mirror_1) > 1:
|
||||
for mir in mirror_1:
|
||||
Mirror.objects.get_or_create(name=mir.strip())
|
||||
current_mirrors.append(mir.strip())
|
||||
elif mirror_1[0] not in consts[3]:
|
||||
Mirror.objects.get_or_create(name=mirror_1[0].strip())
|
||||
current_mirrors.append(mirror_1[0].strip())
|
||||
# Собираем все имена зеркал
|
||||
for mir in mirror_1:
|
||||
if mir.strip() and mir.strip() != "-":
|
||||
mirror_names.append(mir.strip())
|
||||
|
||||
for mir in mirror_2:
|
||||
if mir.strip() and mir.strip() != "-":
|
||||
mirror_names.append(mir.strip())
|
||||
|
||||
if len(mirror_2) > 1:
|
||||
for mir in mirror_2:
|
||||
Mirror.objects.get_or_create(name=mir.strip())
|
||||
current_mirrors.append(mir.strip())
|
||||
elif mirror_2[0] not in consts[3]:
|
||||
Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
||||
current_mirrors.append(mirror_2[0].strip())
|
||||
# Находим спутники-зеркала
|
||||
mirror_satellites = find_mirror_satellites(mirror_names)
|
||||
|
||||
location = row["Местоопределение"].strip()
|
||||
comment = row["Комментарий"]
|
||||
@@ -260,7 +348,10 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
},
|
||||
)
|
||||
geo.save()
|
||||
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||
|
||||
# Устанавливаем связи с спутниками-зеркалами
|
||||
if mirror_satellites:
|
||||
geo.mirrors.set(mirror_satellites)
|
||||
|
||||
# Проверяем, существует ли уже ObjItem с таким же geo
|
||||
existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
|
||||
@@ -380,7 +471,7 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
4. Для каждой записи:
|
||||
a. Проверить, существует ли дубликат (координаты + частота)
|
||||
b. Если дубликат найден, пропустить запись
|
||||
c. Найти ближайший существующий Source (расстояние <= 0.5 градуса)
|
||||
c. Найти ближайший существующий Source (расстояние <= 56 км)
|
||||
d. Если найден:
|
||||
- Обновить coords_average этого Source (инкрементально)
|
||||
- Создать ObjItem и связать с этим Source
|
||||
@@ -460,6 +551,7 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
# Шаг 4c: Найти ближайший существующий Source
|
||||
closest_source = None
|
||||
min_distance = float('inf')
|
||||
best_new_avg = None
|
||||
|
||||
for source in existing_sources:
|
||||
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||
@@ -468,13 +560,12 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_source = source
|
||||
best_new_avg = new_avg
|
||||
|
||||
# Шаг 4d: Если найден близкий Source (расстояние <= 0.5 градуса)
|
||||
if closest_source and min_distance <= 0.5:
|
||||
# Шаг 4d: Если найден близкий Source (расстояние <= 56 км)
|
||||
if closest_source and min_distance <= RANGE_DISTANCE:
|
||||
# Обновить coords_average инкрементально
|
||||
current_avg = (closest_source.coords_average.x, closest_source.coords_average.y)
|
||||
# new_avg = calculate_average_coords_incremental(current_avg, current_coord)
|
||||
closest_source.coords_average = Point(new_avg, srid=4326)
|
||||
closest_source.coords_average = Point(best_new_avg, srid=4326)
|
||||
closest_source.save()
|
||||
|
||||
# Создать ObjItem и связать с существующим Source
|
||||
@@ -565,14 +656,20 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
name=row["sat"], defaults={"norad": row["norad_id"]}
|
||||
)
|
||||
mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
|
||||
mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
|
||||
mir_lst = [row["mir_1"], row["mir_2"]]
|
||||
if not pd.isna(row["mir_3"]):
|
||||
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
||||
mir_lst.append(row["mir_3"])
|
||||
|
||||
# Обработка зеркал - теперь это спутники
|
||||
mirror_names = []
|
||||
if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-":
|
||||
mirror_names.append(row["mir_1"])
|
||||
if not pd.isna(row["mir_2"]) and row["mir_2"].strip() != "-":
|
||||
mirror_names.append(row["mir_2"])
|
||||
if not pd.isna(row["mir_3"]) and row["mir_3"].strip() != "-":
|
||||
mirror_names.append(row["mir_3"])
|
||||
|
||||
# Находим спутники-зеркала
|
||||
mirror_satellites = find_mirror_satellites(mirror_names)
|
||||
|
||||
# Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
|
||||
# Создаем Geo объект
|
||||
geo_obj, _ = Geo.objects.get_or_create(
|
||||
timestamp=row["time"],
|
||||
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||
@@ -580,7 +677,10 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
"is_average": False,
|
||||
},
|
||||
)
|
||||
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||
|
||||
# Устанавливаем связи с спутниками-зеркалами
|
||||
if mirror_satellites:
|
||||
geo_obj.mirrors.set(mirror_satellites)
|
||||
|
||||
# Проверяем, существует ли уже ObjItem с таким же geo
|
||||
existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
|
||||
|
||||
@@ -306,10 +306,14 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
distance_geo_kup = "-"
|
||||
distance_geo_valid = "-"
|
||||
distance_kup_valid = "-"
|
||||
mirrors_list = []
|
||||
|
||||
if hasattr(obj, "geo_obj") and obj.geo_obj:
|
||||
geo_timestamp = obj.geo_obj.timestamp
|
||||
geo_location = obj.geo_obj.location
|
||||
|
||||
# Get mirrors
|
||||
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
|
||||
|
||||
if obj.geo_obj.coords:
|
||||
longitude = obj.geo_obj.coords.coords[0]
|
||||
@@ -417,6 +421,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"standard": standard_name,
|
||||
"has_sigma": has_sigma,
|
||||
"sigma_info": sigma_info,
|
||||
"mirrors": ", ".join(mirrors_list) if mirrors_list else "-",
|
||||
"obj": obj,
|
||||
}
|
||||
)
|
||||
@@ -588,6 +593,10 @@ class ObjItemFormView(
|
||||
self.process_timestamp(geo_instance)
|
||||
|
||||
geo_instance.save()
|
||||
|
||||
# Save ManyToMany relationship for mirrors
|
||||
if geo_form.is_valid():
|
||||
geo_instance.mirrors.set(geo_form.cleaned_data["mirrors"])
|
||||
|
||||
def get_or_create_geo_instance(self):
|
||||
"""Gets or creates Geo instance."""
|
||||
|
||||
Reference in New Issue
Block a user