Compare commits

...

2 Commits

36 changed files with 3512 additions and 4615 deletions

View File

@@ -1,6 +1,3 @@
# Production Environment Variables
# ВАЖНО: Измените все значения перед деплоем!
# Django Settings # Django Settings
DEBUG=False DEBUG=False
ENVIRONMENT=production ENVIRONMENT=production

View File

@@ -1,396 +0,0 @@
# Сводка изменений: Асинхронная обработка данных Lyngsat
## Обзор
Реализована полная асинхронная обработка данных Lyngsat с использованием Celery, Redis и детальным логированием.
## Ключевые улучшения
### 1. ✅ Асинхронная обработка
- Задачи выполняются в фоновом режиме
- Веб-интерфейс не блокируется
- Можно обрабатывать несколько задач одновременно
### 2. ✅ Отслеживание прогресса
- Прогресс-бар в реальном времени
- Текущий статус обработки
- Процент выполнения
### 3. ✅ Детальное логирование
- Логи на уровне задачи
- Логи на уровне спутника
- Логи на уровне источника
- Все ошибки записываются в лог
### 4. ✅ Результаты и статистика
- Количество обработанных спутников
- Количество обработанных источников
- Количество созданных/обновленных записей
- Список всех ошибок
## Новые файлы
### Backend
1. **dbapp/dbapp/celery.py** - конфигурация Celery
2. **dbapp/dbapp/__init__.py** - инициализация Celery app
3. **dbapp/lyngsatapp/tasks.py** - асинхронная задача заполнения данных
4. **dbapp/start_celery_worker.sh** - скрипт запуска worker
### Frontend
5. **dbapp/mainapp/templates/mainapp/lyngsat_task_status.html** - страница отслеживания прогресса
### Документация
6. **ASYNC_LYNGSAT_GUIDE.md** - полное руководство
7. **QUICKSTART_ASYNC.md** - быстрый старт
8. **ASYNC_CHANGES_SUMMARY.md** - этот файл
## Измененные файлы
### Конфигурация
1. **dbapp/requirements.txt**
- Добавлено: `celery>=5.4.0`
- Добавлено: `django-celery-results>=2.5.1`
2. **dbapp/dbapp/settings/base.py**
- Добавлено: `django_celery_results` в INSTALLED_APPS
- Добавлено: полная конфигурация Celery (брокер, результаты, таймауты, логирование)
3. **docker-compose.yaml**
- Добавлено: сервис Redis
- Добавлено: сервис FlareSolver
- Добавлено: volume для Redis
### Backend логика
4. **dbapp/lyngsatapp/utils.py**
- Добавлено: параметр `task_id` для логирования
- Добавлено: параметр `update_progress` для обновления прогресса
- Добавлено: детальное логирование на всех уровнях
- Добавлено: логирование каждые 10 источников
- Улучшено: обработка ошибок с логированием
5. **dbapp/mainapp/views.py**
- Изменено: `FillLyngsatDataView` теперь запускает асинхронную задачу
- Добавлено: `LyngsatTaskStatusView` - страница отслеживания
- Добавлено: `LyngsatTaskStatusAPIView` - API для проверки статуса
6. **dbapp/mainapp/urls.py**
- Добавлено: `/lyngsat-task-status/` - страница статуса
- Добавлено: `/lyngsat-task-status/<task_id>/` - статус конкретной задачи
- Добавлено: `/api/lyngsat-task-status/<task_id>/` - API endpoint
## Технические детали
### Архитектура
```
User Request → Django View → Celery Task → Redis Broker
Celery Worker
┌───────────┴───────────┐
↓ ↓
LyngSat Parser PostgreSQL
↓ ↓
FlareSolver Save Results
```
### Поток данных
1. **Пользователь отправляет форму**
- Django view получает данные
- Создается асинхронная задача Celery
- Возвращается task_id
- Перенаправление на страницу статуса
2. **Celery Worker обрабатывает задачу**
- Логирует начало обработки
- Вызывает `fill_lyngsat_data` с callback
- Обновляет прогресс через `update_state`
- Логирует каждый шаг
- Сохраняет результат в кеш
3. **Страница статуса отслеживает прогресс**
- JavaScript опрашивает API каждые 2 секунды
- Обновляет прогресс-бар
- Показывает текущий статус
- Отображает результаты при завершении
### Логирование
#### Уровни логирования
- **INFO**: Основные события (начало, завершение, прогресс)
- **DEBUG**: Детальная информация (каждая запись)
- **WARNING**: Некритичные ошибки (спутник не найден)
- **ERROR**: Критичные ошибки (с traceback)
#### Формат логов
```
[Timestamp: Level/Process][Task Name(Task ID)] [Task ID] Message
```
Пример:
```
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Начало обработки данных Lyngsat
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Спутники: Astra 4A, Hotbird 13G
[2024-01-15 10:30:46: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Получено данных по 2 спутникам
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработка спутника 1/2: Astra 4A
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Найдено 150 источников для Astra 4A
[2024-01-15 10:31:05: DEBUG/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Создана запись для Astra 4A 11766.0 МГц
[2024-01-15 10:31:10: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработано 10/150 источников для Astra 4A
```
### API Endpoints
#### GET /api/lyngsat-task-status/<task_id>/
**Ответ при выполнении:**
```json
{
"task_id": "abc123",
"state": "PROGRESS",
"status": "Обработка Astra 4A...",
"current": 1,
"total": 2,
"percent": 50
}
```
**Ответ при успехе:**
```json
{
"task_id": "abc123",
"state": "SUCCESS",
"status": "Задача завершена успешно",
"result": {
"total_satellites": 2,
"total_sources": 300,
"created": 250,
"updated": 50,
"errors": []
}
}
```
**Ответ при ошибке:**
```json
{
"task_id": "abc123",
"state": "FAILURE",
"status": "Ошибка при выполнении задачи",
"error": "Connection timeout"
}
```
## Настройки Celery
### Основные параметры
```python
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'django-db'
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 минут
```
### Переменные окружения
Можно переопределить через `.env`:
```bash
CELERY_BROKER_URL=redis://redis:6379/0
```
## Зависимости
### Обязательные сервисы
1. **Redis** - брокер сообщений Celery
2. **FlareSolver** - обход Cloudflare
3. **PostgreSQL** - хранение данных и результатов
### Python пакеты
- `celery>=5.4.0` - асинхронная обработка
- `django-celery-results>=2.5.1` - хранение результатов
- `redis>=6.4.0` - клиент Redis
## Команды для работы
### Запуск сервисов
```bash
# Redis и FlareSolver
docker-compose up -d redis flaresolverr
# Celery Worker
celery -A dbapp worker --loglevel=info
# Celery Worker в фоне
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log --detach
```
### Мониторинг
```bash
# Просмотр логов
tail -f dbapp/logs/celery_worker.log
# Flower (веб-интерфейс)
pip install flower
celery -A dbapp flower
# Откройте http://localhost:5555
```
### Отладка
```bash
# Проверка Redis
redis-cli ping
# Проверка FlareSolver
curl http://localhost:8191/v1
# Django shell
python manage.py shell
>>> from celery.result import AsyncResult
>>> task = AsyncResult('task_id')
>>> print(task.state, task.info)
```
## Производственное развертывание
### Systemd сервис
```bash
sudo systemctl enable celery-worker
sudo systemctl start celery-worker
sudo systemctl status celery-worker
```
### Supervisor
```bash
sudo supervisorctl start celery-worker
sudo supervisorctl status celery-worker
```
### Docker
Можно добавить Celery worker в docker-compose.yaml:
```yaml
celery-worker:
build: ./dbapp
command: celery -A dbapp worker --loglevel=info
depends_on:
- redis
- db
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
```
## Тестирование
### Проверка системы
```bash
# 1. Проверка Django
python manage.py check
# 2. Проверка миграций
python manage.py migrate --check
# 3. Проверка Celery
celery -A dbapp inspect ping
# 4. Проверка Redis
redis-cli ping
# 5. Проверка FlareSolver
curl http://localhost:8191/v1
```
### Тестовый запуск
```python
# Django shell
python manage.py shell
from lyngsatapp.tasks import fill_lyngsat_data_task
# Запуск задачи
task = fill_lyngsat_data_task.delay(['Astra 4A'], ['europe'])
print(f"Task ID: {task.id}")
# Проверка статуса
print(task.state)
print(task.info)
```
## Метрики и мониторинг
### Что отслеживать
- Количество активных workers
- Количество задач в очереди
- Среднее время выполнения задачи
- Количество ошибок
- Использование памяти Redis
### Инструменты
- **Flower** - веб-интерфейс для Celery
- **Redis Commander** - GUI для Redis
- **Prometheus + Grafana** - метрики и дашборды
## Безопасность
### Рекомендации
1. Используйте пароль для Redis в production
2. Ограничьте доступ к Redis только для localhost
3. Используйте SSL для Redis в production
4. Ограничьте время выполнения задач
5. Логируйте все действия
### Пример конфигурации Redis с паролем
```python
CELERY_BROKER_URL = 'redis://:password@localhost:6379/0'
```
## Масштабирование
### Горизонтальное масштабирование
Запустите несколько workers:
```bash
# Worker 1
celery -A dbapp worker --loglevel=info -n worker1@%h
# Worker 2
celery -A dbapp worker --loglevel=info -n worker2@%h
# Worker 3
celery -A dbapp worker --loglevel=info -n worker3@%h
```
### Приоритеты задач
Можно настроить разные очереди для разных типов задач:
```python
@shared_task(queue='high_priority')
def urgent_task():
pass
@shared_task(queue='low_priority')
def background_task():
pass
```
## Следующие шаги
1. ✅ Применить миграции
2. ✅ Запустить Redis и FlareSolver
3. ✅ Запустить Celery Worker
4. ✅ Протестировать через веб-интерфейс
5. ⏳ Настроить production окружение
6. ⏳ Добавить периодические задачи (Celery Beat)
7. ⏳ Настроить email уведомления
8. ⏳ Настроить мониторинг (Flower)
## Заключение
Система асинхронной обработки данных Lyngsat обеспечивает:
- ✅ Неблокирующий веб-интерфейс
- ✅ Отслеживание прогресса в реальном времени
- ✅ Детальное логирование всех операций
- ✅ Масштабируемость (несколько workers)
- ✅ Надежность (retry при ошибках)
- ✅ Мониторинг и отладка
- ✅ Production-ready решение
Для получения дополнительной помощи:
- Полное руководство: `ASYNC_LYNGSAT_GUIDE.md`
- Быстрый старт: `QUICKSTART_ASYNC.md`
- Документация Celery: https://docs.celeryproject.org/

