Compare commits
2 Commits
902eb23bd8
...
7126974aed
| Author | SHA1 | Date | |
|---|---|---|---|
| 7126974aed | |||
| 73ce06deec |
@@ -1,6 +1,3 @@
|
|||||||
# Production Environment Variables
|
|
||||||
# ВАЖНО: Измените все значения перед деплоем!
|
|
||||||
|
|
||||||
# Django Settings
|
# Django Settings
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
|
|||||||
@@ -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/
|
|
||||||
@@ -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/)
|
|
||||||
@@ -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. Протестировать форму заполнения данных через веб-интерфейс
|
|
||||||
|
|
||||||
## Примечания
|
|
||||||
|
|
||||||
- Процесс заполнения может занять продолжительное время (несколько минут на спутник)
|
|
||||||
- Рекомендуется начинать с небольшого количества спутников
|
|
||||||
- Все ошибки логируются и отображаются пользователю
|
|
||||||
- Существующие записи обновляются, новые создаются
|
|
||||||
@@ -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 в репозитории с описанием проблемы и логами
|
|
||||||
|
|||||||
@@ -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` (заменена карточка с картами)
|
|
||||||
|
|
||||||
## Примечания
|
|
||||||
|
|
||||||
- Процесс может занять продолжительное время в зависимости от количества выбранных спутников
|
|
||||||
- Рекомендуется выбирать небольшое количество спутников для первого запуска
|
|
||||||
- Существующие записи будут обновлены, новые - созданы
|
|
||||||
- Все ошибки логируются и отображаются пользователю
|
|
||||||
@@ -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':
|
||||||
|
|||||||
@@ -46,3 +46,10 @@ INTERNAL_IPS = [
|
|||||||
|
|
||||||
# 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"
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# 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
|
|
||||||
import mainapp.models
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +8,6 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -20,14 +17,10 @@ class Migration(migrations.Migration):
|
|||||||
('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')),
|
||||||
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
|
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||||
('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||||
('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления')),
|
||||||
('channel_info', models.CharField(blank=True, max_length=20, 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='Коэффициент коррекции ошибок')),
|
('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')),
|
||||||
('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')),
|
('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')),
|
||||||
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', 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='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={
|
options={
|
||||||
'verbose_name': 'Источник LyngSat',
|
'verbose_name': 'Источник LyngSat',
|
||||||
|
|||||||
@@ -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='Дата посленего обновления'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
38
dbapp/lyngsatapp/migrations/0002_initial.py
Normal file
38
dbapp/lyngsatapp/migrations/0002_initial.py
Normal 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='Стандарт'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -30,7 +30,9 @@ from .models import (
|
|||||||
Mirror,
|
Mirror,
|
||||||
Geo,
|
Geo,
|
||||||
ObjItem,
|
ObjItem,
|
||||||
CustomUser
|
CustomUser,
|
||||||
|
Band,
|
||||||
|
Source
|
||||||
)
|
)
|
||||||
from .filters import (
|
from .filters import (
|
||||||
GeoKupDistanceFilter,
|
GeoKupDistanceFilter,
|
||||||
@@ -96,10 +98,10 @@ class CustomUserInline(admin.StackedInline):
|
|||||||
class LocationForm(forms.ModelForm):
|
class LocationForm(forms.ModelForm):
|
||||||
latitude_geo = forms.FloatField(required=False, label="Широта")
|
latitude_geo = forms.FloatField(required=False, label="Широта")
|
||||||
longitude_geo = forms.FloatField(required=False, label="Долгота")
|
longitude_geo = forms.FloatField(required=False, label="Долгота")
|
||||||
latitude_kupsat = forms.FloatField(required=False, label="Широта")
|
# latitude_kupsat = forms.FloatField(required=False, label="Широта")
|
||||||
longitude_kupsat = forms.FloatField(required=False, label="Долгота")
|
# longitude_kupsat = forms.FloatField(required=False, label="Долгота")
|
||||||
latitude_valid = forms.FloatField(required=False, label="Широта")
|
# latitude_valid = forms.FloatField(required=False, label="Широта")
|
||||||
longitude_valid = forms.FloatField(required=False, label="Долгота")
|
# longitude_valid = forms.FloatField(required=False, label="Долгота")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Geo
|
model = Geo
|
||||||
@@ -110,12 +112,12 @@ class LocationForm(forms.ModelForm):
|
|||||||
if self.instance and self.instance.coords:
|
if self.instance and self.instance.coords:
|
||||||
self.fields['latitude_geo'].initial = self.instance.coords[1]
|
self.fields['latitude_geo'].initial = self.instance.coords[1]
|
||||||
self.fields['longitude_geo'].initial = self.instance.coords[0]
|
self.fields['longitude_geo'].initial = self.instance.coords[0]
|
||||||
if self.instance and self.instance.coords_kupsat:
|
# if self.instance and self.instance.coords_kupsat:
|
||||||
self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
||||||
self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
||||||
if self.instance and self.instance.coords_valid:
|
# if self.instance and self.instance.coords_valid:
|
||||||
self.fields['latitude_valid'].initial = self.instance.coords_valid[1]
|
# self.fields['latitude_valid'].initial = self.instance.coords_valid[1]
|
||||||
self.fields['longitude_valid'].initial = self.instance.coords_valid[0]
|
# self.fields['longitude_valid'].initial = self.instance.coords_valid[0]
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit=False)
|
instance = super().save(commit=False)
|
||||||
@@ -125,15 +127,15 @@ class LocationForm(forms.ModelForm):
|
|||||||
if lat is not None and lon is not None:
|
if lat is not None and lon is not None:
|
||||||
instance.coords = Point(lon, lat, srid=4326)
|
instance.coords = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
lat = self.cleaned_data.get('latitude_kupsat')
|
# lat = self.cleaned_data.get('latitude_kupsat')
|
||||||
lon = self.cleaned_data.get('longitude_kupsat')
|
# lon = self.cleaned_data.get('longitude_kupsat')
|
||||||
if lat is not None and lon is not None:
|
# if lat is not None and lon is not None:
|
||||||
instance.coords_kupsat = Point(lon, lat, srid=4326)
|
# instance.coords_kupsat = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
lat = self.cleaned_data.get('latitude_valid')
|
# lat = self.cleaned_data.get('latitude_valid')
|
||||||
lon = self.cleaned_data.get('longitude_valid')
|
# lon = self.cleaned_data.get('longitude_valid')
|
||||||
if lat is not None and lon is not None:
|
# if lat is not None and lon is not None:
|
||||||
instance.coords_valid = Point(lon, lat, srid=4326)
|
# instance.coords_valid = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
instance.save()
|
instance.save()
|
||||||
@@ -146,23 +148,26 @@ class GeoInline(admin.StackedInline):
|
|||||||
verbose_name = "Гео"
|
verbose_name = "Гео"
|
||||||
verbose_name_plural = "Гео"
|
verbose_name_plural = "Гео"
|
||||||
form = LocationForm
|
form = LocationForm
|
||||||
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
prefetch_related = ("mirrors",)
|
prefetch_related = ("mirrors",)
|
||||||
autocomplete_fields = ('mirrors',)
|
autocomplete_fields = ('mirrors',)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
("Основная информация", {
|
||||||
"fields": ("mirrors", "location", "distance_coords_kup",
|
"fields": ("mirrors", "location",
|
||||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
|
# "distance_coords_kup",
|
||||||
|
# "distance_coords_valid",
|
||||||
|
# "distance_kup_valid",
|
||||||
|
"timestamp", "comment",)
|
||||||
}),
|
}),
|
||||||
("Координаты: геолокация", {
|
("Координаты: геолокация", {
|
||||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||||
}),
|
}),
|
||||||
("Координаты: Кубсат", {
|
# ("Координаты: Кубсат", {
|
||||||
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||||
}),
|
# }),
|
||||||
("Координаты: Оперативный отдел", {
|
# ("Координаты: Оперативный отдел", {
|
||||||
"fields": ("longitude_valid", "latitude_valid", "coords_valid"),
|
# "fields": ("longitude_valid", "latitude_valid", "coords_valid"),
|
||||||
}),
|
# }),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -490,9 +495,12 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
@admin.register(Satellite)
|
@admin.register(Satellite)
|
||||||
class SatelliteAdmin(BaseAdmin):
|
class SatelliteAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели Satellite."""
|
"""Админ-панель для модели Satellite."""
|
||||||
list_display = ("name",)
|
list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at")
|
||||||
search_fields = ("name",)
|
search_fields = ("name", "norad")
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
filter_horizontal = ("band",)
|
||||||
|
autocomplete_fields = ("band",)
|
||||||
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Mirror)
|
@admin.register(Mirror)
|
||||||
@@ -516,22 +524,25 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
"""
|
"""
|
||||||
form = LocationForm
|
form = LocationForm
|
||||||
|
|
||||||
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
("Основная информация", {
|
||||||
"fields": ("mirrors", "location", "distance_coords_kup",
|
"fields": ("mirrors", "location",
|
||||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment")
|
# "distance_coords_kup",
|
||||||
|
# "distance_coords_valid",
|
||||||
|
# "distance_kup_valid",
|
||||||
|
"timestamp", "comment", "transponder")
|
||||||
}),
|
}),
|
||||||
("Координаты: геолокация", {
|
("Координаты: геолокация", {
|
||||||
"fields": ("longitude_geo", "latitude_geo", "coords")
|
"fields": ("longitude_geo", "latitude_geo", "coords")
|
||||||
}),
|
}),
|
||||||
("Координаты: Кубсат", {
|
# ("Координаты: Кубсат", {
|
||||||
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
||||||
}),
|
# }),
|
||||||
("Координаты: Оперативный отдел", {
|
# ("Координаты: Оперативный отдел", {
|
||||||
"fields": ("longitude_valid", "latitude_valid", "coords_valid")
|
# "fields": ("longitude_valid", "latitude_valid", "coords_valid")
|
||||||
}),
|
# }),
|
||||||
)
|
)
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
@@ -539,14 +550,15 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
"location",
|
"location",
|
||||||
"mirrors_names",
|
"mirrors_names",
|
||||||
"geo_coords",
|
"geo_coords",
|
||||||
"kupsat_coords",
|
# "kupsat_coords",
|
||||||
"valid_coords",
|
# "valid_coords",
|
||||||
"is_average",
|
"is_average",
|
||||||
)
|
)
|
||||||
list_display_links = ("formatted_timestamp",)
|
list_display_links = ("formatted_timestamp",)
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("mirrors", MultiSelectRelatedDropdownFilter),
|
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||||
|
("transponder", MultiSelectRelatedDropdownFilter),
|
||||||
"is_average",
|
"is_average",
|
||||||
("location", MultiSelectDropdownFilter),
|
("location", MultiSelectDropdownFilter),
|
||||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
@@ -555,6 +567,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
search_fields = (
|
search_fields = (
|
||||||
"mirrors__name",
|
"mirrors__name",
|
||||||
"location",
|
"location",
|
||||||
|
"transponder__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
autocomplete_fields = ("mirrors", )
|
autocomplete_fields = ("mirrors", )
|
||||||
@@ -569,7 +582,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.prefetch_related("mirrors")
|
return qs.prefetch_related("mirrors", "transponder")
|
||||||
|
|
||||||
def mirrors_names(self, obj):
|
def mirrors_names(self, obj):
|
||||||
"""Отображает список зеркал через запятую."""
|
"""Отображает список зеркал через запятую."""
|
||||||
@@ -596,27 +609,27 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
|||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
geo_coords.short_description = "Координаты геолокации"
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
|
|
||||||
def kupsat_coords(self, obj):
|
# def kupsat_coords(self, obj):
|
||||||
"""Отображает координаты Кубсата в формате широта/долгота."""
|
# """Отображает координаты Кубсата в формате широта/долгота."""
|
||||||
if obj.coords_kupsat is None:
|
# if obj.coords_kupsat is None:
|
||||||
return "-"
|
# return "-"
|
||||||
longitude = obj.coords_kupsat.coords[0]
|
# longitude = obj.coords_kupsat.coords[0]
|
||||||
latitude = obj.coords_kupsat.coords[1]
|
# latitude = obj.coords_kupsat.coords[1]
|
||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
# lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
# lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
# return f"{lat} {lon}"
|
||||||
kupsat_coords.short_description = "Координаты Кубсата"
|
# kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
def valid_coords(self, obj):
|
# def valid_coords(self, obj):
|
||||||
"""Отображает координаты оперативного отдела в формате широта/долгота."""
|
# """Отображает координаты оперативного отдела в формате широта/долгота."""
|
||||||
if obj.coords_valid is None:
|
# if obj.coords_valid is None:
|
||||||
return "-"
|
# return "-"
|
||||||
longitude = obj.coords_valid.coords[0]
|
# longitude = obj.coords_valid.coords[0]
|
||||||
latitude = obj.coords_valid.coords[1]
|
# latitude = obj.coords_valid.coords[1]
|
||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
# lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
# lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
# return f"{lat} {lon}"
|
||||||
valid_coords.short_description = "Координаты оперативного отдела"
|
# valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -642,11 +655,11 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
"modulation",
|
"modulation",
|
||||||
"snr",
|
"snr",
|
||||||
"geo_coords",
|
"geo_coords",
|
||||||
"kupsat_coords",
|
# "kupsat_coords",
|
||||||
"valid_coords",
|
# "valid_coords",
|
||||||
"distance_geo_kup",
|
# "distance_geo_kup",
|
||||||
"distance_geo_valid",
|
# "distance_geo_valid",
|
||||||
"distance_kup_valid",
|
# "distance_kup_valid",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
@@ -733,29 +746,29 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
freq.short_description = "Частота, МГц"
|
freq.short_description = "Частота, МГц"
|
||||||
freq.admin_order_field = "parameter_obj__frequency"
|
freq.admin_order_field = "parameter_obj__frequency"
|
||||||
|
|
||||||
def distance_geo_kup(self, obj):
|
# def distance_geo_kup(self, obj):
|
||||||
"""Отображает расстояние между геолокацией и Кубсатом."""
|
# """Отображает расстояние между геолокацией и Кубсатом."""
|
||||||
geo = obj.geo_obj
|
# geo = obj.geo_obj
|
||||||
if not geo or geo.distance_coords_kup is None:
|
# if not geo or geo.distance_coords_kup is None:
|
||||||
return "-"
|
# return "-"
|
||||||
return round(geo.distance_coords_kup, 3)
|
# return round(geo.distance_coords_kup, 3)
|
||||||
distance_geo_kup.short_description = "Гео-куб, км"
|
# distance_geo_kup.short_description = "Гео-куб, км"
|
||||||
|
|
||||||
def distance_geo_valid(self, obj):
|
# def distance_geo_valid(self, obj):
|
||||||
"""Отображает расстояние между геолокацией и оперативным отделом."""
|
# """Отображает расстояние между геолокацией и оперативным отделом."""
|
||||||
geo = obj.geo_obj
|
# geo = obj.geo_obj
|
||||||
if not geo or geo.distance_coords_valid is None:
|
# if not geo or geo.distance_coords_valid is None:
|
||||||
return "-"
|
# return "-"
|
||||||
return round(geo.distance_coords_valid, 3)
|
# return round(geo.distance_coords_valid, 3)
|
||||||
distance_geo_valid.short_description = "Гео-опер, км"
|
# distance_geo_valid.short_description = "Гео-опер, км"
|
||||||
|
|
||||||
def distance_kup_valid(self, obj):
|
# def distance_kup_valid(self, obj):
|
||||||
"""Отображает расстояние между Кубсатом и оперативным отделом."""
|
# """Отображает расстояние между Кубсатом и оперативным отделом."""
|
||||||
geo = obj.geo_obj
|
# geo = obj.geo_obj
|
||||||
if not geo or geo.distance_kup_valid is None:
|
# if not geo or geo.distance_kup_valid is None:
|
||||||
return "-"
|
# return "-"
|
||||||
return round(geo.distance_kup_valid, 3)
|
# return round(geo.distance_kup_valid, 3)
|
||||||
distance_kup_valid.short_description = "Куб-опер, км"
|
# distance_kup_valid.short_description = "Куб-опер, км"
|
||||||
|
|
||||||
def pol(self, obj):
|
def pol(self, obj):
|
||||||
"""Отображает поляризацию из связанного параметра."""
|
"""Отображает поляризацию из связанного параметра."""
|
||||||
@@ -831,3 +844,33 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
valid_coords.short_description = "Координаты оперативного отдела"
|
valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Band)
|
||||||
|
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
|
"""Админ-панель для модели Band."""
|
||||||
|
list_display = ("name", "border_start", "border_end")
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Source)
|
||||||
|
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||||
|
"""Админ-панель для модели Source."""
|
||||||
|
list_display = ("id", "created_at", "updated_at")
|
||||||
|
list_filter = (
|
||||||
|
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
)
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
("Координаты: геолокация", {
|
||||||
|
"fields": ("coords_kupsat", "coords_valid", "coords_reference")
|
||||||
|
}),
|
||||||
|
("Метаданные", {
|
||||||
|
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||||
|
"classes": ("collapse",)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -14,116 +13,57 @@ 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(
|
||||||
|
name='Band',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Название диапазона', max_length=50, unique=True, verbose_name='Название')),
|
||||||
|
('border_start', models.FloatField(blank=True, null=True, verbose_name='Нижняя граница диапазона, МГц')),
|
||||||
|
('border_end', models.FloatField(blank=True, null=True, verbose_name='Верхняя граница диапазона, МГц')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Диапазон',
|
||||||
|
'verbose_name_plural': 'Диапазоны',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Mirror',
|
name='Mirror',
|
||||||
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(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Зеркало',
|
'verbose_name': 'Зеркало',
|
||||||
'verbose_name_plural': 'Зеркала',
|
'verbose_name_plural': 'Зеркала',
|
||||||
|
'ordering': ['name'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Modulation',
|
name='Modulation',
|
||||||
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(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
|
('name', models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Модуляция',
|
'verbose_name': 'Модуляция',
|
||||||
'verbose_name_plural': 'Модуляции',
|
'verbose_name_plural': 'Модуляции',
|
||||||
},
|
'ordering': ['name'],
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Polarization',
|
|
||||||
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='Поляризация')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Поляризация',
|
|
||||||
'verbose_name_plural': 'Поляризация',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Satellite',
|
|
||||||
fields=[
|
|
||||||
('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')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Спутник',
|
|
||||||
'verbose_name_plural': 'Спутники',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SigmaParMark',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
|
|
||||||
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Отметка',
|
|
||||||
'verbose_name_plural': 'Отметки',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Standard',
|
|
||||||
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='Стандарт')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Стандарт',
|
|
||||||
'verbose_name_plural': 'Стандарты',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CustomUser',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Пользователь',
|
|
||||||
'verbose_name_plural': 'Пользователи',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ObjItem',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
|
|
||||||
('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='Пользователь')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Объект',
|
|
||||||
'verbose_name_plural': 'Объекты',
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Parameter',
|
name='Parameter',
|
||||||
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')),
|
||||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
('frequency', models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц')),
|
||||||
('freq_range', models.FloatField(blank=True, default=0, 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, null=True, verbose_name='Символьная скорость, БОД')),
|
('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')),
|
||||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
|
('snr', models.FloatField(blank=True, default=0, 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='parameter_added', to='mainapp.customuser', 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', to='mainapp.modulation', verbose_name='Модуляция')),
|
|
||||||
('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='Спутник')),
|
|
||||||
('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='Стандарт')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'ВЧ загрузка',
|
'verbose_name': 'ВЧ загрузка',
|
||||||
@@ -131,74 +71,141 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SourceType',
|
name='Polarization',
|
||||||
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=50, unique=True, verbose_name='Тип источника')),
|
('name', models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, 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='Гео')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Тип источника',
|
'verbose_name': 'Поляризация',
|
||||||
'verbose_name_plural': 'Типы источников',
|
'verbose_name_plural': 'Поляризация',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Satellite',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника')),
|
||||||
|
('norad', models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID')),
|
||||||
|
('undersat_point', models.FloatField(blank=True, help_text='Подспутниковая точка в градусах. Восточное полушарие с +, западное с -', null=True, verbose_name='Подспутниковая точка, градусы')),
|
||||||
|
('url', models.URLField(blank=True, help_text='Ссылка на сайт, где можно проверить информацию', null=True, verbose_name='Ссылка на источник')),
|
||||||
|
('comment', models.TextField(blank=True, help_text='Любой возможный комменатрий', null=True, verbose_name='Комментарий')),
|
||||||
|
('launch_date', models.DateField(blank=True, help_text='Дата запуска спутника', null=True, verbose_name='Дата запуска')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Спутник',
|
||||||
|
'verbose_name_plural': 'Спутники',
|
||||||
|
'ordering': ['name'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SigmaParameter',
|
name='SigmaParameter',
|
||||||
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')),
|
||||||
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
|
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте')),
|
||||||
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
|
('status', models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус')),
|
||||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
('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, МГц')),
|
('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='Полоса частот, МГц')),
|
('freq_range', models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц')),
|
||||||
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
|
('power', models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм')),
|
||||||
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
('bod_velocity', models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД')),
|
||||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
|
('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='ОСШ, Дб')),
|
||||||
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
|
('packets', models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность')),
|
||||||
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
|
('datetime_begin', models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения')),
|
||||||
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
|
('datetime_end', models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения')),
|
||||||
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', 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='Модуляция')),
|
|
||||||
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', 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_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='Стандарт')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'ВЧ sigma',
|
'verbose_name': 'ВЧ sigma',
|
||||||
'verbose_name_plural': 'ВЧ sigma',
|
'verbose_name_plural': 'ВЧ sigma',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SigmaParMark',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mark', models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала')),
|
||||||
|
('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Отметка',
|
||||||
|
'verbose_name_plural': 'Отметки',
|
||||||
|
'ordering': ['-timestamp'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Source',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('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='Координаты справочные')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Источник',
|
||||||
|
'verbose_name_plural': 'Источники',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Standard',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Стандарт',
|
||||||
|
'verbose_name_plural': 'Стандарты',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('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='Пользователь')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Пользователь',
|
||||||
|
'verbose_name_plural': 'Пользователи',
|
||||||
|
'ordering': ['user__username'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Geo',
|
name='Geo',
|
||||||
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')),
|
||||||
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
|
('timestamp', models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', 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, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение')),
|
||||||
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
|
('comment', models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий')),
|
||||||
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
|
('is_average', models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое')),
|
||||||
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
|
('coords', 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, null=True, srid=4326, verbose_name='Координаты оперативников')),
|
('mirrors', models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
|
||||||
('is_average', models.BooleanField(blank=True, 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='Расстояние между купсатом и гео, км')),
|
|
||||||
('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='Расстояние между гео и оперативным отделом, км')),
|
|
||||||
('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='Расстояние между купсатом и оперативным отделом, км')),
|
|
||||||
('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='Пользователь')),
|
|
||||||
('mirrors', models.ManyToManyField(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': 'Гео',
|
||||||
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
|
'ordering': ['-timestamp'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.CreateModel(
|
||||||
model_name='parameter',
|
name='ObjItem',
|
||||||
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
|
fields=[
|
||||||
),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
migrations.AddIndex(
|
('name', models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта')),
|
||||||
model_name='parameter',
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания')),
|
||||||
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
|
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения')),
|
||||||
|
('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'],
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
150
dbapp/mainapp/migrations/0002_initial.py
Normal file
150
dbapp/mainapp/migrations/0002_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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='Изменен пользователем'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Дата последнего изменения'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Гео'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Объект'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Мощность, дБм'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -792,6 +792,20 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Filter by source type (lyngsat_source)
|
||||||
|
has_source_type = request.GET.get("has_source_type")
|
||||||
|
if has_source_type == "1":
|
||||||
|
objects = objects.filter(lyngsat_source__isnull=False)
|
||||||
|
elif has_source_type == "0":
|
||||||
|
objects = objects.filter(lyngsat_source__isnull=True)
|
||||||
|
|
||||||
|
# Filter by sigma (sigma parameters)
|
||||||
|
has_sigma = request.GET.get("has_sigma")
|
||||||
|
if has_sigma == "1":
|
||||||
|
objects = objects.filter(parameter_obj__sigma_parameter__isnull=False)
|
||||||
|
elif has_sigma == "0":
|
||||||
|
objects = objects.filter(parameter_obj__sigma_parameter__isnull=True)
|
||||||
|
|
||||||
if search_query:
|
if search_query:
|
||||||
search_query = search_query.strip()
|
search_query = search_query.strip()
|
||||||
if search_query:
|
if search_query:
|
||||||
@@ -955,23 +969,19 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
comment = obj.geo_obj.comment or "-"
|
comment = obj.geo_obj.comment or "-"
|
||||||
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
|
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
|
||||||
|
|
||||||
# Check if LyngSat source is linked
|
|
||||||
source_type = "ТВ" if obj.lyngsat_source else "-"
|
source_type = "ТВ" if obj.lyngsat_source else "-"
|
||||||
|
|
||||||
# Check if SigmaParameter is linked
|
|
||||||
has_sigma = False
|
has_sigma = False
|
||||||
sigma_info = "-"
|
sigma_info = "-"
|
||||||
if param:
|
if param:
|
||||||
sigma_count = param.sigma_parameter.count()
|
sigma_count = param.sigma_parameter.count()
|
||||||
if sigma_count > 0:
|
if sigma_count > 0:
|
||||||
has_sigma = True
|
has_sigma = True
|
||||||
# Get first sigma parameter for preview
|
|
||||||
first_sigma = param.sigma_parameter.first()
|
first_sigma = param.sigma_parameter.first()
|
||||||
if first_sigma:
|
if first_sigma:
|
||||||
sigma_freq = f"{first_sigma.frequency:.3f}" if first_sigma.frequency else "-"
|
sigma_freq = f"{first_sigma.transfer_frequency:.3f}" if first_sigma.transfer_frequency else "-"
|
||||||
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
|
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
|
||||||
sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
|
sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
|
||||||
# Сокращаем поляризацию
|
|
||||||
sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
|
sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
|
||||||
sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
|
sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
|
||||||
|
|
||||||
@@ -1008,6 +1018,10 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
modulations = Modulation.objects.all()
|
modulations = Modulation.objects.all()
|
||||||
polarizations = Polarization.objects.all()
|
polarizations = Polarization.objects.all()
|
||||||
|
|
||||||
|
# Get the new filter values
|
||||||
|
has_source_type = request.GET.get("has_source_type")
|
||||||
|
has_sigma = request.GET.get("has_sigma")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"satellites": satellites,
|
"satellites": satellites,
|
||||||
"selected_satellite_id": selected_sat_id,
|
"selected_satellite_id": selected_sat_id,
|
||||||
@@ -1035,6 +1049,8 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
"has_valid": has_valid,
|
"has_valid": has_valid,
|
||||||
"date_from": date_from,
|
"date_from": date_from,
|
||||||
"date_to": date_to,
|
"date_to": date_to,
|
||||||
|
"has_source_type": has_source_type,
|
||||||
|
"has_sigma": has_sigma,
|
||||||
"modulations": modulations,
|
"modulations": modulations,
|
||||||
"polarizations": polarizations,
|
"polarizations": polarizations,
|
||||||
"full_width_page": True,
|
"full_width_page": True,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 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
|
||||||
@@ -20,18 +20,25 @@ class Migration(migrations.Migration):
|
|||||||
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='Название зоны')),
|
||||||
|
('snr', models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, verbose_name='Полоса')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', 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='Перенос')),
|
('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='Перенос')),
|
||||||
('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_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='Создан пользователем')),
|
||||||
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', 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='Поляризация')),
|
||||||
|
('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={
|
options={
|
||||||
'verbose_name': 'Транспондер',
|
'verbose_name': 'Транспондер',
|
||||||
'verbose_name_plural': 'Транспондеры',
|
'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')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def parse_transponders_from_json(filepath: str):
|
|||||||
# 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 = {
|
||||||
@@ -151,7 +151,7 @@ def parse_transponders_from_xml(data_in: BytesIO):
|
|||||||
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,
|
||||||
@@ -162,4 +162,8 @@ def parse_transponders_from_xml(data_in: BytesIO):
|
|||||||
"sat_id": sat_obj,
|
"sat_id": sat_obj,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if user:
|
||||||
|
if created:
|
||||||
|
trans_obj.created_by = user
|
||||||
|
trans_obj.updated_by = user
|
||||||
trans_obj.save()
|
trans_obj.save()
|
||||||
165
dbapp/mapsapp/utils.py.backup
Normal file
165
dbapp/mapsapp/utils.py.backup
Normal 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()
|
||||||
@@ -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
15
dbapp/uv.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -38,40 +38,17 @@ services:
|
|||||||
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
17
nginx/conf.d/default.conf
Normal 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
39
nginx/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user