From 8d75e47abc7b528295365eee06cc3da29429882f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Mon, 1 Dec 2025 15:48:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=20=D0=BF=D1=80=D0=B8=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=BA=D0=BE=D0=B9=20=D1=81=D0=BF=D1=83=D1=82=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- dbapp/mainapp/tests.py | 852 +++++++++++++++-------------- dbapp/mainapp/utils.py | 135 ++++- dbapp/mainapp/views/data_import.py | 40 +- dbapp/mainapp/views_old.py | 28 +- docker-compose.prod.yaml | 14 +- docker-compose.yaml | 150 ++--- 7 files changed, 691 insertions(+), 531 deletions(-) diff --git a/.gitignore b/.gitignore index 1d5eb97..b154f61 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ tiles # Docker # docker-* maplibre-gl-js-5.10.0.zip -cert.pem \ No newline at end of file +cert.pem +templ.json \ No newline at end of file diff --git a/dbapp/mainapp/tests.py b/dbapp/mainapp/tests.py index 66c4706..8c4911e 100644 --- a/dbapp/mainapp/tests.py +++ b/dbapp/mainapp/tests.py @@ -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") diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index 3cca276..fc0fd47 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -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) diff --git a/dbapp/mainapp/views/data_import.py b/dbapp/mainapp/views/data_import.py index 8617c20..202aa9f 100644 --- a/dbapp/mainapp/views/data_import.py +++ b/dbapp/mainapp/views/data_import.py @@ -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") diff --git a/dbapp/mainapp/views_old.py b/dbapp/mainapp/views_old.py index 3865f61..b787cb0 100644 --- a/dbapp/mainapp/views_old.py +++ b/dbapp/mainapp/views_old.py @@ -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") diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index fe24e87..e6fe266 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -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: [] diff --git a/docker-compose.yaml b/docker-compose.yaml index 1ea5ae2..45cbdc5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 \ No newline at end of file