View File

@@ -1,420 +0,0 @@
# Руководство по асинхронному заполнению данных Lyngsat
## Обзор
Система заполнения данных Lyngsat теперь работает асинхронно с использованием Celery. Это позволяет:
- Не блокировать веб-интерфейс во время долгих операций
- Отслеживать прогресс выполнения задачи в реальном времени
- Просматривать детальные логи обработки
- Получать уведомления о завершении задачи
## Архитектура
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Django │─────▶│ Celery │─────▶│ Redis │
│ Web App │ │ Worker │ │ Broker │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└─────────────▶│ PostgreSQL │
│ Database │
└─────────────┘
```
## Установка и настройка
### 1. Установка зависимостей
```bash
pip install -r requirements.txt
```
Новые зависимости:
- `celery>=5.4.0` - асинхронная обработка задач
- `django-celery-results>=2.5.1` - хранение результатов в БД
### 2. Применение миграций
```bash
cd dbapp
python manage.py migrate
```
Это создаст таблицы для хранения результатов Celery.
### 3. Запуск Redis
Redis используется как брокер сообщений для Celery.
#### Вариант 1: Docker Compose (рекомендуется)
```bash
docker-compose up -d redis
```
#### Вариант 2: Локальная установка
```bash
# Ubuntu/Debian
sudo apt-get install redis-server
sudo systemctl start redis
# macOS
brew install redis
brew services start redis
# Проверка
redis-cli ping
# Должно вернуть: PONG
```
### 4. Запуск FlareSolver
FlareSolver необходим для обхода защиты Cloudflare.
```bash
docker-compose up -d flaresolverr
```
Или отдельно:
```bash
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
```
### 5. Запуск Celery Worker
#### Вариант 1: Используя скрипт
```bash
cd dbapp
./start_celery_worker.sh
```
#### Вариант 2: Напрямую
```bash
cd dbapp
celery -A dbapp worker --loglevel=info
```
#### Вариант 3: В фоновом режиме (Linux/macOS)
```bash
cd dbapp
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log --detach
```
## Использование
### 1. Запуск задачи через веб-интерфейс
1. Откройте страницу действий: `http://localhost:8000/actions/`
2. Нажмите "Заполнить данные Lyngsat"
3. Выберите спутники и регионы
4. Нажмите "Заполнить данные"
5. Вы будете перенаправлены на страницу отслеживания прогресса
### 2. Отслеживание прогресса
На странице статуса задачи вы увидите:
- **Прогресс-бар** с процентом выполнения
- **Текущий статус** (например, "Обработка Astra 4A...")
- **Состояние задачи** (PENDING, PROGRESS, SUCCESS, FAILURE)
- **Результаты** после завершения:
- Количество обработанных спутников
- Количество обработанных источников
- Количество созданных записей
- Количество обновленных записей
- Список ошибок (если есть)
Страница автоматически обновляется каждые 2 секунды.
### 3. Просмотр логов
Логи Celery worker содержат детальную информацию о процессе:
```bash
# Просмотр логов в реальном времени
tail -f dbapp/logs/celery_worker.log
# Поиск по логам
grep "Task" dbapp/logs/celery_worker.log
grep "ERROR" dbapp/logs/celery_worker.log
```
Формат логов:
```
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Начало обработки данных Lyngsat
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Спутники: Astra 4A, Hotbird 13G
[2024-01-15 10:30:46: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Получено данных по 2 спутникам
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработка спутника 1/2: Astra 4A
```
## Технические детали
### Структура задачи
**Файл**: `dbapp/lyngsatapp/tasks.py`
```python
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
def fill_lyngsat_data_task(self, target_sats, regions=None):
# Логирование начала
# Обновление прогресса
# Вызов функции заполнения
# Сохранение результата в кеш
# Обработка ошибок
```
### Обновление прогресса
Функция `fill_lyngsat_data` теперь принимает callback `update_progress`:
```python
def update_progress(current, total, status):
self.update_state(
state='PROGRESS',
meta={
'current': current,
'total': total,
'status': status
}
)
```
### API для проверки статуса
**Endpoint**: `/api/lyngsat-task-status/<task_id>/`
**Ответ**:
```json
{
"task_id": "abc123",
"state": "PROGRESS",
"status": "Обработка Astra 4A...",
"current": 1,
"total": 2,
"percent": 50
}
```
### Логирование
Используется стандартный модуль `logging` Python:
```python
import logging
logger = logging.getLogger(__name__)
logger.info(f"[Task {task_id}] Начало обработки")
logger.debug(f"[Task {task_id}] Детальная информация")
logger.warning(f"[Task {task_id}] Предупреждение")
logger.error(f"[Task {task_id}] Ошибка", exc_info=True)
```
## Настройки Celery
**Файл**: `dbapp/dbapp/settings/base.py`
```python
# Брокер сообщений
CELERY_BROKER_URL = 'redis://localhost:6379/0'
# Хранение результатов
CELERY_RESULT_BACKEND = 'django-db'
# Таймауты
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 минут
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 минут
# Отслеживание прогресса
CELERY_TASK_TRACK_STARTED = True
```
## Мониторинг и отладка
### Flower - веб-интерфейс для мониторинга Celery
Установка:
```bash
pip install flower
```
Запуск:
```bash
celery -A dbapp flower
```
Откройте: `http://localhost:5555`
### Проверка статуса задачи через Django shell
```python
python manage.py shell
from celery.result import AsyncResult
task_id = 'abc123'
task = AsyncResult(task_id)
print(f"State: {task.state}")
print(f"Info: {task.info}")
print(f"Result: {task.result}")
```
### Очистка старых результатов
```bash
# Удалить результаты старше 1 дня
python manage.py celery_results_cleanup --days=1
```
## Решение проблем
### Проблема: Worker не запускается
**Решение**:
1. Проверьте, что Redis запущен: `redis-cli ping`
2. Проверьте настройки в `.env`: `CELERY_BROKER_URL`
3. Проверьте логи: `tail -f logs/celery_worker.log`
### Проблема: Задача зависла в состоянии PENDING
**Решение**:
1. Проверьте, что worker запущен: `ps aux | grep celery`
2. Перезапустите worker
3. Проверьте соединение с Redis
### Проблема: Задача завершается с ошибкой
**Решение**:
1. Проверьте логи worker
2. Проверьте, что FlareSolver запущен: `curl http://localhost:8191/v1`
3. Проверьте, что спутники существуют в базе данных
### Проблема: Прогресс не обновляется
**Решение**:
1. Откройте консоль браузера (F12) и проверьте ошибки
2. Проверьте, что API endpoint доступен: `/api/lyngsat-task-status/<task_id>/`
3. Очистите кеш браузера
## Производственное развертывание
### Systemd сервис для Celery Worker
Создайте файл `/etc/systemd/system/celery-worker.service`:
```ini
[Unit]
Description=Celery Worker for Django Lyngsat
After=network.target redis.service
[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/path/to/dbapp
Environment="PATH=/path/to/venv/bin"
ExecStart=/path/to/venv/bin/celery -A dbapp worker --loglevel=info --logfile=/var/log/celery/worker.log --detach
ExecStop=/path/to/venv/bin/celery -A dbapp control shutdown
Restart=always
[Install]
WantedBy=multi-user.target
```
Запуск:
```bash
sudo systemctl daemon-reload
sudo systemctl enable celery-worker
sudo systemctl start celery-worker
sudo systemctl status celery-worker
```
### Supervisor (альтернатива)
Установка:
```bash
sudo apt-get install supervisor
```
Конфигурация `/etc/supervisor/conf.d/celery.conf`:
```ini
[program:celery-worker]
command=/path/to/venv/bin/celery -A dbapp worker --loglevel=info
directory=/path/to/dbapp
user=www-data
autostart=true
autorestart=true
stdout_logfile=/var/log/celery/worker.log
stderr_logfile=/var/log/celery/worker_error.log
```
Запуск:
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start celery-worker
```
## Дополнительные возможности
### Периодические задачи (Celery Beat)
Для автоматического обновления данных по расписанию:
1. Установите `django-celery-beat`:
```bash
pip install django-celery-beat
```
2. Добавьте в `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
...
'django_celery_beat',
]
```
3. Примените миграции:
```bash
python manage.py migrate django_celery_beat
```
4. Создайте периодическую задачу через админ-панель Django
5. Запустите beat scheduler:
```bash
celery -A dbapp beat --loglevel=info
```
### Уведомления по email
Добавьте в задачу отправку email при завершении:
```python
from django.core.mail import send_mail
@shared_task(bind=True)
def fill_lyngsat_data_task(self, target_sats, regions=None):
# ... обработка ...
# Отправка email
send_mail(
'Задача Lyngsat завершена',
f'Обработано {stats["total_satellites"]} спутников',
'noreply@example.com',
['admin@example.com'],
)
```
## Заключение
Асинхронная обработка данных Lyngsat обеспечивает:
- ✅ Неблокирующий веб-интерфейс
- ✅ Отслеживание прогресса в реальном времени
- ✅ Детальное логирование
- ✅ Масштабируемость (можно запустить несколько workers)
- ✅ Надежность (автоматический retry при ошибках)
Для получения дополнительной помощи обратитесь к документации:
- [Celery Documentation](https://docs.celeryproject.org/)
- [Django Celery Results](https://django-celery-results.readthedocs.io/)

View File

@@ -1,133 +0,0 @@
# Сводка изменений: Модернизация функциональности Lyngsat
## Обзор
Реализована новая функциональность для заполнения данных о транспондерах спутников с сайта Lyngsat через веб-интерфейс.
## Основные изменения
### 1. Удалена карточка с картами 2D/3D
- **Файл**: `dbapp/mainapp/templates/mainapp/actions.html`
- **Изменение**: Заменена карточка "Карты" на карточку "Заполнение данных Lyngsat"
### 2. Создана новая форма для заполнения данных
- **Файл**: `dbapp/mainapp/forms.py`
- **Добавлено**: Класс `FillLyngsatDataForm` с полями:
- `satellites` - мультивыбор спутников из базы данных
- `regions` - мультивыбор регионов (Europe, Asia, America, Atlantic)
### 3. Создан новый view для обработки формы
- **Файл**: `dbapp/mainapp/views.py`
- **Добавлено**: Класс `FillLyngsatDataView` для обработки запросов
- **Функциональность**:
- Валидация формы
- Вызов функции заполнения данных
- Отображение статистики и ошибок
### 4. Добавлен новый URL
- **Файл**: `dbapp/mainapp/urls.py`
- **Добавлено**: `path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data')`
### 5. Создан новый шаблон
- **Файл**: `dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html`
- **Содержимое**:
- Форма с мультивыбором спутников и регионов
- Информационные блоки
- Валидация на стороне клиента
### 6. Доработана функция fill_lyngsat_data
- **Файл**: `dbapp/lyngsatapp/utils.py`
- **Изменения**:
- Добавлен параметр `regions` для выбора регионов
- Реализовано частичное заполнение данных
- Добавлена детальная статистика обработки:
- Количество обработанных спутников
- Количество обработанных источников
- Количество созданных записей
- Количество обновленных записей
- Список ошибок
- Улучшена обработка ошибок (процесс не прерывается при ошибке)
- Добавлена валидация данных перед сохранением
### 7. Исправлен parser.py
- **Файл**: `dbapp/lyngsatapp/parser.py`
- **Изменение**: Удален тестовый код выполнения в конце файла
### 8. Добавлено приложение lyngsatapp в настройки
- **Файл**: `dbapp/dbapp/settings/base.py`
- **Изменение**: Добавлено `'lyngsatapp'` в `INSTALLED_APPS`
### 9. Исправлен admin для LyngSat
- **Файл**: `dbapp/lyngsatapp/admin.py`
- **Изменение**: Обновлены поля в `list_display`, `search_fields`, `ordering` в соответствии с моделью
### 10. Создана миграция для LyngSat
- **Файл**: `dbapp/lyngsatapp/migrations/0001_initial.py`
- **Содержимое**: Создание модели LyngSat
## Новые файлы
1. `dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html` - шаблон формы
2. `dbapp/lyngsatapp/migrations/0001_initial.py` - миграция базы данных
3. `LYNGSAT_FILL_GUIDE.md` - руководство пользователя
4. `CHANGES_SUMMARY.md` - этот файл
## Измененные файлы
1. `dbapp/mainapp/forms.py` - добавлена форма `FillLyngsatDataForm`
2. `dbapp/mainapp/views.py` - добавлен view `FillLyngsatDataView`
3. `dbapp/mainapp/urls.py` - добавлен URL для новой функциональности
4. `dbapp/mainapp/templates/mainapp/actions.html` - заменена карточка
5. `dbapp/lyngsatapp/utils.py` - доработана функция `fill_lyngsat_data`
6. `dbapp/lyngsatapp/parser.py` - удален тестовый код
7. `dbapp/lyngsatapp/admin.py` - исправлены поля админки
8. `dbapp/dbapp/settings/base.py` - добавлено приложение в INSTALLED_APPS
## Технические детали
### Зависимости
- FlareSolver должен быть запущен на `http://localhost:8191`
- Спутники должны быть предварительно добавлены в базу данных
### Модель данных
Модель `LyngSat` содержит следующие поля:
- `id_satellite` - связь со спутником
- `frequency` - частота в МГц
- `polarization` - поляризация сигнала
- `modulation` - тип модуляции
- `standard` - стандарт передачи
- `sym_velocity` - символьная скорость
- `last_update` - дата последнего обновления
- `channel_info` - информация о канале
- `fec` - коэффициент коррекции ошибок
- `url` - ссылка на страницу Lyngsat
### Процесс работы
1. Пользователь выбирает спутники и регионы
2. Система подключается к Lyngsat через FlareSolver
3. Парсит данные для каждого спутника
4. Создает или обновляет записи в базе данных
5. Возвращает статистику обработки
## Тестирование
Выполнены следующие проверки:
-`python manage.py check` - нет ошибок
-`python manage.py makemigrations` - миграция создана
- ✅ Проверка диагностики кода - нет критических ошибок
- ✅ Проверка импортов - все импорты корректны
## Следующие шаги
Для полного тестирования необходимо:
1. Применить миграции: `python manage.py migrate`
2. Запустить FlareSolver: `docker run -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest`
3. Добавить спутники в базу данных (если еще не добавлены)
4. Протестировать форму заполнения данных через веб-интерфейс
## Примечания
- Процесс заполнения может занять продолжительное время (несколько минут на спутник)
- Рекомендуется начинать с небольшого количества спутников
- Все ошибки логируются и отображаются пользователю
- Существующие записи обновляются, новые создаются

