Разбил файлик models.py на отдельные файлы моделей

This commit is contained in:
2025-12-15 15:51:54 +03:00
parent 46dc79b93f
commit 480bb60855
14 changed files with 1723 additions and 1654 deletions

View File

@@ -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='Код разрешения'),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -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',
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
)
]

View File

@@ -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"]),
]

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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']),
]

View File

@@ -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"]

View File

@@ -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 = "Источники"

View File

@@ -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"]),
]

View File

@@ -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"]