""" Представления для управления заявками на источники. """ 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, 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': 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 для получения данных о заявках источника.""" 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': 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': 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': 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': 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': 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': 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'], '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