Разбил файлик models.py на отдельные файлы моделей
This commit is contained in:
18
dbapp/mainapp/migrations/0026_alter_userpermission_code.py
Normal file
18
dbapp/mainapp/migrations/0026_alter_userpermission_code.py
Normal 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
70
dbapp/mainapp/models/__init__.py
Normal file
70
dbapp/mainapp/models/__init__.py
Normal 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',
|
||||||
|
]
|
||||||
27
dbapp/mainapp/models/defaults.py
Normal file
27
dbapp/mainapp/models/defaults.py
Normal 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
|
||||||
9
dbapp/mainapp/models/errors_report.py
Normal file
9
dbapp/mainapp/models/errors_report.py
Normal 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)
|
||||||
92
dbapp/mainapp/models/geo.py
Normal file
92
dbapp/mainapp/models/geo.py
Normal 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"
|
||||||
|
)
|
||||||
|
]
|
||||||
148
dbapp/mainapp/models/objitem.py
Normal file
148
dbapp/mainapp/models/objitem.py
Normal 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"]),
|
||||||
|
]
|
||||||
271
dbapp/mainapp/models/parameters.py
Normal file
271
dbapp/mainapp/models/parameters.py
Normal 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"
|
||||||
136
dbapp/mainapp/models/references.py
Normal file
136
dbapp/mainapp/models/references.py
Normal 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"]
|
||||||
298
dbapp/mainapp/models/requests.py
Normal file
298
dbapp/mainapp/models/requests.py
Normal 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']),
|
||||||
|
]
|
||||||
122
dbapp/mainapp/models/satellite.py
Normal file
122
dbapp/mainapp/models/satellite.py
Normal 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"]
|
||||||
229
dbapp/mainapp/models/source.py
Normal file
229
dbapp/mainapp/models/source.py
Normal 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 = "Источники"
|
||||||
200
dbapp/mainapp/models/tech_analyze.py
Normal file
200
dbapp/mainapp/models/tech_analyze.py
Normal 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"]),
|
||||||
|
]
|
||||||
103
dbapp/mainapp/models/users.py
Normal file
103
dbapp/mainapp/models/users.py
Normal 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"]
|
||||||
Reference in New Issue
Block a user