View File

@@ -1,18 +1,3 @@
# Руководство по установке асинхронной системы Lyngsat
## Вариант 1: Полная установка с Celery (рекомендуется)
### Шаг 1: Установка зависимостей
```bash
pip install -r dbapp/requirements.txt
```
Это установит:
- `celery>=5.4.0`
- `django-celery-results>=2.5.1`
- И все остальные зависимости
### Шаг 2: Применение миграций ### Шаг 2: Применение миграций
```bash ```bash
@@ -58,57 +43,6 @@ celery -A dbapp worker --loglevel=info
--- ---
## Вариант 2: Базовая установка без Celery
Если вы не хотите использовать асинхронную обработку, система будет работать в синхронном режиме.
### Шаг 1: Установка базовых зависимостей
```bash
# Установите все зависимости кроме Celery
pip install -r dbapp/requirements.txt --ignore-installed celery django-celery-results
```
Или вручную удалите из `requirements.txt`:
- `celery>=5.4.0`
- `django-celery-results>=2.5.1`
Затем:
```bash
pip install -r dbapp/requirements.txt
```
### Шаг 2: Применение миграций
```bash
cd dbapp
python manage.py migrate
```
### Шаг 3: Запуск FlareSolver
```bash
docker-compose up -d flaresolverr
```
### Шаг 4: Запуск Django
```bash
cd dbapp
python manage.py runserver
```
### Ограничения базовой установки
⚠️ **Внимание**: В синхронном режиме:
- Веб-интерфейс будет заблокирован во время обработки
- Нет отслеживания прогресса в реальном времени
- Нет детального логирования
- Обработка может занять много времени
---
## Проверка установки
### Проверка Django ### Проверка Django
```bash ```bash
@@ -136,25 +70,6 @@ curl http://localhost:8191/v1
--- ---
## Решение проблем при установке
### Проблема: ModuleNotFoundError: No module named 'celery'
**Решение 1**: Установите Celery
```bash
pip install celery django-celery-results
```
**Решение 2**: Используйте базовую установку (см. Вариант 2)
### Проблема: Redis connection refused
**Решение**: Запустите Redis
```bash
docker-compose up -d redis
# или
sudo systemctl start redis
```
### Проблема: FlareSolver не отвечает ### Проблема: FlareSolver не отвечает
@@ -165,72 +80,6 @@ docker-compose up -d flaresolverr
docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
``` ```
### Проблема: Миграции не применяются
**Решение**: Проверьте подключение к базе данных
```bash
# Проверьте .env файл
cat dbapp/.env
# Проверьте PostgreSQL
docker-compose up -d db
docker-compose logs db
```
---
## Переменные окружения
Создайте файл `dbapp/.env` (если еще не создан):
```bash
# Database
DB_ENGINE=django.contrib.gis.db.backends.postgis
DB_NAME=geodb
DB_USER=geralt
DB_PASSWORD=123456
DB_HOST=localhost
DB_PORT=5432
# Django
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
# Celery (опционально)
CELERY_BROKER_URL=redis://localhost:6379/0
# FlareSolver
FLARESOLVERR_URL=http://localhost:8191/v1
```
---
## Следующие шаги
После успешной установки:
1. **Прочитайте документацию**:
- `QUICKSTART_ASYNC.md` - быстрый старт
- `ASYNC_LYNGSAT_GUIDE.md` - полное руководство
- `ASYNC_CHANGES_SUMMARY.md` - технические детали
2. **Настройте production окружение** (если необходимо):
- Настройте Systemd/Supervisor для Celery
- Настройте Nginx/Apache
- Настройте SSL
- Настройте мониторинг
3. **Добавьте данные**:
- Добавьте спутники через админ-панель
- Запустите заполнение данных Lyngsat
4. **Настройте мониторинг**:
- Установите Flower для мониторинга Celery
- Настройте логирование
- Настройте алерты
---
## Дополнительные инструменты ## Дополнительные инструменты
@@ -263,39 +112,6 @@ docker run -d -p 5050:80 --name pgadmin \
--- ---
## Обновление системы
### Обновление зависимостей
```bash
pip install --upgrade -r dbapp/requirements.txt
```
### Применение новых миграций
```bash
cd dbapp
python manage.py migrate
```
### Перезапуск сервисов
```bash
# Перезапуск Docker контейнеров
docker-compose restart
# Перезапуск Celery Worker
# Найдите PID процесса
ps aux | grep celery
# Остановите процесс
kill <PID>
# Запустите снова
celery -A dbapp worker --loglevel=info
```
---
## Удаление системы
### Остановка сервисов ### Остановка сервисов
@@ -321,27 +137,31 @@ find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete
find dbapp -path "*/migrations/*.pyc" -delete find dbapp -path "*/migrations/*.pyc" -delete
``` ```
--- # Systemd service для запуска с хоста
[Unit]
Description=Django Application
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/path/to/your/app
Environment=PATH=/path/to/venv/bin
Environment=DATABASE_URL=postgresql://user:pass@localhost/geodb
ExecStart=/path/to/venv/bin/python manage.py runserver 0.0.0.0:8000
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=300
Restart=on-failure
[Install]
WantedBy=multi-user.target
## Поддержка ## Поддержка
Если у вас возникли проблемы:
1. Проверьте логи: 1. Проверьте логи:
- Django: консоль где запущен runserver - Django: консоль где запущен runserver
- Celery: `dbapp/logs/celery_worker.log` - Celery: `dbapp/logs/celery_worker.log`
- Docker: `docker-compose logs` - Docker: `docker-compose logs`
2. Проверьте документацию:
- `ASYNC_LYNGSAT_GUIDE.md`
- `QUICKSTART_ASYNC.md`
- `ASYNC_CHANGES_SUMMARY.md`
3. Проверьте статус сервисов:
```bash
docker-compose ps
ps aux | grep celery
redis-cli ping
```
4. Создайте issue в репозитории с описанием проблемы и логами

