diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 7a5a5af..0c5c71c 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -926,10 +926,10 @@ class SourceRequestForm(forms.ModelForm): Форма для создания и редактирования заявок на источники. """ - # Дополнительные поля для координат + # Дополнительные поля для координат ГСО coords_lat = forms.FloatField( required=False, - label='Широта', + label='Широта ГСО', widget=forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.000001', @@ -938,7 +938,27 @@ class SourceRequestForm(forms.ModelForm): ) coords_lon = forms.FloatField( required=False, - label='Долгота', + label='Долгота ГСО', + widget=forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.000001', + 'placeholder': 'Например: 37.618423' + }) + ) + + # Дополнительные поля для координат источника + coords_source_lat = forms.FloatField( + required=False, + label='Широта источника', + widget=forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.000001', + 'placeholder': 'Например: 55.751244' + }) + ) + coords_source_lon = forms.FloatField( + required=False, + label='Долгота источника', widget=forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.000001', @@ -951,10 +971,16 @@ class SourceRequestForm(forms.ModelForm): model = SourceRequest fields = [ 'source', + 'satellite', 'status', 'priority', 'planned_at', 'request_date', + 'card_date', + 'downlink', + 'uplink', + 'transfer', + 'region', 'gso_success', 'kubsat_success', 'comment', @@ -962,7 +988,9 @@ class SourceRequestForm(forms.ModelForm): widgets = { 'source': forms.Select(attrs={ 'class': 'form-select', - 'required': True + }), + 'satellite': forms.Select(attrs={ + 'class': 'form-select', }), 'status': forms.Select(attrs={ 'class': 'form-select' @@ -978,6 +1006,29 @@ class SourceRequestForm(forms.ModelForm): 'class': 'form-control', 'type': 'date' }), + 'card_date': forms.DateInput(attrs={ + 'class': 'form-control', + 'type': 'date' + }), + 'downlink': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.01', + 'placeholder': 'Частота downlink в МГц' + }), + 'uplink': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.01', + 'placeholder': 'Частота uplink в МГц' + }), + 'transfer': forms.NumberInput(attrs={ + 'class': 'form-control', + 'step': '0.01', + 'placeholder': 'Перенос в МГц' + }), + 'region': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Район/местоположение' + }), 'gso_success': forms.Select( choices=[(None, '-'), (True, 'Да'), (False, 'Нет')], attrs={'class': 'form-select'} @@ -994,10 +1045,16 @@ class SourceRequestForm(forms.ModelForm): } labels = { 'source': 'Источник', + 'satellite': 'Спутник', 'status': 'Статус', 'priority': 'Приоритет', 'planned_at': 'Дата и время планирования', 'request_date': 'Дата заявки', + 'card_date': 'Дата формирования карточки', + 'downlink': 'Частота Downlink (МГц)', + 'uplink': 'Частота Uplink (МГц)', + 'transfer': 'Перенос (МГц)', + 'region': 'Район', 'gso_success': 'ГСО успешно?', 'kubsat_success': 'Кубсат успешно?', 'comment': 'Комментарий', @@ -1008,14 +1065,23 @@ class SourceRequestForm(forms.ModelForm): source_id = kwargs.pop('source_id', None) super().__init__(*args, **kwargs) - # Загружаем queryset для источников + # Загружаем queryset для источников и спутников self.fields['source'].queryset = Source.objects.all().order_by('-id') + self.fields['source'].required = False + self.fields['satellite'].queryset = Satellite.objects.all().order_by('name') # Если передан source_id, устанавливаем его как начальное значение if source_id: self.fields['source'].initial = source_id # Можно сделать поле только для чтения self.fields['source'].widget.attrs['readonly'] = True + + # Пытаемся заполнить данные из источника + try: + source = Source.objects.get(pk=source_id) + self._fill_from_source(source) + except Source.DoesNotExist: + pass # Настраиваем виджеты для булевых полей self.fields['gso_success'].widget = forms.Select( @@ -1028,16 +1094,44 @@ class SourceRequestForm(forms.ModelForm): ) # Заполняем координаты из существующего объекта - if self.instance and self.instance.pk and self.instance.coords: - self.fields['coords_lat'].initial = self.instance.coords.y - self.fields['coords_lon'].initial = self.instance.coords.x + if self.instance and self.instance.pk: + if self.instance.coords: + self.fields['coords_lat'].initial = self.instance.coords.y + self.fields['coords_lon'].initial = self.instance.coords.x + if self.instance.coords_source: + self.fields['coords_source_lat'].initial = self.instance.coords_source.y + self.fields['coords_source_lon'].initial = self.instance.coords_source.x + + def _fill_from_source(self, source): + """Заполняет поля формы данными из источника и его связанных объектов.""" + # Получаем первую точку источника с транспондером + objitem = source.source_objitems.select_related( + 'transponder', 'transponder__sat_id', 'parameter_obj' + ).filter(transponder__isnull=False).first() + + if objitem and objitem.transponder: + transponder = objitem.transponder + # Заполняем данные из транспондера + if transponder.downlink: + self.fields['downlink'].initial = transponder.downlink + if transponder.uplink: + self.fields['uplink'].initial = transponder.uplink + if transponder.transfer: + self.fields['transfer'].initial = transponder.transfer + if transponder.sat_id: + self.fields['satellite'].initial = transponder.sat_id.pk + + # Координаты из источника + if source.coords_average: + self.fields['coords_lat'].initial = source.coords_average.y + self.fields['coords_lon'].initial = source.coords_average.x def save(self, commit=True): from django.contrib.gis.geos import Point instance = super().save(commit=False) - # Обрабатываем координаты + # Обрабатываем координаты ГСО coords_lat = self.cleaned_data.get('coords_lat') coords_lon = self.cleaned_data.get('coords_lon') @@ -1046,6 +1140,15 @@ class SourceRequestForm(forms.ModelForm): elif coords_lat is None and coords_lon is None: instance.coords = None + # Обрабатываем координаты источника + coords_source_lat = self.cleaned_data.get('coords_source_lat') + coords_source_lon = self.cleaned_data.get('coords_source_lon') + + if coords_source_lat is not None and coords_source_lon is not None: + instance.coords_source = Point(coords_source_lon, coords_source_lat, srid=4326) + elif coords_source_lat is None and coords_source_lon is None: + instance.coords_source = None + if commit: instance.save() diff --git a/dbapp/mainapp/migrations/0021_add_source_request_fields.py b/dbapp/mainapp/migrations/0021_add_source_request_fields.py new file mode 100644 index 0000000..aeaca25 --- /dev/null +++ b/dbapp/mainapp/migrations/0021_add_source_request_fields.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.7 on 2025-12-09 12:39 + +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0020_satellite_location_place'), + ] + + operations = [ + migrations.AddField( + model_name='sourcerequest', + name='card_date', + field=models.DateField(blank=True, help_text='Дата формирования карточки', null=True, verbose_name='Дата формирования карточки'), + ), + migrations.AddField( + model_name='sourcerequest', + name='coords_source', + field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты источника (WGS84)', null=True, srid=4326, verbose_name='Координаты источника'), + ), + migrations.AddField( + model_name='sourcerequest', + name='downlink', + field=models.FloatField(blank=True, help_text='Частота downlink в МГц', null=True, verbose_name='Частота Downlink, МГц'), + ), + migrations.AddField( + model_name='sourcerequest', + name='region', + field=models.CharField(blank=True, help_text='Район/местоположение', max_length=255, null=True, verbose_name='Район'), + ), + migrations.AddField( + model_name='sourcerequest', + name='satellite', + field=models.ForeignKey(blank=True, help_text='Связанный спутник', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_requests', to='mainapp.satellite', verbose_name='Спутник'), + ), + migrations.AddField( + model_name='sourcerequest', + name='transfer', + field=models.FloatField(blank=True, help_text='Перенос по частоте в МГц', null=True, verbose_name='Перенос, МГц'), + ), + migrations.AddField( + model_name='sourcerequest', + name='uplink', + field=models.FloatField(blank=True, help_text='Частота uplink в МГц', null=True, verbose_name='Частота Uplink, МГц'), + ), + migrations.AlterField( + model_name='sourcerequest', + name='coords', + field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты ГСО (WGS84)', null=True, srid=4326, verbose_name='Координаты ГСО'), + ), + migrations.AlterField( + model_name='sourcerequest', + name='source', + field=models.ForeignKey(blank=True, help_text='Связанный источник', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_requests', to='mainapp.source', verbose_name='Источник'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index 5af1b37..ce631eb 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -1216,15 +1216,28 @@ class SourceRequest(models.Model): ('high', 'Высокий'), ] - # Связь с источником + # Связь с источником (опционально для заявок без привязки) source = models.ForeignKey( Source, on_delete=models.CASCADE, related_name='source_requests', verbose_name='Источник', + null=True, + blank=True, help_text='Связанный источник', ) + # Связь со спутником + satellite = models.ForeignKey( + Satellite, + on_delete=models.SET_NULL, + related_name='satellite_requests', + verbose_name='Спутник', + null=True, + blank=True, + help_text='Связанный спутник', + ) + # Основные поля status = models.CharField( max_length=20, @@ -1256,12 +1269,38 @@ class SourceRequest(models.Model): verbose_name='Дата заявки', help_text='Дата подачи заявки', ) + card_date = models.DateField( + null=True, + blank=True, + verbose_name='Дата формирования карточки', + help_text='Дата формирования карточки', + ) status_updated_at = models.DateTimeField( auto_now=True, verbose_name='Дата обновления статуса', help_text='Дата и время последнего обновления статуса', ) + # Частоты и перенос + downlink = models.FloatField( + null=True, + blank=True, + verbose_name='Частота Downlink, МГц', + help_text='Частота downlink в МГц', + ) + uplink = models.FloatField( + null=True, + blank=True, + verbose_name='Частота Uplink, МГц', + help_text='Частота uplink в МГц', + ) + transfer = models.FloatField( + null=True, + blank=True, + verbose_name='Перенос, МГц', + help_text='Перенос по частоте в МГц', + ) + # Результаты gso_success = models.BooleanField( null=True, @@ -1276,6 +1315,15 @@ class SourceRequest(models.Model): help_text='Успешность Кубсат', ) + # Район + region = models.CharField( + max_length=255, + null=True, + blank=True, + verbose_name='Район', + help_text='Район/местоположение', + ) + # Комментарий comment = models.TextField( null=True, @@ -1284,13 +1332,22 @@ class SourceRequest(models.Model): help_text='Дополнительные комментарии к заявке', ) - # Координаты (усреднённые по выбранным точкам) + # Координаты ГСО (усреднённые по выбранным точкам) coords = gis.PointField( srid=4326, null=True, blank=True, - verbose_name='Координаты', - help_text='Усреднённые координаты по выбранным точкам (WGS84)', + verbose_name='Координаты ГСО', + help_text='Координаты ГСО (WGS84)', + ) + + # Координаты источника + coords_source = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name='Координаты источника', + help_text='Координаты источника (WGS84)', ) # Количество точек, использованных для расчёта координат diff --git a/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html b/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html index 5f607f1..5b244dc 100644 --- a/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html +++ b/dbapp/mainapp/templates/mainapp/components/_source_requests_tab.html @@ -1,10 +1,37 @@ +{% load static %} + + +
Заявки на источники
- +
+ + + +
@@ -41,198 +68,288 @@
- +
- +
-
- - Показано заявок: {{ requests|length }} - -
- -
- - - - - - - - - - - - - - - - - - - - {% for req in requests %} - - - - - - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
IDИсточникСтатусПриоритетКоординатыИмя точкиМодуляцияСимв. скор.Дата планированияГСОКубсатОбновленоДействия
{{ req.id }} - - #{{ req.source_id }} - - - - {{ req.get_status_display }} - - - - {{ req.get_priority_display }} - - - {% if req.coords %} - {{ req.coords.y|floatformat:6 }}, {{ req.coords.x|floatformat:6 }} - {% else %}-{% endif %} - {{ req.objitem_name|default:"-" }}{{ req.modulation|default:"-" }}{{ req.symbol_rate|default:"-" }}{{ req.planned_at|date:"d.m.Y H:i"|default:"-" }} - {% if req.gso_success is True %} - Да - {% elif req.gso_success is False %} - Нет - {% else %}-{% endif %} - - {% if req.kubsat_success is True %} - Да - {% elif req.kubsat_success is False %} - Нет - {% else %}-{% endif %} - {{ req.status_updated_at|date:"d.m.Y H:i"|default:"-" }} -
- - - -
-
Нет заявок
-
- - - {% if page_obj %} - - {% endif %} + +
+ diff --git a/dbapp/mainapp/templates/mainapp/kubsat_tabs.html b/dbapp/mainapp/templates/mainapp/kubsat_tabs.html index 5b7f4b5..7e61045 100644 --- a/dbapp/mainapp/templates/mainapp/kubsat_tabs.html +++ b/dbapp/mainapp/templates/mainapp/kubsat_tabs.html @@ -57,19 +57,28 @@
-
- +
+
# + placeholder="ID источника" min="1" onchange="loadSourceData()">
-
+
+ + +
+
-
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ - +
-
- +
+
-
- +
+
-
- - +
+ + +
+
+ +
-
+
-
+
+
+ + +
@@ -233,9 +276,8 @@ function loadSourceData() { document.getElementById('requestObjitemName').value = data.objitem_name || '-'; document.getElementById('requestModulation').value = data.modulation || '-'; document.getElementById('requestSymbolRate').value = data.symbol_rate || '-'; - document.getElementById('requestPointsCount').value = data.points_count || '0'; - // Заполняем координаты (редактируемые) + // Заполняем координаты ГСО (редактируемые) if (data.coords_lat !== null) { document.getElementById('requestCoordsLat').value = data.coords_lat.toFixed(6); } @@ -243,6 +285,20 @@ function loadSourceData() { document.getElementById('requestCoordsLon').value = data.coords_lon.toFixed(6); } + // Заполняем данные из транспондера + if (data.downlink) { + document.getElementById('requestDownlink').value = data.downlink; + } + if (data.uplink) { + document.getElementById('requestUplink').value = data.uplink; + } + if (data.transfer) { + document.getElementById('requestTransfer').value = data.transfer; + } + if (data.satellite_id) { + document.getElementById('requestSatellite').value = data.satellite_id; + } + sourceDataCard.style.display = 'block'; } else { resultDiv.innerHTML = ` Источник #${sourceId} не найден`; @@ -264,7 +320,14 @@ function clearSourceData() { document.getElementById('requestSymbolRate').value = ''; document.getElementById('requestCoordsLat').value = ''; document.getElementById('requestCoordsLon').value = ''; - document.getElementById('requestPointsCount').value = '-'; + document.getElementById('requestCoordsSourceLat').value = ''; + document.getElementById('requestCoordsSourceLon').value = ''; + document.getElementById('requestDownlink').value = ''; + document.getElementById('requestUplink').value = ''; + document.getElementById('requestTransfer').value = ''; + document.getElementById('requestRegion').value = ''; + document.getElementById('requestSatellite').value = ''; + document.getElementById('requestCardDate').value = ''; } // Открытие модального окна создания заявки @@ -294,11 +357,13 @@ function openEditRequestModal(requestId) { .then(response => response.json()) .then(data => { document.getElementById('requestId').value = data.id; - document.getElementById('requestSourceId').value = data.source_id; + document.getElementById('requestSourceId').value = data.source_id || ''; + document.getElementById('requestSatellite').value = data.satellite_id || ''; document.getElementById('requestStatus').value = data.status; document.getElementById('requestPriority').value = data.priority; document.getElementById('requestPlannedAt').value = data.planned_at || ''; document.getElementById('requestDate').value = data.request_date || ''; + document.getElementById('requestCardDate').value = data.card_date || ''; document.getElementById('requestGsoSuccess').value = data.gso_success === null ? '' : data.gso_success.toString(); document.getElementById('requestKubsatSuccess').value = data.kubsat_success === null ? '' : data.kubsat_success.toString(); document.getElementById('requestComment').value = data.comment || ''; @@ -307,9 +372,14 @@ function openEditRequestModal(requestId) { document.getElementById('requestObjitemName').value = data.objitem_name || '-'; document.getElementById('requestModulation').value = data.modulation || '-'; document.getElementById('requestSymbolRate').value = data.symbol_rate || '-'; - document.getElementById('requestPointsCount').value = data.points_count || '0'; - // Заполняем координаты + // Заполняем частоты + document.getElementById('requestDownlink').value = data.downlink || ''; + document.getElementById('requestUplink').value = data.uplink || ''; + document.getElementById('requestTransfer').value = data.transfer || ''; + document.getElementById('requestRegion').value = data.region || ''; + + // Заполняем координаты ГСО if (data.coords_lat !== null) { document.getElementById('requestCoordsLat').value = data.coords_lat.toFixed(6); } else { @@ -321,7 +391,19 @@ function openEditRequestModal(requestId) { document.getElementById('requestCoordsLon').value = ''; } - document.getElementById('sourceDataCard').style.display = 'block'; + // Заполняем координаты источника + if (data.coords_source_lat !== null) { + document.getElementById('requestCoordsSourceLat').value = data.coords_source_lat.toFixed(6); + } else { + document.getElementById('requestCoordsSourceLat').value = ''; + } + if (data.coords_source_lon !== null) { + document.getElementById('requestCoordsSourceLon').value = data.coords_source_lon.toFixed(6); + } else { + document.getElementById('requestCoordsSourceLon').value = ''; + } + + document.getElementById('sourceDataCard').style.display = data.source_id ? 'block' : 'none'; const modal = new bootstrap.Modal(document.getElementById('requestModal')); modal.show(); diff --git a/dbapp/mainapp/templates/mainapp/source_request_import.html b/dbapp/mainapp/templates/mainapp/source_request_import.html new file mode 100644 index 0000000..6e3fa5b --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_request_import.html @@ -0,0 +1,143 @@ +{% extends 'mainapp/base.html' %} +{% load static %} + +{% block title %}Импорт заявок из Excel{% endblock %} + +{% block content %} +
+
+
+
+
+
Импорт заявок из Excel
+
+
+
+ {% csrf_token %} +
+ + +
+ +
+
Ожидаемые столбцы в файле:
+
    +
  • Дата постановки задачи → Дата заявки
  • +
  • Спутник → Спутник (ищется по NORAD в скобках, например "NSS 12 (36032)")
  • +
  • Дата формирования карточки → Дата формирования карточки
  • +
  • Дата проведения → Дата и время планирования
  • +
  • Частота Downlink → Частота Downlink
  • +
  • Частота Uplink → Частота Uplink
  • +
  • Перенос → Перенос
  • +
  • Координаты ГСО → Координаты ГСО (формат: "широта. долгота")
  • +
  • Район → Район
  • +
  • Результат ГСО → Если "Успешно", то ГСО успешно = Да, иначе Нет + в комментарий
  • +
  • Результат кубсатаКрасная ячейка = Кубсат неуспешно, иначе успешно. Значение добавляется в комментарий
  • +
  • Координаты источника → Координаты источника
  • +
+
+
Логика определения статуса:
+
    +
  • Если есть координаты источника → статус "Результат получен"
  • +
  • Если нет координат источника, но ГСО успешно → статус "Успешно"
  • +
  • Если нет координат источника и ГСО неуспешно → статус "Неуспешно"
  • +
  • Иначе → статус "Запланировано"
  • +
+
+ + + + Назад + +
+ + + +
+
+
+
+
+ + +{% endblock %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 4df0fd8..e7ed816 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -68,9 +68,12 @@ from .views.source_requests import ( SourceRequestCreateView, SourceRequestUpdateView, SourceRequestDeleteView, + SourceRequestBulkDeleteView, + SourceRequestExportView, SourceRequestAPIView, SourceRequestDetailAPIView, SourceDataAPIView, + SourceRequestImportView, ) from .views.tech_analyze import ( TechAnalyzeEntryView, @@ -158,6 +161,9 @@ urlpatterns = [ path('api/source//requests/', SourceRequestAPIView.as_view(), name='source_requests_api'), path('api/source-request//', SourceRequestDetailAPIView.as_view(), name='source_request_detail_api'), path('api/source//data/', SourceDataAPIView.as_view(), name='source_data_api'), + path('source-requests/import/', SourceRequestImportView.as_view(), name='source_request_import'), + path('source-requests/export/', SourceRequestExportView.as_view(), name='source_request_export'), + path('source-requests/bulk-delete/', SourceRequestBulkDeleteView.as_view(), name='source_request_bulk_delete'), path('data-entry/', DataEntryView.as_view(), name='data_entry'), path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'), path('tech-analyze/', TechAnalyzeEntryView.as_view(), name='tech_analyze_entry'), diff --git a/dbapp/mainapp/views/kubsat.py b/dbapp/mainapp/views/kubsat.py index 9a3d8b6..44b2a2c 100644 --- a/dbapp/mainapp/views/kubsat.py +++ b/dbapp/mainapp/views/kubsat.py @@ -27,9 +27,14 @@ class KubsatView(LoginRequiredMixin, FormView): context['full_width_page'] = True # Добавляем данные для вкладки заявок - from mainapp.models import SourceRequest + from mainapp.models import SourceRequest, Satellite + + # Список спутников для формы создания заявки + context['satellites'] = Satellite.objects.all().order_by('name') + requests_qs = SourceRequest.objects.select_related( 'source', 'source__info', 'source__ownership', + 'satellite', 'created_by__user', 'updated_by__user' ).prefetch_related( 'source__source_objitems__parameter_obj__modulation' @@ -72,6 +77,36 @@ class KubsatView(LoginRequiredMixin, FormView): requests_list.append(req) context['requests'] = requests_list + + # Сериализуем заявки в JSON для Tabulator + import json + requests_json_data = [] + for req in requests_list: + requests_json_data.append({ + 'id': req.id, + 'source_id': req.source_id, + 'satellite_name': req.satellite.name if req.satellite else '-', + 'status': req.status, + 'status_display': req.get_status_display(), + 'priority': req.priority, + 'priority_display': req.get_priority_display(), + 'request_date': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-', + 'card_date': req.card_date.strftime('%d.%m.%Y') if req.card_date else '-', + 'planned_at': req.planned_at.strftime('%d.%m.%Y %H:%M') if req.planned_at else '-', + 'downlink': float(req.downlink) if req.downlink else None, + 'uplink': float(req.uplink) if req.uplink else None, + 'transfer': float(req.transfer) if req.transfer else None, + 'coords_lat': float(req.coords.y) if req.coords else None, + 'coords_lon': float(req.coords.x) if req.coords else None, + 'region': req.region or '', + 'gso_success': req.gso_success, + 'kubsat_success': req.kubsat_success, + 'coords_source_lat': float(req.coords_source.y) if req.coords_source else None, + 'coords_source_lon': float(req.coords_source.x) if req.coords_source else None, + 'comment': req.comment or '', + }) + context['requests_json'] = json.dumps(requests_json_data, ensure_ascii=False) + context['status_choices'] = SourceRequest.STATUS_CHOICES context['priority_choices'] = SourceRequest.PRIORITY_CHOICES context['current_status'] = status or '' diff --git a/dbapp/mainapp/views/source_requests.py b/dbapp/mainapp/views/source_requests.py index b891dd0..cff4aa0 100644 --- a/dbapp/mainapp/views/source_requests.py +++ b/dbapp/mainapp/views/source_requests.py @@ -3,14 +3,20 @@ """ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse +from django.shortcuts import render from django.views import View from django.views.generic import ListView, CreateView, UpdateView from django.urls import reverse_lazy from django.db.models import Q -from mainapp.models import SourceRequest, SourceRequestStatusHistory, Source +from mainapp.models import SourceRequest, SourceRequestStatusHistory, Source, Satellite from mainapp.forms import SourceRequestForm +import re +import pandas as pd +from datetime import datetime +from django.contrib.gis.geos import Point + class SourceRequestListView(LoginRequiredMixin, ListView): """Список заявок на источники.""" @@ -22,6 +28,7 @@ class SourceRequestListView(LoginRequiredMixin, ListView): def get_queryset(self): queryset = SourceRequest.objects.select_related( 'source', 'source__info', 'source__ownership', + 'satellite', 'created_by__user', 'updated_by__user' ).order_by('-created_at') @@ -174,6 +181,217 @@ class SourceRequestDeleteView(LoginRequiredMixin, View): }, status=404) +class SourceRequestBulkDeleteView(LoginRequiredMixin, View): + """Массовое удаление заявок.""" + + def post(self, request): + import json + try: + data = json.loads(request.body) + ids = data.get('ids', []) + + if not ids: + return JsonResponse({ + 'success': False, + 'error': 'Не выбраны заявки для удаления' + }, status=400) + + deleted_count, _ = SourceRequest.objects.filter(pk__in=ids).delete() + + return JsonResponse({ + 'success': True, + 'message': f'Удалено заявок: {deleted_count}', + 'deleted_count': deleted_count + }) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': 'Неверный формат данных' + }, status=400) + + +class SourceRequestExportView(LoginRequiredMixin, View): + """Экспорт заявок в Excel.""" + + def get(self, request): + from django.http import HttpResponse + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, PatternFill + from io import BytesIO + + # Получаем заявки с фильтрами + queryset = SourceRequest.objects.select_related( + 'satellite' + ).order_by('-created_at') + + # Применяем фильтры + status = request.GET.get('status') + if status: + queryset = queryset.filter(status=status) + + priority = request.GET.get('priority') + if priority: + queryset = queryset.filter(priority=priority) + + gso_success = request.GET.get('gso_success') + if gso_success == 'true': + queryset = queryset.filter(gso_success=True) + elif gso_success == 'false': + queryset = queryset.filter(gso_success=False) + + kubsat_success = request.GET.get('kubsat_success') + if kubsat_success == 'true': + queryset = queryset.filter(kubsat_success=True) + elif kubsat_success == 'false': + queryset = queryset.filter(kubsat_success=False) + + # Создаём Excel файл + wb = Workbook() + ws = wb.active + ws.title = "Заявки" + + # Заголовки (как в импорте, но без источника + приоритет + статус + комментарий) + headers = [ + 'Дата постановки задачи', + 'Дата формирования карточки', + 'Дата проведения', + 'Спутник', + 'Частота Downlink', + 'Частота Uplink', + 'Перенос', + 'Координаты ГСО', + 'Район', + 'Приоритет', + 'Статус', + 'Результат ГСО', + 'Результат кубсата', + 'Координаты источника', + 'Комментарий', + ] + + # Стили + header_font = Font(bold=True) + header_fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid") + green_fill = PatternFill(start_color="90EE90", end_color="90EE90", fill_type="solid") + red_fill = PatternFill(start_color="FF6B6B", end_color="FF6B6B", fill_type="solid") + gray_fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") + + # Записываем заголовки + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num, value=header) + cell.font = header_font + cell.fill = header_fill + cell.alignment = Alignment(horizontal='center', vertical='center') + + # Записываем данные + for row_num, req in enumerate(queryset, 2): + # Дата постановки задачи + ws.cell(row=row_num, column=1, value=req.request_date.strftime('%d.%m.%Y') if req.request_date else '') + + # Дата формирования карточки + ws.cell(row=row_num, column=2, value=req.card_date.strftime('%d.%m.%Y') if req.card_date else '') + + # Дата проведения + ws.cell(row=row_num, column=3, value=req.planned_at.strftime('%d.%m.%y %H:%M') if req.planned_at else '') + + # Спутник + satellite_str = '' + if req.satellite: + satellite_str = req.satellite.name + if req.satellite.norad: + satellite_str += f' ({req.satellite.norad})' + ws.cell(row=row_num, column=4, value=satellite_str) + + # Частота Downlink + ws.cell(row=row_num, column=5, value=req.downlink if req.downlink else '') + + # Частота Uplink + ws.cell(row=row_num, column=6, value=req.uplink if req.uplink else '') + + # Перенос + ws.cell(row=row_num, column=7, value=req.transfer if req.transfer else '') + + # Координаты ГСО + coords_gso = '' + if req.coords: + coords_gso = f'{req.coords.y:.6f} {req.coords.x:.6f}' + ws.cell(row=row_num, column=8, value=coords_gso) + + # Район + ws.cell(row=row_num, column=9, value=req.region or '') + + # Приоритет + ws.cell(row=row_num, column=10, value=req.get_priority_display()) + + # Статус (с цветом) + status_cell = ws.cell(row=row_num, column=11, value=req.get_status_display()) + if req.status in ['successful', 'result_received']: + status_cell.fill = green_fill + elif req.status == 'unsuccessful': + status_cell.fill = red_fill + else: + status_cell.fill = gray_fill + + # Результат ГСО (с цветом) + gso_cell = ws.cell(row=row_num, column=12) + if req.gso_success is True: + gso_cell.value = 'Да' + gso_cell.fill = green_fill + elif req.gso_success is False: + gso_cell.value = 'Нет' + gso_cell.fill = red_fill + else: + gso_cell.value = '' + + # Результат кубсата (с цветом) + kubsat_cell = ws.cell(row=row_num, column=13) + if req.kubsat_success is True: + kubsat_cell.value = 'Да' + kubsat_cell.fill = green_fill + elif req.kubsat_success is False: + kubsat_cell.value = 'Нет' + kubsat_cell.fill = red_fill + else: + kubsat_cell.value = '' + + # Координаты источника + coords_source = '' + if req.coords_source: + coords_source = f'{req.coords_source.y:.6f} {req.coords_source.x:.6f}' + ws.cell(row=row_num, column=14, value=coords_source) + + # Комментарий + ws.cell(row=row_num, column=15, value=req.comment or '') + + # Автоширина колонок + for column in ws.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 40) + ws.column_dimensions[column_letter].width = adjusted_width + + # Сохраняем в BytesIO + output = BytesIO() + wb.save(output) + output.seek(0) + + # Возвращаем файл + response = HttpResponse( + output.read(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + filename = f'source_requests_{datetime.now().strftime("%Y%m%d_%H%M")}.xlsx' + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + return response + + class SourceRequestAPIView(LoginRequiredMixin, View): """API для получения данных о заявках источника.""" @@ -230,6 +448,7 @@ class SourceRequestDetailAPIView(LoginRequiredMixin, View): try: req = SourceRequest.objects.select_related( 'source', 'source__info', 'source__ownership', + 'satellite', 'created_by__user', 'updated_by__user' ).prefetch_related( 'status_history__changed_by__user', @@ -250,39 +469,59 @@ class SourceRequestDetailAPIView(LoginRequiredMixin, View): }) # Получаем данные из первой точки источника (имя, модуляция, символьная скорость) - source_data = _get_source_extra_data(req.source) + source_data = _get_source_extra_data(req.source) if req.source else { + 'objitem_name': '-', 'modulation': '-', 'symbol_rate': '-' + } - # Координаты из заявки или из источника + # Координаты ГСО из заявки или из источника coords_lat = None coords_lon = None if req.coords: coords_lat = req.coords.y coords_lon = req.coords.x - elif req.source.coords_average: + elif req.source and req.source.coords_average: coords_lat = req.source.coords_average.y coords_lon = req.source.coords_average.x + # Координаты источника + coords_source_lat = None + coords_source_lon = None + if req.coords_source: + coords_source_lat = req.coords_source.y + coords_source_lon = req.coords_source.x + data = { 'id': req.id, 'source_id': req.source_id, + 'satellite_id': req.satellite_id, + 'satellite_name': req.satellite.name if req.satellite else '-', 'status': req.status, 'status_display': req.get_status_display(), 'priority': req.priority, 'priority_display': req.get_priority_display(), - 'planned_at': req.planned_at.isoformat() if req.planned_at else None, + 'planned_at': req.planned_at.strftime('%Y-%m-%dT%H:%M') if req.planned_at else '', 'planned_at_display': req.planned_at.strftime('%d.%m.%Y %H:%M') if req.planned_at else '-', 'request_date': req.request_date.isoformat() if req.request_date else None, 'request_date_display': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-', + 'card_date': req.card_date.isoformat() if req.card_date else None, + 'card_date_display': req.card_date.strftime('%d.%m.%Y') if req.card_date else '-', 'status_updated_at': req.status_updated_at.strftime('%d.%m.%Y %H:%M') if req.status_updated_at else '-', + 'downlink': req.downlink, + 'uplink': req.uplink, + 'transfer': req.transfer, + 'region': req.region or '', 'gso_success': req.gso_success, 'kubsat_success': req.kubsat_success, 'comment': req.comment or '', 'created_at': req.created_at.strftime('%d.%m.%Y %H:%M') if req.created_at else '-', 'created_by': str(req.created_by) if req.created_by else '-', 'history': history, - # Дополнительные данные + # Координаты ГСО 'coords_lat': coords_lat, 'coords_lon': coords_lon, + # Координаты источника + 'coords_source_lat': coords_source_lat, + 'coords_source_lon': coords_source_lon, 'points_count': req.points_count, 'objitem_name': source_data['objitem_name'], 'modulation': source_data['modulation'], @@ -321,15 +560,16 @@ def _get_source_extra_data(source): class SourceDataAPIView(LoginRequiredMixin, View): - """API для получения данных источника (координаты, имя точки, модуляция, символьная скорость).""" + """API для получения данных источника (координаты, имя точки, модуляция, символьная скорость, транспондер).""" def get(self, request, source_id): from mainapp.utils import calculate_mean_coords - from datetime import datetime try: source = Source.objects.select_related('info', 'ownership').prefetch_related( 'source_objitems__parameter_obj__modulation', + 'source_objitems__parameter_obj__id_satellite', + 'source_objitems__transponder__sat_id', 'source_objitems__geo_obj' ).get(pk=source_id) except Source.DoesNotExist: @@ -339,10 +579,18 @@ class SourceDataAPIView(LoginRequiredMixin, View): source_data = _get_source_extra_data(source) # Рассчитываем усреднённые координаты из всех точек (сортируем по дате ГЛ) - objitems = source.source_objitems.select_related('geo_obj').order_by('geo_obj__timestamp') + objitems = source.source_objitems.select_related('geo_obj', 'transponder', 'transponder__sat_id').order_by('geo_obj__timestamp') avg_coords = None points_count = 0 + + # Данные из транспондера + downlink = None + uplink = None + transfer = None + satellite_id = None + satellite_name = None + for objitem in objitems: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords: coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y)) @@ -351,6 +599,24 @@ class SourceDataAPIView(LoginRequiredMixin, View): avg_coords = coord else: avg_coords, _ = calculate_mean_coords(avg_coords, coord) + + # Берём данные из первого транспондера + if downlink is None and objitem.transponder: + transponder = objitem.transponder + downlink = transponder.downlink + uplink = transponder.uplink + transfer = transponder.transfer + if transponder.sat_id: + satellite_id = transponder.sat_id.pk + satellite_name = transponder.sat_id.name + + # Если нет данных из транспондера, пробуем из параметров + if satellite_id is None: + for objitem in objitems: + if objitem.parameter_obj and objitem.parameter_obj.id_satellite: + satellite_id = objitem.parameter_obj.id_satellite.pk + satellite_name = objitem.parameter_obj.id_satellite.name + break # Если нет координат из точек, берём из источника coords_lat = None @@ -373,6 +639,359 @@ class SourceDataAPIView(LoginRequiredMixin, View): 'symbol_rate': source_data['symbol_rate'], 'info': source.info.name if source.info else '-', 'ownership': source.ownership.name if source.ownership else '-', + # Данные из транспондера + 'downlink': downlink, + 'uplink': uplink, + 'transfer': transfer, + 'satellite_id': satellite_id, + 'satellite_name': satellite_name, } return JsonResponse(data) + + +class SourceRequestImportView(LoginRequiredMixin, View): + """Импорт заявок из Excel файла.""" + + def get(self, request): + """Отображает форму загрузки файла.""" + return render(request, 'mainapp/source_request_import.html') + + def post(self, request): + """Обрабатывает загруженный Excel файл.""" + from openpyxl import load_workbook + from openpyxl.styles import PatternFill + + if 'file' not in request.FILES: + return JsonResponse({'success': False, 'error': 'Файл не загружен'}, status=400) + + file = request.FILES['file'] + + try: + # Читаем Excel файл с openpyxl для доступа к цветам + wb = load_workbook(file, data_only=True) + ws = wb.worksheets[0] + + # Получаем заголовки (очищаем от пробелов) + headers = [] + for cell in ws[1]: + val = cell.value + if val: + headers.append(str(val).strip()) + else: + headers.append(None) + + # Находим индекс столбца "Результат кубсата" + kubsat_col_idx = None + for i, h in enumerate(headers): + if h and 'кубсат' in h.lower(): + kubsat_col_idx = i + break + + results = { + 'created': 0, + 'errors': [], + 'skipped': 0, + 'headers': [h for h in headers if h], # Для отладки + } + + custom_user = getattr(request.user, 'customuser', None) + + # Обрабатываем строки начиная со второй (первая - заголовки) + for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=False), start=2): + try: + # Получаем значения + row_values = {headers[i]: cell.value for i, cell in enumerate(row) if i < len(headers)} + + # Получаем цвет ячейки "Результат кубсата" + kubsat_is_red = False + kubsat_value = None + if kubsat_col_idx is not None and kubsat_col_idx < len(row): + kubsat_cell = row[kubsat_col_idx] + kubsat_value = kubsat_cell.value + # Проверяем цвет заливки + if kubsat_cell.fill and kubsat_cell.fill.fgColor: + color = kubsat_cell.fill.fgColor + if color.type == 'rgb' and color.rgb: + # Красный цвет: FF0000 или близкие оттенки + rgb = color.rgb + if isinstance(rgb, str) and len(rgb) >= 6: + # Убираем альфа-канал если есть + rgb = rgb[-6:] + r = int(rgb[0:2], 16) + g = int(rgb[2:4], 16) + b = int(rgb[4:6], 16) + # Считаем красным если R > 200 и G < 100 и B < 100 + if r > 180 and g < 120 and b < 120: + kubsat_is_red = True + + self._process_row(row_values, row_idx, results, custom_user, kubsat_is_red, kubsat_value) + except Exception as e: + results['errors'].append(f"Строка {row_idx}: {str(e)}") + + return JsonResponse({ + 'success': True, + 'created': results['created'], + 'skipped': results['skipped'], + 'errors': results['errors'][:20], + 'total_errors': len(results['errors']), + 'headers': results.get('headers', [])[:15], # Для отладки + }) + + except Exception as e: + return JsonResponse({'success': False, 'error': f'Ошибка чтения файла: {str(e)}'}, status=400) + + def _process_row(self, row, row_idx, results, custom_user, kubsat_is_red=False, kubsat_value=None): + """Обрабатывает одну строку из Excel.""" + # Пропускаем полностью пустые строки (все значения None или пустые) + has_any_data = any(v for v in row.values() if v is not None and str(v).strip()) + if not has_any_data: + results['skipped'] += 1 + return + + # Парсим дату заявки (Дата постановки задачи) + request_date = self._parse_date(row.get('Дата постановки задачи')) + + # Парсим дату формирования карточки + card_date = self._parse_date(row.get('Дата формирования карточки')) + + # Парсим дату и время планирования (Дата проведения) + planned_at = self._parse_datetime(row.get('Дата проведения')) + + # Ищем спутник по NORAD + satellite = self._find_satellite(row.get('Спутник')) + + # Парсим частоты + downlink = self._parse_float(row.get('Частота Downlink')) + uplink = self._parse_float(row.get('Частота Uplink')) + transfer = self._parse_float(row.get('Перенос')) + + # Парсим координаты ГСО + coords = self._parse_coords(row.get('Координаты ГСО')) + + # Район + region = str(row.get('Район', '')).strip() if row.get('Район') else None + + # Результат ГСО + gso_result = row.get('Результат ГСО') + gso_success = None + comment_parts = [] + + if gso_result: + gso_str = str(gso_result).strip().lower() + if gso_str in ('успешно', 'да', 'true', '1'): + gso_success = True + else: + gso_success = False + comment_parts.append(f"Результат ГСО: {str(gso_result).strip()}") + + # Результат кубсата - по цвету ячейки + kubsat_success = None + if kubsat_is_red: + kubsat_success = False + elif kubsat_value: + kubsat_success = True + + # Добавляем значение кубсата в комментарий + if kubsat_value: + comment_parts.append(f"Результат кубсата: {str(kubsat_value).strip()}") + + # Координаты источника + coords_source = self._parse_coords(row.get('Координаты источника')) + + # Определяем статус по логике: + # - если есть координата источника -> result_received + # - если нет координаты источника, но ГСО успешно -> successful + # - если нет координаты источника и ГСО не успешно -> unsuccessful + status = 'planned' + if coords_source: + status = 'result_received' + elif gso_success is True: + status = 'successful' + elif gso_success is False: + status = 'unsuccessful' + + # Собираем комментарий + comment = '; '.join(comment_parts) if comment_parts else None + + # Создаём заявку + source_request = SourceRequest( + source=None, + satellite=satellite, + status=status, + priority='medium', + request_date=request_date, + card_date=card_date, + planned_at=planned_at, + downlink=downlink, + uplink=uplink, + transfer=transfer, + region=region, + gso_success=gso_success, + kubsat_success=kubsat_success, + comment=comment, + created_by=custom_user, + updated_by=custom_user, + ) + + # Устанавливаем координаты + if coords: + source_request.coords = Point(coords[1], coords[0], srid=4326) + + if coords_source: + source_request.coords_source = Point(coords_source[1], coords_source[0], srid=4326) + + source_request.save() + + # Создаём начальную запись в истории + SourceRequestStatusHistory.objects.create( + source_request=source_request, + old_status='', + new_status=source_request.status, + changed_by=custom_user, + ) + + results['created'] += 1 + + def _parse_date(self, value): + """Парсит дату из различных форматов.""" + if pd.isna(value): + return None + + if isinstance(value, datetime): + return value.date() + + value_str = str(value).strip() + + # Пробуем разные форматы + formats = ['%d.%m.%Y', '%d.%m.%y', '%Y-%m-%d', '%d/%m/%Y', '%d/%m/%y'] + for fmt in formats: + try: + return datetime.strptime(value_str, fmt).date() + except ValueError: + continue + + return None + + def _parse_datetime(self, value): + """Парсит дату и время из различных форматов.""" + if pd.isna(value): + return None + + if isinstance(value, datetime): + return value + + value_str = str(value).strip() + + # Пробуем разные форматы + formats = [ + '%d.%m.%y %H:%M', '%d.%m.%Y %H:%M', '%d.%m.%y %H:%M:%S', '%d.%m.%Y %H:%M:%S', + '%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%d/%m/%Y %H:%M', '%d/%m/%y %H:%M' + ] + for fmt in formats: + try: + return datetime.strptime(value_str, fmt) + except ValueError: + continue + + return None + + def _find_satellite(self, value): + """Ищет спутник по названию с NORAD в скобках.""" + if pd.isna(value): + return None + + value_str = str(value).strip() + + # Ищем NORAD в скобках: "NSS 12 (36032)" + match = re.search(r'\((\d+)\)', value_str) + if match: + norad = int(match.group(1)) + try: + return Satellite.objects.get(norad=norad) + except Satellite.DoesNotExist: + pass + + # Пробуем найти по имени + name = re.sub(r'\s*\(\d+\)\s*', '', value_str).strip() + if name: + satellite = Satellite.objects.filter(name__icontains=name).first() + if satellite: + return satellite + + return None + + def _parse_float(self, value): + """Парсит число с плавающей точкой.""" + if pd.isna(value): + return None + + try: + # Заменяем запятую на точку + value_str = str(value).replace(',', '.').strip() + return float(value_str) + except (ValueError, TypeError): + return None + + def _parse_coords(self, value): + """Парсит координаты из строки. Возвращает (lat, lon) или None. + + Поддерживаемые форматы: + - "24.920695 46.733201" (точка как десятичный разделитель, пробел между координатами) + - "24,920695 46,733201" (запятая как десятичный разделитель, пробел между координатами) + - "24.920695, 46.733201" (точка как десятичный разделитель, запятая+пробел между координатами) + - "21.763585. 39.158290" (точка с пробелом между координатами) + """ + if pd.isna(value): + return None + + value_str = str(value).strip() + if not value_str: + return None + + # Формат "21.763585. 39.158290" - точка с пробелом как разделитель координат + if re.search(r'\.\s+', value_str): + parts = re.split(r'\.\s+', value_str) + if len(parts) >= 2: + try: + lat = float(parts[0].replace(',', '.')) + lon = float(parts[1].replace(',', '.')) + return (lat, lon) + except (ValueError, TypeError): + pass + + # Формат "24.920695, 46.733201" - запятая с пробелом как разделитель координат + if ', ' in value_str: + parts = value_str.split(', ') + if len(parts) >= 2: + try: + lat = float(parts[0].replace(',', '.')) + lon = float(parts[1].replace(',', '.')) + return (lat, lon) + except (ValueError, TypeError): + pass + + # Формат "24,920695 46,733201" или "24.920695 46.733201" - пробел как разделитель координат + # Сначала разбиваем по пробелам + parts = value_str.split() + if len(parts) >= 2: + try: + # Заменяем запятую на точку в каждой части отдельно + lat = float(parts[0].replace(',', '.')) + lon = float(parts[1].replace(',', '.')) + return (lat, lon) + except (ValueError, TypeError): + pass + + # Формат "24.920695;46.733201" - точка с запятой как разделитель + if ';' in value_str: + parts = value_str.split(';') + if len(parts) >= 2: + try: + lat = float(parts[0].strip().replace(',', '.')) + lon = float(parts[1].strip().replace(',', '.')) + return (lat, lon) + except (ValueError, TypeError): + pass + + return None diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 1371c93..49b8a22 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -13,6 +13,8 @@ services: - ./logs:/app/logs expose: - 8000 + networks: + - app-network worker: # build: @@ -29,12 +31,16 @@ services: volumes: - ./logs:/app/logs restart: unless-stopped + networks: + - app-network redis: image: redis:7-alpine restart: unless-stopped ports: - 6379:6379 + networks: + - app-network db: image: postgis/postgis:18-3.6 @@ -46,18 +52,21 @@ services: - 5432:5432 volumes: - pgdata:/var/lib/postgresql - # networks: - # - app-network + networks: + - app-network nginx: image: nginx:alpine depends_on: - web + - tileserver ports: - 8080:80 volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - static_volume:/usr/share/nginx/html/static + networks: + - app-network flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest @@ -69,6 +78,8 @@ services: - LOG_LEVEL=info - LOG_HTML=false - CAPTCHA_SOLVER=none + networks: + - app-network tileserver: image: maptiler/tileserver-gl:latest @@ -77,13 +88,20 @@ services: ports: - "8090:8080" volumes: - - ./tileserver_data:/data + # - ./tileserver_data:/data + - /mnt/c/Users/I/Documents/TileServer:/data - tileserver_config:/config environment: - VERBOSE=true - CORS_ENABLED=true + networks: + - app-network volumes: pgdata: static_volume: - tileserver_config \ No newline at end of file + tileserver_config: + +networks: + app-network: + driver: bridge \ No newline at end of file