751 lines
36 KiB
Python
751 lines
36 KiB
Python
"""
|
||
Представления для страницы Кубсат с фильтрацией и экспортом в Excel
|
||
"""
|
||
from datetime import datetime
|
||
from io import BytesIO
|
||
|
||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||
from django.contrib.gis.geos import Point
|
||
from django.db.models import Count, Q
|
||
from django.http import HttpResponse
|
||
from django.views.generic import FormView
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, Alignment
|
||
|
||
from mainapp.forms import KubsatFilterForm
|
||
from mainapp.models import Source, ObjItem
|
||
from mainapp.utils import calculate_mean_coords
|
||
|
||
|
||
class KubsatView(LoginRequiredMixin, FormView):
|
||
"""Страница Кубсат с фильтрами и таблицей источников"""
|
||
template_name = 'mainapp/kubsat_tabs.html'
|
||
form_class = KubsatFilterForm
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['full_width_page'] = True
|
||
|
||
# Добавляем данные для вкладки заявок
|
||
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'
|
||
).order_by('-created_at')
|
||
|
||
# Фильтры для заявок
|
||
status = self.request.GET.get('status')
|
||
if status:
|
||
requests_qs = requests_qs.filter(status=status)
|
||
|
||
priority = self.request.GET.get('priority')
|
||
if priority:
|
||
requests_qs = requests_qs.filter(priority=priority)
|
||
|
||
# Добавляем данные источника к каждой заявке
|
||
requests_list = []
|
||
for req in requests_qs[:100]:
|
||
# Получаем данные из первой точки источника
|
||
objitem_name = '-'
|
||
modulation = '-'
|
||
symbol_rate = '-'
|
||
|
||
if req.source:
|
||
first_objitem = req.source.source_objitems.select_related(
|
||
'parameter_obj__modulation'
|
||
).order_by('geo_obj__timestamp').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))
|
||
|
||
# Добавляем атрибуты к объекту заявки
|
||
req.objitem_name = objitem_name
|
||
req.modulation = modulation
|
||
req.symbol_rate = symbol_rate
|
||
requests_list.append(req)
|
||
|
||
context['requests'] = requests_list
|
||
|
||
# Сериализуем заявки в JSON для Tabulator
|
||
import json
|
||
from django.utils import timezone
|
||
|
||
requests_json_data = []
|
||
for req in requests_list:
|
||
# Конвертируем даты в локальный часовой пояс для отображения
|
||
planned_at_local = None
|
||
planned_at_iso = None
|
||
if req.planned_at:
|
||
planned_at_local = timezone.localtime(req.planned_at)
|
||
planned_at_iso = planned_at_local.isoformat()
|
||
|
||
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(),
|
||
# Даты в ISO формате для правильной сортировки
|
||
'request_date': req.request_date.isoformat() if req.request_date else None,
|
||
'card_date': req.card_date.isoformat() if req.card_date else None,
|
||
'planned_at': planned_at_iso,
|
||
# Отформатированные даты для отображения
|
||
'request_date_display': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-',
|
||
'card_date_display': req.card_date.strftime('%d.%m.%Y') if req.card_date else '-',
|
||
'planned_at_display': (
|
||
planned_at_local.strftime('%d.%m.%Y') if planned_at_local and planned_at_local.hour == 0 and planned_at_local.minute == 0
|
||
else planned_at_local.strftime('%d.%m.%Y %H:%M') if planned_at_local
|
||
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 ''
|
||
context['current_priority'] = priority or ''
|
||
context['search_query'] = self.request.GET.get('search', '')
|
||
|
||
# Если форма была отправлена, применяем фильтры
|
||
if self.request.GET:
|
||
form = self.form_class(self.request.GET)
|
||
if form.is_valid():
|
||
sources = self.apply_filters(form.cleaned_data)
|
||
date_from = form.cleaned_data.get('date_from')
|
||
date_to = form.cleaned_data.get('date_to')
|
||
has_date_filter = bool(date_from or date_to)
|
||
|
||
objitem_count = form.cleaned_data.get('objitem_count')
|
||
sources_with_date_info = []
|
||
for source in sources:
|
||
# Get latest request info for this source
|
||
latest_request = source.source_requests.order_by('-created_at').first()
|
||
requests_count = source.source_requests.count()
|
||
|
||
source_data = {
|
||
'source': source,
|
||
'objitems_data': [],
|
||
'has_lyngsat': False,
|
||
'lyngsat_id': None,
|
||
'has_request': latest_request is not None,
|
||
'request_status': latest_request.get_status_display() if latest_request else None,
|
||
'request_status_raw': latest_request.status if latest_request else None,
|
||
'gso_success': latest_request.gso_success if latest_request else None,
|
||
'kubsat_success': latest_request.kubsat_success if latest_request else None,
|
||
'planned_at': latest_request.planned_at if latest_request else None,
|
||
'requests_count': requests_count,
|
||
'average_coords': None, # Будет рассчитано после сбора точек
|
||
}
|
||
|
||
for objitem in source.source_objitems.all():
|
||
# Check if objitem has LyngSat source
|
||
if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source:
|
||
source_data['has_lyngsat'] = True
|
||
source_data['lyngsat_id'] = objitem.lyngsat_source.id
|
||
|
||
objitem_matches_date = True
|
||
objitem_matches_date = True
|
||
geo_date = None
|
||
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp:
|
||
geo_date = objitem.geo_obj.timestamp.date()
|
||
|
||
# Проверяем попадание в диапазон дат (только если фильтр задан)
|
||
if has_date_filter:
|
||
if date_from and date_to:
|
||
objitem_matches_date = date_from <= geo_date <= date_to
|
||
elif date_from:
|
||
objitem_matches_date = geo_date >= date_from
|
||
elif date_to:
|
||
objitem_matches_date = geo_date <= date_to
|
||
elif has_date_filter:
|
||
# Если фильтр по дате задан, но у точки нет даты - не подходит
|
||
objitem_matches_date = False
|
||
|
||
# Добавляем только точки, подходящие по дате (или все, если фильтр не задан)
|
||
if not has_date_filter or objitem_matches_date:
|
||
source_data['objitems_data'].append({
|
||
'objitem': objitem,
|
||
'matches_date': objitem_matches_date,
|
||
'geo_date': geo_date
|
||
})
|
||
|
||
# ЭТАП 2: Проверяем количество отфильтрованных точек
|
||
filtered_count = len(source_data['objitems_data'])
|
||
|
||
# Применяем фильтр по количеству точек (если задан)
|
||
include_source = True
|
||
if objitem_count:
|
||
if objitem_count == '1':
|
||
include_source = (filtered_count == 1)
|
||
elif objitem_count == '2+':
|
||
include_source = (filtered_count >= 2)
|
||
|
||
# Сортируем точки по дате ГЛ перед расчётом усреднённых координат
|
||
source_data['objitems_data'].sort(
|
||
key=lambda x: x['geo_date'] if x['geo_date'] else datetime.min.date()
|
||
)
|
||
|
||
# Рассчитываем усреднённые координаты из отфильтрованных точек
|
||
if source_data['objitems_data']:
|
||
avg_coords = None
|
||
for objitem_info in source_data['objitems_data']:
|
||
objitem = objitem_info['objitem']
|
||
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))
|
||
if avg_coords is None:
|
||
avg_coords = coord
|
||
else:
|
||
avg_coords, _ = calculate_mean_coords(avg_coords, coord)
|
||
if avg_coords:
|
||
source_data['average_coords'] = avg_coords
|
||
source_data['avg_lat'] = avg_coords[1]
|
||
source_data['avg_lon'] = avg_coords[0]
|
||
|
||
if source_data['objitems_data'] and include_source:
|
||
sources_with_date_info.append(source_data)
|
||
|
||
context['sources_with_date_info'] = sources_with_date_info
|
||
context['form'] = form
|
||
|
||
return context
|
||
|
||
def apply_filters(self, filters):
|
||
"""Применяет фильтры к queryset Source"""
|
||
from mainapp.models import SourceRequest
|
||
from django.db.models import Subquery, OuterRef, Exists
|
||
|
||
queryset = Source.objects.select_related('info', 'ownership').prefetch_related(
|
||
'source_objitems__parameter_obj__id_satellite',
|
||
'source_objitems__parameter_obj__polarization',
|
||
'source_objitems__parameter_obj__modulation',
|
||
'source_objitems__transponder__sat_id',
|
||
'source_objitems__lyngsat_source',
|
||
'source_objitems__geo_obj',
|
||
'source_requests'
|
||
).annotate(objitem_count=Count('source_objitems'))
|
||
|
||
# Фильтр по спутникам
|
||
if filters.get('satellites'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__id_satellite__in=filters['satellites']
|
||
).distinct()
|
||
|
||
# Фильтр по полосе спутника
|
||
if filters.get('band'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__id_satellite__band__in=filters['band']
|
||
).distinct()
|
||
|
||
# Фильтр по поляризации
|
||
if filters.get('polarization'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__polarization__in=filters['polarization']
|
||
).distinct()
|
||
|
||
# Фильтр по центральной частоте
|
||
if filters.get('frequency_min'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__frequency__gte=filters['frequency_min']
|
||
)
|
||
if filters.get('frequency_max'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__frequency__lte=filters['frequency_max']
|
||
)
|
||
|
||
# Фильтр по полосе частот
|
||
if filters.get('freq_range_min'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__freq_range__gte=filters['freq_range_min']
|
||
)
|
||
if filters.get('freq_range_max'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__freq_range__lte=filters['freq_range_max']
|
||
)
|
||
|
||
# Фильтр по модуляции
|
||
if filters.get('modulation'):
|
||
queryset = queryset.filter(
|
||
source_objitems__parameter_obj__modulation__in=filters['modulation']
|
||
).distinct()
|
||
|
||
# Фильтр по типу объекта
|
||
if filters.get('object_type'):
|
||
queryset = queryset.filter(info__in=filters['object_type'])
|
||
|
||
# Фильтр по принадлежности объекта
|
||
if filters.get('object_ownership'):
|
||
queryset = queryset.filter(ownership__in=filters['object_ownership'])
|
||
|
||
# Фильтр по количеству ObjItem
|
||
objitem_count = filters.get('objitem_count')
|
||
if objitem_count == '1':
|
||
queryset = queryset.filter(objitem_count=1)
|
||
elif objitem_count == '2+':
|
||
queryset = queryset.filter(objitem_count__gte=2)
|
||
|
||
# Фильтр по наличию планов (заявок со статусом 'planned')
|
||
has_plans = filters.get('has_plans')
|
||
if has_plans == 'yes':
|
||
queryset = queryset.filter(
|
||
source_requests__status='planned'
|
||
).distinct()
|
||
elif has_plans == 'no':
|
||
queryset = queryset.exclude(
|
||
source_requests__status='planned'
|
||
).distinct()
|
||
|
||
# Фильтр по ГСО успешно
|
||
success_1 = filters.get('success_1')
|
||
if success_1 == 'yes':
|
||
queryset = queryset.filter(
|
||
source_requests__gso_success=True
|
||
).distinct()
|
||
elif success_1 == 'no':
|
||
queryset = queryset.filter(
|
||
source_requests__gso_success=False
|
||
).distinct()
|
||
|
||
# Фильтр по Кубсат успешно
|
||
success_2 = filters.get('success_2')
|
||
if success_2 == 'yes':
|
||
queryset = queryset.filter(
|
||
source_requests__kubsat_success=True
|
||
).distinct()
|
||
elif success_2 == 'no':
|
||
queryset = queryset.filter(
|
||
source_requests__kubsat_success=False
|
||
).distinct()
|
||
|
||
return queryset.distinct()
|
||
|
||
|
||
class KubsatExportView(LoginRequiredMixin, FormView):
|
||
"""Экспорт отфильтрованных данных в Excel"""
|
||
form_class = KubsatFilterForm
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
# Получаем список ID точек (ObjItem) из POST
|
||
objitem_ids = request.POST.getlist('objitem_ids')
|
||
|
||
if not objitem_ids:
|
||
return HttpResponse("Нет данных для экспорта", status=400)
|
||
|
||
# Получаем ObjItem с их источниками
|
||
objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
|
||
'source',
|
||
'source__info',
|
||
'parameter_obj__id_satellite',
|
||
'parameter_obj__polarization',
|
||
'transponder__sat_id',
|
||
'geo_obj'
|
||
).prefetch_related('geo_obj__mirrors')
|
||
|
||
# Группируем ObjItem по Source для расчета инкрементального среднего
|
||
sources_objitems = {}
|
||
for objitem in objitems:
|
||
if objitem.source:
|
||
if objitem.source.id not in sources_objitems:
|
||
sources_objitems[objitem.source.id] = {
|
||
'source': objitem.source,
|
||
'objitems': []
|
||
}
|
||
sources_objitems[objitem.source.id]['objitems'].append(objitem)
|
||
|
||
# Создаем Excel файл с двумя листами
|
||
wb = Workbook()
|
||
|
||
# Первый лист: "Предложения" (только основные данные)
|
||
ws_proposals = wb.active
|
||
ws_proposals.title = "Предложения"
|
||
|
||
# Заголовки для листа "Предложения"
|
||
headers_proposals = [
|
||
'Дата',
|
||
'Широта, град',
|
||
'Долгота, град',
|
||
'Высота, м',
|
||
'Местоположение',
|
||
'ИСЗ',
|
||
'Прямой канал, МГц',
|
||
'Обратный канал, МГц',
|
||
'Перенос'
|
||
]
|
||
|
||
# Стиль заголовков для листа "Предложения"
|
||
for col_num, header in enumerate(headers_proposals, 1):
|
||
cell = ws_proposals.cell(row=1, column=col_num, value=header)
|
||
cell.font = Font(bold=True)
|
||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||
|
||
# Второй лист: "Комментарий" (все данные)
|
||
ws_comments = wb.create_sheet(title="Комментарий")
|
||
|
||
# Заголовки для листа "Комментарий"
|
||
headers_comments = [
|
||
'Дата',
|
||
'Широта, град',
|
||
'Долгота, град',
|
||
'Высота, м',
|
||
'Местоположение',
|
||
'ИСЗ',
|
||
'Прямой канал, МГц',
|
||
'Обратный канал, МГц',
|
||
'Перенос',
|
||
'Получено координат, раз',
|
||
'Период получения координат',
|
||
'Зеркала',
|
||
'СКО, км',
|
||
'Примечание',
|
||
'Оператор'
|
||
]
|
||
|
||
# Стиль заголовков для листа "Комментарий"
|
||
for col_num, header in enumerate(headers_comments, 1):
|
||
cell = ws_comments.cell(row=1, column=col_num, value=header)
|
||
cell.font = Font(bold=True)
|
||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||
|
||
# Заполняем данные
|
||
current_date = datetime.now().strftime('%d.%m.%Y')
|
||
operator_name = f"{request.user.first_name} {request.user.last_name}" if request.user.first_name else request.user.username
|
||
|
||
row_num_proposals = 2
|
||
row_num_comments = 2
|
||
for source_id, data in sources_objitems.items():
|
||
source = data['source']
|
||
objitems_list = data['objitems']
|
||
|
||
# Сортируем точки по дате ГЛ перед расчётом
|
||
objitems_list.sort(
|
||
key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min
|
||
)
|
||
|
||
# Рассчитываем инкрементальное среднее координат из оставшихся точек
|
||
average_coords = None
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
|
||
coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
|
||
|
||
if average_coords is None:
|
||
# Первая точка
|
||
average_coords = coord
|
||
else:
|
||
# Инкрементальное усреднение
|
||
average_coords, _ = calculate_mean_coords(average_coords, coord)
|
||
|
||
# Если нет координат из geo_obj, берем из source
|
||
if average_coords is None:
|
||
coords = source.coords_kupsat or source.coords_average or source.coords_valid or source.coords_reference
|
||
if coords:
|
||
average_coords = (coords.x, coords.y)
|
||
|
||
latitude = average_coords[1] if average_coords else ''
|
||
longitude = average_coords[0] if average_coords else ''
|
||
|
||
# Получаем местоположение из первого ObjItem с geo_obj
|
||
location = ''
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.location:
|
||
location = objitem.geo_obj.location
|
||
break
|
||
|
||
# Получаем данные спутника и частоты
|
||
satellite_info = ''
|
||
reverse_channel = ''
|
||
direct_channel = ''
|
||
transfer = ''
|
||
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
|
||
param = objitem.parameter_obj
|
||
if param.id_satellite:
|
||
sat_name = param.id_satellite.name
|
||
norad = f"({param.id_satellite.norad})" if param.id_satellite.norad else ""
|
||
satellite_info = f"{sat_name} {norad}"
|
||
|
||
if param.frequency:
|
||
reverse_channel = param.frequency
|
||
|
||
if objitem.transponder and objitem.transponder.transfer:
|
||
transfer = objitem.transponder.transfer
|
||
if param.frequency:
|
||
direct_channel = param.frequency + objitem.transponder.transfer
|
||
|
||
break
|
||
|
||
objitem_count = len(objitems_list)
|
||
|
||
# Зеркала
|
||
mirrors = []
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
|
||
for mirror in objitem.geo_obj.mirrors.all():
|
||
if mirror.name not in mirrors:
|
||
mirrors.append(mirror.name)
|
||
mirrors_str = '\n'.join(mirrors)
|
||
|
||
# Диапазон дат ГЛ (самая ранняя - самая поздняя)
|
||
geo_dates = []
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp:
|
||
geo_dates.append(objitem.geo_obj.timestamp.date())
|
||
|
||
date_range_str = '-'
|
||
if geo_dates:
|
||
min_date = min(geo_dates)
|
||
max_date = max(geo_dates)
|
||
# Форматируем даты в формате d.m.Y
|
||
min_date_str = min_date.strftime('%d.%m.%Y')
|
||
max_date_str = max_date.strftime('%d.%m.%Y')
|
||
|
||
if min_date == max_date:
|
||
# Если даты совпадают, показываем только одну
|
||
date_range_str = min_date_str
|
||
else:
|
||
# Иначе показываем диапазон
|
||
date_range_str = f"{min_date_str}-{max_date_str}"
|
||
|
||
# Записываем строку на лист "Предложения" (только основные данные)
|
||
ws_proposals.cell(row=row_num_proposals, column=1, value=current_date)
|
||
ws_proposals.cell(row=row_num_proposals, column=2, value=latitude)
|
||
ws_proposals.cell(row=row_num_proposals, column=3, value=longitude)
|
||
ws_proposals.cell(row=row_num_proposals, column=4, value=0.0)
|
||
ws_proposals.cell(row=row_num_proposals, column=5, value=location)
|
||
ws_proposals.cell(row=row_num_proposals, column=6, value=satellite_info)
|
||
ws_proposals.cell(row=row_num_proposals, column=7, value=direct_channel)
|
||
ws_proposals.cell(row=row_num_proposals, column=8, value=reverse_channel)
|
||
ws_proposals.cell(row=row_num_proposals, column=9, value=transfer)
|
||
|
||
# Записываем строку на лист "Комментарий" (все данные)
|
||
ws_comments.cell(row=row_num_comments, column=1, value=current_date)
|
||
ws_comments.cell(row=row_num_comments, column=2, value=latitude)
|
||
ws_comments.cell(row=row_num_comments, column=3, value=longitude)
|
||
ws_comments.cell(row=row_num_comments, column=4, value=0.0)
|
||
ws_comments.cell(row=row_num_comments, column=5, value=location)
|
||
ws_comments.cell(row=row_num_comments, column=6, value=satellite_info)
|
||
ws_comments.cell(row=row_num_comments, column=7, value=direct_channel)
|
||
ws_comments.cell(row=row_num_comments, column=8, value=reverse_channel)
|
||
ws_comments.cell(row=row_num_comments, column=9, value=transfer)
|
||
ws_comments.cell(row=row_num_comments, column=10, value=objitem_count)
|
||
ws_comments.cell(row=row_num_comments, column=11, value=date_range_str)
|
||
ws_comments.cell(row=row_num_comments, column=12, value=mirrors_str)
|
||
ws_comments.cell(row=row_num_comments, column=13, value='')
|
||
ws_comments.cell(row=row_num_comments, column=14, value='')
|
||
ws_comments.cell(row=row_num_comments, column=15, value=operator_name)
|
||
|
||
row_num_proposals += 1
|
||
row_num_comments += 1
|
||
|
||
# Автоширина колонок для обоих листов
|
||
for ws in [ws_proposals, ws_comments]:
|
||
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, 50)
|
||
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'
|
||
)
|
||
response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d")}.xlsx"'
|
||
|
||
return response
|
||
|
||
|
||
class KubsatCreateRequestsView(LoginRequiredMixin, FormView):
|
||
"""Массовое создание заявок из отфильтрованных данных"""
|
||
form_class = KubsatFilterForm
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
import json
|
||
from django.http import JsonResponse
|
||
from mainapp.models import SourceRequest, CustomUser
|
||
|
||
# Получаем список ID точек (ObjItem) из POST
|
||
objitem_ids = request.POST.getlist('objitem_ids')
|
||
|
||
if not objitem_ids:
|
||
return JsonResponse({'success': False, 'error': 'Нет данных для создания заявок'}, status=400)
|
||
|
||
# Получаем ObjItem с их источниками
|
||
objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
|
||
'source',
|
||
'geo_obj'
|
||
)
|
||
|
||
# Группируем ObjItem по Source
|
||
sources_objitems = {}
|
||
for objitem in objitems:
|
||
if objitem.source:
|
||
if objitem.source.id not in sources_objitems:
|
||
sources_objitems[objitem.source.id] = {
|
||
'source': objitem.source,
|
||
'objitems': []
|
||
}
|
||
sources_objitems[objitem.source.id]['objitems'].append(objitem)
|
||
|
||
# Получаем CustomUser для текущего пользователя
|
||
try:
|
||
custom_user = CustomUser.objects.get(user=request.user)
|
||
except CustomUser.DoesNotExist:
|
||
custom_user = None
|
||
|
||
created_count = 0
|
||
errors = []
|
||
|
||
for source_id, data in sources_objitems.items():
|
||
source = data['source']
|
||
objitems_list = data['objitems']
|
||
|
||
# Сортируем точки по дате ГЛ перед расчётом
|
||
objitems_list.sort(
|
||
key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min
|
||
)
|
||
|
||
# Рассчитываем усреднённые координаты из выбранных точек
|
||
average_coords = None
|
||
points_with_coords = 0
|
||
|
||
for objitem in objitems_list:
|
||
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
|
||
coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
|
||
points_with_coords += 1
|
||
|
||
if average_coords is None:
|
||
average_coords = coord
|
||
else:
|
||
average_coords, _ = calculate_mean_coords(average_coords, coord)
|
||
|
||
# Создаём Point объект если есть координаты
|
||
coords_point = None
|
||
if average_coords:
|
||
coords_point = Point(average_coords[0], average_coords[1], srid=4326)
|
||
|
||
try:
|
||
# Создаём новую заявку со статусом "planned"
|
||
source_request = SourceRequest.objects.create(
|
||
source=source,
|
||
status='planned',
|
||
priority='medium',
|
||
coords=coords_point,
|
||
points_count=points_with_coords,
|
||
created_by=custom_user,
|
||
updated_by=custom_user,
|
||
comment=f'Создано из Кубсат. Точек: {len(objitems_list)}'
|
||
)
|
||
created_count += 1
|
||
except Exception as e:
|
||
errors.append(f'Источник #{source_id}: {str(e)}')
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'created_count': created_count,
|
||
'total_sources': len(sources_objitems),
|
||
'errors': errors
|
||
})
|
||
|
||
|
||
class KubsatRecalculateCoordsView(LoginRequiredMixin, FormView):
|
||
"""API для пересчёта усреднённых координат по списку ObjItem ID"""
|
||
form_class = KubsatFilterForm
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
import json
|
||
from django.http import JsonResponse
|
||
|
||
# Получаем список ID точек (ObjItem) из POST
|
||
objitem_ids = request.POST.getlist('objitem_ids')
|
||
|
||
if not objitem_ids:
|
||
return JsonResponse({'success': False, 'error': 'Нет данных для расчёта'}, status=400)
|
||
|
||
# Получаем ObjItem с их источниками, сортируем по дате ГЛ
|
||
objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
|
||
'source',
|
||
'geo_obj'
|
||
).order_by('geo_obj__timestamp') # Сортировка по дате ГЛ
|
||
|
||
# Группируем ObjItem по Source
|
||
sources_objitems = {}
|
||
for objitem in objitems:
|
||
if objitem.source:
|
||
if objitem.source.id not in sources_objitems:
|
||
sources_objitems[objitem.source.id] = []
|
||
sources_objitems[objitem.source.id].append(objitem)
|
||
|
||
# Рассчитываем усреднённые координаты для каждого источника
|
||
results = {}
|
||
for source_id, objitems_list in sources_objitems.items():
|
||
# Сортируем по дате ГЛ (на случай если порядок сбился)
|
||
objitems_list.sort(key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min)
|
||
|
||
average_coords = None
|
||
points_count = 0
|
||
|
||
for objitem in objitems_list:
|
||
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 average_coords is None:
|
||
average_coords = coord
|
||
else:
|
||
average_coords, _ = calculate_mean_coords(average_coords, coord)
|
||
|
||
if average_coords:
|
||
results[str(source_id)] = {
|
||
'avg_lon': average_coords[0],
|
||
'avg_lat': average_coords[1],
|
||
'points_count': points_count
|
||
}
|
||
else:
|
||
results[str(source_id)] = {
|
||
'avg_lon': None,
|
||
'avg_lat': None,
|
||
'points_count': 0
|
||
}
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'results': results
|
||
})
|