View File

@@ -1,78 +0,0 @@
# Руководство по заполнению данных Lyngsat
## Описание
Новая функциональность позволяет автоматически загружать данные о транспондерах спутников с сайта Lyngsat.
## Как использовать
1. **Перейдите на страницу действий**
- Откройте главную страницу приложения
- Нажмите на "Действия" в меню навигации
2. **Откройте форму заполнения данных Lyngsat**
- На странице действий найдите карточку "Заполнение данных Lyngsat"
- Нажмите кнопку "Заполнить данные Lyngsat"
3. **Заполните форму**
- **Выберите спутники**: Выберите один или несколько спутников из списка (удерживайте Ctrl/Cmd для множественного выбора)
- **Выберите регионы**: Выберите регионы для парсинга (Europe, Asia, America, Atlantic)
4. **Запустите процесс**
- Нажмите кнопку "Заполнить данные"
- Дождитесь завершения процесса (может занять несколько минут)
## Что происходит при заполнении
1. Система подключается к сайту Lyngsat через FlareSolver (требуется запущенный сервис)
2. Парсит данные о транспондерах для выбранных спутников
3. Создает или обновляет записи в базе данных:
- Частота
- Поляризация
- Модуляция
- Стандарт (DVB-S, DVB-S2 и т.д.)
- Символьная скорость
- FEC (коэффициент коррекции ошибок)
- Информация о канале
- Дата последнего обновления
## Требования
- **FlareSolver**: Должен быть запущен на `http://localhost:8191`
- **Спутники в базе**: Спутники должны быть предварительно добавлены в базу данных
- **Интернет-соединение**: Требуется для доступа к сайту Lyngsat
## Результаты
После завершения процесса вы увидите:
- Количество обработанных спутников
- Количество обработанных источников
- Количество созданных записей
- Количество обновленных записей
- Список ошибок (если есть)
## Технические детали
### Функция `fill_lyngsat_data`
Функция была доработана для поддержки:
- Частичного заполнения данных
- Выбора регионов
- Детальной статистики обработки
- Обработки ошибок без прерывания процесса
### Изменения в коде
1. **Новая форма**: `FillLyngsatDataForm` в `mainapp/forms.py`
2. **Новый view**: `FillLyngsatDataView` в `mainapp/views.py`
3. **Новый URL**: `/fill-lyngsat-data/` в `mainapp/urls.py`
4. **Новый шаблон**: `fill_lyngsat_data.html`
5. **Обновленная функция**: `fill_lyngsat_data` в `lyngsatapp/utils.py`
6. **Обновленный шаблон**: `actions.html` (заменена карточка с картами)
## Примечания
- Процесс может занять продолжительное время в зависимости от количества выбранных спутников
- Рекомендуется выбирать небольшое количество спутников для первого запуска
- Существующие записи будут обновлены, новые - созданы
- Все ошибки логируются и отображаются пользователю

View File

@@ -10,11 +10,8 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv() load_dotenv()
# Determine the environment from DJANGO_ENVIRONMENT variable
# Defaults to 'development' for safety
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower() ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
if ENVIRONMENT == 'production': if ENVIRONMENT == 'production':

View File

