"""
Views для управления отметками сигналов (привязаны к TechAnalyze).
"""
import json
from datetime import timedelta
from django.contrib.auth.mixins import LoginRequiredMixin
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.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.views import View
from mainapp.models import (
TechAnalyze,
ObjectMark,
CustomUser,
Satellite,
Polarization,
Modulation,
Standard,
)
class SignalMarksView(LoginRequiredMixin, View):
"""
Главное представление для работы с отметками сигналов.
Содержит две вкладки: история отметок и проставление новых.
"""
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)
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))
if not satellite_id:
return JsonResponse({'error': 'Не выбран спутник'}, status=400)
# Базовый 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
12:00"
return f"{local_start.strftime('%H:%M')}
{local_end.strftime('%H:%M')}"
elif total_days <= 7:
# Показываем день и время с переносом
if local_start.date() == local_end.date():
# Один день: "01.12
10:00-14:00"
return f"{local_start.strftime('%d.%m')}
{local_start.strftime('%H:%M')}-{local_end.strftime('%H:%M')}"
else:
# Разные дни: "01.12 10:00
02.12 10:00"
return f"{local_start.strftime('%d.%m %H:%M')}
{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')[:1],
to_attr='last_marks'
)
).annotate(
mark_count=Count('marks'),
last_mark_date=Max('marks__timestamp'),
).order_by('frequency', 'name')
# Поиск
if search:
tech_analyzes = tech_analyzes.filter(
Q(name__icontains=search) |
Q(id__icontains=search)
)
# Пагинация (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:
last_mark = ta.last_marks[0] if ta.last_marks else None
# Проверяем, можно ли добавить новую отметку (прошло 5 минут)
can_add_mark = True
if last_mark:
time_diff = timezone.now() - last_mark.timestamp
can_add_mark = time_diff >= timedelta(minutes=5)
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 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):
"""Устаревший endpoint - теперь используется SaveSignalMarksView."""
def post(self, request):
return JsonResponse({
'success': False,
'error': 'Этот endpoint устарел. Используйте /api/save-signal-marks/'
}, status=410)
class UpdateObjectMarkView(LoginRequiredMixin, View):
"""Устаревший endpoint."""
def post(self, request):
return JsonResponse({
'success': False,
'error': 'Этот endpoint устарел.'
}, status=410)