Files
dbstorage/dbapp/mainapp/views/source_requests.py

1016 lines
43 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Представления для управления заявками на источники.
"""
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 django.utils import timezone
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):
"""Список заявок на источники."""
model = SourceRequest
template_name = 'mainapp/source_request_list.html'
context_object_name = 'requests'
paginate_by = 50
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')
# Фильтр по статусу
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Фильтр по приоритету
priority = self.request.GET.get('priority')
if priority:
queryset = queryset.filter(priority=priority)
# Фильтр по источнику
source_id = self.request.GET.get('source_id')
if source_id:
queryset = queryset.filter(source_id=source_id)
# Фильтр по ГСО успешно
gso_success = self.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 = self.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)
# Поиск
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(source__id__icontains=search) |
Q(comment__icontains=search)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['status_choices'] = SourceRequest.STATUS_CHOICES
context['priority_choices'] = SourceRequest.PRIORITY_CHOICES
context['current_status'] = self.request.GET.get('status', '')
context['current_priority'] = self.request.GET.get('priority', '')
context['search_query'] = self.request.GET.get('search', '')
context['form'] = SourceRequestForm()
return context
class SourceRequestCreateView(LoginRequiredMixin, CreateView):
"""Создание заявки на источник."""
model = SourceRequest
form_class = SourceRequestForm
template_name = 'mainapp/source_request_form.html'
success_url = reverse_lazy('mainapp:source_request_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# Передаем source_id если он есть в GET параметрах
source_id = self.request.GET.get('source_id')
if source_id:
kwargs['source_id'] = source_id
return kwargs
def form_valid(self, form):
# Устанавливаем created_by
form.instance.created_by = getattr(self.request.user, 'customuser', None)
form.instance.updated_by = getattr(self.request.user, 'customuser', None)
response = super().form_valid(form)
# Создаем начальную запись в истории
SourceRequestStatusHistory.objects.create(
source_request=self.object,
old_status='',
new_status=self.object.status,
changed_by=form.instance.created_by,
)
# Если это AJAX запрос, возвращаем JSON
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': 'Заявка успешно создана',
'request_id': self.object.id
})
return response
def form_invalid(self, form):
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': False,
'errors': form.errors
}, status=400)
return super().form_invalid(form)
class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
"""Редактирование заявки на источник."""
model = SourceRequest
form_class = SourceRequestForm
template_name = 'mainapp/source_request_form.html'
success_url = reverse_lazy('mainapp:source_request_list')
def form_valid(self, form):
# Устанавливаем updated_by
form.instance.updated_by = getattr(self.request.user, 'customuser', None)
response = super().form_valid(form)
# Если это AJAX запрос, возвращаем JSON
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': 'Заявка успешно обновлена',
'request_id': self.object.id
})
return response
def form_invalid(self, form):
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': False,
'errors': form.errors
}, status=400)
return super().form_invalid(form)
class SourceRequestDeleteView(LoginRequiredMixin, View):
"""Удаление заявки на источник."""
def post(self, request, pk):
try:
source_request = SourceRequest.objects.get(pk=pk)
source_request.delete()
return JsonResponse({
'success': True,
'message': 'Заявка успешно удалена'
})
except SourceRequest.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Заявка не найдена'
}, 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': 'Заявки удалены',
'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 '')
# Дата проведения
planned_at_local = timezone.localtime(req.planned_at) if req.planned_at else None
planned_at_str = ''
if planned_at_local:
if planned_at_local.hour == 0 and planned_at_local.minute == 0:
planned_at_str = planned_at_local.strftime('%d.%m.%y')
else:
planned_at_str = planned_at_local.strftime('%d.%m.%y %H:%M')
ws.cell(row=row_num, column=3, value=planned_at_str)
# Спутник
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 для получения данных о заявках источника."""
def get(self, request, source_id):
try:
source = Source.objects.get(pk=source_id)
except Source.DoesNotExist:
return JsonResponse({'error': 'Источник не найден'}, status=404)
requests = SourceRequest.objects.filter(source=source).select_related(
'created_by__user', 'updated_by__user'
).prefetch_related('status_history__changed_by__user').order_by('-created_at')
data = []
for req in requests:
# Получаем историю статусов
history = []
for h in req.status_history.all().order_by('-changed_at'):
history.append({
'old_status': h.get_old_status_display() if h.old_status else '-',
'new_status': h.get_new_status_display(),
'changed_at': timezone.localtime(h.changed_at).strftime('%d.%m.%Y %H:%M') if h.changed_at else '-',
'changed_by': str(h.changed_by) if h.changed_by else '-',
})
data.append({
'id': req.id,
'status': req.status,
'status_display': req.get_status_display(),
'priority': req.priority,
'priority_display': req.get_priority_display(),
'planned_at': (
timezone.localtime(req.planned_at).strftime('%d.%m.%Y')
if req.planned_at and timezone.localtime(req.planned_at).hour == 0 and timezone.localtime(req.planned_at).minute == 0
else timezone.localtime(req.planned_at).strftime('%d.%m.%Y %H:%M') if req.planned_at
else '-'
),
'request_date': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-',
'status_updated_at': timezone.localtime(req.status_updated_at).strftime('%d.%m.%Y %H:%M') if req.status_updated_at else '-',
'gso_success': req.gso_success,
'kubsat_success': req.kubsat_success,
'comment': req.comment or '-',
'created_at': timezone.localtime(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,
})
return JsonResponse({
'source_id': source_id,
'requests': data,
'count': len(data)
})
class SourceRequestDetailAPIView(LoginRequiredMixin, View):
"""API для получения детальной информации о заявке."""
def get(self, request, pk):
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',
'source__source_objitems__parameter_obj__modulation',
'source__source_objitems__geo_obj'
).get(pk=pk)
except SourceRequest.DoesNotExist:
return JsonResponse({'error': 'Заявка не найдена'}, status=404)
# Получаем историю статусов
history = []
for h in req.status_history.all().order_by('-changed_at'):
history.append({
'old_status': h.get_old_status_display() if h.old_status else '-',
'new_status': h.get_new_status_display(),
'changed_at': timezone.localtime(h.changed_at).strftime('%d.%m.%Y %H:%M') if h.changed_at else '-',
'changed_by': str(h.changed_by) if h.changed_by else '-',
})
# Получаем данные из первой точки источника (имя, модуляция, символьная скорость)
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 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': timezone.localtime(req.planned_at).strftime('%Y-%m-%dT%H:%M') if req.planned_at else '',
'planned_at_display': (
timezone.localtime(req.planned_at).strftime('%d.%m.%Y')
if req.planned_at and timezone.localtime(req.planned_at).hour == 0 and timezone.localtime(req.planned_at).minute == 0
else timezone.localtime(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': timezone.localtime(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': timezone.localtime(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'],
'symbol_rate': source_data['symbol_rate'],
}
return JsonResponse(data)
def _get_source_extra_data(source):
"""Получает дополнительные данные из первой точки источника."""
objitem_name = '-'
modulation = '-'
symbol_rate = '-'
if source:
# Получаем первую точку источника (сортируем по дате ГЛ)
objitems = source.source_objitems.select_related(
'parameter_obj__modulation', 'geo_obj'
).order_by('geo_obj__timestamp')
first_objitem = objitems.first()
if first_objitem:
objitem_name = first_objitem.name or '-'
if first_objitem.parameter_obj:
if first_objitem.parameter_obj.modulation:
modulation = first_objitem.parameter_obj.modulation.name
if first_objitem.parameter_obj.bod_velocity and first_objitem.parameter_obj.bod_velocity > 0:
symbol_rate = str(int(first_objitem.parameter_obj.bod_velocity))
return {
'objitem_name': objitem_name,
'modulation': modulation,
'symbol_rate': symbol_rate,
}
class SourceDataAPIView(LoginRequiredMixin, View):
"""API для получения данных источника (координаты, имя точки, модуляция, символьная скорость, транспондер)."""
def get(self, request, source_id):
from mainapp.utils import calculate_mean_coords
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:
return JsonResponse({'error': 'Источник не найден', 'found': False}, status=404)
# Получаем данные из точек источника
source_data = _get_source_extra_data(source)
# Рассчитываем усреднённые координаты из всех точек (сортируем по дате ГЛ)
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))
points_count += 1
if avg_coords is None:
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
coords_lon = None
if avg_coords:
coords_lon = avg_coords[0]
coords_lat = avg_coords[1]
elif source.coords_average:
coords_lat = source.coords_average.y
coords_lon = source.coords_average.x
data = {
'found': True,
'source_id': source_id,
'coords_lat': coords_lat,
'coords_lon': coords_lon,
'points_count': points_count,
'objitem_name': source_data['objitem_name'],
'modulation': source_data['modulation'],
'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