@@ -1,48 +1,55 @@
""" """
Development-specific settings. Development-specific settings.
""" """
from .base import * from .base import *
# ============================================================================ # ============================================================================
# DEBUG CONFIGURATION # DEBUG CONFIGURATION
# ============================================================================ # ============================================================================
DEBUG = True DEBUG = True
# ============================================================================ # ============================================================================
# ALLOWED HOSTS # ALLOWED HOSTS
# ============================================================================ # ============================================================================
# Allow all hosts in development # Allow all hosts in development
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
# ============================================================================ # ============================================================================
# INSTALLED APPS - Development additions # INSTALLED APPS - Development additions
# ============================================================================ # ============================================================================
INSTALLED_APPS += [ INSTALLED_APPS += [
'debug_toolbar', 'debug_toolbar',
] ]
# ============================================================================ # ============================================================================
# MIDDLEWARE - Development additions # MIDDLEWARE - Development additions
# ============================================================================ # ============================================================================
# Add debug toolbar middleware at the beginning # Add debug toolbar middleware at the beginning
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
# ============================================================================ # ============================================================================
# DEBUG TOOLBAR CONFIGURATION # DEBUG TOOLBAR CONFIGURATION
# ============================================================================ # ============================================================================
INTERNAL_IPS = [ INTERNAL_IPS = [
'127.0.0.1', '127.0.0.1',
] ]
# ============================================================================ # ============================================================================
# EMAIL CONFIGURATION # EMAIL CONFIGURATION
# ============================================================================ # ============================================================================
# Use console backend for development # Use console backend for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# ============================================================================
# STATIC FILES CONFIGURATION FOR DEVELOPMENT
# ============================================================================
# Define STATIC_ROOT for collectstatic command to work in development
STATIC_ROOT = BASE_DIR.parent / "staticfiles"

View File

@@ -1,37 +1,30 @@
# Generated by Django 5.2.7 on 2025-11-10 20:03 # Generated by Django 5.2.7 on 2025-11-12 14:21
import django.db.models.deletion from django.db import migrations, models
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
class Migration(migrations.Migration): initial = True
initial = True dependencies = [
]
dependencies = [
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'), operations = [
] migrations.CreateModel(
name='LyngSat',
operations = [ fields=[
migrations.CreateModel( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
name='LyngSat', ('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
fields=[ ('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления')),
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')), ('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')),
('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), ('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')),
('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Время')), ('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')),
('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')), ],
('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')), options={
('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')), 'verbose_name': 'Источник LyngSat',
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник')), 'verbose_name_plural': 'Источники LyngSat',
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция')), },
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация')), ),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт')), ]
],
options={
'verbose_name': 'Источник LyngSat',
'verbose_name_plural': 'Источники LyngSat',
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-11 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyngsatapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='lyngsat',
name='last_update',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления'),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.7 on 2025-11-12 14:21
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('lyngsatapp', '0001_initial'),
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='lyngsat',
name='id_satellite',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AddField(
model_name='lyngsat',
name='modulation',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция'),
),
migrations.AddField(
model_name='lyngsat',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AddField(
model_name='lyngsat',
name='standard',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт'),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,204 +1,211 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36 # Generated by Django 5.2.7 on 2025-11-12 14:21
import django.contrib.gis.db.models.fields import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions import django.db.models.expressions
import mainapp.models from django.conf import settings
from django.conf import settings from django.db import migrations, models
from django.db import migrations, models
class Migration(migrations.Migration):
class Migration(migrations.Migration):
initial = True
initial = True
dependencies = [
dependencies = [ ('lyngsatapp', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Mirror', name='Band',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')), ('name', models.CharField(help_text='Название диапазона', max_length=50, unique=True, verbose_name='Название')),
], ('border_start', models.FloatField(blank=True, null=True, verbose_name='Нижняя граница диапазона, МГц')),
options={ ('border_end', models.FloatField(blank=True, null=True, verbose_name='Верхняя граница диапазона, МГц')),
'verbose_name': 'Зеркало', ],
'verbose_name_plural': 'Зеркала', options={
}, 'verbose_name': 'Диапазон',
), 'verbose_name_plural': 'Диапазоны',
migrations.CreateModel( 'ordering': ['name'],
name='Modulation', },
fields=[ ),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), migrations.CreateModel(
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')), name='Mirror',
], fields=[
options={ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name': 'Модуляция', ('name', models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала')),
'verbose_name_plural': 'Модуляции', ],
}, options={
), 'verbose_name': 'Зеркало',
migrations.CreateModel( 'verbose_name_plural': 'Зеркала',
name='Polarization', 'ordering': ['name'],
fields=[ },
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ),
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')), migrations.CreateModel(
], name='Modulation',
options={ fields=[
'verbose_name': 'Поляризация', ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name_plural': 'Поляризация', ('name', models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция')),
}, ],
), options={
migrations.CreateModel( 'verbose_name': 'Модуляция',
name='Satellite', 'verbose_name_plural': 'Модуляции',
fields=[ 'ordering': ['name'],
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), },
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')), ),
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')), migrations.CreateModel(
], name='Parameter',
options={ fields=[
'verbose_name': 'Спутник', ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name_plural': 'Спутники', ('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')),
}, ('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц')),
), ('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')),
migrations.CreateModel( ('snr', models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ')),
name='SigmaParMark', ],
fields=[ options={
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 'verbose_name': 'ВЧ загрузка',
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')), 'verbose_name_plural': 'ВЧ загрузки',
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')), },
], ),
options={ migrations.CreateModel(
'verbose_name': 'Отметка', name='Polarization',
'verbose_name_plural': 'Отметки', fields=[
}, ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
), ('name', models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация')),
migrations.CreateModel( ],
name='Standard', options={
fields=[ 'verbose_name': 'Поляризация',
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 'verbose_name_plural': 'Поляризация',
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')), 'ordering': ['name'],
], },
options={ ),
'verbose_name': 'Стандарт', migrations.CreateModel(
'verbose_name_plural': 'Стандарты', name='Satellite',
}, fields=[
), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
migrations.CreateModel( ('name', models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника')),
name='CustomUser', ('norad', models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID')),
fields=[ ('undersat_point', models.FloatField(blank=True, help_text='Подспутниковая точка в градусах. Восточное полушарие с +, западное с -', null=True, verbose_name='Подспутниковая точка, градусы')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('url', models.URLField(blank=True, help_text='Ссылка на сайт, где можно проверить информацию', null=True, verbose_name='Ссылка на источник')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')), ('comment', models.TextField(blank=True, help_text='Любой возможный комменатрий', null=True, verbose_name='Комментарий')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('launch_date', models.DateField(blank=True, help_text='Дата запуска спутника', null=True, verbose_name='Дата запуска')),
], ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
options={ ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
'verbose_name': 'Пользователь', ],
'verbose_name_plural': 'Пользователи', options={
}, 'verbose_name': 'Спутник',
), 'verbose_name_plural': 'Спутники',
migrations.CreateModel( 'ordering': ['name'],
name='ObjItem', },
fields=[ ),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), migrations.CreateModel(
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')), name='SigmaParameter',
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')), fields=[
], ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
options={ ('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте')),
'verbose_name': 'Объект', ('status', models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус')),
'verbose_name_plural': 'Объекты', ('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')),
}, ('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
), ('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц')),
migrations.CreateModel( ('power', models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм')),
name='Parameter', ('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')),
fields=[ ('snr', models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('packets', models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')), ('datetime_begin', models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')), ('datetime_end', models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения')),
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), ],
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')), options={
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')), 'verbose_name': 'ВЧ sigma',
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')), 'verbose_name_plural': 'ВЧ sigma',
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')), },
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')), ),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')), migrations.CreateModel(
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')), name='SigmaParMark',
], fields=[
options={ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name': 'ВЧ загрузка', ('mark', models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала')),
'verbose_name_plural': 'ВЧ загрузки', ('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время')),
}, ],
), options={
migrations.CreateModel( 'verbose_name': 'Отметка',
name='SourceType', 'verbose_name_plural': 'Отметки',
fields=[ 'ordering': ['-timestamp'],
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), },
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')), ),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')), migrations.CreateModel(
], name='Source',
options={ fields=[
'verbose_name': 'Тип источника', ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name_plural': 'Типы источников', ('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата')),
}, ('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников')),
), ('coords_reference', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, ещё кем-то проверенные (WGS84)', null=True, srid=4326, verbose_name='Координаты справочные')),
migrations.CreateModel( ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
name='SigmaParameter', ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
fields=[ ],
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), options={
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')), 'verbose_name': 'Источник',
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')), 'verbose_name_plural': 'Источники',
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')), },
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')), ),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')), migrations.CreateModel(
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')), name='Standard',
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')), fields=[
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')), ('name', models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт')),
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')), ],
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')), options={
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')), 'verbose_name': 'Стандарт',
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')), 'verbose_name_plural': 'Стандарты',
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')), 'ordering': ['name'],
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')), },
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')), ),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')), migrations.CreateModel(
], name='CustomUser',
options={ fields=[
'verbose_name': 'ВЧ sigma', ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
'verbose_name_plural': 'ВЧ sigma', ('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя')),
}, ('user', models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
), ],
migrations.CreateModel( options={
name='Geo', 'verbose_name': 'Пользователь',
fields=[ 'verbose_name_plural': 'Пользователи',
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 'ordering': ['user__username'],
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')), },
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')), ),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')), migrations.CreateModel(
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')), name='Geo',
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')), fields=[
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')), ('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время')),
('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')), ('location', models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение')),
('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')), ('comment', models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий')),
('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')), ('is_average', models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')), ('coords', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации')),
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')), ('mirrors', models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')), ],
], options={
options={ 'verbose_name': 'Гео',
'verbose_name': 'Гео', 'verbose_name_plural': 'Гео',
'verbose_name_plural': 'Гео', 'ordering': ['-timestamp'],
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')], },
}, ),
), migrations.CreateModel(
migrations.AddIndex( name='ObjItem',
model_name='parameter', fields=[
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
), ('name', models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта')),
migrations.AddIndex( ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
model_name='parameter', ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'), ('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем')),
), ('lyngsat_source', models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat')),
] ],
options={
'verbose_name': 'Объект',
'verbose_name_plural': 'Объекты',
'ordering': ['-updated_at'],
},
),
]

