Переосмыслил отметки по ВЧ загрузке. Улучшил статистику
This commit is contained in:
@@ -1,312 +1,535 @@
|
||||
"""
|
||||
Views для управления отметками объектов.
|
||||
Views для управления отметками сигналов (привязаны к TechAnalyze).
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Prefetch
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Max, Min, Prefetch, Q
|
||||
from django.http import JsonResponse
|
||||
from django.views.generic import ListView, View
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
|
||||
from mainapp.models import Source, ObjectMark, CustomUser, Satellite
|
||||
from mainapp.models import (
|
||||
TechAnalyze,
|
||||
ObjectMark,
|
||||
CustomUser,
|
||||
Satellite,
|
||||
Polarization,
|
||||
Modulation,
|
||||
Standard,
|
||||
)
|
||||
|
||||
|
||||
class ObjectMarksListView(LoginRequiredMixin, ListView):
|
||||
class SignalMarksView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Представление списка источников с отметками.
|
||||
Главное представление для работы с отметками сигналов.
|
||||
Содержит две вкладки: история отметок и проставление новых.
|
||||
"""
|
||||
model = Source
|
||||
template_name = "mainapp/object_marks.html"
|
||||
context_object_name = "sources"
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
"""Получить количество элементов на странице из параметров запроса"""
|
||||
from mainapp.utils import parse_pagination_params
|
||||
_, items_per_page = parse_pagination_params(self.request, default_per_page=50)
|
||||
return items_per_page
|
||||
def get(self, request):
|
||||
satellites = Satellite.objects.filter(
|
||||
tech_analyzes__isnull=False
|
||||
).distinct().order_by('name')
|
||||
|
||||
satellite_id = request.GET.get('satellite_id')
|
||||
selected_satellite = None
|
||||
|
||||
if satellite_id:
|
||||
try:
|
||||
selected_satellite = Satellite.objects.get(id=satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Справочники для модального окна создания теханализа
|
||||
polarizations = Polarization.objects.all().order_by('name')
|
||||
modulations = Modulation.objects.all().order_by('name')
|
||||
standards = Standard.objects.all().order_by('name')
|
||||
|
||||
context = {
|
||||
'satellites': satellites,
|
||||
'selected_satellite': selected_satellite,
|
||||
'selected_satellite_id': int(satellite_id) if satellite_id and satellite_id.isdigit() else None,
|
||||
'full_width_page': True,
|
||||
'polarizations': polarizations,
|
||||
'modulations': modulations,
|
||||
'standards': standards,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/signal_marks.html', context)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Получить queryset с предзагруженными связанными данными"""
|
||||
from django.db.models import Count, Max, Min
|
||||
|
||||
class SignalMarksHistoryAPIView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения истории отметок с фиксированными 15 колонками.
|
||||
Делит выбранный временной диапазон на 15 равных периодов.
|
||||
"""
|
||||
|
||||
NUM_COLUMNS = 15 # Фиксированное количество колонок
|
||||
|
||||
def get(self, request):
|
||||
from datetime import datetime
|
||||
from django.utils.dateparse import parse_date
|
||||
|
||||
satellite_id = request.GET.get('satellite_id')
|
||||
date_from = request.GET.get('date_from')
|
||||
date_to = request.GET.get('date_to')
|
||||
page = int(request.GET.get('page', 1))
|
||||
size = int(request.GET.get('size', 50))
|
||||
|
||||
# Проверяем, выбран ли спутник
|
||||
satellite_id = self.request.GET.get('satellite_id')
|
||||
if not satellite_id:
|
||||
# Если спутник не выбран, возвращаем пустой queryset
|
||||
return Source.objects.none()
|
||||
return JsonResponse({'error': 'Не выбран спутник'}, status=400)
|
||||
|
||||
queryset = Source.objects.prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
'source_objitems__parameter_obj__polarization',
|
||||
'source_objitems__parameter_obj__modulation',
|
||||
# Базовый queryset теханализов для спутника
|
||||
tech_analyzes = TechAnalyze.objects.filter(
|
||||
satellite_id=satellite_id
|
||||
).select_related(
|
||||
'polarization', 'modulation', 'standard'
|
||||
).order_by('frequency', 'name')
|
||||
|
||||
# Базовый фильтр отметок по спутнику
|
||||
marks_base_qs = ObjectMark.objects.filter(
|
||||
tech_analyze__satellite_id=satellite_id
|
||||
).select_related('created_by__user', 'tech_analyze')
|
||||
|
||||
# Определяем диапазон дат
|
||||
parsed_date_from = None
|
||||
parsed_date_to = None
|
||||
|
||||
if date_from:
|
||||
parsed_date_from = parse_date(date_from)
|
||||
if parsed_date_from:
|
||||
marks_base_qs = marks_base_qs.filter(timestamp__date__gte=parsed_date_from)
|
||||
|
||||
if date_to:
|
||||
parsed_date_to = parse_date(date_to)
|
||||
if parsed_date_to:
|
||||
marks_base_qs = marks_base_qs.filter(timestamp__date__lte=parsed_date_to)
|
||||
|
||||
# Если даты не указаны, берём из данных
|
||||
date_range = marks_base_qs.aggregate(
|
||||
min_date=Min('timestamp'),
|
||||
max_date=Max('timestamp')
|
||||
)
|
||||
|
||||
min_date = date_range['min_date']
|
||||
max_date = date_range['max_date']
|
||||
|
||||
if not min_date or not max_date:
|
||||
return JsonResponse({
|
||||
'periods': [],
|
||||
'data': [],
|
||||
'last_page': 1,
|
||||
'total': 0,
|
||||
'message': 'Нет отметок в выбранном диапазоне',
|
||||
})
|
||||
|
||||
# Используем указанные даты или данные из БД
|
||||
start_dt = datetime.combine(parsed_date_from, datetime.min.time()) if parsed_date_from else min_date
|
||||
end_dt = datetime.combine(parsed_date_to, datetime.max.time()) if parsed_date_to else max_date
|
||||
|
||||
# Делаем timezone-aware если нужно
|
||||
if timezone.is_naive(start_dt):
|
||||
start_dt = timezone.make_aware(start_dt)
|
||||
if timezone.is_naive(end_dt):
|
||||
end_dt = timezone.make_aware(end_dt)
|
||||
|
||||
# Вычисляем длительность периода
|
||||
total_duration = end_dt - start_dt
|
||||
period_duration = total_duration / self.NUM_COLUMNS
|
||||
|
||||
# Генерируем границы периодов
|
||||
periods = []
|
||||
for i in range(self.NUM_COLUMNS):
|
||||
period_start = start_dt + (period_duration * i)
|
||||
period_end = start_dt + (period_duration * (i + 1))
|
||||
periods.append({
|
||||
'start': period_start,
|
||||
'end': period_end,
|
||||
'label': self._format_period_label(period_start, period_end, total_duration),
|
||||
})
|
||||
|
||||
# Пагинация теханализов (size=0 означает "все записи")
|
||||
if size == 0:
|
||||
# Все записи без пагинации
|
||||
page_obj = tech_analyzes
|
||||
num_pages = 1
|
||||
total_count = tech_analyzes.count()
|
||||
else:
|
||||
paginator = Paginator(tech_analyzes, size)
|
||||
page_obj = paginator.get_page(page)
|
||||
num_pages = paginator.num_pages
|
||||
total_count = paginator.count
|
||||
|
||||
# Формируем данные
|
||||
data = []
|
||||
for ta in page_obj:
|
||||
row = {
|
||||
'id': ta.id,
|
||||
'name': ta.name,
|
||||
'marks': [],
|
||||
}
|
||||
|
||||
# Получаем все отметки для этого теханализа
|
||||
ta_marks = list(marks_base_qs.filter(tech_analyze=ta).order_by('-timestamp'))
|
||||
|
||||
# Для каждого периода находим последнюю отметку
|
||||
for period in periods:
|
||||
mark_in_period = None
|
||||
for mark in ta_marks:
|
||||
if period['start'] <= mark.timestamp < period['end']:
|
||||
mark_in_period = mark
|
||||
break # Берём первую (последнюю по времени, т.к. сортировка -timestamp)
|
||||
|
||||
if mark_in_period:
|
||||
# Конвертируем в локальное время (Europe/Moscow)
|
||||
local_time = timezone.localtime(mark_in_period.timestamp)
|
||||
row['marks'].append({
|
||||
'mark': mark_in_period.mark,
|
||||
'user': str(mark_in_period.created_by) if mark_in_period.created_by else '-',
|
||||
'time': local_time.strftime('%d.%m %H:%M'),
|
||||
})
|
||||
else:
|
||||
row['marks'].append(None)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return JsonResponse({
|
||||
'periods': [p['label'] for p in periods],
|
||||
'data': data,
|
||||
'last_page': num_pages,
|
||||
'total': total_count,
|
||||
})
|
||||
|
||||
def _format_period_label(self, start, end, total_duration):
|
||||
"""Форматирует метку периода (диапазон) в зависимости от общей длительности."""
|
||||
# Конвертируем в локальное время
|
||||
local_start = timezone.localtime(start)
|
||||
local_end = timezone.localtime(end)
|
||||
total_days = total_duration.days
|
||||
|
||||
if total_days <= 1:
|
||||
# Показываем часы: "10:00<br>12:00"
|
||||
return f"{local_start.strftime('%H:%M')}<br>{local_end.strftime('%H:%M')}"
|
||||
elif total_days <= 7:
|
||||
# Показываем день и время с переносом
|
||||
if local_start.date() == local_end.date():
|
||||
# Один день: "01.12<br>10:00-14:00"
|
||||
return f"{local_start.strftime('%d.%m')}<br>{local_start.strftime('%H:%M')}-{local_end.strftime('%H:%M')}"
|
||||
else:
|
||||
# Разные дни: "01.12 10:00<br>02.12 10:00"
|
||||
return f"{local_start.strftime('%d.%m %H:%M')}<br>{local_end.strftime('%d.%m %H:%M')}"
|
||||
elif total_days <= 60:
|
||||
# Показываем дату: "01.12-05.12"
|
||||
return f"{local_start.strftime('%d.%m')}-{local_end.strftime('%d.%m')}"
|
||||
else:
|
||||
# Показываем месяц: "01.12.24-15.12.24"
|
||||
return f"{local_start.strftime('%d.%m.%y')}-{local_end.strftime('%d.%m.%y')}"
|
||||
|
||||
|
||||
class SignalMarksEntryAPIView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения данных теханализов для проставления отметок.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
satellite_id = request.GET.get('satellite_id')
|
||||
page = int(request.GET.get('page', 1))
|
||||
size_param = request.GET.get('size', '100')
|
||||
search = request.GET.get('search', '').strip()
|
||||
|
||||
# Обработка size: "true" означает "все записи", иначе число
|
||||
if size_param == 'true' or size_param == '0':
|
||||
size = 0 # Все записи
|
||||
else:
|
||||
try:
|
||||
size = int(size_param)
|
||||
except (ValueError, TypeError):
|
||||
size = 100
|
||||
|
||||
if not satellite_id:
|
||||
return JsonResponse({'error': 'Не выбран спутник'}, status=400)
|
||||
|
||||
# Базовый queryset
|
||||
tech_analyzes = TechAnalyze.objects.filter(
|
||||
satellite_id=satellite_id
|
||||
).select_related(
|
||||
'polarization', 'modulation', 'standard'
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
'marks',
|
||||
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')
|
||||
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')[:1],
|
||||
to_attr='last_marks'
|
||||
)
|
||||
).annotate(
|
||||
mark_count=Count('marks'),
|
||||
last_mark_date=Max('marks__timestamp'),
|
||||
# Аннотации для сортировки по параметрам (берем минимальное значение из связанных объектов)
|
||||
min_frequency=Min('source_objitems__parameter_obj__frequency'),
|
||||
min_freq_range=Min('source_objitems__parameter_obj__freq_range'),
|
||||
min_bod_velocity=Min('source_objitems__parameter_obj__bod_velocity')
|
||||
)
|
||||
).order_by('frequency', 'name')
|
||||
|
||||
# Фильтрация по выбранному спутнику (обязательно)
|
||||
queryset = queryset.filter(source_objitems__parameter_obj__id_satellite_id=satellite_id).distinct()
|
||||
# Поиск
|
||||
if search:
|
||||
tech_analyzes = tech_analyzes.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(id__icontains=search)
|
||||
)
|
||||
|
||||
# Фильтрация по статусу (есть/нет отметок)
|
||||
mark_status = self.request.GET.get('mark_status')
|
||||
if mark_status == 'with_marks':
|
||||
queryset = queryset.filter(mark_count__gt=0)
|
||||
elif mark_status == 'without_marks':
|
||||
queryset = queryset.filter(mark_count=0)
|
||||
|
||||
# Фильтрация по дате отметки
|
||||
date_from = self.request.GET.get('date_from')
|
||||
date_to = self.request.GET.get('date_to')
|
||||
if date_from:
|
||||
from django.utils.dateparse import parse_date
|
||||
parsed_date = parse_date(date_from)
|
||||
if parsed_date:
|
||||
queryset = queryset.filter(marks__timestamp__date__gte=parsed_date).distinct()
|
||||
if date_to:
|
||||
from django.utils.dateparse import parse_date
|
||||
parsed_date = parse_date(date_to)
|
||||
if parsed_date:
|
||||
queryset = queryset.filter(marks__timestamp__date__lte=parsed_date).distinct()
|
||||
|
||||
# Фильтрация по пользователям (мультивыбор)
|
||||
user_ids = self.request.GET.getlist('user_id')
|
||||
if user_ids:
|
||||
queryset = queryset.filter(marks__created_by_id__in=user_ids).distinct()
|
||||
|
||||
# Поиск по имени объекта или ID
|
||||
search_query = self.request.GET.get('search', '').strip()
|
||||
if search_query:
|
||||
from django.db.models import Q
|
||||
try:
|
||||
# Попытка поиска по ID
|
||||
source_id = int(search_query)
|
||||
queryset = queryset.filter(Q(id=source_id) | Q(source_objitems__name__icontains=search_query)).distinct()
|
||||
except ValueError:
|
||||
# Поиск только по имени
|
||||
queryset = queryset.filter(source_objitems__name__icontains=search_query).distinct()
|
||||
|
||||
# Сортировка
|
||||
sort = self.request.GET.get('sort', '-id')
|
||||
allowed_sorts = [
|
||||
'id', '-id',
|
||||
'created_at', '-created_at',
|
||||
'last_mark_date', '-last_mark_date',
|
||||
'mark_count', '-mark_count',
|
||||
'frequency', '-frequency',
|
||||
'freq_range', '-freq_range',
|
||||
'bod_velocity', '-bod_velocity'
|
||||
]
|
||||
|
||||
if sort in allowed_sorts:
|
||||
# Для сортировки по last_mark_date нужно обработать NULL значения
|
||||
if 'last_mark_date' in sort:
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Coalesce
|
||||
queryset = queryset.order_by(
|
||||
Coalesce(F('last_mark_date'), F('created_at')).desc() if sort.startswith('-') else Coalesce(F('last_mark_date'), F('created_at')).asc()
|
||||
)
|
||||
# Сортировка по частоте
|
||||
elif sort == 'frequency':
|
||||
queryset = queryset.order_by('min_frequency')
|
||||
elif sort == '-frequency':
|
||||
queryset = queryset.order_by('-min_frequency')
|
||||
# Сортировка по полосе
|
||||
elif sort == 'freq_range':
|
||||
queryset = queryset.order_by('min_freq_range')
|
||||
elif sort == '-freq_range':
|
||||
queryset = queryset.order_by('-min_freq_range')
|
||||
# Сортировка по бодовой скорости
|
||||
elif sort == 'bod_velocity':
|
||||
queryset = queryset.order_by('min_bod_velocity')
|
||||
elif sort == '-bod_velocity':
|
||||
queryset = queryset.order_by('-min_bod_velocity')
|
||||
else:
|
||||
queryset = queryset.order_by(sort)
|
||||
# Пагинация (size=0 означает "все записи")
|
||||
if size == 0:
|
||||
page_obj = tech_analyzes
|
||||
num_pages = 1
|
||||
total_count = tech_analyzes.count()
|
||||
else:
|
||||
queryset = queryset.order_by('-id')
|
||||
paginator = Paginator(tech_analyzes, size)
|
||||
page_obj = paginator.get_page(page)
|
||||
num_pages = paginator.num_pages
|
||||
total_count = paginator.count
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Добавить дополнительные данные в контекст"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
from mainapp.utils import parse_pagination_params
|
||||
|
||||
# Все спутники для выбора
|
||||
context['satellites'] = Satellite.objects.filter(
|
||||
parameters__objitem__source__isnull=False
|
||||
).distinct().order_by('name')
|
||||
|
||||
# Выбранный спутник
|
||||
satellite_id = self.request.GET.get('satellite_id')
|
||||
context['selected_satellite_id'] = int(satellite_id) if satellite_id and satellite_id.isdigit() else None
|
||||
|
||||
context['users'] = CustomUser.objects.select_related('user').filter(
|
||||
marks_created__isnull=False
|
||||
).distinct().order_by('user__username')
|
||||
|
||||
# Параметры пагинации
|
||||
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
|
||||
context['items_per_page'] = items_per_page
|
||||
context['available_items_per_page'] = [25, 50, 100, 200, 500]
|
||||
|
||||
# Параметры поиска и сортировки
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
context['sort'] = self.request.GET.get('sort', '-id')
|
||||
|
||||
# Параметры фильтров для отображения в UI
|
||||
context['selected_users'] = [int(x) for x in self.request.GET.getlist('user_id') if x.isdigit()]
|
||||
context['filter_mark_status'] = self.request.GET.get('mark_status', '')
|
||||
context['filter_date_from'] = self.request.GET.get('date_from', '')
|
||||
context['filter_date_to'] = self.request.GET.get('date_to', '')
|
||||
|
||||
# Полноэкранный режим
|
||||
context['full_width_page'] = True
|
||||
|
||||
# Добавить информацию о параметрах для каждого источника
|
||||
for source in context['sources']:
|
||||
# Получить первый объект для параметров (они должны быть одинаковыми)
|
||||
first_objitem = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation'
|
||||
).first()
|
||||
# Формируем данные
|
||||
data = []
|
||||
for ta in page_obj:
|
||||
last_mark = ta.last_marks[0] if ta.last_marks else None
|
||||
|
||||
if first_objitem:
|
||||
source.objitem_name = first_objitem.name if first_objitem.name else '-'
|
||||
|
||||
# Получить параметры
|
||||
if first_objitem.parameter_obj:
|
||||
param = first_objitem.parameter_obj
|
||||
source.frequency = param.frequency if param.frequency else '-'
|
||||
source.freq_range = param.freq_range if param.freq_range else '-'
|
||||
source.polarization = param.polarization.name if param.polarization else '-'
|
||||
source.modulation = param.modulation.name if param.modulation else '-'
|
||||
source.bod_velocity = param.bod_velocity if param.bod_velocity else '-'
|
||||
else:
|
||||
source.frequency = '-'
|
||||
source.freq_range = '-'
|
||||
source.polarization = '-'
|
||||
source.modulation = '-'
|
||||
source.bod_velocity = '-'
|
||||
else:
|
||||
source.objitem_name = '-'
|
||||
source.frequency = '-'
|
||||
source.freq_range = '-'
|
||||
source.polarization = '-'
|
||||
source.modulation = '-'
|
||||
source.bod_velocity = '-'
|
||||
# Проверяем, можно ли добавить новую отметку (прошло 5 минут)
|
||||
can_add_mark = True
|
||||
if last_mark:
|
||||
time_diff = timezone.now() - last_mark.timestamp
|
||||
can_add_mark = time_diff >= timedelta(minutes=5)
|
||||
|
||||
# Проверка возможности редактирования отметок
|
||||
for mark in source.marks.all():
|
||||
mark.editable = mark.can_edit()
|
||||
data.append({
|
||||
'id': ta.id,
|
||||
'name': ta.name,
|
||||
'frequency': float(ta.frequency) if ta.frequency else 0,
|
||||
'freq_range': float(ta.freq_range) if ta.freq_range else 0,
|
||||
'polarization': ta.polarization.name if ta.polarization else '-',
|
||||
'bod_velocity': float(ta.bod_velocity) if ta.bod_velocity else 0,
|
||||
'modulation': ta.modulation.name if ta.modulation else '-',
|
||||
'standard': ta.standard.name if ta.standard else '-',
|
||||
'mark_count': ta.mark_count,
|
||||
'last_mark': {
|
||||
'mark': last_mark.mark,
|
||||
'timestamp': last_mark.timestamp.strftime('%d.%m.%Y %H:%M'),
|
||||
'user': str(last_mark.created_by) if last_mark.created_by else '-',
|
||||
} if last_mark else None,
|
||||
'can_add_mark': can_add_mark,
|
||||
})
|
||||
|
||||
return context
|
||||
return JsonResponse({
|
||||
'data': data,
|
||||
'last_page': num_pages,
|
||||
'total': total_count,
|
||||
})
|
||||
|
||||
|
||||
class SaveSignalMarksView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для сохранения отметок сигналов.
|
||||
Принимает массив отметок и сохраняет их в базу.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
marks = data.get('marks', [])
|
||||
|
||||
if not marks:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Нет данных для сохранения'
|
||||
}, status=400)
|
||||
|
||||
# Получаем CustomUser
|
||||
custom_user = None
|
||||
if hasattr(request.user, 'customuser'):
|
||||
custom_user = request.user.customuser
|
||||
else:
|
||||
custom_user, _ = CustomUser.objects.get_or_create(user=request.user)
|
||||
|
||||
created_count = 0
|
||||
skipped_count = 0
|
||||
errors = []
|
||||
|
||||
with transaction.atomic():
|
||||
for item in marks:
|
||||
tech_analyze_id = item.get('tech_analyze_id')
|
||||
mark_value = item.get('mark')
|
||||
|
||||
if tech_analyze_id is None or mark_value is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
tech_analyze = TechAnalyze.objects.get(id=tech_analyze_id)
|
||||
|
||||
# Проверяем, можно ли добавить отметку
|
||||
last_mark = tech_analyze.marks.first()
|
||||
if last_mark:
|
||||
time_diff = timezone.now() - last_mark.timestamp
|
||||
if time_diff < timedelta(minutes=5):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Создаём отметку
|
||||
ObjectMark.objects.create(
|
||||
tech_analyze=tech_analyze,
|
||||
mark=mark_value,
|
||||
created_by=custom_user,
|
||||
)
|
||||
created_count += 1
|
||||
|
||||
except TechAnalyze.DoesNotExist:
|
||||
errors.append(f'Теханализ {tech_analyze_id} не найден')
|
||||
except Exception as e:
|
||||
errors.append(f'Ошибка для {tech_analyze_id}: {str(e)}')
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'created': created_count,
|
||||
'skipped': skipped_count,
|
||||
'errors': errors if errors else None,
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
class CreateTechAnalyzeView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для создания нового теханализа из модального окна.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
|
||||
satellite_id = data.get('satellite_id')
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not satellite_id:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не указан спутник'
|
||||
}, status=400)
|
||||
|
||||
if not name:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Не указано имя'
|
||||
}, status=400)
|
||||
|
||||
# Проверяем уникальность имени
|
||||
if TechAnalyze.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Теханализ с именем "{name}" уже существует'
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
satellite = Satellite.objects.get(id=satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Спутник не найден'
|
||||
}, status=404)
|
||||
|
||||
# Получаем или создаём справочные данные
|
||||
polarization_name = data.get('polarization', '').strip() or '-'
|
||||
polarization, _ = Polarization.objects.get_or_create(name=polarization_name)
|
||||
|
||||
modulation_name = data.get('modulation', '').strip() or '-'
|
||||
modulation, _ = Modulation.objects.get_or_create(name=modulation_name)
|
||||
|
||||
standard_name = data.get('standard', '').strip() or '-'
|
||||
standard, _ = Standard.objects.get_or_create(name=standard_name)
|
||||
|
||||
# Обработка числовых полей
|
||||
def parse_float(val):
|
||||
if val:
|
||||
try:
|
||||
return float(str(val).replace(',', '.'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
# Получаем CustomUser
|
||||
custom_user = None
|
||||
if hasattr(request.user, 'customuser'):
|
||||
custom_user = request.user.customuser
|
||||
|
||||
# Создаём теханализ
|
||||
tech_analyze = TechAnalyze.objects.create(
|
||||
name=name,
|
||||
satellite=satellite,
|
||||
frequency=parse_float(data.get('frequency')),
|
||||
freq_range=parse_float(data.get('freq_range')),
|
||||
bod_velocity=parse_float(data.get('bod_velocity')),
|
||||
polarization=polarization,
|
||||
modulation=modulation,
|
||||
standard=standard,
|
||||
note=data.get('note', '').strip(),
|
||||
created_by=custom_user,
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'tech_analyze': {
|
||||
'id': tech_analyze.id,
|
||||
'name': tech_analyze.name,
|
||||
'frequency': float(tech_analyze.frequency) if tech_analyze.frequency else 0,
|
||||
'freq_range': float(tech_analyze.freq_range) if tech_analyze.freq_range else 0,
|
||||
'polarization': polarization.name,
|
||||
'bod_velocity': float(tech_analyze.bod_velocity) if tech_analyze.bod_velocity else 0,
|
||||
'modulation': modulation.name,
|
||||
'standard': standard.name,
|
||||
}
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Неверный формат данных'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
# Оставляем старые views для обратной совместимости (редирект на новую страницу)
|
||||
class ObjectMarksListView(LoginRequiredMixin, View):
|
||||
"""Редирект на новую страницу отметок."""
|
||||
|
||||
def get(self, request):
|
||||
from django.shortcuts import redirect
|
||||
return redirect('mainapp:signal_marks')
|
||||
|
||||
|
||||
class AddObjectMarkView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для добавления отметки источника.
|
||||
"""
|
||||
"""Устаревший endpoint - теперь используется SaveSignalMarksView."""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Создать новую отметку"""
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
source_id = request.POST.get('source_id')
|
||||
mark = request.POST.get('mark') == 'true'
|
||||
|
||||
if not source_id:
|
||||
return JsonResponse({'success': False, 'error': 'Не указан ID источника'}, status=400)
|
||||
|
||||
source = get_object_or_404(Source, pk=source_id)
|
||||
|
||||
# Проверить последнюю отметку источника
|
||||
last_mark = source.marks.first()
|
||||
if last_mark:
|
||||
time_diff = timezone.now() - last_mark.timestamp
|
||||
if time_diff < timedelta(minutes=5):
|
||||
minutes_left = 5 - int(time_diff.total_seconds() / 60)
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Нельзя добавить отметку. Подождите ещё {minutes_left} мин.'
|
||||
}, status=400)
|
||||
|
||||
# Получить или создать CustomUser для текущего пользователя
|
||||
custom_user, _ = CustomUser.objects.get_or_create(user=request.user)
|
||||
|
||||
# Создать отметку
|
||||
object_mark = ObjectMark.objects.create(
|
||||
source=source,
|
||||
mark=mark,
|
||||
created_by=custom_user
|
||||
)
|
||||
|
||||
# Обновляем дату последнего сигнала источника
|
||||
source.update_last_signal_at()
|
||||
source.save()
|
||||
|
||||
def post(self, request):
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'mark': {
|
||||
'id': object_mark.id,
|
||||
'mark': object_mark.mark,
|
||||
'timestamp': object_mark.timestamp.strftime('%d.%m.%Y %H:%M'),
|
||||
'created_by': str(object_mark.created_by) if object_mark.created_by else 'Неизвестно',
|
||||
'can_edit': object_mark.can_edit()
|
||||
}
|
||||
})
|
||||
'success': False,
|
||||
'error': 'Этот endpoint устарел. Используйте /api/save-signal-marks/'
|
||||
}, status=410)
|
||||
|
||||
|
||||
class UpdateObjectMarkView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для обновления отметки объекта (в течение 5 минут).
|
||||
"""
|
||||
"""Устаревший endpoint."""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Обновить существующую отметку"""
|
||||
mark_id = request.POST.get('mark_id')
|
||||
new_mark_value = request.POST.get('mark') == 'true'
|
||||
|
||||
if not mark_id:
|
||||
return JsonResponse({'success': False, 'error': 'Не указан ID отметки'}, status=400)
|
||||
|
||||
object_mark = get_object_or_404(ObjectMark, pk=mark_id)
|
||||
|
||||
# Проверить возможность редактирования
|
||||
if not object_mark.can_edit():
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Время редактирования истекло (более 5 минут)'
|
||||
}, status=400)
|
||||
|
||||
# Обновить отметку
|
||||
object_mark.mark = new_mark_value
|
||||
object_mark.save()
|
||||
|
||||
# Обновляем дату последнего сигнала источника
|
||||
object_mark.source.update_last_signal_at()
|
||||
object_mark.source.save()
|
||||
|
||||
def post(self, request):
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'mark': {
|
||||
'id': object_mark.id,
|
||||
'mark': object_mark.mark,
|
||||
'timestamp': object_mark.timestamp.strftime('%d.%m.%Y %H:%M'),
|
||||
'created_by': str(object_mark.created_by) if object_mark.created_by else 'Неизвестно',
|
||||
'can_edit': object_mark.can_edit()
|
||||
}
|
||||
})
|
||||
'success': False,
|
||||
'error': 'Этот endpoint устарел.'
|
||||
}, status=410)
|
||||
|
||||
Reference in New Issue
Block a user