From 480bb60855f1b514135c9ac0ce73f66fc1dfec0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Mon, 15 Dec 2025 15:51:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B1=D0=B8=D0=BB=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B8=D0=BA=20models.py=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0026_alter_userpermission_code.py | 18 + dbapp/mainapp/models.py | 1654 ----------------- dbapp/mainapp/models/__init__.py | 70 + dbapp/mainapp/models/defaults.py | 27 + dbapp/mainapp/models/errors_report.py | 9 + dbapp/mainapp/models/geo.py | 92 + dbapp/mainapp/models/objitem.py | 148 ++ dbapp/mainapp/models/parameters.py | 271 +++ dbapp/mainapp/models/references.py | 136 ++ dbapp/mainapp/models/requests.py | 298 +++ dbapp/mainapp/models/satellite.py | 122 ++ dbapp/mainapp/models/source.py | 229 +++ dbapp/mainapp/models/tech_analyze.py | 200 ++ dbapp/mainapp/models/users.py | 103 + 14 files changed, 1723 insertions(+), 1654 deletions(-) create mode 100644 dbapp/mainapp/migrations/0026_alter_userpermission_code.py delete mode 100644 dbapp/mainapp/models.py create mode 100644 dbapp/mainapp/models/__init__.py create mode 100644 dbapp/mainapp/models/defaults.py create mode 100644 dbapp/mainapp/models/errors_report.py create mode 100644 dbapp/mainapp/models/geo.py create mode 100644 dbapp/mainapp/models/objitem.py create mode 100644 dbapp/mainapp/models/parameters.py create mode 100644 dbapp/mainapp/models/references.py create mode 100644 dbapp/mainapp/models/requests.py create mode 100644 dbapp/mainapp/models/satellite.py create mode 100644 dbapp/mainapp/models/source.py create mode 100644 dbapp/mainapp/models/tech_analyze.py create mode 100644 dbapp/mainapp/models/users.py diff --git a/dbapp/mainapp/migrations/0026_alter_userpermission_code.py b/dbapp/mainapp/migrations/0026_alter_userpermission_code.py new file mode 100644 index 0000000..bea5e20 --- /dev/null +++ b/dbapp/mainapp/migrations/0026_alter_userpermission_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-12-15 11:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0025_add_user_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='userpermission', + name='code', + field=models.CharField(db_index=True, help_text='Уникальный код разрешения', max_length=50, verbose_name='Код разрешения'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py deleted file mode 100644 index de3950a..0000000 --- a/dbapp/mainapp/models.py +++ /dev/null @@ -1,1654 +0,0 @@ -# Django imports -from django.contrib.auth.models import User -from django.contrib.gis.db import models as gis -from django.contrib.gis.db.models import functions -from django.core.exceptions import ValidationError -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models -from django.db.models import ExpressionWrapper, F -from django.utils import timezone - - -def get_default_polarization(): - obj, created = Polarization.objects.get_or_create(name="-") - return obj.id - - -def get_default_modulation(): - obj, created = Modulation.objects.get_or_create(name="-") - return obj.id - - -def get_default_standard(): - obj, created = Standard.objects.get_or_create(name="-") - return obj.id - - -def get_permission_choices(): - """Ленивая загрузка choices для избежания циклического импорта.""" - from .permissions import PERMISSION_CHOICES - return PERMISSION_CHOICES - - -class UserPermission(models.Model): - """ - Модель разрешения пользователя. - - Хранит гранулярные разрешения для конкретных действий в системе. - """ - - code = models.CharField( - max_length=50, - verbose_name="Код разрешения", - db_index=True, - help_text="Уникальный код разрешения", - ) - - def __str__(self): - from .permissions import PERMISSION_CHOICES - choices_dict = dict(PERMISSION_CHOICES) - return choices_dict.get(self.code, self.code) - - class Meta: - verbose_name = "Разрешение" - verbose_name_plural = "Разрешения" - ordering = ["code"] - - -class CustomUser(models.Model): - """ - Расширенная модель пользователя с ролями. - - Добавляет систему ролей к стандартной модели User Django. - """ - - ROLE_CHOICES = [ - ("admin", "Администратор"), - ("moderator", "Модератор"), - ("user", "Пользователь"), - ] - - # Связи - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - verbose_name="Пользователь", - help_text="Связанный пользователь Django", - ) - - # Основные поля - role = models.CharField( - max_length=20, - choices=ROLE_CHOICES, - default="user", - verbose_name="Роль пользователя", - db_index=True, - help_text="Роль пользователя в системе", - ) - - # Индивидуальные разрешения (если пусто - используются права роли по умолчанию) - user_permissions = models.ManyToManyField( - UserPermission, - related_name="users", - verbose_name="Индивидуальные разрешения", - blank=True, - help_text="Если указаны - используются вместо прав роли по умолчанию", - ) - - # Флаг использования индивидуальных разрешений - use_custom_permissions = models.BooleanField( - default=False, - verbose_name="Использовать индивидуальные разрешения", - help_text="Если включено - используются индивидуальные разрешения вместо прав роли", - ) - - def __str__(self): - return ( - f"{self.user.first_name} {self.user.last_name}" - if self.user.first_name and self.user.last_name - else self.user.username - ) - - def has_perm(self, permission_code): - """ - Проверяет наличие разрешения у пользователя. - - Args: - permission_code: Код разрешения - - Returns: - bool: True если пользователь имеет разрешение - """ - from .permissions import has_permission - return has_permission(self.user, permission_code) - - class Meta: - verbose_name = "Пользователь" - verbose_name_plural = "Пользователи" - ordering = ["user__username"] - -class ObjectInfo(models.Model): - name = models.CharField( - max_length=255, - unique=True, - verbose_name="Тип объекта", - help_text="Информация о типе объекта", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Тип объекта" - verbose_name_plural = "Типы объектов" - ordering = ["name"] - - -class ObjectOwnership(models.Model): - """ - Модель принадлежности объекта. - """ - name = models.CharField( - max_length=255, - unique=True, - verbose_name="Принадлежность", - help_text="Принадлежность объекта", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Принадлежность объекта" - verbose_name_plural = "Принадлежности объектов" - ordering = ["name"] - - -class ObjectMark(models.Model): - """ - Модель отметки о наличии сигнала. - - Используется для фиксации моментов времени когда сигнал был обнаружен или отсутствовал. - Привязывается к записям технического анализа (TechAnalyze). - """ - - # Основные поля - mark = models.BooleanField( - null=True, - blank=True, - verbose_name="Наличие сигнала", - help_text="True - сигнал обнаружен, False - сигнал отсутствует", - ) - timestamp = models.DateTimeField( - verbose_name="Время", - db_index=True, - help_text="Время фиксации отметки", - null=True, - blank=True, - ) - tech_analyze = models.ForeignKey( - 'TechAnalyze', - on_delete=models.CASCADE, - related_name="marks", - verbose_name="Тех. анализ", - help_text="Связанный технический анализ", - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="marks_created", - null=True, - blank=True, - verbose_name="Создан пользователем", - help_text="Пользователь, создавший отметку", - ) - - def can_edit(self): - """Проверка возможности редактирования отметки (в течение 5 минут)""" - from datetime import timedelta - if not self.timestamp: - return False - time_diff = timezone.now() - self.timestamp - return time_diff < timedelta(minutes=5) - - def can_add_new_mark_for_object(self): - """Проверка возможности добавления новой отметки для объекта (прошло 5 минут с последней)""" - from datetime import timedelta - if not self.timestamp: - return True - time_diff = timezone.now() - self.timestamp - return time_diff >= timedelta(minutes=5) - - def __str__(self): - if self.timestamp: - timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") - 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 = "Отметки сигналов" - ordering = ["-timestamp"] - indexes = [ - models.Index(fields=["tech_analyze", "-timestamp"]), - ] - - -# Для обратной совместимости с SigmaParameter -# class SigmaParMark(models.Model): -# """ -# Модель отметки о наличии сигнала (для Sigma). - -# Используется для фиксации моментов времени когда сигнал был обнаружен или потерян. -# """ - -# # Основные поля -# mark = models.BooleanField( -# null=True, -# blank=True, -# verbose_name="Наличие сигнала", -# help_text="True - сигнал обнаружен, False - сигнал отсутствует", -# ) -# timestamp = models.DateTimeField( -# null=True, -# blank=True, -# verbose_name="Время", -# db_index=True, -# help_text="Время фиксации отметки", -# ) - -# def __str__(self): -# if self.timestamp: -# timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") -# return f"+ {timestamp}" if self.mark else f"- {timestamp}" -# return "Отметка без времени" - -# class Meta: -# verbose_name = "Отметка сигнала" -# verbose_name_plural = "Отметки сигналов" -# ordering = ["-timestamp"] - -class Polarization(models.Model): - """ - Модель поляризации сигнала. - - Определяет тип поляризации спутникового сигнала (H, V, L, R и т.д.). - """ - - # Основные поля - name = models.CharField( - max_length=20, - unique=True, - verbose_name="Поляризация", - db_index=True, - help_text="Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Поляризация" - verbose_name_plural = "Поляризация" - ordering = ["name"] - - -class Modulation(models.Model): - """ - Модель типа модуляции сигнала. - - Определяет схему модуляции (QPSK, 8PSK, 16APSK и т.д.). - """ - - # Основные поля - name = models.CharField( - max_length=20, - unique=True, - verbose_name="Модуляция", - db_index=True, - help_text="Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Модуляция" - verbose_name_plural = "Модуляции" - ordering = ["name"] - - -class Standard(models.Model): - """ - Модель стандарта передачи данных. - - Определяет стандарт передачи (DVB-S, DVB-S2, DVB-S2X и т.д.). - """ - - # Основные поля - name = models.CharField( - max_length=80, - unique=True, - verbose_name="Стандарт", - db_index=True, - help_text="Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Стандарт" - verbose_name_plural = "Стандарты" - ordering = ["name"] - - -class Band(models.Model): - name = models.CharField( - max_length=50, - unique=True, - verbose_name="Название", - help_text="Название диапазона", - ) - border_start = models.FloatField( - blank=True, null=True, verbose_name="Нижняя граница диапазона, МГц" - ) - border_end = models.FloatField( - blank=True, null=True, verbose_name="Верхняя граница диапазона, МГц" - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Диапазон" - verbose_name_plural = "Диапазоны" - ordering = ["name"] - - -class Satellite(models.Model): - """ - Модель спутника. - - Представляет спутник связи с его основными характеристиками. - """ - PLACES = [ - ("kr", "КР"), - ("dv", "ДВ") - ] - # Основные поля - name = models.CharField( - max_length=100, - unique=True, - verbose_name="Имя спутника", - db_index=True, - help_text="Название спутника", - ) - alternative_name = models.CharField( - max_length=100, - blank=True, - null=True, - verbose_name="Альтернативное имя", - db_index=True, - help_text="Альтернативное название спутника", - ) - location_place = models.CharField( - max_length=30, - choices=PLACES, - null=True, - default="kr", - verbose_name="Комплекс", - help_text="К какому комплексу принадлежит спутник", - ) - norad = models.IntegerField( - blank=True, - null=True, - verbose_name="NORAD ID", - help_text="Идентификатор NORAD для отслеживания спутника", - ) - international_code = models.CharField( - max_length=50, - blank=True, - null=True, - verbose_name="Международный код", - help_text="Международный идентификатор спутника (например, 2011-074A)", - ) - band = models.ManyToManyField( - Band, - related_name="bands", - verbose_name="Диапазоны", - blank=True, - help_text="Диапазоны работы спутника", - ) - undersat_point = models.FloatField( - blank=True, - null=True, - verbose_name="Подспутниковая точка, градусы", - help_text="Подспутниковая точка в градусах. Восточное полушарие с +, западное с -", - ) - url = models.URLField( - blank=True, - null=True, - verbose_name="Ссылка на источник", - help_text="Ссылка на сайт, где можно проверить информацию", - ) - comment = models.TextField( - blank=True, - null=True, - verbose_name="Комментарий", - help_text="Любой возможный комменатрий", - ) - launch_date = models.DateField( - blank=True, - null=True, - verbose_name="Дата запуска", - help_text="Дата запуска спутника", - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="Дата создания", - help_text="Дата и время создания записи", - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="satellite_created", - null=True, - blank=True, - verbose_name="Создан пользователем", - help_text="Пользователь, создавший запись", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего изменения", - help_text="Дата и время последнего изменения", - ) - updated_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="satellite_updated", - null=True, - blank=True, - verbose_name="Изменен пользователем", - help_text="Пользователь, последним изменивший запись", - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = "Спутник" - verbose_name_plural = "Спутники" - ordering = ["name"] - - -class ObjItemQuerySet(models.QuerySet): - """Custom QuerySet для модели ObjItem с оптимизированными запросами""" - - def with_related(self): - """Оптимизирует запросы, загружая связанные объекты""" - return self.select_related( - "geo_obj", - "updated_by__user", - "created_by__user", - "lyngsat_source", - "parameter_obj", - "parameter_obj__id_satellite", - "parameter_obj__polarization", - "parameter_obj__modulation", - "parameter_obj__standard", - ) - - def recent(self, days=30): - """Возвращает объекты, созданные за последние N дней""" - from datetime import timedelta - - cutoff_date = timezone.now() - timedelta(days=days) - return self.filter(created_at__gte=cutoff_date) - - def by_user(self, user): - """Возвращает объекты, созданные указанным пользователем""" - return self.filter(created_by=user) - - -class ObjItemManager(models.Manager): - """Custom Manager для модели ObjItem""" - - def get_queryset(self): - return ObjItemQuerySet(self.model, using=self._db) - - def with_related(self): - """Возвращает queryset с предзагруженными связанными объектами""" - return self.get_queryset().with_related() - - def recent(self, days=30): - """Возвращает недавно созданные объекты""" - return self.get_queryset().recent(days) - - def by_user(self, user): - """Возвращает объекты пользователя""" - return self.get_queryset().by_user(user) - - -class TechAnalyze(models.Model): - """ - Модель технического анализа сигнала. - - Хранит информацию о технических параметрах сигнала для анализа. - """ - - # Основные поля - name = models.CharField( - max_length=255, - unique=True, - verbose_name="Имя", - db_index=True, - help_text="Уникальное название для технического анализа", - ) - satellite = models.ForeignKey( - Satellite, - on_delete=models.PROTECT, - related_name="tech_analyzes", - verbose_name="Спутник", - help_text="Спутник, к которому относится анализ", - ) - polarization = models.ForeignKey( - Polarization, - default=get_default_polarization, - on_delete=models.SET_DEFAULT, - related_name="tech_analyze_polarizations", - null=True, - blank=True, - verbose_name="Поляризация", - ) - frequency = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Частота, МГц", - db_index=True, - help_text="Центральная частота сигнала", - ) - freq_range = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Полоса частот, МГц", - help_text="Полоса частот сигнала", - ) - bod_velocity = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Символьная скорость, БОД", - help_text="Символьная скорость", - ) - modulation = models.ForeignKey( - Modulation, - default=get_default_modulation, - on_delete=models.SET_DEFAULT, - related_name="tech_analyze_modulations", - null=True, - blank=True, - verbose_name="Модуляция", - ) - standard = models.ForeignKey( - Standard, - default=get_default_standard, - on_delete=models.SET_DEFAULT, - related_name="tech_analyze_standards", - null=True, - blank=True, - verbose_name="Стандарт", - ) - note = models.TextField( - null=True, - blank=True, - verbose_name="Примечание", - help_text="Дополнительные примечания", - ) - - # Метаданные - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="Дата создания", - help_text="Дата и время создания записи", - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="tech_analyze_created", - null=True, - blank=True, - verbose_name="Создан пользователем", - help_text="Пользователь, создавший запись", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего изменения", - help_text="Дата и время последнего изменения", - ) - updated_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="tech_analyze_updated", - null=True, - blank=True, - verbose_name="Изменен пользователем", - help_text="Пользователь, последним изменивший запись", - ) - - def __str__(self): - return f"{self.name} ({self.satellite.name if self.satellite else '-'})" - - class Meta: - verbose_name = "Тех. анализ" - verbose_name_plural = "Тех. анализы" - ordering = ["-created_at"] - - -class Source(models.Model): - """ - Модель источника сигнала. - """ - - info = models.ForeignKey( - ObjectInfo, - on_delete=models.SET_NULL, - related_name="source_info", - null=True, - blank=True, - verbose_name="Тип объекта", - help_text="Тип объекта", - ) - ownership = models.ForeignKey( - 'ObjectOwnership', - on_delete=models.SET_NULL, - related_name="source_ownership", - null=True, - blank=True, - verbose_name="Принадлежность объекта", - help_text="Принадлежность объекта (страна, организация и т.д.)", - ) - note = models.TextField( - null=True, - blank=True, - verbose_name="Примечание", - help_text="Дополнительное описание объекта", - ) - confirm_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="Дата подтверждения", - help_text="Дата и время добавления последней полученной точки ГЛ", - ) - last_signal_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="Последний сигнал", - help_text="Дата и время последней отметки о наличии сигнала", - ) - - coords_average = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты ГЛ", - help_text="Усреднённые координаты, полученные от в ходе геолокации (WGS84)", - ) - coords_kupsat = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты Кубсата", - help_text="Координаты, полученные от кубсата (WGS84)", - ) - coords_valid = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты оперативников", - help_text="Координаты, предоставленные оперативным отделом (WGS84)", - ) - coords_reference = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координаты справочные", - help_text="Координаты, ещё кем-то проверенные (WGS84)", - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="Дата создания", - help_text="Дата и время создания записи", - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="source_created", - null=True, - blank=True, - verbose_name="Создан пользователем", - help_text="Пользователь, создавший запись", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего изменения", - help_text="Дата и время последнего изменения", - ) - updated_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="source_updated", - null=True, - blank=True, - verbose_name="Изменен пользователем", - help_text="Пользователь, последним изменивший запись", - ) - - def update_coords_average(self, new_coord_tuple): - """ - Обновляет coords_average в зависимости от типа объекта (info). - - Логика: - - Если info == "Подвижные": coords_average = последняя добавленная координата - - Иначе (Стационарные и др.): coords_average = инкрементальное среднее - - Args: - new_coord_tuple: кортеж (longitude, latitude) новой координаты - """ - from django.contrib.gis.geos import Point - from .utils import calculate_mean_coords - - # Если тип объекта "Подвижные" - просто устанавливаем последнюю координату - if self.info and self.info.name == "Подвижные": - self.coords_average = Point(new_coord_tuple, srid=4326) - else: - # Для стационарных объектов - вычисляем среднее - if self.coords_average: - # Есть предыдущее среднее - вычисляем новое среднее - current_coord = (self.coords_average.x, self.coords_average.y) - new_avg, _ = calculate_mean_coords(current_coord, new_coord_tuple) - self.coords_average = Point(new_avg, srid=4326) - else: - # Первая координата - просто устанавливаем её - self.coords_average = Point(new_coord_tuple, srid=4326) - - def get_last_geo_coords(self): - """ - Получает координаты последней добавленной точки ГЛ для этого источника. - Сортировка по ID (последняя добавленная в базу). - - Returns: - tuple: (longitude, latitude) или None если точек нет - """ - # Получаем последний ObjItem для этого Source (по ID) - last_objitem = self.source_objitems.filter( - geo_obj__coords__isnull=False - ).select_related('geo_obj').order_by('-id').first() - - if last_objitem and last_objitem.geo_obj and last_objitem.geo_obj.coords: - return (last_objitem.geo_obj.coords.x, last_objitem.geo_obj.coords.y) - - return None - - def update_confirm_at(self): - """ - Обновляет дату confirm_at на дату создания последней добавленной точки ГЛ. - """ - last_objitem = self.source_objitems.order_by('-created_at').first() - if last_objitem: - self.confirm_at = last_objitem.created_at - - def save(self, *args, **kwargs): - """ - Переопределенный метод save для автоматического обновления coords_average - при изменении типа объекта. - """ - from django.contrib.gis.geos import Point - - # Проверяем, изменился ли тип объекта - if self.pk: # Объект уже существует - try: - old_instance = Source.objects.get(pk=self.pk) - old_info = old_instance.info - new_info = self.info - - # Если тип изменился на "Подвижные" - if new_info and new_info.name == "Подвижные" and (not old_info or old_info.name != "Подвижные"): - # Устанавливаем координату последней точки - last_coords = self.get_last_geo_coords() - if last_coords: - self.coords_average = Point(last_coords, srid=4326) - - # Если тип изменился с "Подвижные" на что-то другое - elif old_info and old_info.name == "Подвижные" and (not new_info or new_info.name != "Подвижные"): - # Пересчитываем среднюю координату по всем точкам - self._recalculate_average_coords() - - except Source.DoesNotExist: - pass - - super().save(*args, **kwargs) - - def _recalculate_average_coords(self): - """ - Пересчитывает среднюю координату по всем точкам источника. - Используется при переключении с "Подвижные" на "Стационарные". - - Сортировка по ID (порядок добавления в базу), инкрементальное усреднение - как в функциях импорта. - """ - from django.contrib.gis.geos import Point - from .utils import calculate_mean_coords - - # Получаем все точки для этого источника, сортируем по ID (порядок добавления) - objitems = self.source_objitems.filter( - geo_obj__coords__isnull=False - ).select_related('geo_obj').order_by('id') - - if not objitems.exists(): - return - - # Вычисляем среднюю координату инкрементально (как в функциях импорта) - coords_average = None - for objitem in objitems: - if objitem.geo_obj and objitem.geo_obj.coords: - coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y) - if coords_average is None: - # Первая точка - просто устанавливаем её - coords_average = coord - else: - # Последующие точки - вычисляем среднее между текущим средним и новой точкой - coords_average, _ = calculate_mean_coords(coords_average, coord) - - if coords_average: - self.coords_average = Point(coords_average, srid=4326) - - class Meta: - verbose_name = "Источник" - verbose_name_plural = "Источники" - - -class ObjItem(models.Model): - """ - Модель точки ГЛ. - - Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации. - """ - - # Основные поля - name = models.CharField( - null=True, - blank=True, - max_length=100, - verbose_name="Имя объекта", - db_index=True, - help_text="Название объекта/источника сигнала", - ) - source = models.ForeignKey( - Source, - on_delete=models.CASCADE, - null=True, - verbose_name="ИРИ", - related_name="source_objitems", - ) - transponder = models.ForeignKey( - "mapsapp.Transponders", - on_delete=models.SET_NULL, - related_name="transponder_objitems", - null=True, - blank=True, - verbose_name="Транспондер", - help_text="Транспондер, с помощью которого была получена точка", - ) - is_automatic = models.BooleanField( - default=False, - verbose_name="Автоматическая", - db_index=True, - help_text="Если True, точка не добавляется к объектам (Source), а хранится отдельно", - ) - - # Метаданные - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="Дата создания", - help_text="Дата и время создания записи", - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="objitems_created", - null=True, - blank=True, - verbose_name="Создан пользователем", - help_text="Пользователь, создавший запись", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего изменения", - help_text="Дата и время последнего изменения", - ) - updated_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name="objitems_updated", - null=True, - blank=True, - verbose_name="Изменен пользователем", - help_text="Пользователь, последним изменивший запись", - ) - lyngsat_source = models.ForeignKey( - "lyngsatapp.LyngSat", - on_delete=models.SET_NULL, - related_name="objitems", - null=True, - blank=True, - verbose_name="Источник LyngSat", - help_text="Связанный источник из базы LyngSat (ТВ)", - ) - - # Custom manager - objects = ObjItemManager() - - def __str__(self): - return f"Объект {self.name}" if self.name else f"Объект #{self.pk}" - - class Meta: - verbose_name = "Объект" - verbose_name_plural = "Объекты" - ordering = ["-updated_at"] - indexes = [ - models.Index(fields=["name"]), - models.Index(fields=["-updated_at"]), - models.Index(fields=["-created_at"]), - ] - - -class Parameter(models.Model): - id_satellite = models.ForeignKey( - Satellite, - on_delete=models.PROTECT, - related_name="parameters", - verbose_name="Спутник", - null=True, - ) - polarization = models.ForeignKey( - Polarization, - default=get_default_polarization, - on_delete=models.SET_DEFAULT, - related_name="polarizations", - null=True, - blank=True, - verbose_name="Поляризация", - ) - frequency = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Частота, МГц", - db_index=True, - # validators=[MinValueValidator(0), MaxValueValidator(50000)], - help_text="Центральная частота сигнала", - ) - freq_range = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Полоса частот, МГц", - # validators=[MinValueValidator(0), MaxValueValidator(1000)], - help_text="Полоса частот сигнала", - ) - bod_velocity = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Символьная скорость, БОД", - # validators=[MinValueValidator(0)], - help_text="Символьная скорость должна быть положительной", - ) - modulation = models.ForeignKey( - Modulation, - default=get_default_modulation, - on_delete=models.SET_DEFAULT, - related_name="modulations", - null=True, - blank=True, - verbose_name="Модуляция", - ) - snr = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="ОСШ", - # validators=[MinValueValidator(-50), MaxValueValidator(100)], - help_text="Отношение сигнал/шум", - ) - standard = models.ForeignKey( - Standard, - default=get_default_standard, - on_delete=models.SET_DEFAULT, - related_name="standards", - null=True, - blank=True, - verbose_name="Стандарт", - ) - # id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True) - objitem = models.OneToOneField( - ObjItem, - on_delete=models.CASCADE, - related_name="parameter_obj", - verbose_name="Объект", - null=True, - blank=True, - help_text="Связанный объект", - ) - # id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True) - # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True) - - def clean(self): - """Валидация на уровне модели""" - super().clean() - - # Проверка что частота больше полосы частот - if self.frequency and self.freq_range: - if self.freq_range > self.frequency: - raise ValidationError( - {"freq_range": "Полоса частот не может быть больше частоты"} - ) - - # Проверка что символьная скорость соответствует полосе частот - if self.bod_velocity and self.freq_range: - if self.bod_velocity > self.freq_range * 1000000: # Конвертация МГц в Гц - raise ValidationError( - { - "bod_velocity": "Символьная скорость не может превышать полосу частот" - } - ) - - def __str__(self): - polarization_name = self.polarization.name if self.polarization else "-" - modulation_name = self.modulation.name if self.modulation else "-" - return f"Источник-{self.frequency}:{self.freq_range} МГц:{polarization_name}:{modulation_name}" - - class Meta: - verbose_name = "ВЧ загрузка" - verbose_name_plural = "ВЧ загрузки" - indexes = [ - models.Index(fields=["id_satellite", "frequency"]), - models.Index(fields=["frequency", "polarization"]), - ] - # constraints = [ - # models.UniqueConstraint( - # fields=[ - # 'polarization', 'frequency', 'freq_range', - # 'bod_velocity', 'modulation', 'snr', 'standard' - # ], - # name='unique_parameter_combination' - # ) - # ] - - -class SigmaParameter(models.Model): - TRANSFERS = [(-1.0, "-"), (9750.0, "9750 МГц"), (10750.0, "10750 МГц")] - - id_satellite = models.ForeignKey( - Satellite, - on_delete=models.PROTECT, - related_name="sigmapar_sat", - verbose_name="Спутник", - ) - transfer = models.FloatField( - choices=TRANSFERS, - default=-1.0, - verbose_name="Перенос по частоте", - help_text="Выберите перенос по частоте", - ) - status = models.CharField( - max_length=20, - blank=True, - null=True, - verbose_name="Статус", - help_text="Статус измерения", - ) - frequency = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Частота, МГц", - db_index=True, - # validators=[MinValueValidator(0), MaxValueValidator(50000)], - help_text="Центральная частота сигнала", - ) - transfer_frequency = models.GeneratedField( - expression=ExpressionWrapper( - F("frequency") + F("transfer"), output_field=models.FloatField() - ), - output_field=models.FloatField(), - db_persist=True, - null=True, - blank=True, - verbose_name="Частота в Ku, МГц", - ) - freq_range = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Полоса частот, МГц", - # validators=[MinValueValidator(0), MaxValueValidator(1000)], - help_text="Полоса частот", - ) - power = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Мощность, дБм", - # validators=[MinValueValidator(-100), MaxValueValidator(100)], - help_text="Мощность сигнала", - ) - bod_velocity = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="Символьная скорость, БОД", - # validators=[MinValueValidator(0)], - help_text="Символьная скорость должна быть положительной", - ) - polarization = models.ForeignKey( - Polarization, - default=get_default_polarization, - on_delete=models.SET_DEFAULT, - related_name="polarizations_sigma", - null=True, - blank=True, - verbose_name="Поляризация", - ) - modulation = models.ForeignKey( - Modulation, - default=get_default_modulation, - on_delete=models.SET_DEFAULT, - related_name="modulations_sigma", - null=True, - blank=True, - verbose_name="Модуляция", - ) - snr = models.FloatField( - default=0, - null=True, - blank=True, - verbose_name="ОСШ, Дб", - validators=[MinValueValidator(-50), MaxValueValidator(100)], - help_text="Отношение сигнал/шум в диапазоне от -50 до 100 дБ", - ) - standard = models.ForeignKey( - Standard, - default=get_default_standard, - on_delete=models.SET_DEFAULT, - related_name="standards_sigma", - null=True, - blank=True, - verbose_name="Стандарт", - ) - packets = models.BooleanField( - null=True, - blank=True, - verbose_name="Пакетность", - help_text="Наличие пакетной передачи", - ) - datetime_begin = models.DateTimeField( - null=True, - blank=True, - verbose_name="Время начала измерения", - help_text="Дата и время начала измерения", - ) - datetime_end = models.DateTimeField( - null=True, - blank=True, - verbose_name="Время окончания измерения", - help_text="Дата и время окончания измерения", - ) - # mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True) - parameter = models.ForeignKey( - Parameter, - on_delete=models.SET_NULL, - related_name="sigma_parameter", - verbose_name="ВЧ", - null=True, - blank=True, - ) - - def clean(self): - """Валидация на уровне модели""" - super().clean() - - # Проверка что время окончания больше времени начала - if self.datetime_begin and self.datetime_end: - if self.datetime_end < self.datetime_begin: - raise ValidationError( - {"datetime_end": "Время окончания должно быть позже времени начала"} - ) - - # Проверка что частота больше полосы частот - if self.frequency and self.freq_range: - if self.freq_range > self.frequency: - raise ValidationError( - {"freq_range": "Полоса частот не может быть больше частоты"} - ) - - def __str__(self): - modulation_name = self.modulation.name if self.modulation else "-" - return f"Sigma-{self.frequency}:{self.freq_range} МГц:{modulation_name}" - - class Meta: - verbose_name = "ВЧ sigma" - verbose_name_plural = "ВЧ sigma" - - -class SourceRequest(models.Model): - """ - Модель заявки на источник. - - Хранит информацию о заявках на обработку источников с различными статусами. - """ - - STATUS_CHOICES = [ - ('planned', 'Запланировано'), - ('canceled_gso', 'Отменено ГСО'), - ('canceled_kub', 'Отменено МКА'), - ('conducted', 'Проведён'), - ('successful', 'Успешно'), - ('no_correlation', 'Нет корреляции'), - ('no_signal', 'Нет сигнала в спектре'), - ('unsuccessful', 'Неуспешно'), - ('downloading', 'Скачивание'), - ('processing', 'Обработка'), - ('result_received', 'Результат получен'), - ] - - PRIORITY_CHOICES = [ - ('low', 'Низкий'), - ('medium', 'Средний'), - ('high', 'Высокий'), - ] - - # Связь с источником (опционально для заявок без привязки) - source = models.ForeignKey( - Source, - on_delete=models.CASCADE, - related_name='source_requests', - verbose_name='Источник', - null=True, - blank=True, - help_text='Связанный источник', - ) - - # Связь со спутником - satellite = models.ForeignKey( - Satellite, - on_delete=models.SET_NULL, - related_name='satellite_requests', - verbose_name='Спутник', - null=True, - blank=True, - help_text='Связанный спутник', - ) - - # Основные поля - status = models.CharField( - max_length=20, - choices=STATUS_CHOICES, - default='planned', - verbose_name='Статус', - db_index=True, - help_text='Текущий статус заявки', - ) - priority = models.CharField( - max_length=10, - choices=PRIORITY_CHOICES, - default='medium', - verbose_name='Приоритет', - db_index=True, - help_text='Приоритет заявки', - ) - - # Даты - planned_at = models.DateTimeField( - null=True, - blank=True, - verbose_name='Дата и время планирования', - help_text='Запланированная дата и время', - ) - request_date = models.DateField( - null=True, - blank=True, - verbose_name='Дата заявки', - help_text='Дата подачи заявки', - ) - card_date = models.DateField( - null=True, - blank=True, - verbose_name='Дата формирования карточки', - help_text='Дата формирования карточки', - ) - status_updated_at = models.DateTimeField( - auto_now=True, - verbose_name='Дата обновления статуса', - help_text='Дата и время последнего обновления статуса', - ) - - # Частоты и перенос - downlink = models.FloatField( - null=True, - blank=True, - verbose_name='Частота Downlink, МГц', - help_text='Частота downlink в МГц', - ) - uplink = models.FloatField( - null=True, - blank=True, - verbose_name='Частота Uplink, МГц', - help_text='Частота uplink в МГц', - ) - transfer = models.FloatField( - null=True, - blank=True, - verbose_name='Перенос, МГц', - help_text='Перенос по частоте в МГц', - ) - - # Результаты - gso_success = models.BooleanField( - null=True, - blank=True, - verbose_name='ГСО успешно?', - help_text='Успешность ГСО', - ) - kubsat_success = models.BooleanField( - null=True, - blank=True, - verbose_name='Кубсат успешно?', - help_text='Успешность Кубсат', - ) - - # Район - region = models.CharField( - max_length=255, - null=True, - blank=True, - verbose_name='Район', - help_text='Район/местоположение', - ) - - # Комментарий - comment = models.TextField( - null=True, - blank=True, - verbose_name='Комментарий', - help_text='Дополнительные комментарии к заявке', - ) - - # Координаты ГСО (усреднённые по выбранным точкам) - coords = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name='Координаты ГСО', - help_text='Координаты ГСО (WGS84)', - ) - - # Координаты источника - coords_source = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name='Координаты источника', - help_text='Координаты источника (WGS84)', - ) - - # Координаты объекта - coords_object = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name='Координаты объекта', - help_text='Координаты объекта (WGS84)', - ) - - # Количество точек, использованных для расчёта координат - points_count = models.PositiveIntegerField( - default=0, - verbose_name='Количество точек', - help_text='Количество точек ГЛ, использованных для расчёта координат', - ) - - # Метаданные - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name='Дата создания', - help_text='Дата и время создания записи', - ) - created_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name='source_requests_created', - null=True, - blank=True, - verbose_name='Создан пользователем', - help_text='Пользователь, создавший запись', - ) - updated_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name='source_requests_updated', - null=True, - blank=True, - verbose_name='Изменен пользователем', - help_text='Пользователь, последним изменивший запись', - ) - - def __str__(self): - return f"Заявка #{self.pk} - {self.source_id} ({self.get_status_display()})" - - def save(self, *args, **kwargs): - # Определяем, изменился ли статус - old_status = None - if self.pk: - try: - old_instance = SourceRequest.objects.get(pk=self.pk) - old_status = old_instance.status - except SourceRequest.DoesNotExist: - pass - - super().save(*args, **kwargs) - - # Если статус изменился, создаем запись в истории - if old_status is not None and old_status != self.status: - SourceRequestStatusHistory.objects.create( - source_request=self, - old_status=old_status, - new_status=self.status, - changed_by=self.updated_by, - ) - - class Meta: - verbose_name = 'Заявка на источник' - verbose_name_plural = 'Заявки на источники' - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['-created_at']), - models.Index(fields=['status']), - models.Index(fields=['priority']), - models.Index(fields=['source', '-created_at']), - ] - - -class SourceRequestStatusHistory(models.Model): - """ - Модель истории изменений статусов заявок. - - Хранит полную хронологию изменений статусов заявок. - """ - - source_request = models.ForeignKey( - SourceRequest, - on_delete=models.CASCADE, - related_name='status_history', - verbose_name='Заявка', - help_text='Связанная заявка', - ) - old_status = models.CharField( - max_length=20, - choices=SourceRequest.STATUS_CHOICES, - verbose_name='Старый статус', - help_text='Статус до изменения', - ) - new_status = models.CharField( - max_length=20, - choices=SourceRequest.STATUS_CHOICES, - verbose_name='Новый статус', - help_text='Статус после изменения', - ) - changed_at = models.DateTimeField( - auto_now_add=True, - verbose_name='Дата изменения', - db_index=True, - help_text='Дата и время изменения статуса', - ) - changed_by = models.ForeignKey( - CustomUser, - on_delete=models.SET_NULL, - related_name='status_changes', - null=True, - blank=True, - verbose_name='Изменен пользователем', - help_text='Пользователь, изменивший статус', - ) - - def __str__(self): - return f"{self.source_request_id}: {self.get_old_status_display()} → {self.get_new_status_display()}" - - class Meta: - verbose_name = 'История статуса заявки' - verbose_name_plural = 'История статусов заявок' - ordering = ['-changed_at'] - indexes = [ - models.Index(fields=['-changed_at']), - models.Index(fields=['source_request', '-changed_at']), - ] - - -class Geo(models.Model): - """ - Модель геолокационных данных. - - Хранит информацию о местоположении источника сигнала, включая координаты, - данные от различных источников (геолокация, кубсат, оперативники) и расстояния между ними. - """ - - # Основные поля - timestamp = models.DateTimeField( - null=True, - blank=True, - verbose_name="Время", - db_index=True, - help_text="Время фиксации геолокации", - ) - location = models.CharField( - max_length=255, - null=True, - blank=True, - verbose_name="Местоположение", - help_text="Текстовое описание местоположения", - ) - comment = models.CharField( - max_length=255, - blank=True, - verbose_name="Комментарий", - help_text="Дополнительные комментарии", - ) - is_average = models.BooleanField( - null=True, - blank=True, - verbose_name="Усреднённое", - help_text="Является ли координата усредненной", - ) - - # Координаты - coords = gis.PointField( - srid=4326, - null=True, - blank=True, - verbose_name="Координата геолокации", - help_text="Основные координаты геолокации (WGS84)", - ) - - # Вычисляемые поля - расстояния - # distance_coords_kup = models.GeneratedField( - # expression=functions.Distance("coords", "coords_kupsat") / 1000, - # output_field=models.FloatField(), - # db_persist=True, - # null=True, - # blank=True, - # verbose_name="Расстояние между кубсатом и гео, км", - # ) - # distance_coords_valid = models.GeneratedField( - # expression=functions.Distance("coords", "coords_valid") / 1000, - # output_field=models.FloatField(), - # db_persist=True, - # null=True, - # blank=True, - # verbose_name="Расстояние между гео и оперативным отделом, км", - # ) - # distance_kup_valid = models.GeneratedField( - # expression=functions.Distance("coords_valid", "coords_kupsat") / 1000, - # output_field=models.FloatField(), - # db_persist=True, - # null=True, - # blank=True, - # verbose_name="Расстояние между кубсатом и оперативным отделом, км", - # ) - - # Связи - mirrors = models.ManyToManyField( - Satellite, - related_name="geo_mirrors", - verbose_name="Зеркала", - blank=True, - help_text="Спутники-зеркала, использованные для приема", - ) - objitem = models.OneToOneField( - ObjItem, - on_delete=models.CASCADE, - verbose_name="Объект", - related_name="geo_obj", - null=True, - help_text="Связанный объект", - ) - - def __str__(self): - if self.coords: - longitude = self.coords.coords[0] - latitude = self.coords.coords[1] - lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" - lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" - location_str = f", {self.location}" if self.location else "" - return f"{lat} {lon}{location_str}" - return f"Гео #{self.pk}" - - class Meta: - verbose_name = "Гео" - verbose_name_plural = "Гео" - ordering = ["-timestamp"] - indexes = [ - models.Index(fields=["-timestamp"]), - models.Index(fields=["location"]), - ] - constraints = [ - models.UniqueConstraint( - fields=["timestamp", "coords"], name="unique_geo_combination" - ) - ] diff --git a/dbapp/mainapp/models/__init__.py b/dbapp/mainapp/models/__init__.py new file mode 100644 index 0000000..8795094 --- /dev/null +++ b/dbapp/mainapp/models/__init__.py @@ -0,0 +1,70 @@ +# Пользователи и разрешения +from .users import UserPermission, CustomUser + +# Справочники +from .references import ( + ObjectInfo, + ObjectOwnership, + Polarization, + Modulation, + Standard, + Band, +) + +# Спутники +from .satellite import Satellite + +# Источники и объекты +from .source import Source +from .objitem import ObjItem, ObjItemQuerySet, ObjItemManager +from .geo import Geo + +# Параметры и анализ +from .parameters import Parameter, SigmaParameter +from .tech_analyze import TechAnalyze, ObjectMark + +# Заявки +from .requests import SourceRequest, SourceRequestStatusHistory + +# Вспомогательные функции для default значений +from .defaults import ( + get_default_polarization, + get_default_modulation, + get_default_standard, + get_permission_choices, +) + +__all__ = [ + # Пользователи + 'UserPermission', + 'CustomUser', + # Справочники + 'ObjectInfo', + 'ObjectOwnership', + 'Polarization', + 'Modulation', + 'Standard', + 'Band', + # Спутники + 'Satellite', + # Источники и объекты + 'Source', + 'ObjItem', + 'ObjItemQuerySet', + 'ObjItemManager', + 'Geo', + # Параметры + 'Parameter', + 'SigmaParameter', + # Анализ + 'TechAnalyze', + 'ObjectMark', + # Заявки + 'SourceRequest', + 'SourceRequestStatusHistory', + # Функции + 'get_default_polarization', + 'get_default_modulation', + 'get_default_standard', + 'get_permission_choices', +] diff --git a/dbapp/mainapp/models/defaults.py b/dbapp/mainapp/models/defaults.py new file mode 100644 index 0000000..e883446 --- /dev/null +++ b/dbapp/mainapp/models/defaults.py @@ -0,0 +1,27 @@ +""" +Вспомогательные функции для default значений моделей. +""" + + +def get_default_polarization(): + from .references import Polarization + obj, created = Polarization.objects.get_or_create(name="-") + return obj.id + + +def get_default_modulation(): + from .references import Modulation + obj, created = Modulation.objects.get_or_create(name="-") + return obj.id + + +def get_default_standard(): + from .references import Standard + obj, created = Standard.objects.get_or_create(name="-") + return obj.id + + +def get_permission_choices(): + """Ленивая загрузка choices для избежания циклического импорта.""" + from ..permissions import PERMISSION_CHOICES + return PERMISSION_CHOICES diff --git a/dbapp/mainapp/models/errors_report.py b/dbapp/mainapp/models/errors_report.py new file mode 100644 index 0000000..856cfda --- /dev/null +++ b/dbapp/mainapp/models/errors_report.py @@ -0,0 +1,9 @@ +from django.db import models + +# class IssueType(models.Model): +# name = models.CharField(max_length=100) +# CATEGORY_CHOICES = [ +# ('error', 'Ошибка'), +# ('malfunction', 'Неисправность'), +# ] +# category = models.CharField(max_length=12, choices=CATEGORY_CHOICES) \ No newline at end of file diff --git a/dbapp/mainapp/models/geo.py b/dbapp/mainapp/models/geo.py new file mode 100644 index 0000000..cec0571 --- /dev/null +++ b/dbapp/mainapp/models/geo.py @@ -0,0 +1,92 @@ +""" +Модель геолокационных данных. +""" +from django.contrib.gis.db import models as gis +from django.db import models + + +class Geo(models.Model): + """ + Модель геолокационных данных. + + Хранит информацию о местоположении источника сигнала, включая координаты, + данные от различных источников (геолокация, кубсат, оперативники) и расстояния между ними. + """ + + # Основные поля + timestamp = models.DateTimeField( + null=True, + blank=True, + verbose_name="Время", + db_index=True, + help_text="Время фиксации геолокации", + ) + location = models.CharField( + max_length=255, + null=True, + blank=True, + verbose_name="Местоположение", + help_text="Текстовое описание местоположения", + ) + comment = models.CharField( + max_length=255, + blank=True, + verbose_name="Комментарий", + help_text="Дополнительные комментарии", + ) + is_average = models.BooleanField( + null=True, + blank=True, + verbose_name="Усреднённое", + help_text="Является ли координата усредненной", + ) + + # Координаты + coords = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координата геолокации", + help_text="Основные координаты геолокации (WGS84)", + ) + + # Связи + mirrors = models.ManyToManyField( + 'mainapp.Satellite', + related_name="geo_mirrors", + verbose_name="Зеркала", + blank=True, + help_text="Спутники-зеркала, использованные для приема", + ) + objitem = models.OneToOneField( + 'mainapp.ObjItem', + on_delete=models.CASCADE, + verbose_name="Объект", + related_name="geo_obj", + null=True, + help_text="Связанный объект", + ) + + def __str__(self): + if self.coords: + longitude = self.coords.coords[0] + latitude = self.coords.coords[1] + lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" + lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" + location_str = f", {self.location}" if self.location else "" + return f"{lat} {lon}{location_str}" + return f"Гео #{self.pk}" + + class Meta: + verbose_name = "Гео" + verbose_name_plural = "Гео" + ordering = ["-timestamp"] + indexes = [ + models.Index(fields=["-timestamp"]), + models.Index(fields=["location"]), + ] + constraints = [ + models.UniqueConstraint( + fields=["timestamp", "coords"], name="unique_geo_combination" + ) + ] diff --git a/dbapp/mainapp/models/objitem.py b/dbapp/mainapp/models/objitem.py new file mode 100644 index 0000000..88bf471 --- /dev/null +++ b/dbapp/mainapp/models/objitem.py @@ -0,0 +1,148 @@ +""" +Модель точки ГЛ (ObjItem). +""" +from django.db import models +from django.utils import timezone + + +class ObjItemQuerySet(models.QuerySet): + """Custom QuerySet для модели ObjItem с оптимизированными запросами""" + + def with_related(self): + """Оптимизирует запросы, загружая связанные объекты""" + return self.select_related( + "geo_obj", + "updated_by__user", + "created_by__user", + "lyngsat_source", + "parameter_obj", + "parameter_obj__id_satellite", + "parameter_obj__polarization", + "parameter_obj__modulation", + "parameter_obj__standard", + ) + + def recent(self, days=30): + """Возвращает объекты, созданные за последние N дней""" + from datetime import timedelta + + cutoff_date = timezone.now() - timedelta(days=days) + return self.filter(created_at__gte=cutoff_date) + + def by_user(self, user): + """Возвращает объекты, созданные указанным пользователем""" + return self.filter(created_by=user) + + +class ObjItemManager(models.Manager): + """Custom Manager для модели ObjItem""" + + def get_queryset(self): + return ObjItemQuerySet(self.model, using=self._db) + + def with_related(self): + """Возвращает queryset с предзагруженными связанными объектами""" + return self.get_queryset().with_related() + + def recent(self, days=30): + """Возвращает недавно созданные объекты""" + return self.get_queryset().recent(days) + + def by_user(self, user): + """Возвращает объекты пользователя""" + return self.get_queryset().by_user(user) + + +class ObjItem(models.Model): + """ + Модель точки ГЛ. + + Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации. + """ + + # Основные поля + name = models.CharField( + null=True, + blank=True, + max_length=100, + verbose_name="Имя объекта", + db_index=True, + help_text="Название объекта/источника сигнала", + ) + source = models.ForeignKey( + 'mainapp.Source', + on_delete=models.CASCADE, + null=True, + verbose_name="ИРИ", + related_name="source_objitems", + ) + transponder = models.ForeignKey( + "mapsapp.Transponders", + on_delete=models.SET_NULL, + related_name="transponder_objitems", + null=True, + blank=True, + verbose_name="Транспондер", + help_text="Транспондер, с помощью которого была получена точка", + ) + is_automatic = models.BooleanField( + default=False, + verbose_name="Автоматическая", + db_index=True, + help_text="Если True, точка не добавляется к объектам (Source), а хранится отдельно", + ) + + # Метаданные + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="objitems_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="objitems_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) + lyngsat_source = models.ForeignKey( + "lyngsatapp.LyngSat", + on_delete=models.SET_NULL, + related_name="objitems", + null=True, + blank=True, + verbose_name="Источник LyngSat", + help_text="Связанный источник из базы LyngSat (ТВ)", + ) + + # Custom manager + objects = ObjItemManager() + + def __str__(self): + return f"Объект {self.name}" if self.name else f"Объект #{self.pk}" + + class Meta: + verbose_name = "Объект" + verbose_name_plural = "Объекты" + ordering = ["-updated_at"] + indexes = [ + models.Index(fields=["name"]), + models.Index(fields=["-updated_at"]), + models.Index(fields=["-created_at"]), + ] diff --git a/dbapp/mainapp/models/parameters.py b/dbapp/mainapp/models/parameters.py new file mode 100644 index 0000000..ea0a068 --- /dev/null +++ b/dbapp/mainapp/models/parameters.py @@ -0,0 +1,271 @@ +""" +Модели параметров сигнала (Parameter, SigmaParameter). +""" +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.db.models import ExpressionWrapper, F + +from .defaults import ( + get_default_polarization, + get_default_modulation, + get_default_standard, +) + + +class Parameter(models.Model): + id_satellite = models.ForeignKey( + 'mainapp.Satellite', + on_delete=models.PROTECT, + related_name="parameters", + verbose_name="Спутник", + null=True, + ) + polarization = models.ForeignKey( + 'mainapp.Polarization', + default=get_default_polarization, + on_delete=models.SET_DEFAULT, + related_name="polarizations", + null=True, + blank=True, + verbose_name="Поляризация", + ) + frequency = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Частота, МГц", + db_index=True, + help_text="Центральная частота сигнала", + ) + freq_range = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Полоса частот, МГц", + help_text="Полоса частот сигнала", + ) + bod_velocity = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Символьная скорость, БОД", + help_text="Символьная скорость должна быть положительной", + ) + modulation = models.ForeignKey( + 'mainapp.Modulation', + default=get_default_modulation, + on_delete=models.SET_DEFAULT, + related_name="modulations", + null=True, + blank=True, + verbose_name="Модуляция", + ) + snr = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="ОСШ", + help_text="Отношение сигнал/шум", + ) + standard = models.ForeignKey( + 'mainapp.Standard', + default=get_default_standard, + on_delete=models.SET_DEFAULT, + related_name="standards", + null=True, + blank=True, + verbose_name="Стандарт", + ) + objitem = models.OneToOneField( + 'mainapp.ObjItem', + on_delete=models.CASCADE, + related_name="parameter_obj", + verbose_name="Объект", + null=True, + blank=True, + help_text="Связанный объект", + ) + + def clean(self): + """Валидация на уровне модели""" + super().clean() + + # Проверка что частота больше полосы частот + if self.frequency and self.freq_range: + if self.freq_range > self.frequency: + raise ValidationError( + {"freq_range": "Полоса частот не может быть больше частоты"} + ) + + # Проверка что символьная скорость соответствует полосе частот + if self.bod_velocity and self.freq_range: + if self.bod_velocity > self.freq_range * 1000000: # Конвертация МГц в Гц + raise ValidationError( + { + "bod_velocity": "Символьная скорость не может превышать полосу частот" + } + ) + + def __str__(self): + polarization_name = self.polarization.name if self.polarization else "-" + modulation_name = self.modulation.name if self.modulation else "-" + return f"Источник-{self.frequency}:{self.freq_range} МГц:{polarization_name}:{modulation_name}" + + class Meta: + verbose_name = "ВЧ загрузка" + verbose_name_plural = "ВЧ загрузки" + indexes = [ + models.Index(fields=["id_satellite", "frequency"]), + models.Index(fields=["frequency", "polarization"]), + ] + + +class SigmaParameter(models.Model): + TRANSFERS = [(-1.0, "-"), (9750.0, "9750 МГц"), (10750.0, "10750 МГц")] + + id_satellite = models.ForeignKey( + 'mainapp.Satellite', + on_delete=models.PROTECT, + related_name="sigmapar_sat", + verbose_name="Спутник", + ) + transfer = models.FloatField( + choices=TRANSFERS, + default=-1.0, + verbose_name="Перенос по частоте", + help_text="Выберите перенос по частоте", + ) + status = models.CharField( + max_length=20, + blank=True, + null=True, + verbose_name="Статус", + help_text="Статус измерения", + ) + frequency = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Частота, МГц", + db_index=True, + help_text="Центральная частота сигнала", + ) + transfer_frequency = models.GeneratedField( + expression=ExpressionWrapper( + F("frequency") + F("transfer"), output_field=models.FloatField() + ), + output_field=models.FloatField(), + db_persist=True, + null=True, + blank=True, + verbose_name="Частота в Ku, МГц", + ) + freq_range = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Полоса частот, МГц", + help_text="Полоса частот", + ) + power = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Мощность, дБм", + help_text="Мощность сигнала", + ) + bod_velocity = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Символьная скорость, БОД", + help_text="Символьная скорость должна быть положительной", + ) + polarization = models.ForeignKey( + 'mainapp.Polarization', + default=get_default_polarization, + on_delete=models.SET_DEFAULT, + related_name="polarizations_sigma", + null=True, + blank=True, + verbose_name="Поляризация", + ) + modulation = models.ForeignKey( + 'mainapp.Modulation', + default=get_default_modulation, + on_delete=models.SET_DEFAULT, + related_name="modulations_sigma", + null=True, + blank=True, + verbose_name="Модуляция", + ) + snr = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="ОСШ, Дб", + validators=[MinValueValidator(-50), MaxValueValidator(100)], + help_text="Отношение сигнал/шум в диапазоне от -50 до 100 дБ", + ) + standard = models.ForeignKey( + 'mainapp.Standard', + default=get_default_standard, + on_delete=models.SET_DEFAULT, + related_name="standards_sigma", + null=True, + blank=True, + verbose_name="Стандарт", + ) + packets = models.BooleanField( + null=True, + blank=True, + verbose_name="Пакетность", + help_text="Наличие пакетной передачи", + ) + datetime_begin = models.DateTimeField( + null=True, + blank=True, + verbose_name="Время начала измерения", + help_text="Дата и время начала измерения", + ) + datetime_end = models.DateTimeField( + null=True, + blank=True, + verbose_name="Время окончания измерения", + help_text="Дата и время окончания измерения", + ) + parameter = models.ForeignKey( + 'mainapp.Parameter', + on_delete=models.SET_NULL, + related_name="sigma_parameter", + verbose_name="ВЧ", + null=True, + blank=True, + ) + + def clean(self): + """Валидация на уровне модели""" + super().clean() + + # Проверка что время окончания больше времени начала + if self.datetime_begin and self.datetime_end: + if self.datetime_end < self.datetime_begin: + raise ValidationError( + {"datetime_end": "Время окончания должно быть позже времени начала"} + ) + + # Проверка что частота больше полосы частот + if self.frequency and self.freq_range: + if self.freq_range > self.frequency: + raise ValidationError( + {"freq_range": "Полоса частот не может быть больше частоты"} + ) + + def __str__(self): + modulation_name = self.modulation.name if self.modulation else "-" + return f"Sigma-{self.frequency}:{self.freq_range} МГц:{modulation_name}" + + class Meta: + verbose_name = "ВЧ sigma" + verbose_name_plural = "ВЧ sigma" diff --git a/dbapp/mainapp/models/references.py b/dbapp/mainapp/models/references.py new file mode 100644 index 0000000..11bc481 --- /dev/null +++ b/dbapp/mainapp/models/references.py @@ -0,0 +1,136 @@ +""" +Справочные модели (справочники). +""" +from django.db import models + + +class ObjectInfo(models.Model): + name = models.CharField( + max_length=255, + unique=True, + verbose_name="Тип объекта", + help_text="Информация о типе объекта", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Тип объекта" + verbose_name_plural = "Типы объектов" + ordering = ["name"] + + +class ObjectOwnership(models.Model): + """ + Модель принадлежности объекта. + """ + name = models.CharField( + max_length=255, + unique=True, + verbose_name="Принадлежность", + help_text="Принадлежность объекта", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Принадлежность объекта" + verbose_name_plural = "Принадлежности объектов" + ordering = ["name"] + + +class Polarization(models.Model): + """ + Модель поляризации сигнала. + + Определяет тип поляризации спутникового сигнала (H, V, L, R и т.д.). + """ + + name = models.CharField( + max_length=20, + unique=True, + verbose_name="Поляризация", + db_index=True, + help_text="Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Поляризация" + verbose_name_plural = "Поляризация" + ordering = ["name"] + + +class Modulation(models.Model): + """ + Модель типа модуляции сигнала. + + Определяет схему модуляции (QPSK, 8PSK, 16APSK и т.д.). + """ + + name = models.CharField( + max_length=20, + unique=True, + verbose_name="Модуляция", + db_index=True, + help_text="Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Модуляция" + verbose_name_plural = "Модуляции" + ordering = ["name"] + + +class Standard(models.Model): + """ + Модель стандарта передачи данных. + + Определяет стандарт передачи (DVB-S, DVB-S2, DVB-S2X и т.д.). + """ + + name = models.CharField( + max_length=80, + unique=True, + verbose_name="Стандарт", + db_index=True, + help_text="Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Стандарт" + verbose_name_plural = "Стандарты" + ordering = ["name"] + + +class Band(models.Model): + name = models.CharField( + max_length=50, + unique=True, + verbose_name="Название", + help_text="Название диапазона", + ) + border_start = models.FloatField( + blank=True, null=True, verbose_name="Нижняя граница диапазона, МГц" + ) + border_end = models.FloatField( + blank=True, null=True, verbose_name="Верхняя граница диапазона, МГц" + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Диапазон" + verbose_name_plural = "Диапазоны" + ordering = ["name"] diff --git a/dbapp/mainapp/models/requests.py b/dbapp/mainapp/models/requests.py new file mode 100644 index 0000000..c4f349a --- /dev/null +++ b/dbapp/mainapp/models/requests.py @@ -0,0 +1,298 @@ +""" +Модели заявок на источники (SourceRequest, SourceRequestStatusHistory). +""" +from django.contrib.gis.db import models as gis +from django.db import models + + +class SourceRequest(models.Model): + """ + Модель заявки на источник. + + Хранит информацию о заявках на обработку источников с различными статусами. + """ + + STATUS_CHOICES = [ + ('planned', 'Запланировано'), + ('canceled_gso', 'Отменено ГСО'), + ('canceled_kub', 'Отменено МКА'), + ('conducted', 'Проведён'), + ('successful', 'Успешно'), + ('no_correlation', 'Нет корреляции'), + ('no_signal', 'Нет сигнала в спектре'), + ('unsuccessful', 'Неуспешно'), + ('downloading', 'Скачивание'), + ('processing', 'Обработка'), + ('result_received', 'Результат получен'), + ] + + PRIORITY_CHOICES = [ + ('low', 'Низкий'), + ('medium', 'Средний'), + ('high', 'Высокий'), + ] + + # Связь с источником (опционально для заявок без привязки) + source = models.ForeignKey( + 'mainapp.Source', + on_delete=models.CASCADE, + related_name='source_requests', + verbose_name='Источник', + null=True, + blank=True, + help_text='Связанный источник', + ) + + # Связь со спутником + satellite = models.ForeignKey( + 'mainapp.Satellite', + on_delete=models.SET_NULL, + related_name='satellite_requests', + verbose_name='Спутник', + null=True, + blank=True, + help_text='Связанный спутник', + ) + + # Основные поля + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='planned', + verbose_name='Статус', + db_index=True, + help_text='Текущий статус заявки', + ) + priority = models.CharField( + max_length=10, + choices=PRIORITY_CHOICES, + default='medium', + verbose_name='Приоритет', + db_index=True, + help_text='Приоритет заявки', + ) + + # Даты + planned_at = models.DateTimeField( + null=True, + blank=True, + verbose_name='Дата и время планирования', + help_text='Запланированная дата и время', + ) + request_date = models.DateField( + null=True, + blank=True, + verbose_name='Дата заявки', + help_text='Дата подачи заявки', + ) + card_date = models.DateField( + null=True, + blank=True, + verbose_name='Дата формирования карточки', + help_text='Дата формирования карточки', + ) + status_updated_at = models.DateTimeField( + auto_now=True, + verbose_name='Дата обновления статуса', + help_text='Дата и время последнего обновления статуса', + ) + + # Частоты и перенос + downlink = models.FloatField( + null=True, + blank=True, + verbose_name='Частота Downlink, МГц', + help_text='Частота downlink в МГц', + ) + uplink = models.FloatField( + null=True, + blank=True, + verbose_name='Частота Uplink, МГц', + help_text='Частота uplink в МГц', + ) + transfer = models.FloatField( + null=True, + blank=True, + verbose_name='Перенос, МГц', + help_text='Перенос по частоте в МГц', + ) + + # Результаты + gso_success = models.BooleanField( + null=True, + blank=True, + verbose_name='ГСО успешно?', + help_text='Успешность ГСО', + ) + kubsat_success = models.BooleanField( + null=True, + blank=True, + verbose_name='Кубсат успешно?', + help_text='Успешность Кубсат', + ) + + # Район + region = models.CharField( + max_length=255, + null=True, + blank=True, + verbose_name='Район', + help_text='Район/местоположение', + ) + + # Комментарий + comment = models.TextField( + null=True, + blank=True, + verbose_name='Комментарий', + help_text='Дополнительные комментарии к заявке', + ) + + # Координаты ГСО (усреднённые по выбранным точкам) + coords = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name='Координаты ГСО', + help_text='Координаты ГСО (WGS84)', + ) + + # Координаты источника + coords_source = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name='Координаты источника', + help_text='Координаты источника (WGS84)', + ) + + # Координаты объекта + coords_object = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name='Координаты объекта', + help_text='Координаты объекта (WGS84)', + ) + + # Количество точек, использованных для расчёта координат + points_count = models.PositiveIntegerField( + default=0, + verbose_name='Количество точек', + help_text='Количество точек ГЛ, использованных для расчёта координат', + ) + + # Метаданные + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name='Дата создания', + help_text='Дата и время создания записи', + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name='source_requests_created', + null=True, + blank=True, + verbose_name='Создан пользователем', + help_text='Пользователь, создавший запись', + ) + updated_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name='source_requests_updated', + null=True, + blank=True, + verbose_name='Изменен пользователем', + help_text='Пользователь, последним изменивший запись', + ) + + def __str__(self): + return f"Заявка #{self.pk} - {self.source_id} ({self.get_status_display()})" + + def save(self, *args, **kwargs): + # Определяем, изменился ли статус + old_status = None + if self.pk: + try: + old_instance = SourceRequest.objects.get(pk=self.pk) + old_status = old_instance.status + except SourceRequest.DoesNotExist: + pass + + super().save(*args, **kwargs) + + # Если статус изменился, создаем запись в истории + if old_status is not None and old_status != self.status: + SourceRequestStatusHistory.objects.create( + source_request=self, + old_status=old_status, + new_status=self.status, + changed_by=self.updated_by, + ) + + class Meta: + verbose_name = 'Заявка на источник' + verbose_name_plural = 'Заявки на источники' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + models.Index(fields=['status']), + models.Index(fields=['priority']), + models.Index(fields=['source', '-created_at']), + ] + + +class SourceRequestStatusHistory(models.Model): + """ + Модель истории изменений статусов заявок. + + Хранит полную хронологию изменений статусов заявок. + """ + + source_request = models.ForeignKey( + SourceRequest, + on_delete=models.CASCADE, + related_name='status_history', + verbose_name='Заявка', + help_text='Связанная заявка', + ) + old_status = models.CharField( + max_length=20, + choices=SourceRequest.STATUS_CHOICES, + verbose_name='Старый статус', + help_text='Статус до изменения', + ) + new_status = models.CharField( + max_length=20, + choices=SourceRequest.STATUS_CHOICES, + verbose_name='Новый статус', + help_text='Статус после изменения', + ) + changed_at = models.DateTimeField( + auto_now_add=True, + verbose_name='Дата изменения', + db_index=True, + help_text='Дата и время изменения статуса', + ) + changed_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name='status_changes', + null=True, + blank=True, + verbose_name='Изменен пользователем', + help_text='Пользователь, изменивший статус', + ) + + def __str__(self): + return f"{self.source_request_id}: {self.get_old_status_display()} → {self.get_new_status_display()}" + + class Meta: + verbose_name = 'История статуса заявки' + verbose_name_plural = 'История статусов заявок' + ordering = ['-changed_at'] + indexes = [ + models.Index(fields=['-changed_at']), + models.Index(fields=['source_request', '-changed_at']), + ] diff --git a/dbapp/mainapp/models/satellite.py b/dbapp/mainapp/models/satellite.py new file mode 100644 index 0000000..2c840af --- /dev/null +++ b/dbapp/mainapp/models/satellite.py @@ -0,0 +1,122 @@ +""" +Модель спутника. +""" +from django.db import models + + +class Satellite(models.Model): + """ + Модель спутника. + + Представляет спутник связи с его основными характеристиками. + """ + PLACES = [ + ("kr", "КР"), + ("dv", "ДВ") + ] + + # Основные поля + name = models.CharField( + max_length=100, + unique=True, + verbose_name="Имя спутника", + db_index=True, + help_text="Название спутника", + ) + alternative_name = models.CharField( + max_length=100, + blank=True, + null=True, + verbose_name="Альтернативное имя", + db_index=True, + help_text="Альтернативное название спутника", + ) + location_place = models.CharField( + max_length=30, + choices=PLACES, + null=True, + default="kr", + verbose_name="Комплекс", + help_text="К какому комплексу принадлежит спутник", + ) + norad = models.IntegerField( + blank=True, + null=True, + verbose_name="NORAD ID", + help_text="Идентификатор NORAD для отслеживания спутника", + ) + international_code = models.CharField( + max_length=50, + blank=True, + null=True, + verbose_name="Международный код", + help_text="Международный идентификатор спутника (например, 2011-074A)", + ) + band = models.ManyToManyField( + 'mainapp.Band', + related_name="bands", + verbose_name="Диапазоны", + blank=True, + help_text="Диапазоны работы спутника", + ) + undersat_point = models.FloatField( + blank=True, + null=True, + verbose_name="Подспутниковая точка, градусы", + help_text="Подспутниковая точка в градусах. Восточное полушарие с +, западное с -", + ) + url = models.URLField( + blank=True, + null=True, + verbose_name="Ссылка на источник", + help_text="Ссылка на сайт, где можно проверить информацию", + ) + comment = models.TextField( + blank=True, + null=True, + verbose_name="Комментарий", + help_text="Любой возможный комменатрий", + ) + launch_date = models.DateField( + blank=True, + null=True, + verbose_name="Дата запуска", + help_text="Дата запуска спутника", + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="satellite_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="satellite_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Спутник" + verbose_name_plural = "Спутники" + ordering = ["name"] diff --git a/dbapp/mainapp/models/source.py b/dbapp/mainapp/models/source.py new file mode 100644 index 0000000..0cf7de8 --- /dev/null +++ b/dbapp/mainapp/models/source.py @@ -0,0 +1,229 @@ +""" +Модель источника сигнала (ИРИ). +""" +from django.contrib.gis.db import models as gis +from django.db import models + + +class Source(models.Model): + """ + Модель источника сигнала. + """ + + info = models.ForeignKey( + 'mainapp.ObjectInfo', + on_delete=models.SET_NULL, + related_name="source_info", + null=True, + blank=True, + verbose_name="Тип объекта", + help_text="Тип объекта", + ) + ownership = models.ForeignKey( + 'mainapp.ObjectOwnership', + on_delete=models.SET_NULL, + related_name="source_ownership", + null=True, + blank=True, + verbose_name="Принадлежность объекта", + help_text="Принадлежность объекта (страна, организация и т.д.)", + ) + note = models.TextField( + null=True, + blank=True, + verbose_name="Примечание", + help_text="Дополнительное описание объекта", + ) + confirm_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Дата подтверждения", + help_text="Дата и время добавления последней полученной точки ГЛ", + ) + last_signal_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Последний сигнал", + help_text="Дата и время последней отметки о наличии сигнала", + ) + + coords_average = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты ГЛ", + help_text="Усреднённые координаты, полученные от в ходе геолокации (WGS84)", + ) + coords_kupsat = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты Кубсата", + help_text="Координаты, полученные от кубсата (WGS84)", + ) + coords_valid = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты оперативников", + help_text="Координаты, предоставленные оперативным отделом (WGS84)", + ) + coords_reference = gis.PointField( + srid=4326, + null=True, + blank=True, + verbose_name="Координаты справочные", + help_text="Координаты, ещё кем-то проверенные (WGS84)", + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="source_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="source_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) + + def update_coords_average(self, new_coord_tuple): + """ + Обновляет coords_average в зависимости от типа объекта (info). + + Логика: + - Если info == "Подвижные": coords_average = последняя добавленная координата + - Иначе (Стационарные и др.): coords_average = инкрементальное среднее + + Args: + new_coord_tuple: кортеж (longitude, latitude) новой координаты + """ + from django.contrib.gis.geos import Point + from ..utils import calculate_mean_coords + + # Если тип объекта "Подвижные" - просто устанавливаем последнюю координату + if self.info and self.info.name == "Подвижные": + self.coords_average = Point(new_coord_tuple, srid=4326) + else: + # Для стационарных объектов - вычисляем среднее + if self.coords_average: + # Есть предыдущее среднее - вычисляем новое среднее + current_coord = (self.coords_average.x, self.coords_average.y) + new_avg, _ = calculate_mean_coords(current_coord, new_coord_tuple) + self.coords_average = Point(new_avg, srid=4326) + else: + # Первая координата - просто устанавливаем её + self.coords_average = Point(new_coord_tuple, srid=4326) + + def get_last_geo_coords(self): + """ + Получает координаты последней добавленной точки ГЛ для этого источника. + Сортировка по ID (последняя добавленная в базу). + + Returns: + tuple: (longitude, latitude) или None если точек нет + """ + # Получаем последний ObjItem для этого Source (по ID) + last_objitem = self.source_objitems.filter( + geo_obj__coords__isnull=False + ).select_related('geo_obj').order_by('-id').first() + + if last_objitem and last_objitem.geo_obj and last_objitem.geo_obj.coords: + return (last_objitem.geo_obj.coords.x, last_objitem.geo_obj.coords.y) + + return None + + def update_confirm_at(self): + """ + Обновляет дату confirm_at на дату создания последней добавленной точки ГЛ. + """ + last_objitem = self.source_objitems.order_by('-created_at').first() + if last_objitem: + self.confirm_at = last_objitem.created_at + + def save(self, *args, **kwargs): + """ + Переопределенный метод save для автоматического обновления coords_average + при изменении типа объекта. + """ + from django.contrib.gis.geos import Point + + # Проверяем, изменился ли тип объекта + if self.pk: # Объект уже существует + try: + old_instance = Source.objects.get(pk=self.pk) + old_info = old_instance.info + new_info = self.info + + # Если тип изменился на "Подвижные" + if new_info and new_info.name == "Подвижные" and (not old_info or old_info.name != "Подвижные"): + # Устанавливаем координату последней точки + last_coords = self.get_last_geo_coords() + if last_coords: + self.coords_average = Point(last_coords, srid=4326) + + # Если тип изменился с "Подвижные" на что-то другое + elif old_info and old_info.name == "Подвижные" and (not new_info or new_info.name != "Подвижные"): + # Пересчитываем среднюю координату по всем точкам + self._recalculate_average_coords() + + except Source.DoesNotExist: + pass + + super().save(*args, **kwargs) + + def _recalculate_average_coords(self): + """ + Пересчитывает среднюю координату по всем точкам источника. + Используется при переключении с "Подвижные" на "Стационарные". + + Сортировка по ID (порядок добавления в базу), инкрементальное усреднение + как в функциях импорта. + """ + from django.contrib.gis.geos import Point + from ..utils import calculate_mean_coords + + # Получаем все точки для этого источника, сортируем по ID (порядок добавления) + objitems = self.source_objitems.filter( + geo_obj__coords__isnull=False + ).select_related('geo_obj').order_by('id') + + if not objitems.exists(): + return + + # Вычисляем среднюю координату инкрементально (как в функциях импорта) + coords_average = None + for objitem in objitems: + if objitem.geo_obj and objitem.geo_obj.coords: + coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y) + if coords_average is None: + # Первая точка - просто устанавливаем её + coords_average = coord + else: + # Последующие точки - вычисляем среднее между текущим средним и новой точкой + coords_average, _ = calculate_mean_coords(coords_average, coord) + + if coords_average: + self.coords_average = Point(coords_average, srid=4326) + + class Meta: + verbose_name = "Источник" + verbose_name_plural = "Источники" diff --git a/dbapp/mainapp/models/tech_analyze.py b/dbapp/mainapp/models/tech_analyze.py new file mode 100644 index 0000000..2050ff5 --- /dev/null +++ b/dbapp/mainapp/models/tech_analyze.py @@ -0,0 +1,200 @@ +""" +Модели технического анализа (TechAnalyze, ObjectMark). +""" +from django.db import models +from django.utils import timezone + +from .defaults import ( + get_default_polarization, + get_default_modulation, + get_default_standard, +) + + +class TechAnalyze(models.Model): + """ + Модель технического анализа сигнала. + + Хранит информацию о технических параметрах сигнала для анализа. + """ + + # Основные поля + name = models.CharField( + max_length=255, + unique=True, + verbose_name="Имя", + db_index=True, + help_text="Уникальное название для технического анализа", + ) + satellite = models.ForeignKey( + 'mainapp.Satellite', + on_delete=models.PROTECT, + related_name="tech_analyzes", + verbose_name="Спутник", + help_text="Спутник, к которому относится анализ", + ) + polarization = models.ForeignKey( + 'mainapp.Polarization', + default=get_default_polarization, + on_delete=models.SET_DEFAULT, + related_name="tech_analyze_polarizations", + null=True, + blank=True, + verbose_name="Поляризация", + ) + frequency = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Частота, МГц", + db_index=True, + help_text="Центральная частота сигнала", + ) + freq_range = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Полоса частот, МГц", + help_text="Полоса частот сигнала", + ) + bod_velocity = models.FloatField( + default=0, + null=True, + blank=True, + verbose_name="Символьная скорость, БОД", + help_text="Символьная скорость", + ) + modulation = models.ForeignKey( + 'mainapp.Modulation', + default=get_default_modulation, + on_delete=models.SET_DEFAULT, + related_name="tech_analyze_modulations", + null=True, + blank=True, + verbose_name="Модуляция", + ) + standard = models.ForeignKey( + 'mainapp.Standard', + default=get_default_standard, + on_delete=models.SET_DEFAULT, + related_name="tech_analyze_standards", + null=True, + blank=True, + verbose_name="Стандарт", + ) + note = models.TextField( + null=True, + blank=True, + verbose_name="Примечание", + help_text="Дополнительные примечания", + ) + + # Метаданные + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + help_text="Дата и время создания записи", + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="tech_analyze_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший запись", + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата последнего изменения", + help_text="Дата и время последнего изменения", + ) + updated_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="tech_analyze_updated", + null=True, + blank=True, + verbose_name="Изменен пользователем", + help_text="Пользователь, последним изменивший запись", + ) + + def __str__(self): + return f"{self.name} ({self.satellite.name if self.satellite else '-'})" + + class Meta: + verbose_name = "Тех. анализ" + verbose_name_plural = "Тех. анализы" + ordering = ["-created_at"] + + +class ObjectMark(models.Model): + """ + Модель отметки о наличии сигнала. + + Используется для фиксации моментов времени когда сигнал был обнаружен или отсутствовал. + Привязывается к записям технического анализа (TechAnalyze). + """ + + # Основные поля + mark = models.BooleanField( + null=True, + blank=True, + verbose_name="Наличие сигнала", + help_text="True - сигнал обнаружен, False - сигнал отсутствует", + ) + timestamp = models.DateTimeField( + verbose_name="Время", + db_index=True, + help_text="Время фиксации отметки", + null=True, + blank=True, + ) + tech_analyze = models.ForeignKey( + TechAnalyze, + on_delete=models.CASCADE, + related_name="marks", + verbose_name="Тех. анализ", + help_text="Связанный технический анализ", + ) + created_by = models.ForeignKey( + 'mainapp.CustomUser', + on_delete=models.SET_NULL, + related_name="marks_created", + null=True, + blank=True, + verbose_name="Создан пользователем", + help_text="Пользователь, создавший отметку", + ) + + def can_edit(self): + """Проверка возможности редактирования отметки (в течение 5 минут)""" + from datetime import timedelta + if not self.timestamp: + return False + time_diff = timezone.now() - self.timestamp + return time_diff < timedelta(minutes=5) + + def can_add_new_mark_for_object(self): + """Проверка возможности добавления новой отметки для объекта (прошло 5 минут с последней)""" + from datetime import timedelta + if not self.timestamp: + return True + time_diff = timezone.now() - self.timestamp + return time_diff >= timedelta(minutes=5) + + def __str__(self): + if self.timestamp: + timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") + 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 = "Отметки сигналов" + ordering = ["-timestamp"] + indexes = [ + models.Index(fields=["tech_analyze", "-timestamp"]), + ] diff --git a/dbapp/mainapp/models/users.py b/dbapp/mainapp/models/users.py new file mode 100644 index 0000000..07f93f0 --- /dev/null +++ b/dbapp/mainapp/models/users.py @@ -0,0 +1,103 @@ +""" +Модели пользователей и разрешений. +""" +from django.contrib.auth.models import User +from django.db import models + + +class UserPermission(models.Model): + """ + Модель разрешения пользователя. + + Хранит гранулярные разрешения для конкретных действий в системе. + """ + + code = models.CharField( + max_length=50, + verbose_name="Код разрешения", + db_index=True, + help_text="Уникальный код разрешения", + ) + + def __str__(self): + from ..permissions import PERMISSION_CHOICES + choices_dict = dict(PERMISSION_CHOICES) + return choices_dict.get(self.code, self.code) + + class Meta: + verbose_name = "Разрешение" + verbose_name_plural = "Разрешения" + ordering = ["code"] + + +class CustomUser(models.Model): + """ + Расширенная модель пользователя с ролями. + + Добавляет систему ролей к стандартной модели User Django. + """ + + ROLE_CHOICES = [ + ("admin", "Администратор"), + ("moderator", "Модератор"), + ("user", "Пользователь"), + ] + + # Связи + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + verbose_name="Пользователь", + help_text="Связанный пользователь Django", + ) + + # Основные поля + role = models.CharField( + max_length=20, + choices=ROLE_CHOICES, + default="user", + verbose_name="Роль пользователя", + db_index=True, + help_text="Роль пользователя в системе", + ) + + # Индивидуальные разрешения (если пусто - используются права роли по умолчанию) + user_permissions = models.ManyToManyField( + UserPermission, + related_name="users", + verbose_name="Индивидуальные разрешения", + blank=True, + help_text="Если указаны - используются вместо прав роли по умолчанию", + ) + + # Флаг использования индивидуальных разрешений + use_custom_permissions = models.BooleanField( + default=False, + verbose_name="Использовать индивидуальные разрешения", + help_text="Если включено - используются индивидуальные разрешения вместо прав роли", + ) + + def __str__(self): + return ( + f"{self.user.first_name} {self.user.last_name}" + if self.user.first_name and self.user.last_name + else self.user.username + ) + + def has_perm(self, permission_code): + """ + Проверяет наличие разрешения у пользователя. + + Args: + permission_code: Код разрешения + + Returns: + bool: True если пользователь имеет разрешение + """ + from ..permissions import has_permission + return has_permission(self.user, permission_code) + + class Meta: + verbose_name = "Пользователь" + verbose_name_plural = "Пользователи" + ordering = ["user__username"]