View File

@@ -0,0 +1,150 @@
# Generated by Django 5.2.7 on 2025-11-12 14:21
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objitem',
name='transponder',
field=models.ForeignKey(blank=True, help_text='Транспондер, с помощью которого была получена точка', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder', to='mapsapp.transponders', verbose_name='Транспондер'),
),
migrations.AddField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AddField(
model_name='geo',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AddField(
model_name='parameter',
name='modulation',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция'),
),
migrations.AddField(
model_name='parameter',
name='objitem',
field=models.OneToOneField(blank=True, help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parameter_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AddField(
model_name='parameter',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AddField(
model_name='satellite',
name='band',
field=models.ManyToManyField(blank=True, help_text='Диапазоны работы спутника', related_name='bands', to='mainapp.band', verbose_name='Диапазоны'),
),
migrations.AddField(
model_name='satellite',
name='created_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='satellite',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AddField(
model_name='parameter',
name='id_satellite',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AddField(
model_name='sigmaparameter',
name='id_satellite',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AddField(
model_name='sigmaparameter',
name='modulation',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция'),
),
migrations.AddField(
model_name='sigmaparameter',
name='parameter',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ'),
),
migrations.AddField(
model_name='sigmaparameter',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AddField(
model_name='sigmaparameter',
name='mark',
field=models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка'),
),
migrations.AddField(
model_name='source',
name='created_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='source',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AddField(
model_name='objitem',
name='source',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source', to='mainapp.source', verbose_name='ИРИ'),
),
migrations.AddField(
model_name='sigmaparameter',
name='standard',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт'),
),
migrations.AddField(
model_name='parameter',
name='standard',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
),
migrations.AddConstraint(
model_name='geo',
constraint=models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
),
]

View File

@@ -1,35 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-31 13:56
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
),
migrations.AddField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
),
migrations.AddField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-31 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-01 07:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
]
operations = [
migrations.RemoveField(
model_name='geo',
name='id_user_add',
),
migrations.RemoveField(
model_name='objitem',
name='id_user_add',
),
migrations.RemoveField(
model_name='parameter',
name='id_user_add',
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-07 19:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
]
operations = [
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
),
]

View File

@@ -1,290 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.core.validators
import django.db.models.deletion
import django.db.models.expressions
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0005_alter_geo_objitem'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='customuser',
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
),
migrations.AlterModelOptions(
name='geo',
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
),
migrations.AlterModelOptions(
name='mirror',
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
),
migrations.AlterModelOptions(
name='modulation',
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
),
migrations.AlterModelOptions(
name='objitem',
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
),
migrations.AlterModelOptions(
name='polarization',
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
),
migrations.AlterModelOptions(
name='satellite',
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
),
migrations.AlterModelOptions(
name='sigmaparmark',
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
),
migrations.AlterModelOptions(
name='sourcetype',
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
),
migrations.AlterModelOptions(
name='standard',
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
),
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
),
migrations.AlterField(
model_name='customuser',
name='user',
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
),
migrations.AlterField(
model_name='geo',
name='comment',
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
),
migrations.AlterField(
model_name='geo',
name='coords',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
),
migrations.AlterField(
model_name='geo',
name='coords_kupsat',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
),
migrations.AlterField(
model_name='geo',
name='coords_valid',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
),
migrations.AlterField(
model_name='geo',
name='distance_coords_kup',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
),
migrations.AlterField(
model_name='geo',
name='distance_kup_valid',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
),
migrations.AlterField(
model_name='geo',
name='is_average',
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
),
migrations.AlterField(
model_name='geo',
name='location',
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
),
migrations.AlterField(
model_name='geo',
name='mirrors',
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
),
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='geo',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='mirror',
name='name',
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
),
migrations.AlterField(
model_name='modulation',
name='name',
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
),
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AlterField(
model_name='objitem',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
),
migrations.AlterField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AlterField(
model_name='parameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='parameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
),
migrations.AlterField(
model_name='polarization',
name='name',
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='satellite',
name='name',
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
),
migrations.AlterField(
model_name='satellite',
name='norad',
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_begin',
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_end',
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='packets',
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='power',
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='status',
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='transfer',
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='mark',
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='sourcetype',
name='name',
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
),
migrations.AlterField(
model_name='sourcetype',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='standard',
name='name',
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-10 18:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
]
operations = [
migrations.RemoveField(
model_name='parameter',
name='objitems',
),
migrations.AddField(
model_name='parameter',
name='objitem',
field=models.OneToOneField(blank=True, help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parameter_obj', to='mainapp.objitem', verbose_name='Объект'),
),
]

View File

@@ -1,63 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-11 13:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
]
operations = [
migrations.RemoveField(
model_name='sourcetype',
name='objitem',
),
migrations.AddField(
model_name='objitem',
name='source_type_id',
field=models.ForeignKey(blank=True, help_text='Тип источника сигнала', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_sourcetype', to='mainapp.sourcetype', verbose_name='Тип источника'),
),
migrations.AlterField(
model_name='parameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='parameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='power',
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм'),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-11 19:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyngsatapp', '0002_alter_lyngsat_last_update'),
('mainapp', '0008_remove_sourcetype_objitem_objitem_source_type_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='objitem',
name='source_type_id',
),
migrations.AddField(
model_name='objitem',
name='lyngsat_source',
field=models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat'),
),
migrations.DeleteModel(
name='SourceType',
),
]

View File

@@ -201,6 +201,32 @@ class Standard(models.Model):
verbose_name_plural = "Стандарты" verbose_name_plural = "Стандарты"
ordering = ["name"] ordering = ["name"]
class Band(models.Model):
name = models.CharField(
max_length=50,
unique=True,
verbose_name="Название",
help_text="Название диапазона",
)
border_start = models.FloatField(
blank=True,
null=True,
verbose_name="Нижняя граница диапазона, МГц"
)
border_end = models.FloatField(
blank=True,
null=True,
verbose_name="Верхняя граница диапазона, МГц"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Диапазон"
verbose_name_plural = "Диапазоны"
ordering = ["name"]
class Satellite(models.Model): class Satellite(models.Model):
""" """
@@ -223,6 +249,66 @@ class Satellite(models.Model):
verbose_name="NORAD ID", verbose_name="NORAD ID",
help_text="Идентификатор NORAD для отслеживания спутника", help_text="Идентификатор NORAD для отслеживания спутника",
) )
band = models.ManyToManyField(
Band,
related_name="bands",
verbose_name="Диапазоны",
blank=True,
help_text="Диапазоны работы спутника",
)
undersat_point = models.FloatField(
blank=True,
null=True,
verbose_name="Подспутниковая точка, градусы",
help_text="Подспутниковая точка в градусах. Восточное полушарие с +, западное с -",
)
url = models.URLField(
blank=True,
null=True,
verbose_name="Ссылка на источник",
help_text="Ссылка на сайт, где можно проверить информацию",
)
comment = models.TextField(
blank=True,
null=True,
verbose_name="Комментарий",
help_text="Любой возможный комменатрий",
)
launch_date = models.DateField(
blank=True,
null=True,
verbose_name="Дата запуска",
help_text="Дата запуска спутника",
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания",
help_text="Дата и время создания записи",
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="satellite_created",
null=True,
blank=True,
verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись",
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения",
)
updated_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="satellite_updated",
null=True,
blank=True,
verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись",
)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -283,11 +369,73 @@ class ObjItemManager(models.Manager):
return self.get_queryset().by_user(user) return self.get_queryset().by_user(user)
class Source(models.Model):
"""
Модель источника сигнала.
"""
coords_kupsat = gis.PointField(
srid=4326,
null=True,
blank=True,
verbose_name="Координаты Кубсата",
help_text="Координаты, полученные от кубсата (WGS84)",
)
coords_valid = gis.PointField(
srid=4326,
null=True,
blank=True,
verbose_name="Координаты оперативников",
help_text="Координаты, предоставленные оперативным отделом (WGS84)",
)
coords_reference = gis.PointField(
srid=4326,
null=True,
blank=True,
verbose_name="Координаты справочные",
help_text="Координаты, ещё кем-то проверенные (WGS84)",
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания",
help_text="Дата и время создания записи",
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="source_created",
null=True,
blank=True,
verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись",
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения",
)
updated_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="source_updated",
null=True,
blank=True,
verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись",
)
class Meta:
verbose_name = "Источник"
verbose_name_plural = "Источники"
class ObjItem(models.Model): class ObjItem(models.Model):
""" """
Модель объекта (источника сигнала). Модель точки ГЛ.
Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации и типе источника. Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации.
""" """
# Основные поля # Основные поля
@@ -299,6 +447,22 @@ class ObjItem(models.Model):
db_index=True, db_index=True,
help_text="Название объекта/источника сигнала", help_text="Название объекта/источника сигнала",
) )
source = models.ForeignKey(
Source,
on_delete=models.CASCADE,
null=True,
verbose_name="ИРИ",
related_name="source",
)
transponder = models.ForeignKey(
"mapsapp.Transponders",
on_delete=models.SET_NULL,
related_name="transponder",
null=True,
blank=True,
verbose_name="Транспондер",
help_text="Транспондер, с помощью которого была получена точка",
)
# Метаданные # Метаданные
created_at = models.DateTimeField( created_at = models.DateTimeField(
@@ -679,46 +843,32 @@ class Geo(models.Model):
verbose_name="Координата геолокации", verbose_name="Координата геолокации",
help_text="Основные координаты геолокации (WGS84)", 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)",
)
# Вычисляемые поля - расстояния # Вычисляемые поля - расстояния
distance_coords_kup = models.GeneratedField( # distance_coords_kup = models.GeneratedField(
expression=functions.Distance("coords", "coords_kupsat") / 1000, # expression=functions.Distance("coords", "coords_kupsat") / 1000,
output_field=models.FloatField(), # output_field=models.FloatField(),
db_persist=True, # db_persist=True,
null=True, # null=True,
blank=True, # blank=True,
verbose_name="Расстояние между кубсатом и гео, км", # verbose_name="Расстояние между кубсатом и гео, км",
) # )
distance_coords_valid = models.GeneratedField( # distance_coords_valid = models.GeneratedField(
expression=functions.Distance("coords", "coords_valid") / 1000, # expression=functions.Distance("coords", "coords_valid") / 1000,
output_field=models.FloatField(), # output_field=models.FloatField(),
db_persist=True, # db_persist=True,
null=True, # null=True,
blank=True, # blank=True,
verbose_name="Расстояние между гео и оперативным отделом, км", # verbose_name="Расстояние между гео и оперативным отделом, км",
) # )
distance_kup_valid = models.GeneratedField( # distance_kup_valid = models.GeneratedField(
expression=functions.Distance("coords_valid", "coords_kupsat") / 1000, # expression=functions.Distance("coords_valid", "coords_kupsat") / 1000,
output_field=models.FloatField(), # output_field=models.FloatField(),
db_persist=True, # db_persist=True,
null=True, # null=True,
blank=True, # blank=True,
verbose_name="Расстояние между кубсатом и оперативным отделом, км", # verbose_name="Расстояние между кубсатом и оперативным отделом, км",
) # )
# Связи # Связи
mirrors = models.ManyToManyField( mirrors = models.ManyToManyField(

View File

@@ -95,8 +95,8 @@
</div> </div>
<h3 class="card-title mb-0">Добавление транспондеров</h3> <h3 class="card-title mb-0">Добавление транспондеров</h3>
</div> </div>
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p> <p class="card-text">Добавьте список транспондеров в базу данных.</p>
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning disabled"> <a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
Добавить транспондеры Добавить транспондеры
</a> </a>
</div> </div>

View File

@@ -408,6 +408,40 @@
</div> </div>
</div> </div>
<!-- Source Type Filter -->
<div class="mb-2">
<label class="form-label">Тип источника:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1"
value="1" {% if has_source_type == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0"
value="0" {% if has_source_type == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_source_type_0">Нет</label>
</div>
</div>
</div>
<!-- Sigma Filter -->
<div class="mb-2">
<label class="form-label">Sigma:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1"
value="1" {% if has_sigma == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0"
value="0" {% if has_sigma == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_sigma_0">Нет</label>
</div>
</div>
</div>
<!-- Date Filter --> <!-- Date Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Дата ГЛ:</label> <label class="form-label">Дата ГЛ:</label>
@@ -759,6 +793,8 @@
setupRadioLikeCheckboxes('has_kupsat'); setupRadioLikeCheckboxes('has_kupsat');
setupRadioLikeCheckboxes('has_valid'); setupRadioLikeCheckboxes('has_valid');
setupRadioLikeCheckboxes('has_source_type');
setupRadioLikeCheckboxes('has_sigma');
// Date range quick selection functions // Date range quick selection functions
window.setDateRange = function (period) { window.setDateRange = function (period) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,44 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36 # Generated by Django 5.2.7 on 2025-11-12 14:21
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions import django.db.models.expressions
import django.db.models.functions.math import django.db.models.functions.math
import mainapp.models import mainapp.models
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('mainapp', '0001_initial'), ('mainapp', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Transponders', name='Transponders',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')), ('name', models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')), ('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')), ('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')), ('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')), ('zone_name', models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны')),
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')), ('snr', models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, verbose_name='Полоса')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')), ('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')), ('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
], ('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')),
options={ ('created_by', models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_created', to='mainapp.customuser', verbose_name='Создан пользователем')),
'verbose_name': 'Транспондер', ('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
'verbose_name_plural': 'Транспондеры', ('sat_id', models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
}, ('updated_by', models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_updated', to='mainapp.customuser', verbose_name='Изменен пользователем')),
), ],
] options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
'ordering': ['sat_id', 'downlink'],
'indexes': [models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'), models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx')],
},
),
]

