diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 25f311a..7e9842d 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -347,17 +347,17 @@ class ParameterInline(admin.StackedInline): class ObjectMarkAdmin(BaseAdmin): """Админ-панель для модели ObjectMark.""" - list_display = ("source", "mark", "timestamp", "created_by") - list_select_related = ("source", "created_by__user") - search_fields = ("source__id",) + list_display = ("tech_analyze", "mark", "timestamp", "created_by") + list_select_related = ("tech_analyze", "tech_analyze__satellite", "created_by__user") + search_fields = ("tech_analyze__name", "tech_analyze__id") ordering = ("-timestamp",) list_filter = ( "mark", ("timestamp", DateRangeQuickSelectListFilterBuilder()), - ("source", MultiSelectRelatedDropdownFilter), + ("tech_analyze__satellite", MultiSelectRelatedDropdownFilter), ) readonly_fields = ("timestamp", "created_by") - autocomplete_fields = ("source",) + autocomplete_fields = ("tech_analyze",) # @admin.register(SigmaParMark) diff --git a/dbapp/mainapp/management/__init__.py b/dbapp/mainapp/management/__init__.py new file mode 100644 index 0000000..65cb83a --- /dev/null +++ b/dbapp/mainapp/management/__init__.py @@ -0,0 +1 @@ +# Management commands package diff --git a/dbapp/mainapp/management/commands/__init__.py b/dbapp/mainapp/management/commands/__init__.py new file mode 100644 index 0000000..b5a3a84 --- /dev/null +++ b/dbapp/mainapp/management/commands/__init__.py @@ -0,0 +1 @@ +# Commands package diff --git a/dbapp/mainapp/management/commands/generate_test_marks.py b/dbapp/mainapp/management/commands/generate_test_marks.py new file mode 100644 index 0000000..f34914a --- /dev/null +++ b/dbapp/mainapp/management/commands/generate_test_marks.py @@ -0,0 +1,204 @@ +""" +Management command для генерации тестовых отметок сигналов. + +Использование: + python manage.py generate_test_marks --satellite_id=1 --days=90 --marks_per_day=5 + +Параметры: + --satellite_id: ID спутника (обязательный) + --days: Количество дней для генерации (по умолчанию 90) + --marks_per_day: Количество отметок в день (по умолчанию 3) + --clear: Удалить существующие отметки перед генерацией +""" + +import random +from datetime import timedelta + +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +from mainapp.models import TechAnalyze, ObjectMark, Satellite, CustomUser + + +class Command(BaseCommand): + help = 'Генерирует тестовые отметки сигналов для теханализов выбранного спутника' + + def add_arguments(self, parser): + parser.add_argument( + '--satellite_id', + type=int, + required=True, + help='ID спутника для генерации отметок' + ) + parser.add_argument( + '--days', + type=int, + default=90, + help='Количество дней для генерации (по умолчанию 90)' + ) + parser.add_argument( + '--marks_per_day', + type=int, + default=3, + help='Среднее количество отметок в день на теханализ (по умолчанию 3)' + ) + parser.add_argument( + '--clear', + action='store_true', + help='Удалить существующие отметки перед генерацией' + ) + + def handle(self, *args, **options): + satellite_id = options['satellite_id'] + days = options['days'] + marks_per_day = options['marks_per_day'] + clear = options['clear'] + + # Проверяем существование спутника + try: + satellite = Satellite.objects.get(id=satellite_id) + except Satellite.DoesNotExist: + raise CommandError(f'Спутник с ID {satellite_id} не найден') + + # Получаем теханализы для спутника + tech_analyzes = TechAnalyze.objects.filter(satellite=satellite) + ta_count = tech_analyzes.count() + + 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}') + + # Удаляем существующие отметки если указан флаг + if clear: + deleted_count = ObjectMark.objects.filter( + tech_analyze__satellite=satellite + ).delete()[0] + self.stdout.write( + 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 = [] + + 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) + + 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 + + marks_to_create.append(ObjectMark( + tech_analyze=ta, + mark=signal_present, + created_by=random.choice(test_users), + )) + total_marks += 1 + + current_date += timedelta(days=1) + + # Bulk create для производительности + self.stdout.write(f'Создание {total_marks} отметок...') + + # Создаём партиями по 1000 + batch_size = 1000 + for i in range(0, len(marks_to_create), batch_size): + batch = marks_to_create[i:i + batch_size] + 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} теханализов' + ) + ) + + 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/0022_change_objectmark_to_techanalyze.py b/dbapp/mainapp/migrations/0022_change_objectmark_to_techanalyze.py new file mode 100644 index 0000000..0247325 --- /dev/null +++ b/dbapp/mainapp/migrations/0022_change_objectmark_to_techanalyze.py @@ -0,0 +1,73 @@ +""" +Миграция для изменения модели ObjectMark: +- Удаление всех существующих отметок +- Удаление поля source +- Добавление поля tech_analyze +""" + +from django.db import migrations, models +import django.db.models.deletion + + +def delete_all_marks(apps, schema_editor): + """Удаляем все существующие отметки перед изменением структуры.""" + ObjectMark = apps.get_model('mainapp', 'ObjectMark') + count = ObjectMark.objects.count() + ObjectMark.objects.all().delete() + print(f"Удалено {count} отметок ObjectMark") + + +def noop(apps, schema_editor): + """Обратная операция - ничего не делаем.""" + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0021_add_source_request_fields'), + ] + + operations = [ + # Сначала удаляем все отметки + migrations.RunPython(delete_all_marks, noop), + + # Удаляем старое поле source + migrations.RemoveField( + model_name='objectmark', + name='source', + ), + + # Добавляем новое поле tech_analyze + migrations.AddField( + model_name='objectmark', + name='tech_analyze', + field=models.ForeignKey( + help_text='Связанный технический анализ', + on_delete=django.db.models.deletion.CASCADE, + related_name='marks', + to='mainapp.techanalyze', + verbose_name='Тех. анализ', + ), + preserve_default=False, + ), + + # Обновляем метаданные модели + migrations.AlterModelOptions( + name='objectmark', + options={ + 'ordering': ['-timestamp'], + 'verbose_name': 'Отметка сигнала', + 'verbose_name_plural': 'Отметки сигналов' + }, + ), + + # Добавляем индекс для оптимизации запросов + migrations.AddIndex( + model_name='objectmark', + index=models.Index( + fields=['tech_analyze', '-timestamp'], + name='mainapp_obj_tech_an_idx' + ), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index ce631eb..fde75e6 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -106,17 +106,18 @@ class ObjectOwnership(models.Model): class ObjectMark(models.Model): """ - Модель отметки о наличии объекта. + Модель отметки о наличии сигнала. - Используется для фиксации моментов времени когда объект был обнаружен или отсутствовал. + Используется для фиксации моментов времени когда сигнал был обнаружен или отсутствовал. + Привязывается к записям технического анализа (TechAnalyze). """ # Основные поля mark = models.BooleanField( null=True, blank=True, - verbose_name="Наличие объекта", - help_text="True - объект обнаружен, False - объект отсутствует", + verbose_name="Наличие сигнала", + help_text="True - сигнал обнаружен, False - сигнал отсутствует", ) timestamp = models.DateTimeField( auto_now_add=True, @@ -124,12 +125,12 @@ class ObjectMark(models.Model): db_index=True, help_text="Время фиксации отметки", ) - source = models.ForeignKey( - 'Source', + tech_analyze = models.ForeignKey( + 'TechAnalyze', on_delete=models.CASCADE, related_name="marks", - verbose_name="Источник", - help_text="Связанный источник", + verbose_name="Тех. анализ", + help_text="Связанный технический анализ", ) created_by = models.ForeignKey( CustomUser, @@ -160,13 +161,18 @@ class ObjectMark(models.Model): def __str__(self): if self.timestamp: timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") - return f"+ {timestamp}" if self.mark else f"- {timestamp}" + tech_name = self.tech_analyze.name if self.tech_analyze else "?" + mark_str = "+" if self.mark else "-" + return f"{tech_name}: {mark_str} {timestamp}" return "Отметка без времени" class Meta: - verbose_name = "Отметка источника" - verbose_name_plural = "Отметки источников" + verbose_name = "Отметка сигнала" + verbose_name_plural = "Отметки сигналов" ordering = ["-timestamp"] + indexes = [ + models.Index(fields=["tech_analyze", "-timestamp"]), + ] # Для обратной совместимости с SigmaParameter @@ -737,16 +743,6 @@ class Source(models.Model): if last_objitem: self.confirm_at = last_objitem.created_at - def update_last_signal_at(self): - """ - Обновляет дату last_signal_at на дату последней отметки о наличии сигнала (mark=True). - """ - last_signal_mark = self.marks.filter(mark=True).order_by('-timestamp').first() - if last_signal_mark: - self.last_signal_at = last_signal_mark.timestamp - else: - self.last_signal_at = None - def save(self, *args, **kwargs): """ Переопределенный метод save для автоматического обновления coords_average diff --git a/dbapp/mainapp/templates/mainapp/components/_navbar.html b/dbapp/mainapp/templates/mainapp/components/_navbar.html index c9156cb..ced4840 100644 --- a/dbapp/mainapp/templates/mainapp/components/_navbar.html +++ b/dbapp/mainapp/templates/mainapp/components/_navbar.html @@ -35,7 +35,7 @@ Действия