Files
dbstorage/dbapp/mainapp/models.py

1594 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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):
"""
Модель отметки о наличии сигнала.
Используется для фиксации моментов времени когда сигнал был обнаружен или отсутствовал.
Привязывается к записям технического анализа (TechAnalyze).
"""
# Основные поля
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="Время фиксации отметки",
)
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"
)
]