diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 7e9842d..4ce241e 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -347,8 +347,10 @@ class ParameterInline(admin.StackedInline): class ObjectMarkAdmin(BaseAdmin): """Админ-панель для модели 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_editable = ("tech_analyze", "mark", "timestamp") search_fields = ("tech_analyze__name", "tech_analyze__id") ordering = ("-timestamp",) list_filter = ( @@ -356,7 +358,6 @@ class ObjectMarkAdmin(BaseAdmin): ("timestamp", DateRangeQuickSelectListFilterBuilder()), ("tech_analyze__satellite", MultiSelectRelatedDropdownFilter), ) - readonly_fields = ("timestamp", "created_by") autocomplete_fields = ("tech_analyze",) diff --git a/dbapp/mainapp/management/commands/generate_test_marks.py b/dbapp/mainapp/management/commands/generate_test_marks.py index f34914a..f9ac8c7 100644 --- a/dbapp/mainapp/management/commands/generate_test_marks.py +++ b/dbapp/mainapp/management/commands/generate_test_marks.py @@ -2,17 +2,24 @@ 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 спутника (обязательный) - --days: Количество дней для генерации (по умолчанию 90) - --marks_per_day: Количество отметок в день (по умолчанию 3) + --user_id: ID пользователя CustomUser (обязательный) + --date_range: Диапазон дат в формате ДД.ММ.ГГГГ-ДД.ММ.ГГГГ (обязательный) --clear: Удалить существующие отметки перед генерацией + +Особенности: + - Генерирует отметки только в будние дни (пн-пт) + - Время отметок: утро с 8:00 до 11:00 + - Одна отметка в день для всех сигналов спутника + - Все отметки в один день имеют одинаковый timestamp (пакетное сохранение) + - Все отметки имеют значение True (сигнал присутствует) """ import random -from datetime import timedelta +from datetime import datetime, timedelta from django.core.management.base import BaseCommand, CommandError from django.utils import timezone @@ -31,16 +38,16 @@ class Command(BaseCommand): help='ID спутника для генерации отметок' ) parser.add_argument( - '--days', + '--user_id', type=int, - default=90, - help='Количество дней для генерации (по умолчанию 90)' + required=True, + help='ID пользователя CustomUser - автор всех отметок' ) parser.add_argument( - '--marks_per_day', - type=int, - default=3, - help='Среднее количество отметок в день на теханализ (по умолчанию 3)' + '--date_range', + type=str, + required=True, + help='Диапазон дат в формате ДД.ММ.ГГГГ-ДД.ММ.ГГГГ (например: 10.10.2025-15.10.2025)' ) parser.add_argument( '--clear', @@ -50,10 +57,34 @@ class Command(BaseCommand): def handle(self, *args, **options): satellite_id = options['satellite_id'] - days = options['days'] - marks_per_day = options['marks_per_day'] + user_id = options['user_id'] + date_range = options['date_range'] 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: satellite = Satellite.objects.get(id=satellite_id) @@ -61,16 +92,17 @@ class Command(BaseCommand): raise CommandError(f'Спутник с ID {satellite_id} не найден') # Получаем теханализы для спутника - tech_analyzes = TechAnalyze.objects.filter(satellite=satellite) - ta_count = tech_analyzes.count() + tech_analyzes = list(TechAnalyze.objects.filter(satellite=satellite)) + ta_count = len(tech_analyzes) if ta_count == 0: raise CommandError(f'Нет теханализов для спутника "{satellite.name}"') self.stdout.write(f'Спутник: {satellite.name}') self.stdout.write(f'Теханализов: {ta_count}') - self.stdout.write(f'Период: {days} дней') - self.stdout.write(f'Отметок в день: ~{marks_per_day}') + self.stdout.write(f'Пользователь: {custom_user}') + self.stdout.write(f'Период: {start_str} - {end_str} (только будние дни)') + self.stdout.write(f'Время: 8:00 - 11:00') # Удаляем существующие отметки если указан флаг if clear: @@ -81,56 +113,46 @@ class Command(BaseCommand): 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 marks_to_create = [] + workdays_count = 0 - for ta in tech_analyzes: - # Для каждого теханализа генерируем отметки - current_date = start_date - - # Начальное состояние сигнала (случайное) - signal_present = random.choice([True, False]) - - while current_date < now: - # Случайное количество отметок в этот день (от 0 до marks_per_day * 2) - day_marks = random.randint(0, marks_per_day * 2) + current_date = start_date + # Включаем конечную дату в диапазон + end_date_inclusive = end_date + timedelta(days=1) + + while current_date < end_date_inclusive: + # Проверяем, что это будний день (0=пн, 4=пт) + if current_date.weekday() < 5: + workdays_count += 1 - for _ in range(day_marks): - # Случайное время в течение дня - random_hours = random.randint(0, 23) - random_minutes = random.randint(0, 59) - mark_time = current_date.replace( - hour=random_hours, - minute=random_minutes, - second=random.randint(0, 59) - ) - - # Пропускаем если время в будущем - if mark_time > now: - continue - - # С вероятностью 70% сигнал остаётся в том же состоянии - # С вероятностью 30% меняется - if random.random() > 0.7: - signal_present = not signal_present - + # Генерируем случайное время в диапазоне 8:00-11:00 + random_hour = random.randint(8, 10) + random_minute = random.randint(0, 59) + random_second = random.randint(0, 59) + + mark_time = current_date.replace( + hour=random_hour, + minute=random_minute, + second=random_second, + microsecond=0 + ) + + # Создаём отметки для всех теханализов с одинаковым timestamp + for ta in tech_analyzes: marks_to_create.append(ObjectMark( tech_analyze=ta, - mark=signal_present, - created_by=random.choice(test_users), + mark=True, # Всегда True + timestamp=mark_time, + created_by=custom_user, )) total_marks += 1 - - current_date += timedelta(days=1) + + current_date += timedelta(days=1) # Bulk create для производительности + self.stdout.write(f'Рабочих дней: {workdays_count}') self.stdout.write(f'Создание {total_marks} отметок...') # Создаём партиями по 1000 @@ -140,65 +162,8 @@ class Command(BaseCommand): ObjectMark.objects.bulk_create(batch) 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.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 diff --git a/dbapp/mainapp/migrations/0024_change_objectmark_timestamp_editable.py b/dbapp/mainapp/migrations/0024_change_objectmark_timestamp_editable.py new file mode 100644 index 0000000..bcf9b93 --- /dev/null +++ b/dbapp/mainapp/migrations/0024_change_objectmark_timestamp_editable.py @@ -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='Старый статус'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index 531a667..c2c9cd9 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -120,10 +120,11 @@ class ObjectMark(models.Model): help_text="True - сигнал обнаружен, False - сигнал отсутствует", ) timestamp = models.DateTimeField( - auto_now_add=True, verbose_name="Время", db_index=True, help_text="Время фиксации отметки", + null=True, + blank=True, ) tech_analyze = models.ForeignKey( 'TechAnalyze', diff --git a/dbapp/mainapp/views/marks.py b/dbapp/mainapp/views/marks.py index 83a9221..1e98293 100644 --- a/dbapp/mainapp/views/marks.py +++ b/dbapp/mainapp/views/marks.py @@ -295,7 +295,7 @@ class SignalMarksEntryAPIView(LoginRequiredMixin, View): # Проверяем, можно ли добавить новую отметку (прошло 5 минут) can_add_mark = True - if last_mark: + if last_mark and last_mark.timestamp: time_diff = timezone.now() - last_mark.timestamp 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) # Проверяем, можно ли добавить отметку - last_mark = tech_analyze.marks.first() - if last_mark: + last_mark = tech_analyze.marks.order_by('-timestamp').first() + if last_mark and last_mark.timestamp: 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, + timestamp=timezone.now(), created_by=custom_user, ) created_count += 1