Исправил импорт данных с привязкой спутников

This commit is contained in:
2025-12-01 15:48:00 +03:00
parent c72bf12d41
commit 8d75e47abc
7 changed files with 691 additions and 531 deletions

3
.gitignore vendored
View File

@@ -33,4 +33,5 @@ tiles
# Docker
# docker-*
maplibre-gl-js-5.10.0.zip
cert.pem
cert.pem
templ.json

View File

@@ -1,422 +1,430 @@
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View
class FormatCoordinatesTestCase(TestCase):
"""Тесты для функции format_coordinates"""
def test_format_positive_coordinates(self):
"""Тест форматирования положительных координат"""
result = format_coordinates(37.62, 55.75)
self.assertEqual(result, "55.75N 37.62E")
def test_format_negative_longitude(self):
"""Тест форматирования с отрицательной долготой"""
result = format_coordinates(-122.42, 37.77)
self.assertEqual(result, "37.77N 122.42W")
def test_format_negative_latitude(self):
"""Тест форматирования с отрицательной широтой"""
result = format_coordinates(151.21, -33.87)
self.assertEqual(result, "33.87S 151.21E")
def test_format_both_negative(self):
"""Тест форматирования с обеими отрицательными координатами"""
result = format_coordinates(-58.38, -34.60)
self.assertEqual(result, "34.6S 58.38W")
class ParsePaginationParamsTestCase(TestCase):
"""Тесты для функции parse_pagination_params"""
def setUp(self):
self.factory = RequestFactory()
def test_default_values(self):
"""Тест значений по умолчанию"""
request = self.factory.get("/")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
self.assertEqual(per_page, 50)
def test_custom_values(self):
"""Тест пользовательских значений"""
request = self.factory.get("/?page=3&items_per_page=100")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 3)
self.assertEqual(per_page, 100)
def test_invalid_page_number(self):
"""Тест невалидного номера страницы"""
request = self.factory.get("/?page=invalid")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_negative_page_number(self):
"""Тест отрицательного номера страницы"""
request = self.factory.get("/?page=-5")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_max_items_per_page_limit(self):
"""Тест ограничения максимального количества элементов"""
request = self.factory.get("/?items_per_page=20000")
page, per_page = parse_pagination_params(request)
self.assertEqual(per_page, 10000)
class RoleRequiredMixinTestCase(TestCase):
"""Тесты для RoleRequiredMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_admin_has_access(self):
"""Тест что администратор имеет доступ"""
user = User.objects.create_user(username="testuser", password="12345")
# Get the automatically created CustomUser and set role to 'admin'
custom_user = CustomUser.objects.get(user=user)
custom_user.role = "admin"
custom_user.save()
# Refresh user to get updated customuser
user.refresh_from_db()
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user
view.request = request
self.assertTrue(view.test_func())
def test_user_without_role_denied(self):
"""Тест что пользователь без роли не имеет доступа"""
user_no_role = User.objects.create_user(username="norole", password="12345")
# Get the automatically created CustomUser - default role is 'user'
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
self.assertEqual(custom_user_no_role.role, "user")
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user_no_role
view.request = request
self.assertFalse(view.test_func())
class CoordinateProcessingMixinTestCase(TestCase):
"""Тесты для CoordinateProcessingMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_extract_geo_coordinates(self):
"""Тест извлечения координат геолокации"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNotNone(coords)
self.assertEqual(coords, (37.62, 55.75))
def test_extract_invalid_coordinates(self):
"""Тест извлечения невалидных координат"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNone(coords)
def test_process_coordinates(self):
"""Тест обработки координат и применения к объекту Geo"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/",
{
"geo_longitude": "37.62",
"geo_latitude": "55.75",
},
)
view.request = request
geo_instance = Geo()
view.process_coordinates(geo_instance)
self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
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")
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View
class FormatCoordinatesTestCase(TestCase):
"""Тесты для функции format_coordinates"""
def test_format_positive_coordinates(self):
"""Тест форматирования положительных координат"""
result = format_coordinates(37.62, 55.75)
self.assertEqual(result, "55.75N 37.62E")
def test_format_negative_longitude(self):
"""Тест форматирования с отрицательной долготой"""
result = format_coordinates(-122.42, 37.77)
self.assertEqual(result, "37.77N 122.42W")
def test_format_negative_latitude(self):
"""Тест форматирования с отрицательной широтой"""
result = format_coordinates(151.21, -33.87)
self.assertEqual(result, "33.87S 151.21E")
def test_format_both_negative(self):
"""Тест форматирования с обеими отрицательными координатами"""
result = format_coordinates(-58.38, -34.60)
self.assertEqual(result, "34.6S 58.38W")
class ParsePaginationParamsTestCase(TestCase):
"""Тесты для функции parse_pagination_params"""
def setUp(self):
self.factory = RequestFactory()
def test_default_values(self):
"""Тест значений по умолчанию"""
request = self.factory.get("/")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
self.assertEqual(per_page, 50)
def test_custom_values(self):
"""Тест пользовательских значений"""
request = self.factory.get("/?page=3&items_per_page=100")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 3)
self.assertEqual(per_page, 100)
def test_invalid_page_number(self):
"""Тест невалидного номера страницы"""
request = self.factory.get("/?page=invalid")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_negative_page_number(self):
"""Тест отрицательного номера страницы"""
request = self.factory.get("/?page=-5")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_max_items_per_page_limit(self):
"""Тест ограничения максимального количества элементов"""
request = self.factory.get("/?items_per_page=20000")
page, per_page = parse_pagination_params(request)
self.assertEqual(per_page, 10000)
class RoleRequiredMixinTestCase(TestCase):
"""Тесты для RoleRequiredMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_admin_has_access(self):
"""Тест что администратор имеет доступ"""
user = User.objects.create_user(username="testuser", password="12345")
# Get the automatically created CustomUser and set role to 'admin'
custom_user = CustomUser.objects.get(user=user)
custom_user.role = "admin"
custom_user.save()
# Refresh user to get updated customuser
user.refresh_from_db()
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user
view.request = request
self.assertTrue(view.test_func())
def test_user_without_role_denied(self):
"""Тест что пользователь без роли не имеет доступа"""
user_no_role = User.objects.create_user(username="norole", password="12345")
# Get the automatically created CustomUser - default role is 'user'
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
self.assertEqual(custom_user_no_role.role, "user")
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user_no_role
view.request = request
self.assertFalse(view.test_func())
class CoordinateProcessingMixinTestCase(TestCase):
"""Тесты для CoordinateProcessingMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_extract_geo_coordinates(self):
"""Тест извлечения координат геолокации"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNotNone(coords)
self.assertEqual(coords, (37.62, 55.75))
def test_extract_invalid_coordinates(self):
"""Тест извлечения невалидных координат"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNone(coords)
def test_process_coordinates(self):
"""Тест обработки координат и применения к объекту Geo"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/",
{
"geo_longitude": "37.62",
"geo_latitude": "55.75",
},
)
view.request = request
geo_instance = Geo()
view.process_coordinates(geo_instance)
self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
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;"""
# Выполняем импорт
result = get_points_from_csv(csv_content, self.custom_user)
# Проверяем результаты
# Первые две точки близко (Москва), третья далеко (Екатеринбург)
# Должно быть создано 2 источника
self.assertEqual(result['new_sources'], 2)
self.assertEqual(result['added'], 3)
self.assertEqual(result['skipped'], 0)
self.assertEqual(len(result['errors']), 0)
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;"""
result_1 = get_points_from_csv(csv_content_1, self.custom_user)
self.assertEqual(result_1['new_sources'], 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;"""
result_2 = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем результаты
# Должен быть создан 1 новый источник (для точки 4)
self.assertEqual(result_2['new_sources'], 1)
self.assertEqual(result_2['added'], 2)
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;"""
result = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем, что дубликат пропущен
self.assertEqual(result['new_sources'], 0)
self.assertEqual(result['added'], 0)
self.assertEqual(result['skipped'], 1)
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;"""
result = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем результаты
self.assertEqual(result['new_sources'], 1) # Только для Екатеринбурга
self.assertEqual(result['added'], 3) # Точки 3, 4, 5
self.assertEqual(result['skipped'], 1) # Точка 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")

View File

@@ -122,6 +122,72 @@ MINIMUM_BANDWIDTH_MHZ = 0.08
RANGE_DISTANCE = 56
# ============================================================================
# Вспомогательные функции для работы со спутниками
# ============================================================================
class SatelliteNotFoundError(Exception):
"""Исключение, возникающее когда спутник не найден в базе данных."""
pass
def get_satellite_by_norad(norad_id: int) -> Satellite:
"""
Получает спутник по NORAD ID с обработкой ошибок.
Args:
norad_id: NORAD ID спутника
Returns:
Satellite: объект спутника
Raises:
SatelliteNotFoundError: если спутник не найден
ValueError: если norad_id некорректен
"""
if not norad_id or norad_id == -1:
raise ValueError(f"Некорректный NORAD ID: {norad_id}")
try:
return Satellite.objects.get(norad=norad_id)
except Satellite.DoesNotExist:
raise SatelliteNotFoundError(
f"Спутник с NORAD ID {norad_id} не найден в базе данных. "
f"Добавьте спутник в справочник перед импортом данных."
)
except Satellite.MultipleObjectsReturned:
# Если по какой-то причине есть дубликаты, берем первый
return Satellite.objects.filter(norad=norad_id).first()
def get_satellite_by_name(name: str) -> Satellite:
"""
Получает спутник по имени с обработкой ошибок.
Args:
name: имя спутника
Returns:
Satellite: объект спутника
Raises:
SatelliteNotFoundError: если спутник не найден
"""
if not name or name.strip() == "-":
raise ValueError(f"Некорректное имя спутника: {name}")
try:
return Satellite.objects.get(name=name.strip())
except Satellite.DoesNotExist:
raise SatelliteNotFoundError(
f"Спутник '{name}' не найден в базе данных. "
f"Добавьте спутник в справочник перед импортом данных."
)
except Satellite.MultipleObjectsReturned:
# Если есть дубликаты по имени, берем первый
return Satellite.objects.filter(name=name.strip()).first()
def get_all_constants():
sats = [sat.name for sat in Satellite.objects.all()]
standards = [sat.name for sat in Standard.objects.all()]
@@ -307,7 +373,12 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_au
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns:
int: количество созданных Source (или 0 если is_automatic=True)
dict: словарь с результатами импорта {
'new_sources': количество созданных Source,
'added': количество добавленных точек,
'skipped': количество пропущенных дубликатов,
'errors': список ошибок
}
"""
try:
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
@@ -321,6 +392,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_au
new_sources_count = 0
added_count = 0
skipped_count = 0
errors = []
# Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, id Source), Значение: объект Source
@@ -391,13 +463,21 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_au
added_count += 1
except Exception as e:
error_msg = f"Строка {idx + 2}: {str(e)}"
print(f"Ошибка при обработке строки {idx}: {e}")
errors.append(error_msg)
continue
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов, "
f"ошибок: {len(errors)}")
return new_sources_count
return {
'new_sources': new_sources_count,
'added': added_count,
'skipped': skipped_count,
'errors': errors
}
def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic=False):
@@ -574,6 +654,11 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic
def add_satellite_list():
"""
Добавляет список спутников в базу данных (если их еще нет).
Примечание: Эта функция устарела. Используйте админ-панель для добавления спутников.
"""
sats = [
"AZERSPACE 2",
"Amos 4",
@@ -673,7 +758,12 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns:
int: количество созданных Source (или 0 если is_automatic=True)
dict: словарь с результатами импорта {
'new_sources': количество созданных Source,
'added': количество добавленных точек,
'skipped': количество пропущенных дубликатов,
'errors': список ошибок
}
"""
df = pd.read_csv(
io.StringIO(file_content),
@@ -711,6 +801,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
new_sources_count = 0
added_count = 0
skipped_count = 0
errors = []
# Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, имя спутника, id Source), Значение: объект Source
@@ -733,14 +824,15 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
skipped_count += 1
continue
# Получаем или создаем объект спутника
# sat_obj, _ = Satellite.objects.get_or_create(
# name=sat_name, defaults={"norad": row["norad_id"]}
# )
# Получаем объект спутника по NORAD ID
try:
sat_obj = get_satellite_by_norad(row["norad_id"])
except (SatelliteNotFoundError, ValueError) as e:
error_msg = f"Строка {idx + 2}: {str(e)}"
print(error_msg)
errors.append(error_msg)
continue
sat_obj, _ = Satellite.objects.get_or_create(
norad=row["norad_id"], defaults={"name": sat_name}
)
source = None
# Если is_automatic=False, работаем с Source
@@ -785,13 +877,21 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
added_count += 1
except Exception as e:
error_msg = f"Строка {idx + 2}: {str(e)}"
print(f"Ошибка при обработке строки {idx}: {e}")
errors.append(error_msg)
continue
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов, "
f"ошибок: {len(errors)}")
return new_sources_count
return {
'new_sources': new_sources_count,
'added': added_count,
'skipped': skipped_count,
'errors': errors
}
def _is_duplicate_by_coords_and_time(coord_tuple, timestamp, tolerance_km=0.001):
@@ -923,9 +1023,12 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
pol = "-"
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=row["sat"], defaults={"norad": row["norad_id"]}
)
# Получаем объект спутника по NORAD ID
try:
sat_obj = get_satellite_by_norad(row["norad_id"])
except (SatelliteNotFoundError, ValueError) as e:
raise Exception(f"Не удалось получить спутник: {str(e)}")
# Ищем данные в TechAnalyze
tech_data = _find_tech_analyze_data(row["obj"], sat_obj)

View File

@@ -88,14 +88,25 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
df = df.head(number)
result = fill_data_from_df(df, selected_sat, self.request.user.customuser, is_automatic)
# Формируем сообщение об успехе
if is_automatic:
messages.success(
self.request, f"Данные успешно загружены как автоматические! Добавлено точек: {len(df)}"
)
success_msg = f"Данные успешно загружены как автоматические! Добавлено точек: {result['added']}"
else:
messages.success(
self.request, f"Данные успешно загружены! Создано источников: {result}"
success_msg = f"Данные успешно загружены! Создано источников: {result['new_sources']}, добавлено точек: {result['added']}"
if result['skipped'] > 0:
success_msg += f", пропущено дубликатов: {result['skipped']}"
messages.success(self.request, success_msg)
# Показываем ошибки, если они есть
if result['errors']:
error_count = len(result['errors'])
messages.warning(
self.request,
f"Обнаружено ошибок: {error_count}. Первые ошибки: " + "; ".join(result['errors'][:5])
)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
@@ -124,10 +135,25 @@ class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
result = get_points_from_csv(content, self.request.user.customuser, is_automatic)
# Формируем сообщение об успехе
if is_automatic:
messages.success(self.request, "Данные успешно загружены как автоматические!")
success_msg = f"Данные успешно загружены как автоматические! Добавлено точек: {result['added']}"
else:
messages.success(self.request, f"Данные успешно загружены! Создано источников: {result}")
success_msg = f"Данные успешно загружены! Создано источников: {result['new_sources']}, добавлено точек: {result['added']}"
if result['skipped'] > 0:
success_msg += f", пропущено дубликатов: {result['skipped']}"
messages.success(self.request, success_msg)
# Показываем ошибки, если они есть
if result['errors']:
error_count = len(result['errors'])
messages.warning(
self.request,
f"Обнаружено ошибок: {error_count}. Первые ошибки: " + "; ".join(result['errors'][:5])
)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:load_csv_data")

View File

@@ -113,9 +113,17 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
df = df.head(number)
result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
messages.success(
self.request, f"Данные успешно загружены! Обработано строк: {result}"
)
# Обработка нового формата результата
success_msg = f"Данные успешно загружены! Создано источников: {result['new_sources']}, добавлено точек: {result['added']}"
if result['skipped'] > 0:
success_msg += f", пропущено дубликатов: {result['skipped']}"
messages.success(self.request, success_msg)
if result['errors']:
messages.warning(
self.request,
f"Обнаружено ошибок: {len(result['errors'])}. Первые ошибки: " + "; ".join(result['errors'][:5])
)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
@@ -180,7 +188,19 @@ class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
if isinstance(content, bytes):
content = content.decode("utf-8")
get_points_from_csv(content, self.request.user.customuser)
result = get_points_from_csv(content, self.request.user.customuser)
# Обработка нового формата результата
success_msg = f"Данные успешно загружены! Создано источников: {result['new_sources']}, добавлено точек: {result['added']}"
if result['skipped'] > 0:
success_msg += f", пропущено дубликатов: {result['skipped']}"
messages.success(self.request, success_msg)
if result['errors']:
messages.warning(
self.request,
f"Обнаружено ошибок: {len(result['errors'])}. Первые ошибки: " + "; ".join(result['errors'][:5])
)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:load_csv_data")

View File

@@ -1,8 +1,9 @@
services:
web:
build:
context: ./dbapp
dockerfile: Dockerfile
# build:
# context: ./dbapp
# dockerfile: Dockerfile
image: https://registry.geraltserv.ru/geolocation:latest
env_file:
- .env.prod
depends_on:
@@ -14,9 +15,10 @@ services:
- 8000
worker:
build:
context: ./dbapp
dockerfile: Dockerfile
# build:
# context: ./dbapp
# dockerfile: Dockerfile
image: https://registry.geraltserv.ru/geolocation:latest
env_file:
- .env.prod
#entrypoint: []

View File

@@ -1,76 +1,76 @@
services:
db:
image: postgis/postgis:18-3.6
container_name: postgres-postgis
restart: unless-stopped
environment:
POSTGRES_DB: geodb
POSTGRES_USER: geralt
POSTGRES_PASSWORD: 123456
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
networks:
- app-network
redis:
image: redis:8.2.3-alpine
container_name: redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr-dev
restart: unless-stopped
ports:
- "8191:8191"
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=none
networks:
- app-network
# nginx:
# image: nginx:alpine
# container_name: nginx
# restart: unless-stopped
# ports:
# - "80:80"
# # - "443:443"
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./nginx/conf.d:/etc/nginx/conf.d:ro
# - ./dbapp/staticfiles:/app/staticfiles:ro
# networks:
# - app-network
# tileserver:
# image: maptiler/tileserver-gl:latest
# container_name: tileserver-gl-dev
# restart: unless-stopped
# ports:
# - "8080:8080"
# volumes:
# - ./tiles:/data
# - tileserver_config_dev:/config
# environment:
# - VERBOSE=true
# networks:
# - app-network
volumes:
postgres_data:
redis_data:
# tileserver_config_dev:
networks:
app-network:
services:
db:
image: postgis/postgis:18-3.6
container_name: postgres-postgis
restart: unless-stopped
environment:
POSTGRES_DB: geodb
POSTGRES_USER: geralt
POSTGRES_PASSWORD: 123456
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
networks:
- app-network
redis:
image: redis:8.2.3-alpine
container_name: redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr-dev
restart: unless-stopped
ports:
- "8191:8191"
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=none
networks:
- app-network
# nginx:
# image: nginx:alpine
# container_name: nginx
# restart: unless-stopped
# ports:
# - "80:80"
# # - "443:443"
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./nginx/conf.d:/etc/nginx/conf.d:ro
# - ./dbapp/staticfiles:/app/staticfiles:ro
# networks:
# - app-network
# tileserver:
# image: maptiler/tileserver-gl:latest
# container_name: tileserver-gl-dev
# restart: unless-stopped
# ports:
# - "8080:8080"
# volumes:
# - ./tiles:/data
# - tileserver_config_dev:/config
# environment:
# - VERBOSE=true
# networks:
# - app-network
volumes:
postgres_data:
redis_data:
# tileserver_config_dev:
networks:
app-network:
driver: bridge