Добавил абуз вч отметок
This commit is contained in:
@@ -347,8 +347,10 @@ class ParameterInline(admin.StackedInline):
|
|||||||
class ObjectMarkAdmin(BaseAdmin):
|
class ObjectMarkAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели ObjectMark."""
|
"""Админ-панель для модели ObjectMark."""
|
||||||
|
|
||||||
list_display = ("tech_analyze", "mark", "timestamp", "created_by")
|
list_display = ("id", "tech_analyze", "mark", "timestamp", "created_by")
|
||||||
|
list_display_links = ("id",)
|
||||||
list_select_related = ("tech_analyze", "tech_analyze__satellite", "created_by__user")
|
list_select_related = ("tech_analyze", "tech_analyze__satellite", "created_by__user")
|
||||||
|
list_editable = ("tech_analyze", "mark", "timestamp")
|
||||||
search_fields = ("tech_analyze__name", "tech_analyze__id")
|
search_fields = ("tech_analyze__name", "tech_analyze__id")
|
||||||
ordering = ("-timestamp",)
|
ordering = ("-timestamp",)
|
||||||
list_filter = (
|
list_filter = (
|
||||||
@@ -356,7 +358,6 @@ class ObjectMarkAdmin(BaseAdmin):
|
|||||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
("tech_analyze__satellite", MultiSelectRelatedDropdownFilter),
|
("tech_analyze__satellite", MultiSelectRelatedDropdownFilter),
|
||||||
)
|
)
|
||||||
readonly_fields = ("timestamp", "created_by")
|
|
||||||
autocomplete_fields = ("tech_analyze",)
|
autocomplete_fields = ("tech_analyze",)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,24 @@
|
|||||||
Management command для генерации тестовых отметок сигналов.
|
Management command для генерации тестовых отметок сигналов.
|
||||||
|
|
||||||
Использование:
|
Использование:
|
||||||
python manage.py generate_test_marks --satellite_id=1 --days=90 --marks_per_day=5
|
python manage.py generate_test_marks --satellite_id=1 --user_id=1 --date_range=10.10.2025-15.10.2025
|
||||||
|
|
||||||
Параметры:
|
Параметры:
|
||||||
--satellite_id: ID спутника (обязательный)
|
--satellite_id: ID спутника (обязательный)
|
||||||
--days: Количество дней для генерации (по умолчанию 90)
|
--user_id: ID пользователя CustomUser (обязательный)
|
||||||
--marks_per_day: Количество отметок в день (по умолчанию 3)
|
--date_range: Диапазон дат в формате ДД.ММ.ГГГГ-ДД.ММ.ГГГГ (обязательный)
|
||||||
--clear: Удалить существующие отметки перед генерацией
|
--clear: Удалить существующие отметки перед генерацией
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
- Генерирует отметки только в будние дни (пн-пт)
|
||||||
|
- Время отметок: утро с 8:00 до 11:00
|
||||||
|
- Одна отметка в день для всех сигналов спутника
|
||||||
|
- Все отметки в один день имеют одинаковый timestamp (пакетное сохранение)
|
||||||
|
- Все отметки имеют значение True (сигнал присутствует)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -31,16 +38,16 @@ class Command(BaseCommand):
|
|||||||
help='ID спутника для генерации отметок'
|
help='ID спутника для генерации отметок'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--days',
|
'--user_id',
|
||||||
type=int,
|
type=int,
|
||||||
default=90,
|
required=True,
|
||||||
help='Количество дней для генерации (по умолчанию 90)'
|
help='ID пользователя CustomUser - автор всех отметок'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--marks_per_day',
|
'--date_range',
|
||||||
type=int,
|
type=str,
|
||||||
default=3,
|
required=True,
|
||||||
help='Среднее количество отметок в день на теханализ (по умолчанию 3)'
|
help='Диапазон дат в формате ДД.ММ.ГГГГ-ДД.ММ.ГГГГ (например: 10.10.2025-15.10.2025)'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--clear',
|
'--clear',
|
||||||
@@ -50,10 +57,34 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
satellite_id = options['satellite_id']
|
satellite_id = options['satellite_id']
|
||||||
days = options['days']
|
user_id = options['user_id']
|
||||||
marks_per_day = options['marks_per_day']
|
date_range = options['date_range']
|
||||||
clear = options['clear']
|
clear = options['clear']
|
||||||
|
|
||||||
|
# Проверяем существование пользователя
|
||||||
|
try:
|
||||||
|
custom_user = CustomUser.objects.select_related('user').get(id=user_id)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
raise CommandError(f'Пользователь CustomUser с ID {user_id} не найден')
|
||||||
|
|
||||||
|
# Парсим диапазон дат
|
||||||
|
try:
|
||||||
|
start_str, end_str = date_range.split('-')
|
||||||
|
start_date = datetime.strptime(start_str.strip(), '%d.%m.%Y')
|
||||||
|
end_date = datetime.strptime(end_str.strip(), '%d.%m.%Y')
|
||||||
|
|
||||||
|
# Делаем timezone-aware
|
||||||
|
start_date = timezone.make_aware(start_date)
|
||||||
|
end_date = timezone.make_aware(end_date)
|
||||||
|
|
||||||
|
if start_date > end_date:
|
||||||
|
raise CommandError('Начальная дата должна быть раньше конечной')
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise CommandError(
|
||||||
|
f'Неверный формат даты. Используйте ДД.ММ.ГГГГ-ДД.ММ.ГГГГ (например: 10.10.2025-15.10.2025). Ошибка: {e}'
|
||||||
|
)
|
||||||
|
|
||||||
# Проверяем существование спутника
|
# Проверяем существование спутника
|
||||||
try:
|
try:
|
||||||
satellite = Satellite.objects.get(id=satellite_id)
|
satellite = Satellite.objects.get(id=satellite_id)
|
||||||
@@ -61,16 +92,17 @@ class Command(BaseCommand):
|
|||||||
raise CommandError(f'Спутник с ID {satellite_id} не найден')
|
raise CommandError(f'Спутник с ID {satellite_id} не найден')
|
||||||
|
|
||||||
# Получаем теханализы для спутника
|
# Получаем теханализы для спутника
|
||||||
tech_analyzes = TechAnalyze.objects.filter(satellite=satellite)
|
tech_analyzes = list(TechAnalyze.objects.filter(satellite=satellite))
|
||||||
ta_count = tech_analyzes.count()
|
ta_count = len(tech_analyzes)
|
||||||
|
|
||||||
if ta_count == 0:
|
if ta_count == 0:
|
||||||
raise CommandError(f'Нет теханализов для спутника "{satellite.name}"')
|
raise CommandError(f'Нет теханализов для спутника "{satellite.name}"')
|
||||||
|
|
||||||
self.stdout.write(f'Спутник: {satellite.name}')
|
self.stdout.write(f'Спутник: {satellite.name}')
|
||||||
self.stdout.write(f'Теханализов: {ta_count}')
|
self.stdout.write(f'Теханализов: {ta_count}')
|
||||||
self.stdout.write(f'Период: {days} дней')
|
self.stdout.write(f'Пользователь: {custom_user}')
|
||||||
self.stdout.write(f'Отметок в день: ~{marks_per_day}')
|
self.stdout.write(f'Период: {start_str} - {end_str} (только будние дни)')
|
||||||
|
self.stdout.write(f'Время: 8:00 - 11:00')
|
||||||
|
|
||||||
# Удаляем существующие отметки если указан флаг
|
# Удаляем существующие отметки если указан флаг
|
||||||
if clear:
|
if clear:
|
||||||
@@ -81,56 +113,46 @@ class Command(BaseCommand):
|
|||||||
self.style.WARNING(f'Удалено существующих отметок: {deleted_count}')
|
self.style.WARNING(f'Удалено существующих отметок: {deleted_count}')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Получаем или создаём тестового пользователя
|
|
||||||
test_users = self._get_or_create_test_users()
|
|
||||||
|
|
||||||
# Генерируем отметки
|
# Генерируем отметки
|
||||||
now = timezone.now()
|
|
||||||
start_date = now - timedelta(days=days)
|
|
||||||
|
|
||||||
total_marks = 0
|
total_marks = 0
|
||||||
marks_to_create = []
|
marks_to_create = []
|
||||||
|
workdays_count = 0
|
||||||
|
|
||||||
for ta in tech_analyzes:
|
current_date = start_date
|
||||||
# Для каждого теханализа генерируем отметки
|
# Включаем конечную дату в диапазон
|
||||||
current_date = start_date
|
end_date_inclusive = end_date + timedelta(days=1)
|
||||||
|
|
||||||
# Начальное состояние сигнала (случайное)
|
while current_date < end_date_inclusive:
|
||||||
signal_present = random.choice([True, False])
|
# Проверяем, что это будний день (0=пн, 4=пт)
|
||||||
|
if current_date.weekday() < 5:
|
||||||
while current_date < now:
|
workdays_count += 1
|
||||||
# Случайное количество отметок в этот день (от 0 до marks_per_day * 2)
|
|
||||||
day_marks = random.randint(0, marks_per_day * 2)
|
|
||||||
|
|
||||||
for _ in range(day_marks):
|
# Генерируем случайное время в диапазоне 8:00-11:00
|
||||||
# Случайное время в течение дня
|
random_hour = random.randint(8, 10)
|
||||||
random_hours = random.randint(0, 23)
|
random_minute = random.randint(0, 59)
|
||||||
random_minutes = random.randint(0, 59)
|
random_second = random.randint(0, 59)
|
||||||
mark_time = current_date.replace(
|
|
||||||
hour=random_hours,
|
mark_time = current_date.replace(
|
||||||
minute=random_minutes,
|
hour=random_hour,
|
||||||
second=random.randint(0, 59)
|
minute=random_minute,
|
||||||
)
|
second=random_second,
|
||||||
|
microsecond=0
|
||||||
# Пропускаем если время в будущем
|
)
|
||||||
if mark_time > now:
|
|
||||||
continue
|
# Создаём отметки для всех теханализов с одинаковым timestamp
|
||||||
|
for ta in tech_analyzes:
|
||||||
# С вероятностью 70% сигнал остаётся в том же состоянии
|
|
||||||
# С вероятностью 30% меняется
|
|
||||||
if random.random() > 0.7:
|
|
||||||
signal_present = not signal_present
|
|
||||||
|
|
||||||
marks_to_create.append(ObjectMark(
|
marks_to_create.append(ObjectMark(
|
||||||
tech_analyze=ta,
|
tech_analyze=ta,
|
||||||
mark=signal_present,
|
mark=True, # Всегда True
|
||||||
created_by=random.choice(test_users),
|
timestamp=mark_time,
|
||||||
|
created_by=custom_user,
|
||||||
))
|
))
|
||||||
total_marks += 1
|
total_marks += 1
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
# Bulk create для производительности
|
# Bulk create для производительности
|
||||||
|
self.stdout.write(f'Рабочих дней: {workdays_count}')
|
||||||
self.stdout.write(f'Создание {total_marks} отметок...')
|
self.stdout.write(f'Создание {total_marks} отметок...')
|
||||||
|
|
||||||
# Создаём партиями по 1000
|
# Создаём партиями по 1000
|
||||||
@@ -140,65 +162,8 @@ class Command(BaseCommand):
|
|||||||
ObjectMark.objects.bulk_create(batch)
|
ObjectMark.objects.bulk_create(batch)
|
||||||
self.stdout.write(f' Создано: {min(i + batch_size, len(marks_to_create))}/{total_marks}')
|
self.stdout.write(f' Создано: {min(i + batch_size, len(marks_to_create))}/{total_marks}')
|
||||||
|
|
||||||
# Обновляем timestamp для созданных отметок (bulk_create не вызывает auto_now_add корректно)
|
|
||||||
self.stdout.write('Обновление временных меток...')
|
|
||||||
|
|
||||||
# Получаем созданные отметки и обновляем их timestamp
|
|
||||||
created_marks = ObjectMark.objects.filter(
|
|
||||||
tech_analyze__satellite=satellite
|
|
||||||
).order_by('id')
|
|
||||||
|
|
||||||
# Распределяем временные метки
|
|
||||||
current_date = start_date
|
|
||||||
mark_index = 0
|
|
||||||
|
|
||||||
for ta in tech_analyzes:
|
|
||||||
ta_marks = list(created_marks.filter(tech_analyze=ta).order_by('id'))
|
|
||||||
if not ta_marks:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Распределяем отметки по времени
|
|
||||||
time_step = timedelta(days=days) / len(ta_marks) if ta_marks else timedelta(hours=1)
|
|
||||||
|
|
||||||
for i, mark in enumerate(ta_marks):
|
|
||||||
mark_time = start_date + (time_step * i)
|
|
||||||
# Добавляем случайное смещение
|
|
||||||
mark_time += timedelta(
|
|
||||||
hours=random.randint(0, 23),
|
|
||||||
minutes=random.randint(0, 59)
|
|
||||||
)
|
|
||||||
if mark_time > now:
|
|
||||||
mark_time = now - timedelta(minutes=random.randint(1, 60))
|
|
||||||
|
|
||||||
ObjectMark.objects.filter(id=mark.id).update(timestamp=mark_time)
|
|
||||||
|
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
f'Успешно создано {total_marks} отметок для {ta_count} теханализов'
|
f'Успешно создано {total_marks} отметок для {ta_count} теханализов за {workdays_count} рабочих дней'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_or_create_test_users(self):
|
|
||||||
"""Получает или создаёт тестовых пользователей для отметок."""
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
test_usernames = ['operator1', 'operator2', 'operator3', 'analyst1', 'analyst2']
|
|
||||||
custom_users = []
|
|
||||||
|
|
||||||
for username in test_usernames:
|
|
||||||
user, created = User.objects.get_or_create(
|
|
||||||
username=username,
|
|
||||||
defaults={
|
|
||||||
'first_name': username.capitalize(),
|
|
||||||
'last_name': 'Тестовый',
|
|
||||||
'is_active': True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
custom_user, _ = CustomUser.objects.get_or_create(
|
|
||||||
user=user,
|
|
||||||
defaults={'role': 'user'}
|
|
||||||
)
|
|
||||||
custom_users.append(custom_user)
|
|
||||||
|
|
||||||
return custom_users
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-12 12:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0023_add_coords_object_to_sourcerequest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectmark',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sourcerequest',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('planned', 'Запланировано'), ('canceled_gso', 'Отменено ГСО'), ('canceled_kub', 'Отменено МКА'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], db_index=True, default='planned', help_text='Текущий статус заявки', max_length=20, verbose_name='Статус'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sourcerequeststatushistory',
|
||||||
|
name='new_status',
|
||||||
|
field=models.CharField(choices=[('planned', 'Запланировано'), ('canceled_gso', 'Отменено ГСО'), ('canceled_kub', 'Отменено МКА'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], help_text='Статус после изменения', max_length=20, verbose_name='Новый статус'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sourcerequeststatushistory',
|
||||||
|
name='old_status',
|
||||||
|
field=models.CharField(choices=[('planned', 'Запланировано'), ('canceled_gso', 'Отменено ГСО'), ('canceled_kub', 'Отменено МКА'), ('conducted', 'Проведён'), ('successful', 'Успешно'), ('no_correlation', 'Нет корреляции'), ('no_signal', 'Нет сигнала в спектре'), ('unsuccessful', 'Неуспешно'), ('downloading', 'Скачивание'), ('processing', 'Обработка'), ('result_received', 'Результат получен')], help_text='Статус до изменения', max_length=20, verbose_name='Старый статус'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -120,10 +120,11 @@ class ObjectMark(models.Model):
|
|||||||
help_text="True - сигнал обнаружен, False - сигнал отсутствует",
|
help_text="True - сигнал обнаружен, False - сигнал отсутствует",
|
||||||
)
|
)
|
||||||
timestamp = models.DateTimeField(
|
timestamp = models.DateTimeField(
|
||||||
auto_now_add=True,
|
|
||||||
verbose_name="Время",
|
verbose_name="Время",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Время фиксации отметки",
|
help_text="Время фиксации отметки",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
tech_analyze = models.ForeignKey(
|
tech_analyze = models.ForeignKey(
|
||||||
'TechAnalyze',
|
'TechAnalyze',
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class SignalMarksEntryAPIView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
# Проверяем, можно ли добавить новую отметку (прошло 5 минут)
|
# Проверяем, можно ли добавить новую отметку (прошло 5 минут)
|
||||||
can_add_mark = True
|
can_add_mark = True
|
||||||
if last_mark:
|
if last_mark and last_mark.timestamp:
|
||||||
time_diff = timezone.now() - last_mark.timestamp
|
time_diff = timezone.now() - last_mark.timestamp
|
||||||
can_add_mark = time_diff >= timedelta(minutes=5)
|
can_add_mark = time_diff >= timedelta(minutes=5)
|
||||||
|
|
||||||
@@ -364,17 +364,18 @@ class SaveSignalMarksView(LoginRequiredMixin, View):
|
|||||||
tech_analyze = TechAnalyze.objects.get(id=tech_analyze_id)
|
tech_analyze = TechAnalyze.objects.get(id=tech_analyze_id)
|
||||||
|
|
||||||
# Проверяем, можно ли добавить отметку
|
# Проверяем, можно ли добавить отметку
|
||||||
last_mark = tech_analyze.marks.first()
|
last_mark = tech_analyze.marks.order_by('-timestamp').first()
|
||||||
if last_mark:
|
if last_mark and last_mark.timestamp:
|
||||||
time_diff = timezone.now() - last_mark.timestamp
|
time_diff = timezone.now() - last_mark.timestamp
|
||||||
if time_diff < timedelta(minutes=5):
|
if time_diff < timedelta(minutes=5):
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Создаём отметку
|
# Создаём отметку с текущим временем
|
||||||
ObjectMark.objects.create(
|
ObjectMark.objects.create(
|
||||||
tech_analyze=tech_analyze,
|
tech_analyze=tech_analyze,
|
||||||
mark=mark_value,
|
mark=mark_value,
|
||||||
|
timestamp=timezone.now(),
|
||||||
created_by=custom_user,
|
created_by=custom_user,
|
||||||
)
|
)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user