View File

@@ -1,64 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.core.validators
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='transponders',
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
),
migrations.AlterField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
),
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
),
migrations.AlterField(
model_name='transponders',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
),
migrations.AlterField(
model_name='transponders',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AlterField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
),
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
),
]

View File

@@ -6,7 +6,7 @@ from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs from django.db.models.functions import Abs
# Local imports # Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser
class Transponders(models.Model): class Transponders(models.Model):
@@ -29,22 +29,22 @@ class Transponders(models.Model):
blank=True, blank=True,
null=True, null=True,
verbose_name="Downlink", verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)" # help_text="Частота downlink в МГц (0-50000)"
) )
frequency_range = models.FloatField( frequency_range = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)" # help_text="Полоса частот в МГц (0-1000)"
) )
uplink = models.FloatField( uplink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Uplink", verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)" # help_text="Частота uplink в МГц (0-50000)"
) )
zone_name = models.CharField( zone_name = models.CharField(
max_length=255, max_length=255,
@@ -54,6 +54,41 @@ class Transponders(models.Model):
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера" help_text="Название зоны покрытия транспондера"
) )
snr = models.FloatField(
blank=True,
null=True,
verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)"
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания",
help_text="Дата и время создания записи",
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="transponder_created",
null=True,
blank=True,
verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись",
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения",
)
updated_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
related_name="transponder_updated",
null=True,
blank=True,
verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись",
)
# Связи # Связи
polarization = models.ForeignKey( polarization = models.ForeignKey(
@@ -88,17 +123,17 @@ class Transponders(models.Model):
verbose_name="Перенос" verbose_name="Перенос"
) )
def clean(self): # def clean(self):
"""Валидация на уровне модели""" # """Валидация на уровне модели"""
super().clean() # super().clean()
# Проверка что downlink и uplink заданы # # Проверка что downlink и uplink заданы
if self.downlink and self.uplink: # if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи # # Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink: # if self.uplink < self.downlink:
raise ValidationError({ # raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink' # 'uplink': 'Частота uplink обычно выше частоты downlink'
}) # })
def __str__(self): def __str__(self):
if self.name: if self.name:

View File

