Доделал таблицу с кубсатом
This commit is contained in:
@@ -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 ''
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user