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

751 lines
36 KiB
Python
Raw 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.

"""
Представления для страницы Кубсат с фильтрацией и экспортом в 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
})