@@ -1,165 +1,169 @@
# Standard library imports # Standard library imports
import json import json
import re import re
from io import BytesIO from io import BytesIO
# Third-party imports # Third-party imports
import requests import requests
# Local imports # Local imports
from mainapp.models import Polarization, Satellite from mainapp.models import Polarization, Satellite
from .models import Transponders from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str): def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items(): for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']: for name in value['satellites']:
if name['other_names'] is None: if name['other_names'] is None:
name['other_names'] = '' name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower(): if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id'] return pos, name['id']
return '', '' return '', ''
def get_footprint_data(position: str = 62) -> dict: def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе""" """Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}") response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status() response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match: if match:
json_str = match.group(1) json_str = match.group(1)
try: try:
data = json.loads(json_str) data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[]) return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e) print("Ошибка парсинга JSON:", e)
else: else:
print("Нужных данных не найдено") print("Нужных данных не найдено")
return {} return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict: def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице""" """Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url) response = requests.get(url)
response.raise_for_status() response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL) match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match: if match:
json_str = match.group(1) json_str = match.group(1)
try: try:
data = json.loads(json_str) data = json.loads(json_str)
# Файл json на диске для достоверности # Файл json на диске для достоверности
with open('data.json', 'w') as jf: with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2) json.dump(data, jf, indent=2)
return data return data
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e) print("Ошибка парсинга JSON:", e)
else: else:
print("Нужных данных не найдено") print("Нужных данных не найдено")
return {} return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]: def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = [] names = []
for beam in footprint_data: for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']: if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append( names.append(
{ {
"name": beam['name'], "name": beam['name'],
"fullname": beam['fullname'][8:] "fullname": beam['fullname'][8:]
} }
) )
return names return names
def get_band_names(satellite_name: str) -> list[str]: def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data() data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name) pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos) footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id) names = get_names_footprints_for_satellite(footprints, sat_id)
return names return names
def parse_transponders_from_json(filepath: str): def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf: with open(filepath, encoding="utf-8") as jf:
data = json.load(jf) data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items(): for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items(): for zone, trans in trans_zone.items():
for tran in trans: for tran in trans:
f_b, f_e = tran["freq"][0].split("-") f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3) f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3) f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create( tran_obj = Transponders.objects.create(
name=tran["name"], name=tran["name"],
frequency=f, frequency=f,
frequency_range=f_range, frequency_range=f_range,
zone_name=zone, zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]), polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name) sat_id=Satellite.objects.get(name__iexact=sat_name)
) )
tran_obj.save() tran_obj.save()
# Third-party imports (additional) # Third-party imports (additional)
from lxml import etree from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO): def parse_transponders_from_xml(data_in: BytesIO, user=None):
tree = etree.parse(data_in) tree = etree.parse(data_in)
ns = { ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance', 'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos', 'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
} }
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]: for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0] name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name: if name == 'X' or 'DONT USE' in name:
continue continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns) norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {} zones = {}
for zone in beams: for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name, "name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
} }
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders: for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id] tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-' # p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']: match tr_data['pol']:
case 'Horizontal': case 'Horizontal':
pol = 'Горизонтальная' pol = 'Горизонтальная'
case 'Vertical': case 'Vertical':
pol = 'Вертикальная' pol = 'Вертикальная'
case 'CircularRight': case 'CircularRight':
pol = 'Правая' pol = 'Правая'
case 'CircularLeft': case 'CircularLeft':
pol = 'Левая' pol = 'Левая'
case _: case _:
pol = '-' pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol) pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create( sat_obj, _ = Satellite.objects.get_or_create(
name=name, name=name,
defaults={ defaults={
"norad": int(norad[0]) if norad else -1 "norad": int(norad[0]) if norad else -1
}) })
trans_obj, _ = Transponders.objects.get_or_create( trans_obj, created = Transponders.objects.get_or_create(
polarization=pol_obj, polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000, downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000, uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000, frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name, name=tr_name,
defaults={ defaults={
"zone_name": tr_data['name'], "zone_name": tr_data['name'],
"sat_id": sat_obj, "sat_id": sat_obj,
} }
) )
trans_obj.save() if user:
if created:
trans_obj.created_by = user
trans_obj.updated_by = user
trans_obj.save()

View File

@@ -0,0 +1,165 @@
# Standard library imports
import json
import re
from io import BytesIO
# Third-party imports
import requests
# Local imports
from mainapp.models import Polarization, Satellite
from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']:
if name['other_names'] is None:
name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id']
return '', ''
def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url)
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
# Файл json на диске для достоверности
with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2)
return data
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = []
for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append(
{
"name": beam['name'],
"fullname": beam['fullname'][8:]
}
)
return names
def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id)
return names
def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf:
data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items():
for tran in trans:
f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create(
name=tran["name"],
frequency=f,
frequency_range=f_range,
zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name)
)
tran_obj.save()
# Third-party imports (additional)
from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO):
tree = etree.parse(data_in)
ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
}
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name:
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
}
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']:
case 'Horizontal':
pol = 'Горизонтальная'
case 'Vertical':
pol = 'Вертикальная'
case 'CircularRight':
pol = 'Правая'
case 'CircularLeft':
pol = 'Левая'
case _:
pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
})
trans_obj, _ = Transponders.objects.get_or_create(
polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name,
defaults={
"zone_name": tr_data['name'],
"sat_id": sat_obj,
}
)
trans_obj.save()

View File

@@ -41,6 +41,7 @@ dependencies = [
"scikit-learn>=1.7.2", "scikit-learn>=1.7.2",
"selenium>=4.38.0", "selenium>=4.38.0",
"setuptools>=80.9.0", "setuptools>=80.9.0",
"uvicorn>=0.38.0",
] ]

15
dbapp/uv.lock generated
View File

@@ -386,6 +386,7 @@ dependencies = [
{ name = "scikit-learn" }, { name = "scikit-learn" },
{ name = "selenium" }, { name = "selenium" },
{ name = "setuptools" }, { name = "setuptools" },
{ name = "uvicorn" },
] ]
[package.metadata] [package.metadata]
@@ -426,6 +427,7 @@ requires-dist = [
{ name = "scikit-learn", specifier = ">=1.7.2" }, { name = "scikit-learn", specifier = ">=1.7.2" },
{ name = "selenium", specifier = ">=4.38.0" }, { name = "selenium", specifier = ">=4.38.0" },
{ name = "setuptools", specifier = ">=80.9.0" }, { name = "setuptools", specifier = ">=80.9.0" },
{ name = "uvicorn", specifier = ">=0.38.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -1578,6 +1580,19 @@ socks = [
{ name = "pysocks" }, { name = "pysocks" },
] ]
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]
[[package]] [[package]]
name = "vine" name = "vine"
version = "5.1.0" version = "5.1.0"

View File

@@ -37,41 +37,18 @@ services:
- CAPTCHA_SOLVER=none - CAPTCHA_SOLVER=none
networks: networks:
- app-network - app-network
# web: # nginx:
# build: # image: nginx:alpine
# context: ./dbapp # container_name: nginx
# dockerfile: Dockerfile
# container_name: django-app-dev
# restart: unless-stopped # restart: unless-stopped
# environment:
# - DEBUG=True
# - ENVIRONMENT=development
# - DJANGO_SETTINGS_MODULE=dbapp.settings.development
# - SECRET_KEY=django-insecure-dev-key-change-in-production
# - DB_ENGINE=django.contrib.gis.db.backends.postgis
# - DB_NAME=geodb
# - DB_USER=geralt
# - DB_PASSWORD=123456
# - DB_HOST=db
# - DB_PORT=5432
# - ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# ports: # ports:
# - "8000:8000" # - "80:80"
# # - "443:443"
# volumes: # volumes:
# # Монтируем только код приложения, не весь проект # - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./dbapp/dbapp:/app/dbapp # - ./nginx/conf.d:/etc/nginx/conf.d:ro
# - ./dbapp/mainapp:/app/mainapp # - ./dbapp/staticfiles:/app/staticfiles:ro
# - ./dbapp/mapsapp:/app/mapsapp
# - ./dbapp/lyngsatapp:/app/lyngsatapp
# - ./dbapp/static:/app/static
# - ./dbapp/manage.py:/app/manage.py
# - static_volume_dev:/app/staticfiles
# - media_volume_dev:/app/media
# - logs_volume_dev:/app/logs
# depends_on:
# db:
# condition: service_healthy
# networks: # networks:
# - app-network # - app-network
@@ -92,9 +69,6 @@ services:
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
# static_volume_dev:
# media_volume_dev:
# logs_volume_dev:
# tileserver_config_dev: # tileserver_config_dev:
networks: networks:

17
nginx/conf.d/default.conf Normal file
View File

@@ -0,0 +1,17 @@
server {
listen 80;
server_name localhost;
location /static/ {
alias /home/vesemir/DataStorage/dbapp/staticfiles/;
expires 30d;
}
location / {
proxy_pass http://host.docker.internal:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

39
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,39 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Log format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Proxy settings
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
# gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Include server blocks
include /etc/nginx/conf.d/*.conf;
}