# 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 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="Роль пользователя в системе", ) 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 ) 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): """ Модель отметки о наличии объекта. Используется для фиксации моментов времени когда объект был обнаружен или отсутствовал. """ # Основные поля mark = models.BooleanField( null=True, blank=True, verbose_name="Наличие объекта", help_text="True - объект обнаружен, False - объект отсутствует", ) timestamp = models.DateTimeField( auto_now_add=True, verbose_name="Время", db_index=True, help_text="Время фиксации отметки", ) source = models.ForeignKey( 'Source', 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") return f"+ {timestamp}" if self.mark else f"- {timestamp}" return "Отметка без времени" class Meta: verbose_name = "Отметка источника" verbose_name_plural = "Отметки источников" ordering = ["-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 Mirror(models.Model): # """ # Модель зеркала антенны. # Представляет физическое зеркало антенны для приема спутникового сигнала. # """ # # Основные поля # name = models.CharField( # max_length=30, # unique=True, # verbose_name="Имя зеркала", # db_index=True, # 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=20, 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): """ Модель спутника. Представляет спутник связи с его основными характеристиками. """ # Основные поля name = models.CharField( max_length=100, unique=True, verbose_name="Имя спутника", db_index=True, help_text="Название спутника", ) norad = models.IntegerField( blank=True, null=True, verbose_name="NORAD ID", help_text="Идентификатор NORAD для отслеживания спутника", ) 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 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="Принадлежность объекта (страна, организация и т.д.)", ) 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 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 при изменении типа объекта. """ 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="Транспондер, с помощью которого была получена точка", ) # Метаданные 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 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" ) ]