Compare commits
2 Commits
b24ef940ce
...
65e6c9a323
| Author | SHA1 | Date | |
|---|---|---|---|
| 65e6c9a323 | |||
| 1b345a3fd9 |
396
ASYNC_CHANGES_SUMMARY.md
Normal file
396
ASYNC_CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
# Сводка изменений: Асинхронная обработка данных 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/
|
||||||
420
ASYNC_LYNGSAT_GUIDE.md
Normal file
420
ASYNC_LYNGSAT_GUIDE.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# Руководство по асинхронному заполнению данных 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/)
|
||||||
133
CHANGES_SUMMARY.md
Normal file
133
CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Сводка изменений: Модернизация функциональности 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. Протестировать форму заполнения данных через веб-интерфейс
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
- Процесс заполнения может занять продолжительное время (несколько минут на спутник)
|
||||||
|
- Рекомендуется начинать с небольшого количества спутников
|
||||||
|
- Все ошибки логируются и отображаются пользователю
|
||||||
|
- Существующие записи обновляются, новые создаются
|
||||||
102
DEPLOYMENT_INSTRUCTIONS.md
Normal file
102
DEPLOYMENT_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Инструкция по развертыванию изменений
|
||||||
|
|
||||||
|
## Шаг 1: Применение миграций
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Это создаст таблицу `lyngsatapp_lyngsat` в базе данных.
|
||||||
|
|
||||||
|
## Шаг 2: Запуск FlareSolver (если еще не запущен)
|
||||||
|
|
||||||
|
FlareSolver необходим для обхода защиты Cloudflare на сайте Lyngsat.
|
||||||
|
|
||||||
|
### Вариант 1: Docker
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Docker Compose
|
||||||
|
Добавьте в `docker-compose.yaml`:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
flaresolverr:
|
||||||
|
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
container_name: flaresolverr
|
||||||
|
ports:
|
||||||
|
- "8191:8191"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем запустите:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d flaresolverr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 3: Проверка работоспособности
|
||||||
|
|
||||||
|
1. Запустите сервер разработки:
|
||||||
|
```bash
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Откройте браузер и перейдите на:
|
||||||
|
```
|
||||||
|
http://localhost:8000/actions/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Найдите карточку "Заполнение данных Lyngsat" и нажмите на кнопку
|
||||||
|
|
||||||
|
4. Выберите один-два спутника для тестирования
|
||||||
|
|
||||||
|
5. Выберите регионы (например, только Europe)
|
||||||
|
|
||||||
|
6. Нажмите "Заполнить данные" и дождитесь завершения
|
||||||
|
|
||||||
|
## Шаг 4: Проверка результатов
|
||||||
|
|
||||||
|
1. Перейдите в админ-панель Django:
|
||||||
|
```
|
||||||
|
http://localhost:8000/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Откройте раздел "Lyngsatapp" → "Источники LyngSat"
|
||||||
|
|
||||||
|
3. Проверьте, что данные загружены корректно
|
||||||
|
|
||||||
|
## Возможные проблемы и решения
|
||||||
|
|
||||||
|
### Проблема: FlareSolver не отвечает
|
||||||
|
**Решение**: Проверьте, что FlareSolver запущен:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8191/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: Спутники не найдены в базе
|
||||||
|
**Решение**: Убедитесь, что спутники добавлены в базу данных. Используйте функцию "Добавление списка спутников" на странице действий.
|
||||||
|
|
||||||
|
### Проблема: Долгое выполнение
|
||||||
|
**Решение**: Это нормально. Процесс может занять несколько минут на спутник. Начните с 1-2 спутников для тестирования.
|
||||||
|
|
||||||
|
### Проблема: Ошибки при парсинге
|
||||||
|
**Решение**: Проверьте логи. Некоторые ошибки (например, некорректные частоты) не критичны и не прерывают процесс.
|
||||||
|
|
||||||
|
## Откат изменений (если необходимо)
|
||||||
|
|
||||||
|
Если нужно откатить изменения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Откатить миграцию
|
||||||
|
python manage.py migrate lyngsatapp zero
|
||||||
|
|
||||||
|
# Откатить изменения в коде
|
||||||
|
git checkout HEAD -- dbapp/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Дополнительная информация
|
||||||
|
|
||||||
|
- Подробное руководство пользователя: `LYNGSAT_FILL_GUIDE.md`
|
||||||
|
- Сводка изменений: `CHANGES_SUMMARY.md`
|
||||||
|
- Документация по проекту: `README.md`
|
||||||
347
INSTALLATION_GUIDE.md
Normal file
347
INSTALLATION_GUIDE.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# Руководство по установке асинхронной системы Lyngsat
|
||||||
|
|
||||||
|
## Вариант 1: Полная установка с Celery (рекомендуется)
|
||||||
|
|
||||||
|
### Шаг 1: Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r dbapp/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Это установит:
|
||||||
|
- `celery>=5.4.0`
|
||||||
|
- `django-celery-results>=2.5.1`
|
||||||
|
- И все остальные зависимости
|
||||||
|
|
||||||
|
### Шаг 2: Применение миграций
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Это создаст:
|
||||||
|
- Таблицу `lyngsatapp_lyngsat` для данных Lyngsat
|
||||||
|
- Таблицы `django_celery_results_*` для результатов Celery
|
||||||
|
|
||||||
|
### Шаг 3: Запуск сервисов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск Redis и FlareSolver
|
||||||
|
docker-compose up -d redis flaresolverr
|
||||||
|
|
||||||
|
# Проверка
|
||||||
|
redis-cli ping # Должно вернуть PONG
|
||||||
|
curl http://localhost:8191/v1 # Должно вернуть JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Запуск приложения
|
||||||
|
|
||||||
|
**Терминал 1 - Django:**
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
**Терминал 2 - Celery Worker:**
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
celery -A dbapp worker --loglevel=info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 5: Тестирование
|
||||||
|
|
||||||
|
1. Откройте `http://localhost:8000/actions/`
|
||||||
|
2. Нажмите "Заполнить данные Lyngsat"
|
||||||
|
3. Выберите спутники и регионы
|
||||||
|
4. Наблюдайте за прогрессом!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вариант 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
|
||||||
|
```bash
|
||||||
|
python dbapp/manage.py check
|
||||||
|
# Должно вывести: System check identified no issues (0 silenced).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка Celery (если установлен)
|
||||||
|
```bash
|
||||||
|
celery -A dbapp inspect ping
|
||||||
|
# Должно вывести: pong
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка Redis (если установлен)
|
||||||
|
```bash
|
||||||
|
redis-cli ping
|
||||||
|
# Должно вывести: PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка FlareSolver
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8191/v1
|
||||||
|
# Должно вернуть JSON с информацией о сервисе
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Решение проблем при установке
|
||||||
|
|
||||||
|
### Проблема: 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
|
||||||
|
```bash
|
||||||
|
docker-compose up -d flaresolverr
|
||||||
|
# или
|
||||||
|
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
|
||||||
|
- Настройте логирование
|
||||||
|
- Настройте алерты
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Дополнительные инструменты
|
||||||
|
|
||||||
|
### Flower - мониторинг Celery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install flower
|
||||||
|
celery -A dbapp flower
|
||||||
|
# Откройте http://localhost:5555
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Commander - GUI для Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8081:8081 --name redis-commander \
|
||||||
|
--env REDIS_HOSTS=local:localhost:6379 \
|
||||||
|
rediscommander/redis-commander
|
||||||
|
# Откройте http://localhost:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
### pgAdmin - GUI для PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 5050:80 --name pgadmin \
|
||||||
|
-e PGADMIN_DEFAULT_EMAIL=admin@admin.com \
|
||||||
|
-e PGADMIN_DEFAULT_PASSWORD=admin \
|
||||||
|
dpage/pgadmin4
|
||||||
|
# Откройте http://localhost:5050
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обновление системы
|
||||||
|
|
||||||
|
### Обновление зависимостей
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Удаление системы
|
||||||
|
|
||||||
|
### Остановка сервисов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановка Docker контейнеров
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Остановка Celery Worker
|
||||||
|
pkill -f "celery worker"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Удаление данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Удаление Docker volumes
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Удаление виртуального окружения
|
||||||
|
rm -rf dbapp/.venv
|
||||||
|
|
||||||
|
# Удаление миграций (опционально)
|
||||||
|
find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete
|
||||||
|
find dbapp -path "*/migrations/*.pyc" -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
Если у вас возникли проблемы:
|
||||||
|
|
||||||
|
1. Проверьте логи:
|
||||||
|
- Django: консоль где запущен runserver
|
||||||
|
- Celery: `dbapp/logs/celery_worker.log`
|
||||||
|
- 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 в репозитории с описанием проблемы и логами
|
||||||
78
LYNGSAT_FILL_GUIDE.md
Normal file
78
LYNGSAT_FILL_GUIDE.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Руководство по заполнению данных 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` (заменена карточка с картами)
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
- Процесс может занять продолжительное время в зависимости от количества выбранных спутников
|
||||||
|
- Рекомендуется выбирать небольшое количество спутников для первого запуска
|
||||||
|
- Существующие записи будут обновлены, новые - созданы
|
||||||
|
- Все ошибки логируются и отображаются пользователю
|
||||||
117
QUICKSTART_ASYNC.md
Normal file
117
QUICKSTART_ASYNC.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Быстрый старт: Асинхронное заполнение данных Lyngsat
|
||||||
|
|
||||||
|
## Минимальная настройка (5 минут)
|
||||||
|
|
||||||
|
### 1. Установите зависимости
|
||||||
|
```bash
|
||||||
|
pip install -r dbapp/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Примените миграции
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запустите необходимые сервисы
|
||||||
|
|
||||||
|
**Терминал 1 - Redis и FlareSolver:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d redis flaresolverr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Терминал 2 - Django:**
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
**Терминал 3 - Celery Worker:**
|
||||||
|
```bash
|
||||||
|
cd dbapp
|
||||||
|
celery -A dbapp worker --loglevel=info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Используйте систему
|
||||||
|
|
||||||
|
1. Откройте браузер: `http://localhost:8000/actions/`
|
||||||
|
2. Нажмите "Заполнить данные Lyngsat"
|
||||||
|
3. Выберите 1-2 спутника для теста
|
||||||
|
4. Выберите регион (например, Europe)
|
||||||
|
5. Нажмите "Заполнить данные"
|
||||||
|
6. Наблюдайте за прогрессом в реальном времени!
|
||||||
|
|
||||||
|
## Проверка работоспособности
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
```bash
|
||||||
|
redis-cli ping
|
||||||
|
# Должно вернуть: PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
### FlareSolver
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8191/v1
|
||||||
|
# Должно вернуть JSON с информацией о сервисе
|
||||||
|
```
|
||||||
|
|
||||||
|
### Celery Worker
|
||||||
|
Проверьте вывод в терминале 3 - должны быть сообщения:
|
||||||
|
```
|
||||||
|
[2024-01-15 10:30:00,000: INFO/MainProcess] Connected to redis://localhost:6379/0
|
||||||
|
[2024-01-15 10:30:00,000: INFO/MainProcess] celery@hostname ready.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Остановка сервисов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановить Docker контейнеры
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Остановить Django (Ctrl+C в терминале 2)
|
||||||
|
|
||||||
|
# Остановить Celery Worker (Ctrl+C в терминале 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Просмотр логов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Логи Celery Worker (если запущен с --logfile)
|
||||||
|
tail -f dbapp/logs/celery_worker.log
|
||||||
|
|
||||||
|
# Логи Docker контейнеров
|
||||||
|
docker-compose logs -f redis
|
||||||
|
docker-compose logs -f flaresolverr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что дальше?
|
||||||
|
|
||||||
|
- Прочитайте полную документацию: `ASYNC_LYNGSAT_GUIDE.md`
|
||||||
|
- Настройте production окружение
|
||||||
|
- Добавьте периодические задачи
|
||||||
|
- Настройте email уведомления
|
||||||
|
|
||||||
|
## Решение проблем
|
||||||
|
|
||||||
|
**Worker не запускается:**
|
||||||
|
```bash
|
||||||
|
# Проверьте Redis
|
||||||
|
redis-cli ping
|
||||||
|
|
||||||
|
# Проверьте переменные окружения
|
||||||
|
echo $CELERY_BROKER_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Задача не выполняется:**
|
||||||
|
```bash
|
||||||
|
# Проверьте FlareSolver
|
||||||
|
curl http://localhost:8191/v1
|
||||||
|
|
||||||
|
# Проверьте логи worker
|
||||||
|
tail -f dbapp/logs/celery_worker.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Прогресс не обновляется:**
|
||||||
|
- Откройте консоль браузера (F12)
|
||||||
|
- Проверьте Network tab на наличие ошибок
|
||||||
|
- Обновите страницу
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# This will make sure the app is always imported when
|
||||||
|
# Django starts so that shared_task will use this app.
|
||||||
|
try:
|
||||||
|
from .celery import app as celery_app
|
||||||
|
__all__ = ('celery_app',)
|
||||||
|
except ImportError:
|
||||||
|
# Celery is not installed, skip initialization
|
||||||
|
pass
|
||||||
|
|||||||
24
dbapp/dbapp/celery.py
Normal file
24
dbapp/dbapp/celery.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
Celery configuration for dbapp project.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# Set the default Django settings module for the 'celery' program.
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development')
|
||||||
|
|
||||||
|
app = Celery('dbapp')
|
||||||
|
|
||||||
|
# Using a string here means the worker doesn't have to serialize
|
||||||
|
# the configuration object to child processes.
|
||||||
|
# - namespace='CELERY' means all celery-related configuration keys
|
||||||
|
# should have a `CELERY_` prefix.
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
|
||||||
|
# Load task modules from all registered Django apps.
|
||||||
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True, ignore_result=True)
|
||||||
|
def debug_task(self):
|
||||||
|
print(f'Request: {self.request!r}')
|
||||||
@@ -21,19 +21,21 @@ load_dotenv()
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# GDAL/GEOS configuration for Windows
|
# GDAL/GEOS configuration for Windows
|
||||||
if os.name == 'nt':
|
if os.name == "nt":
|
||||||
OSGEO4W = r"C:\Program Files\OSGeo4W"
|
OSGEO4W = r"C:\Program Files\OSGeo4W"
|
||||||
assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
|
assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
|
||||||
os.environ['OSGEO4W_ROOT'] = OSGEO4W
|
os.environ["OSGEO4W_ROOT"] = OSGEO4W
|
||||||
os.environ['PROJ_LIB'] = os.path.join(OSGEO4W, r"share\proj")
|
os.environ["PROJ_LIB"] = os.path.join(OSGEO4W, r"share\proj")
|
||||||
os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH']
|
os.environ["PATH"] = OSGEO4W + r"\bin;" + os.environ["PATH"]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SECURITY SETTINGS
|
# SECURITY SETTINGS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq')
|
SECRET_KEY = os.getenv(
|
||||||
|
"SECRET_KEY", "django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq"
|
||||||
|
)
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
# This should be overridden in environment-specific settings
|
# This should be overridden in environment-specific settings
|
||||||
@@ -49,38 +51,42 @@ ALLOWED_HOSTS = []
|
|||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# Django Autocomplete Light (must be before admin)
|
# Django Autocomplete Light (must be before admin)
|
||||||
'dal',
|
"dal",
|
||||||
'dal_select2',
|
"dal_select2",
|
||||||
|
|
||||||
# Admin interface customization
|
# Admin interface customization
|
||||||
'admin_interface',
|
"admin_interface",
|
||||||
'colorfield',
|
"colorfield",
|
||||||
|
|
||||||
# Django GIS
|
# Django GIS
|
||||||
'django.contrib.gis',
|
"django.contrib.gis",
|
||||||
|
|
||||||
# Django core apps
|
# Django core apps
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'django.contrib.humanize',
|
"django.contrib.humanize",
|
||||||
|
|
||||||
# Third-party apps
|
# Third-party apps
|
||||||
'leaflet',
|
"leaflet",
|
||||||
'dynamic_raw_id',
|
"dynamic_raw_id",
|
||||||
'rangefilter',
|
"rangefilter",
|
||||||
'django_admin_multiple_choice_list_filter',
|
"django_admin_multiple_choice_list_filter",
|
||||||
'more_admin_filters',
|
"more_admin_filters",
|
||||||
'import_export',
|
"import_export",
|
||||||
|
|
||||||
# Project apps
|
# Project apps
|
||||||
'mainapp',
|
"mainapp",
|
||||||
'mapsapp',
|
"mapsapp",
|
||||||
|
"lyngsatapp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Add Celery results app if available
|
||||||
|
try:
|
||||||
|
import django_celery_results
|
||||||
|
|
||||||
|
INSTALLED_APPS.append("django_celery_results")
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Note: Custom user model is implemented via OneToOneField relationship
|
# Note: Custom user model is implemented via OneToOneField relationship
|
||||||
# If you need a custom user model, uncomment and configure:
|
# If you need a custom user model, uncomment and configure:
|
||||||
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
||||||
@@ -90,17 +96,17 @@ INSTALLED_APPS = [
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'dbapp.urls'
|
ROOT_URLCONF = "dbapp.urls"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEMPLATES CONFIGURATION
|
# TEMPLATES CONFIGURATION
|
||||||
@@ -108,36 +114,36 @@ ROOT_URLCONF = 'dbapp.urls'
|
|||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [
|
"DIRS": [
|
||||||
BASE_DIR / 'templates',
|
BASE_DIR / "templates",
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.debug',
|
"django.template.context_processors.debug",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'dbapp.wsgi.application'
|
WSGI_APPLICATION = "dbapp.wsgi.application"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATABASE CONFIGURATION
|
# DATABASE CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': os.getenv('DB_ENGINE', 'django.contrib.gis.db.backends.postgis'),
|
"ENGINE": os.getenv("DB_ENGINE", "django.contrib.gis.db.backends.postgis"),
|
||||||
'NAME': os.getenv('DB_NAME', 'db'),
|
"NAME": os.getenv("DB_NAME", "db"),
|
||||||
'USER': os.getenv('DB_USER', 'user'),
|
"USER": os.getenv("DB_USER", "user"),
|
||||||
'PASSWORD': os.getenv('DB_PASSWORD', 'password'),
|
"PASSWORD": os.getenv("DB_PASSWORD", "password"),
|
||||||
'HOST': os.getenv('DB_HOST', 'localhost'),
|
"HOST": os.getenv("DB_HOST", "localhost"),
|
||||||
'PORT': os.getenv('DB_PORT', '5432'),
|
"PORT": os.getenv("DB_PORT", "5432"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,16 +154,16 @@ DATABASES = {
|
|||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -165,9 +171,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# INTERNATIONALIZATION
|
# INTERNATIONALIZATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
LANGUAGE_CODE = 'ru'
|
LANGUAGE_CODE = "ru"
|
||||||
|
|
||||||
TIME_ZONE = 'Europe/Moscow'
|
TIME_ZONE = "Europe/Moscow"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
@@ -177,18 +183,18 @@ USE_TZ = True
|
|||||||
# AUTHENTICATION CONFIGURATION
|
# AUTHENTICATION CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = "login"
|
||||||
LOGIN_REDIRECT_URL = 'mainapp:home'
|
LOGIN_REDIRECT_URL = "mainapp:home"
|
||||||
LOGOUT_REDIRECT_URL = 'mainapp:home'
|
LOGOUT_REDIRECT_URL = "mainapp:home"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# STATIC FILES CONFIGURATION
|
# STATIC FILES CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR.parent / 'static',
|
BASE_DIR.parent / "static",
|
||||||
]
|
]
|
||||||
|
|
||||||
# STATIC_ROOT will be set in production.py
|
# STATIC_ROOT will be set in production.py
|
||||||
@@ -198,7 +204,7 @@ STATICFILES_DIRS = [
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# THIRD-PARTY APP CONFIGURATION
|
# THIRD-PARTY APP CONFIGURATION
|
||||||
@@ -210,17 +216,53 @@ SILENCED_SYSTEM_CHECKS = ["security.W019"]
|
|||||||
|
|
||||||
# Leaflet Configuration
|
# Leaflet Configuration
|
||||||
LEAFLET_CONFIG = {
|
LEAFLET_CONFIG = {
|
||||||
'ATTRIBUTION_PREFIX': '',
|
"ATTRIBUTION_PREFIX": "",
|
||||||
'TILES': [
|
"TILES": [
|
||||||
(
|
(
|
||||||
'Satellite',
|
"Satellite",
|
||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||||
{'attribution': '© Esri', 'maxZoom': 16}
|
{"attribution": "© Esri", "maxZoom": 16},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'Streets',
|
"Streets",
|
||||||
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
"http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
{'attribution': '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'}
|
{
|
||||||
)
|
"attribution": '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CELERY CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Celery Configuration Options
|
||||||
|
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||||
|
CELERY_RESULT_BACKEND = "django-db"
|
||||||
|
CELERY_CACHE_BACKEND = "default"
|
||||||
|
|
||||||
|
# Celery Task Configuration
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
|
||||||
|
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes
|
||||||
|
CELERY_TASK_ALWAYS_EAGER = False # Set to True for synchronous execution in development
|
||||||
|
|
||||||
|
# Celery Beat Configuration (for periodic tasks)
|
||||||
|
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||||
|
|
||||||
|
# Celery Result Backend Configuration
|
||||||
|
CELERY_RESULT_EXTENDED = True
|
||||||
|
CELERY_RESULT_EXPIRES = 3600 # Results expire after 1 hour
|
||||||
|
|
||||||
|
# Celery Logging
|
||||||
|
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||||
|
CELERY_WORKER_LOG_FORMAT = "[%(asctime)s: %(levelname)s/%(processName)s] %(message)s"
|
||||||
|
CELERY_WORKER_TASK_LOG_FORMAT = "[%(asctime)s: %(levelname)s/%(processName)s][%(task_name)s(%(task_id)s)] %(message)s"
|
||||||
|
|
||||||
|
# Celery Accept Content
|
||||||
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
|
CELERY_RESULT_SERIALIZER = "json"
|
||||||
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from .models import LyngSat
|
|||||||
|
|
||||||
@admin.register(LyngSat)
|
@admin.register(LyngSat)
|
||||||
class LyngSatAdmin(admin.ModelAdmin):
|
class LyngSatAdmin(admin.ModelAdmin):
|
||||||
list_display = ("mark", "timestamp")
|
list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update")
|
||||||
search_fields = ("mark", )
|
search_fields = ("id_satellite__name", "channel_info")
|
||||||
ordering = ("timestamp",)
|
list_filter = ("id_satellite", "polarization", "modulation", "standard")
|
||||||
|
ordering = ("-last_update",)
|
||||||
|
readonly_fields = ("last_update",)
|
||||||
37
dbapp/lyngsatapp/migrations/0001_initial.py
Normal file
37
dbapp/lyngsatapp/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-10 20:03
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mainapp.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LyngSat',
|
||||||
|
fields=[
|
||||||
|
('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='Частота, МГц')),
|
||||||
|
('sym_velocity', models.FloatField(blank=True, default=0, 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='Описание источника')),
|
||||||
|
('fec', models.CharField(blank=True, max_length=30, 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={
|
||||||
|
'verbose_name': 'Источник LyngSat',
|
||||||
|
'verbose_name_plural': 'Источники LyngSat',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,8 +4,10 @@ from datetime import datetime
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
class LyngSatParser:
|
class LyngSatParser:
|
||||||
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
|
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
flaresolver_url: str = "http://localhost:8191/v1",
|
flaresolver_url: str = "http://localhost:8191/v1",
|
||||||
@@ -14,35 +16,37 @@ class LyngSatParser:
|
|||||||
):
|
):
|
||||||
self.flaresolver_url = flaresolver_url
|
self.flaresolver_url = flaresolver_url
|
||||||
self.regions = regions
|
self.regions = regions
|
||||||
self.target_sats = list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
|
self.target_sats = (
|
||||||
|
list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
|
||||||
|
)
|
||||||
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
|
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
|
||||||
self.BASE_URL = "https://www.lyngsat.com"
|
self.BASE_URL = "https://www.lyngsat.com"
|
||||||
|
|
||||||
def parse_metadata(self, metadata: str) -> dict:
|
def parse_metadata(self, metadata: str) -> dict:
|
||||||
if not metadata or not metadata.strip():
|
if not metadata or not metadata.strip():
|
||||||
return {
|
return {
|
||||||
'standard': None,
|
"standard": None,
|
||||||
'modulation': None,
|
"modulation": None,
|
||||||
'symbol_rate': None,
|
"symbol_rate": None,
|
||||||
'fec': None
|
"fec": None,
|
||||||
}
|
}
|
||||||
normalized = re.sub(r'\s+', '', metadata.strip())
|
normalized = re.sub(r"\s+", "", metadata.strip())
|
||||||
fec_match = re.search(r'([1-9]/[1-9])$', normalized)
|
fec_match = re.search(r"([1-9]/[1-9])$", normalized)
|
||||||
fec = fec_match.group(1) if fec_match else None
|
fec = fec_match.group(1) if fec_match else None
|
||||||
if fec_match:
|
if fec_match:
|
||||||
core = normalized[: fec_match.start()]
|
core = normalized[: fec_match.start()]
|
||||||
else:
|
else:
|
||||||
core = normalized
|
core = normalized
|
||||||
std_match = re.match(r'(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)', core)
|
std_match = re.match(r"(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)", core)
|
||||||
standard = std_match.group(1) if std_match else None
|
standard = std_match.group(1) if std_match else None
|
||||||
rest = core[len(standard) :] if standard else core
|
rest = core[len(standard) :] if standard else core
|
||||||
modulation = None
|
modulation = None
|
||||||
mod_match = re.match(r'(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)', rest)
|
mod_match = re.match(r"(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)", rest)
|
||||||
if mod_match:
|
if mod_match:
|
||||||
modulation = mod_match.group(1)
|
modulation = mod_match.group(1)
|
||||||
rest = rest[len(modulation) :]
|
rest = rest[len(modulation) :]
|
||||||
symbol_rate = None
|
symbol_rate = None
|
||||||
sr_match = re.search(r'(\d+)$', rest)
|
sr_match = re.search(r"(\d+)$", rest)
|
||||||
if sr_match:
|
if sr_match:
|
||||||
try:
|
try:
|
||||||
symbol_rate = int(sr_match.group(1))
|
symbol_rate = int(sr_match.group(1))
|
||||||
@@ -50,30 +54,30 @@ class LyngSatParser:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'standard': standard,
|
"standard": standard,
|
||||||
'modulation': modulation,
|
"modulation": modulation,
|
||||||
'symbol_rate': symbol_rate,
|
"symbol_rate": symbol_rate,
|
||||||
'fec': fec
|
"fec": fec,
|
||||||
}
|
}
|
||||||
|
|
||||||
def extract_date(self, s: str) -> datetime | None:
|
def extract_date(self, s: str) -> datetime | None:
|
||||||
s = s.strip()
|
s = s.strip()
|
||||||
match = re.search(r'(\d{6})$', s)
|
match = re.search(r"(\d{6})$", s)
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
yymmdd = match.group(1)
|
yymmdd = match.group(1)
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(yymmdd, '%y%m%d').date()
|
return datetime.strptime(yymmdd, "%y%m%d").date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def convert_polarization(self, polarization: str) -> str:
|
def convert_polarization(self, polarization: str) -> str:
|
||||||
"""Преобразовать код поляризации в понятное название на русском"""
|
"""Преобразовать код поляризации в понятное название на русском"""
|
||||||
polarization_map = {
|
polarization_map = {
|
||||||
'V': 'Вертикальная',
|
"V": "Вертикальная",
|
||||||
'H': 'Горизонтальная',
|
"H": "Горизонтальная",
|
||||||
'R': 'Правая',
|
"R": "Правая",
|
||||||
'L': 'Левая'
|
"L": "Левая",
|
||||||
}
|
}
|
||||||
return polarization_map.get(polarization.upper(), polarization)
|
return polarization_map.get(polarization.upper(), polarization)
|
||||||
|
|
||||||
@@ -83,11 +87,7 @@ class LyngSatParser:
|
|||||||
regions = self.regions
|
regions = self.regions
|
||||||
for region in regions:
|
for region in regions:
|
||||||
url = f"{self.BASE_URL}/{region}.html"
|
url = f"{self.BASE_URL}/{region}.html"
|
||||||
payload = {
|
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
|
||||||
"cmd": "request.get",
|
|
||||||
"url": url,
|
|
||||||
"maxTimeout": 60000
|
|
||||||
}
|
|
||||||
response = requests.post(self.flaresolver_url, json=payload)
|
response = requests.post(self.flaresolver_url, json=payload)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
continue
|
continue
|
||||||
@@ -104,19 +104,19 @@ class LyngSatParser:
|
|||||||
|
|
||||||
col_table = soup.find_all("div", class_="desktab")[0]
|
col_table = soup.find_all("div", class_="desktab")[0]
|
||||||
|
|
||||||
tables = col_table.find_next_sibling('table').find_all('table')
|
tables = col_table.find_next_sibling("table").find_all("table")
|
||||||
trs = []
|
trs = []
|
||||||
for table in tables:
|
for table in tables:
|
||||||
trs.extend(table.find_all('tr'))
|
trs.extend(table.find_all("tr"))
|
||||||
for tr in trs:
|
for tr in trs:
|
||||||
sat_name = tr.find('span').text
|
sat_name = tr.find("span").text
|
||||||
if self.target_sats is not None:
|
if self.target_sats is not None:
|
||||||
if sat_name.strip().lower() not in self.target_sats:
|
if sat_name.strip().lower() not in self.target_sats:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
sat_url = tr.find_all('a')[2]['href']
|
sat_url = tr.find_all("a")[2]["href"]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
sat_url = tr.find_all('a')[0]['href']
|
sat_url = tr.find_all("a")[0]["href"]
|
||||||
sat_names.append(sat_name)
|
sat_names.append(sat_name)
|
||||||
sat_urls.append(sat_url)
|
sat_urls.append(sat_url)
|
||||||
return sat_names, sat_urls
|
return sat_names, sat_urls
|
||||||
@@ -128,58 +128,65 @@ class LyngSatParser:
|
|||||||
|
|
||||||
col_table = soup.find_all("div", class_="desktab")[0]
|
col_table = soup.find_all("div", class_="desktab")[0]
|
||||||
|
|
||||||
tables = col_table.find_next_sibling('table').find_all('table')
|
tables = col_table.find_next_sibling("table").find_all("table")
|
||||||
trs = []
|
trs = []
|
||||||
for table in tables:
|
for table in tables:
|
||||||
trs.extend(table.find_all('tr'))
|
trs.extend(table.find_all("tr"))
|
||||||
for tr in trs:
|
for tr in trs:
|
||||||
sat_name = tr.find('span').text
|
sat_name = tr.find("span").text
|
||||||
if self.target_sats is not None:
|
if self.target_sats is not None:
|
||||||
if sat_name.strip().lower() not in self.target_sats:
|
if sat_name.strip().lower() not in self.target_sats:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
sat_url = tr.find_all('a')[2]['href']
|
sat_url = tr.find_all("a")[2]["href"]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
sat_url = tr.find_all('a')[0]['href']
|
sat_url = tr.find_all("a")[0]["href"]
|
||||||
|
|
||||||
update_date = tr.find_all('td')[-1].text
|
update_date = tr.find_all("td")[-1].text
|
||||||
sat_response = requests.post(self.flaresolver_url, json={
|
sat_response = requests.post(
|
||||||
|
self.flaresolver_url,
|
||||||
|
json={
|
||||||
"cmd": "request.get",
|
"cmd": "request.get",
|
||||||
"url": f"{self.BASE_URL}/{sat_url}",
|
"url": f"{self.BASE_URL}/{sat_url}",
|
||||||
"maxTimeout": 60000
|
"maxTimeout": 60000,
|
||||||
})
|
},
|
||||||
html_content = sat_response.json().get("solution", {}).get("response", "")
|
)
|
||||||
|
html_content = (
|
||||||
|
sat_response.json().get("solution", {}).get("response", "")
|
||||||
|
)
|
||||||
sat_page_data = self.get_satellite_content(html_content)
|
sat_page_data = self.get_satellite_content(html_content)
|
||||||
sat_data[sat_name] = {
|
sat_data[sat_name] = {
|
||||||
"url": f"{self.BASE_URL}/{sat_url}",
|
"url": f"{self.BASE_URL}/{sat_url}",
|
||||||
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
|
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
|
||||||
"sources": sat_page_data
|
"sources": sat_page_data,
|
||||||
}
|
}
|
||||||
return sat_data
|
return sat_data
|
||||||
|
|
||||||
def get_satellite_content(self, html_content: str) -> dict:
|
def get_satellite_content(self, html_content: str) -> dict:
|
||||||
sat_soup = BeautifulSoup(html_content, "html.parser")
|
sat_soup = BeautifulSoup(html_content, "html.parser")
|
||||||
big_table = sat_soup.find('table', class_='bigtable')
|
big_table = sat_soup.find("table", class_="bigtable")
|
||||||
all_tables = big_table.find_all("div", class_="desktab")[:-1]
|
all_tables = big_table.find_all("div", class_="desktab")[:-1]
|
||||||
data = []
|
data = []
|
||||||
for table in all_tables:
|
for table in all_tables:
|
||||||
trs = table.find_next_sibling('table').find_all('tr')
|
trs = table.find_next_sibling("table").find_all("tr")
|
||||||
for idx, tr in enumerate(trs):
|
for idx, tr in enumerate(trs):
|
||||||
tds = tr.find_all('td')
|
tds = tr.find_all("td")
|
||||||
if len(tds) < 9 or idx < 2:
|
if len(tds) < 9 or idx < 2:
|
||||||
continue
|
continue
|
||||||
freq, polarization = tds[0].find('b').text.strip().split('\xa0')
|
freq, polarization = tds[0].find("b").text.strip().split("\xa0")
|
||||||
polarization = self.convert_polarization(polarization)
|
polarization = self.convert_polarization(polarization)
|
||||||
meta = self.parse_metadata(tds[1].text)
|
meta = self.parse_metadata(tds[1].text)
|
||||||
provider_name = tds[3].text
|
provider_name = tds[3].text
|
||||||
last_update = self.extract_date(tds[-1].text)
|
last_update = self.extract_date(tds[-1].text)
|
||||||
data.append({
|
data.append(
|
||||||
|
{
|
||||||
"freq": freq,
|
"freq": freq,
|
||||||
"pol": polarization,
|
"pol": polarization,
|
||||||
"metadata": meta,
|
"metadata": meta,
|
||||||
"provider_name": provider_name,
|
"provider_name": provider_name,
|
||||||
"last_update": last_update
|
"last_update": last_update,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -193,17 +200,19 @@ class KingOfSatParser:
|
|||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.max_satellites = max_satellites
|
self.max_satellites = max_satellites
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
{
|
||||||
})
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def convert_polarization(self, polarization):
|
def convert_polarization(self, polarization):
|
||||||
"""Преобразовать код поляризации в понятное название на русском"""
|
"""Преобразовать код поляризации в понятное название на русском"""
|
||||||
polarization_map = {
|
polarization_map = {
|
||||||
'V': 'Вертикальная',
|
"V": "Вертикальная",
|
||||||
'H': 'Горизонтальная',
|
"H": "Горизонтальная",
|
||||||
'R': 'Правая',
|
"R": "Правая",
|
||||||
'L': 'Левая'
|
"L": "Левая",
|
||||||
}
|
}
|
||||||
return polarization_map.get(polarization.upper(), polarization)
|
return polarization_map.get(polarization.upper(), polarization)
|
||||||
|
|
||||||
@@ -219,23 +228,23 @@ class KingOfSatParser:
|
|||||||
|
|
||||||
def parse_satellite_table(self, html_content):
|
def parse_satellite_table(self, html_content):
|
||||||
"""Распарсить таблицу со спутниками"""
|
"""Распарсить таблицу со спутниками"""
|
||||||
soup = BeautifulSoup(html_content, 'html.parser')
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
satellites = []
|
satellites = []
|
||||||
table = soup.find('table')
|
table = soup.find("table")
|
||||||
if not table:
|
if not table:
|
||||||
print("Таблица не найдена")
|
print("Таблица не найдена")
|
||||||
return satellites
|
return satellites
|
||||||
|
|
||||||
rows = table.find_all('tr')[1:]
|
rows = table.find_all("tr")[1:]
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
cols = row.find_all('td')
|
cols = row.find_all("td")
|
||||||
if len(cols) < 13:
|
if len(cols) < 13:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
position_cell = cols[0].text.strip()
|
position_cell = cols[0].text.strip()
|
||||||
position_match = re.search(r'([\d\.]+)°([EW])', position_cell)
|
position_match = re.search(r"([\d\.]+)°([EW])", position_cell)
|
||||||
if position_match:
|
if position_match:
|
||||||
position_value = position_match.group(1)
|
position_value = position_match.group(1)
|
||||||
position_direction = position_match.group(2)
|
position_direction = position_match.group(2)
|
||||||
@@ -247,7 +256,7 @@ class KingOfSatParser:
|
|||||||
satellite_cell = cols[1]
|
satellite_cell = cols[1]
|
||||||
satellite_name = satellite_cell.get_text(strip=True)
|
satellite_name = satellite_cell.get_text(strip=True)
|
||||||
# Удаляем возможные лишние символы или пробелы
|
# Удаляем возможные лишние символы или пробелы
|
||||||
satellite_name = re.sub(r'\s+', ' ', satellite_name).strip()
|
satellite_name = re.sub(r"\s+", " ", satellite_name).strip()
|
||||||
|
|
||||||
# NORAD (3-я колонка)
|
# NORAD (3-я колонка)
|
||||||
norad = cols[2].text.strip()
|
norad = cols[2].text.strip()
|
||||||
@@ -256,20 +265,22 @@ class KingOfSatParser:
|
|||||||
|
|
||||||
ini_link = None
|
ini_link = None
|
||||||
ini_cell = cols[3]
|
ini_cell = cols[3]
|
||||||
ini_img = ini_cell.find('img', src=lambda x: x and 'disquette.gif' in x)
|
ini_img = ini_cell.find("img", src=lambda x: x and "disquette.gif" in x)
|
||||||
if ini_img and position:
|
if ini_img and position:
|
||||||
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
|
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
|
||||||
|
|
||||||
update_date = cols[12].text.strip() if len(cols) > 12 else None
|
update_date = cols[12].text.strip() if len(cols) > 12 else None
|
||||||
|
|
||||||
if satellite_name and ini_link and position:
|
if satellite_name and ini_link and position:
|
||||||
satellites.append({
|
satellites.append(
|
||||||
'position': position,
|
{
|
||||||
'name': satellite_name,
|
"position": position,
|
||||||
'norad': norad,
|
"name": satellite_name,
|
||||||
'ini_url': ini_link,
|
"norad": norad,
|
||||||
'update_date': update_date
|
"ini_url": ini_link,
|
||||||
})
|
"update_date": update_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при обработке строки таблицы: {e}")
|
print(f"Ошибка при обработке строки таблицы: {e}")
|
||||||
@@ -279,11 +290,7 @@ class KingOfSatParser:
|
|||||||
|
|
||||||
def parse_ini_file(self, ini_content):
|
def parse_ini_file(self, ini_content):
|
||||||
"""Распарсить содержимое .ini файла"""
|
"""Распарсить содержимое .ini файла"""
|
||||||
data = {
|
data = {"metadata": {}, "sattype": {}, "dvb": {}}
|
||||||
'metadata': {},
|
|
||||||
'sattype': {},
|
|
||||||
'dvb': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# # Извлекаем метаданные из комментариев
|
# # Извлекаем метаданные из комментариев
|
||||||
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
|
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
|
||||||
@@ -291,35 +298,35 @@ class KingOfSatParser:
|
|||||||
# data['metadata']['downloaded'] = metadata_match.group(1)
|
# data['metadata']['downloaded'] = metadata_match.group(1)
|
||||||
|
|
||||||
# Парсим секцию [SATTYPE]
|
# Парсим секцию [SATTYPE]
|
||||||
sattype_match = re.search(r'\[SATTYPE\](.*?)\n\[', ini_content, re.DOTALL)
|
sattype_match = re.search(r"\[SATTYPE\](.*?)\n\[", ini_content, re.DOTALL)
|
||||||
if sattype_match:
|
if sattype_match:
|
||||||
sattype_content = sattype_match.group(1).strip()
|
sattype_content = sattype_match.group(1).strip()
|
||||||
for line in sattype_content.split('\n'):
|
for line in sattype_content.split("\n"):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if '=' in line:
|
if "=" in line:
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split("=", 1)
|
||||||
data['sattype'][key.strip()] = value.strip()
|
data["sattype"][key.strip()] = value.strip()
|
||||||
|
|
||||||
# Парсим секцию [DVB]
|
# Парсим секцию [DVB]
|
||||||
dvb_match = re.search(r'\[DVB\](.*?)(?:\n\[|$)', ini_content, re.DOTALL)
|
dvb_match = re.search(r"\[DVB\](.*?)(?:\n\[|$)", ini_content, re.DOTALL)
|
||||||
if dvb_match:
|
if dvb_match:
|
||||||
dvb_content = dvb_match.group(1).strip()
|
dvb_content = dvb_match.group(1).strip()
|
||||||
for line in dvb_content.split('\n'):
|
for line in dvb_content.split("\n"):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if '=' in line:
|
if "=" in line:
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split("=", 1)
|
||||||
params = [p.strip() for p in value.split(',')]
|
params = [p.strip() for p in value.split(",")]
|
||||||
polarization = params[1] if len(params) > 1 else ''
|
polarization = params[1] if len(params) > 1 else ""
|
||||||
if polarization:
|
if polarization:
|
||||||
polarization = self.convert_polarization(polarization)
|
polarization = self.convert_polarization(polarization)
|
||||||
|
|
||||||
data['dvb'][key.strip()] = {
|
data["dvb"][key.strip()] = {
|
||||||
'frequency': params[0] if len(params) > 0 else '',
|
"frequency": params[0] if len(params) > 0 else "",
|
||||||
'polarization': polarization,
|
"polarization": polarization,
|
||||||
'symbol_rate': params[2] if len(params) > 2 else '',
|
"symbol_rate": params[2] if len(params) > 2 else "",
|
||||||
'fec': params[3] if len(params) > 3 else '',
|
"fec": params[3] if len(params) > 3 else "",
|
||||||
'standard': params[4] if len(params) > 4 else '',
|
"standard": params[4] if len(params) > 4 else "",
|
||||||
'modulation': params[5] if len(params) > 5 else ''
|
"modulation": params[5] if len(params) > 5 else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -336,7 +343,7 @@ class KingOfSatParser:
|
|||||||
|
|
||||||
def get_all_satellites_data(self):
|
def get_all_satellites_data(self):
|
||||||
"""Получить данные всех спутников с учетом ограничения max_satellites"""
|
"""Получить данные всех спутников с учетом ограничения max_satellites"""
|
||||||
html_content = self.fetch_page(self.base_url + '/satellites')
|
html_content = self.fetch_page(self.base_url + "/satellites")
|
||||||
if not html_content:
|
if not html_content:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -351,7 +358,7 @@ class KingOfSatParser:
|
|||||||
for satellite in satellites:
|
for satellite in satellites:
|
||||||
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
|
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
|
||||||
|
|
||||||
ini_content = self.download_ini_file(satellite['ini_url'])
|
ini_content = self.download_ini_file(satellite["ini_url"])
|
||||||
if not ini_content:
|
if not ini_content:
|
||||||
print(f"Не удалось скачать .ini файл для {satellite['name']}")
|
print(f"Не удалось скачать .ini файл для {satellite['name']}")
|
||||||
continue
|
continue
|
||||||
@@ -359,12 +366,12 @@ class KingOfSatParser:
|
|||||||
parsed_ini = self.parse_ini_file(ini_content)
|
parsed_ini = self.parse_ini_file(ini_content)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'satellite_name': satellite['name'],
|
"satellite_name": satellite["name"],
|
||||||
'position': satellite['position'],
|
"position": satellite["position"],
|
||||||
'norad': satellite['norad'],
|
"norad": satellite["norad"],
|
||||||
'update_date': satellite['update_date'],
|
"update_date": satellite["update_date"],
|
||||||
'ini_url': satellite['ini_url'],
|
"ini_url": satellite["ini_url"],
|
||||||
'ini_data': parsed_ini
|
"ini_data": parsed_ini,
|
||||||
}
|
}
|
||||||
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
@@ -384,20 +391,15 @@ class KingOfSatParser:
|
|||||||
for data in satellites_data:
|
for data in satellites_data:
|
||||||
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
|
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
|
||||||
satellite_dict[key] = {
|
satellite_dict[key] = {
|
||||||
'name': data['satellite_name'],
|
"name": data["satellite_name"],
|
||||||
'position': data['position'],
|
"position": data["position"],
|
||||||
'norad': data['norad'],
|
"norad": data["norad"],
|
||||||
'update_date': data['update_date'],
|
"update_date": data["update_date"],
|
||||||
'ini_url': data['ini_url'],
|
"ini_url": data["ini_url"],
|
||||||
'transponders_count': len(data['ini_data']['dvb']),
|
"transponders_count": len(data["ini_data"]["dvb"]),
|
||||||
'transponders': data['ini_data']['dvb'],
|
"transponders": data["ini_data"]["dvb"],
|
||||||
'sattype_info': data['ini_data']['sattype'],
|
"sattype_info": data["ini_data"]["sattype"],
|
||||||
'metadata': data['ini_data']['metadata']
|
"metadata": data["ini_data"]["metadata"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return satellite_dict
|
return satellite_dict
|
||||||
|
|
||||||
from pprint import pprint
|
|
||||||
lyngsat = LyngSatParser(regions=['europe'], target_sats=['Türksat 3A', 'Intelsat 22'])
|
|
||||||
html_regions = lyngsat.get_region_pages()
|
|
||||||
pprint(lyngsat.get_satellite_urls(html_regions))
|
|
||||||
73
dbapp/lyngsatapp/tasks.py
Normal file
73
dbapp/lyngsatapp/tasks.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Celery tasks for Lyngsat data processing.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from celery import shared_task
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from .utils import fill_lyngsat_data
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
|
||||||
|
def fill_lyngsat_data_task(self, target_sats, regions=None):
|
||||||
|
"""
|
||||||
|
Асинхронная задача для заполнения данных Lyngsat.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_sats: Список названий спутников для обработки
|
||||||
|
regions: Список регионов для парсинга (по умолчанию все)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Статистика обработки
|
||||||
|
"""
|
||||||
|
task_id = self.request.id
|
||||||
|
logger.info(f"[Task {task_id}] Начало обработки данных Lyngsat")
|
||||||
|
logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}")
|
||||||
|
logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}")
|
||||||
|
|
||||||
|
# Обновляем статус задачи
|
||||||
|
self.update_state(
|
||||||
|
state='PROGRESS',
|
||||||
|
meta={
|
||||||
|
'current': 0,
|
||||||
|
'total': len(target_sats),
|
||||||
|
'status': 'Инициализация...'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Вызываем функцию заполнения данных
|
||||||
|
stats = fill_lyngsat_data(
|
||||||
|
target_sats=target_sats,
|
||||||
|
regions=regions,
|
||||||
|
task_id=task_id,
|
||||||
|
update_progress=lambda current, total, status: self.update_state(
|
||||||
|
state='PROGRESS',
|
||||||
|
meta={
|
||||||
|
'current': current,
|
||||||
|
'total': total,
|
||||||
|
'status': status
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"[Task {task_id}] Обработка завершена успешно")
|
||||||
|
logger.info(f"[Task {task_id}] Статистика: {stats}")
|
||||||
|
|
||||||
|
# Сохраняем результат в кеш для отображения на странице
|
||||||
|
cache.set(f'lyngsat_task_{task_id}', stats, timeout=3600)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True)
|
||||||
|
self.update_state(
|
||||||
|
state='FAILURE',
|
||||||
|
meta={
|
||||||
|
'error': str(e),
|
||||||
|
'status': 'Ошибка при обработке'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise
|
||||||
@@ -1,58 +1,170 @@
|
|||||||
|
import logging
|
||||||
from .parser import LyngSatParser
|
from .parser import LyngSatParser
|
||||||
from .models import LyngSat
|
from .models import LyngSat
|
||||||
from mainapp.models import Polarization, Standard, Modulation, Satellite
|
from mainapp.models import Polarization, Standard, Modulation, Satellite
|
||||||
|
|
||||||
def fill_lyngsat_data(target_sats: list[str]):
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fill_lyngsat_data(
|
||||||
|
target_sats: list[str],
|
||||||
|
regions: list[str] = None,
|
||||||
|
task_id: str = None,
|
||||||
|
update_progress=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Заполняет данные Lyngsat для указанных спутников и регионов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_sats: Список названий спутников для обработки
|
||||||
|
regions: Список регионов для парсинга (по умолчанию все)
|
||||||
|
task_id: ID задачи Celery для логирования
|
||||||
|
update_progress: Функция для обновления прогресса (current, total, status)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Статистика обработки с ключами:
|
||||||
|
- total_satellites: общее количество спутников
|
||||||
|
- total_sources: общее количество источников
|
||||||
|
- created: количество созданных записей
|
||||||
|
- updated: количество обновленных записей
|
||||||
|
- errors: список ошибок
|
||||||
|
"""
|
||||||
|
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]"
|
||||||
|
stats = {
|
||||||
|
'total_satellites': 0,
|
||||||
|
'total_sources': 0,
|
||||||
|
'created': 0,
|
||||||
|
'updated': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
if regions is None:
|
||||||
|
regions = ["europe", "asia", "america", "atlantic"]
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Начало парсинга данных")
|
||||||
|
logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}")
|
||||||
|
logger.info(f"{log_prefix} Регионы: {', '.join(regions)}")
|
||||||
|
|
||||||
|
if update_progress:
|
||||||
|
update_progress(0, len(target_sats), "Инициализация парсера...")
|
||||||
|
|
||||||
|
try:
|
||||||
parser = LyngSatParser(
|
parser = LyngSatParser(
|
||||||
target_sats=target_sats,
|
target_sats=target_sats,
|
||||||
|
regions=regions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Получение данных со спутников...")
|
||||||
|
if update_progress:
|
||||||
|
update_progress(0, len(target_sats), "Получение данных со спутников...")
|
||||||
|
|
||||||
lyngsat_data = parser.get_satellites_data()
|
lyngsat_data = parser.get_satellites_data()
|
||||||
for sat_name, data in lyngsat_data.items():
|
stats['total_satellites'] = len(lyngsat_data)
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам")
|
||||||
|
|
||||||
|
for idx, (sat_name, data) in enumerate(lyngsat_data.items(), 1):
|
||||||
|
logger.info(f"{log_prefix} Обработка спутника {idx}/{stats['total_satellites']}: {sat_name}")
|
||||||
|
|
||||||
|
if update_progress:
|
||||||
|
update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...")
|
||||||
|
|
||||||
url = data['url']
|
url = data['url']
|
||||||
sources = data['sources']
|
sources = data['sources']
|
||||||
for source in sources:
|
stats['total_sources'] += len(sources)
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
|
||||||
|
|
||||||
|
# Находим спутник в базе
|
||||||
|
try:
|
||||||
|
sat_obj = Satellite.objects.get(name__icontains=sat_name)
|
||||||
|
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
|
||||||
|
except Satellite.DoesNotExist:
|
||||||
|
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
||||||
|
logger.warning(f"{log_prefix} {error_msg}")
|
||||||
|
stats['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
except Satellite.MultipleObjectsReturned:
|
||||||
|
error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
|
||||||
|
logger.warning(f"{log_prefix} {error_msg}")
|
||||||
|
stats['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for source_idx, source in enumerate(sources, 1):
|
||||||
|
try:
|
||||||
|
# Парсим частоту
|
||||||
try:
|
try:
|
||||||
freq = float(source['freq'])
|
freq = float(source['freq'])
|
||||||
except Exception as e:
|
except (ValueError, TypeError):
|
||||||
freq = -1.0
|
freq = -1.0
|
||||||
print("Беда с частотой")
|
error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}"
|
||||||
|
logger.debug(f"{log_prefix} {error_msg}")
|
||||||
|
stats['errors'].append(error_msg)
|
||||||
|
|
||||||
last_update = source['last_update']
|
last_update = source['last_update']
|
||||||
fec = source['metadata']['fec']
|
fec = source['metadata'].get('fec')
|
||||||
modulation = source['metadata']['modulation']
|
modulation_name = source['metadata'].get('modulation')
|
||||||
standard = source['metadata']['standard']
|
standard_name = source['metadata'].get('standard')
|
||||||
symbol_velocity = source['metadata']['symbol_rate']
|
symbol_velocity = source['metadata'].get('symbol_rate')
|
||||||
polarization = source['pol']
|
polarization_name = source['pol']
|
||||||
channel_info = source['provider_name']
|
channel_info = source['provider_name']
|
||||||
|
|
||||||
|
# Создаем или получаем связанные объекты
|
||||||
pol_obj, _ = Polarization.objects.get_or_create(
|
pol_obj, _ = Polarization.objects.get_or_create(
|
||||||
name=polarization
|
name=polarization_name if polarization_name else "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
mod_obj, _ = Modulation.objects.get_or_create(
|
mod_obj, _ = Modulation.objects.get_or_create(
|
||||||
name=modulation
|
name=modulation_name if modulation_name else "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
standard_obj, _ = Standard.objects.get_or_create(
|
standard_obj, _ = Standard.objects.get_or_create(
|
||||||
name=standard
|
name=standard_name if standard_name else "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
sat_obj, _ = Satellite.objects.get(
|
# Создаем или обновляем запись Lyngsat
|
||||||
name__contains=sat_name
|
lyng_obj, created = LyngSat.objects.update_or_create(
|
||||||
)
|
|
||||||
lyng_obj, _ = LyngSat.objects.get_or_create(
|
|
||||||
id_satellite=sat_obj,
|
id_satellite=sat_obj,
|
||||||
frequency=freq,
|
frequency=freq,
|
||||||
polarization=pol_obj,
|
polarization=pol_obj,
|
||||||
defaults={
|
defaults={
|
||||||
"modulation": mod_obj,
|
"modulation": mod_obj,
|
||||||
"standard": standard_obj,
|
"standard": standard_obj,
|
||||||
"sym_velocity": symbol_velocity,
|
"sym_velocity": symbol_velocity if symbol_velocity else 0,
|
||||||
"channel_info": channel_info,
|
"channel_info": channel_info[:20] if channel_info else "",
|
||||||
"last_update": last_update,
|
"last_update": last_update,
|
||||||
"fec": fec,
|
"fec": fec[:30] if fec else "",
|
||||||
"url": url
|
"url": url
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
lyng_obj.objects.update_or_create()
|
|
||||||
# TODO: сделать карточку и форму для действий и выбора спутника
|
if created:
|
||||||
lyng_obj.save()
|
stats['created'] += 1
|
||||||
|
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц")
|
||||||
|
else:
|
||||||
|
stats['updated'] += 1
|
||||||
|
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц")
|
||||||
|
|
||||||
|
# Логируем прогресс каждые 10 источников
|
||||||
|
if source_idx % 10 == 0:
|
||||||
|
logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
|
||||||
|
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
|
||||||
|
stats['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Критическая ошибка: {str(e)}"
|
||||||
|
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
|
||||||
|
stats['errors'].append(error_msg)
|
||||||
|
|
||||||
|
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}")
|
||||||
|
|
||||||
|
if update_progress:
|
||||||
|
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|||||||
@@ -218,11 +218,11 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
queryset = queryset.select_related(
|
queryset = queryset.select_related(
|
||||||
'geo_obj',
|
'geo_obj',
|
||||||
'created_by__user',
|
'created_by__user',
|
||||||
'updated_by__user'
|
'updated_by__user',
|
||||||
).prefetch_related(
|
'parameter_obj',
|
||||||
'parameters_obj__id_satellite',
|
'parameter_obj__id_satellite',
|
||||||
'parameters_obj__polarization',
|
'parameter_obj__polarization',
|
||||||
'parameters_obj__modulation'
|
'parameter_obj__modulation'
|
||||||
)
|
)
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||||
@@ -248,7 +248,7 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
])
|
])
|
||||||
|
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
|
|
||||||
# Форматирование координат
|
# Форматирование координат
|
||||||
@@ -284,12 +284,25 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
|||||||
# Inline Admin Classes
|
# Inline Admin Classes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
class ParameterObjItemInline(admin.StackedInline):
|
class ParameterInline(admin.StackedInline):
|
||||||
model = ObjItem.parameters_obj.through
|
"""Inline для редактирования параметра объекта."""
|
||||||
|
model = Parameter
|
||||||
extra = 0
|
extra = 0
|
||||||
max_num = 1
|
max_num = 1
|
||||||
|
can_delete = True
|
||||||
verbose_name = "ВЧ загрузка"
|
verbose_name = "ВЧ загрузка"
|
||||||
verbose_name_plural = "ВЧ загрузки"
|
verbose_name_plural = "ВЧ загрузка"
|
||||||
|
fields = (
|
||||||
|
'id_satellite',
|
||||||
|
'frequency',
|
||||||
|
'freq_range',
|
||||||
|
'polarization',
|
||||||
|
'modulation',
|
||||||
|
'bod_velocity',
|
||||||
|
'snr',
|
||||||
|
'standard'
|
||||||
|
)
|
||||||
|
autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -370,13 +383,15 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
"bod_velocity",
|
"bod_velocity",
|
||||||
"snr",
|
"snr",
|
||||||
"standard",
|
"standard",
|
||||||
|
"related_objitem",
|
||||||
"sigma_parameter"
|
"sigma_parameter"
|
||||||
)
|
)
|
||||||
list_display_links = ("frequency", "id_satellite")
|
list_display_links = ("frequency", "id_satellite")
|
||||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite")
|
list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem")
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
HasSigmaParameterFilter,
|
HasSigmaParameterFilter,
|
||||||
|
("objitem", MultiSelectRelatedDropdownFilter),
|
||||||
("id_satellite", MultiSelectRelatedDropdownFilter),
|
("id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
("polarization__name", MultiSelectDropdownFilter),
|
("polarization__name", MultiSelectDropdownFilter),
|
||||||
("modulation", MultiSelectRelatedDropdownFilter),
|
("modulation", MultiSelectRelatedDropdownFilter),
|
||||||
@@ -395,12 +410,21 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
"modulation__name",
|
"modulation__name",
|
||||||
"polarization__name",
|
"polarization__name",
|
||||||
"standard__name",
|
"standard__name",
|
||||||
|
"objitem__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
ordering = ("-frequency",)
|
ordering = ("-frequency",)
|
||||||
autocomplete_fields = ("objitems",)
|
autocomplete_fields = ("objitem",)
|
||||||
inlines = [SigmaParameterInline]
|
inlines = [SigmaParameterInline]
|
||||||
|
|
||||||
|
def related_objitem(self, obj):
|
||||||
|
"""Отображает связанный ObjItem."""
|
||||||
|
if hasattr(obj, 'objitem') and obj.objitem:
|
||||||
|
return obj.objitem.name
|
||||||
|
return "-"
|
||||||
|
related_objitem.short_description = "Объект"
|
||||||
|
related_objitem.admin_order_field = "objitem__name"
|
||||||
|
|
||||||
def sigma_parameter(self, obj):
|
def sigma_parameter(self, obj):
|
||||||
"""Отображает связанный параметр Sigma."""
|
"""Отображает связанный параметр Sigma."""
|
||||||
sigma_obj = obj.sigma_parameter.all()
|
sigma_obj = obj.sigma_parameter.all()
|
||||||
@@ -636,16 +660,25 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
list_display_links = ("name",)
|
list_display_links = ("name",)
|
||||||
list_select_related = ("geo_obj", "created_by__user", "updated_by__user")
|
list_select_related = (
|
||||||
|
"geo_obj",
|
||||||
|
"created_by__user",
|
||||||
|
"updated_by__user",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard"
|
||||||
|
)
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
UniqueToggleFilter,
|
UniqueToggleFilter,
|
||||||
("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
("parameter_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
("parameters_obj__frequency", NumericRangeFilterBuilder()),
|
("parameter_obj__frequency", NumericRangeFilterBuilder()),
|
||||||
("parameters_obj__freq_range", NumericRangeFilterBuilder()),
|
("parameter_obj__freq_range", NumericRangeFilterBuilder()),
|
||||||
("parameters_obj__snr", NumericRangeFilterBuilder()),
|
("parameter_obj__snr", NumericRangeFilterBuilder()),
|
||||||
("parameters_obj__modulation", MultiSelectRelatedDropdownFilter),
|
("parameter_obj__modulation", MultiSelectRelatedDropdownFilter),
|
||||||
("parameters_obj__polarization", MultiSelectRelatedDropdownFilter),
|
("parameter_obj__polarization", MultiSelectRelatedDropdownFilter),
|
||||||
GeoKupDistanceFilter,
|
GeoKupDistanceFilter,
|
||||||
GeoValidDistanceFilter,
|
GeoValidDistanceFilter,
|
||||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
@@ -655,12 +688,12 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
search_fields = (
|
search_fields = (
|
||||||
"name",
|
"name",
|
||||||
"geo_obj__location",
|
"geo_obj__location",
|
||||||
"parameters_obj__frequency",
|
"parameter_obj__frequency",
|
||||||
"parameters_obj__id_satellite__name",
|
"parameter_obj__id_satellite__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
ordering = ("-updated_at",)
|
ordering = ("-updated_at",)
|
||||||
inlines = [ParameterObjItemInline, GeoInline]
|
inlines = [GeoInline, ParameterInline]
|
||||||
actions = [show_selected_on_map, export_objects_to_csv]
|
actions = [show_selected_on_map, export_objects_to_csv]
|
||||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
@@ -676,7 +709,7 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""
|
"""
|
||||||
Оптимизированный queryset с использованием select_related и prefetch_related.
|
Оптимизированный queryset с использованием select_related.
|
||||||
|
|
||||||
Загружает связанные объекты одним запросом для улучшения производительности.
|
Загружает связанные объекты одним запросом для улучшения производительности.
|
||||||
"""
|
"""
|
||||||
@@ -684,31 +717,30 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
return qs.select_related(
|
return qs.select_related(
|
||||||
"geo_obj",
|
"geo_obj",
|
||||||
"created_by__user",
|
"created_by__user",
|
||||||
"updated_by__user"
|
"updated_by__user",
|
||||||
).prefetch_related(
|
"parameter_obj",
|
||||||
"parameters_obj__id_satellite",
|
"parameter_obj__id_satellite",
|
||||||
"parameters_obj__polarization",
|
"parameter_obj__polarization",
|
||||||
"parameters_obj__modulation",
|
"parameter_obj__modulation",
|
||||||
"parameters_obj__standard"
|
"parameter_obj__standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
def sat_name(self, obj):
|
def sat_name(self, obj):
|
||||||
"""Отображает название спутника из связанного параметра."""
|
"""Отображает название спутника из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param and param.id_satellite:
|
if obj.parameter_obj.id_satellite:
|
||||||
return param.id_satellite.name
|
return obj.parameter_obj.id_satellite.name
|
||||||
return "-"
|
return "-"
|
||||||
sat_name.short_description = "Спутник"
|
sat_name.short_description = "Спутник"
|
||||||
sat_name.admin_order_field = "parameters_obj__id_satellite__name"
|
sat_name.admin_order_field = "parameter_obj__id_satellite__name"
|
||||||
|
|
||||||
def freq(self, obj):
|
def freq(self, obj):
|
||||||
"""Отображает частоту из связанного параметра."""
|
"""Отображает частоту из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param:
|
return obj.parameter_obj.frequency
|
||||||
return param.frequency
|
|
||||||
return "-"
|
return "-"
|
||||||
freq.short_description = "Частота, МГц"
|
freq.short_description = "Частота, МГц"
|
||||||
freq.admin_order_field = "parameters_obj__frequency"
|
freq.admin_order_field = "parameter_obj__frequency"
|
||||||
|
|
||||||
def distance_geo_kup(self, obj):
|
def distance_geo_kup(self, obj):
|
||||||
"""Отображает расстояние между геолокацией и Кубсатом."""
|
"""Отображает расстояние между геолокацией и Кубсатом."""
|
||||||
@@ -736,42 +768,39 @@ class ObjItemAdmin(BaseAdmin):
|
|||||||
|
|
||||||
def pol(self, obj):
|
def pol(self, obj):
|
||||||
"""Отображает поляризацию из связанного параметра."""
|
"""Отображает поляризацию из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param and param.polarization:
|
if obj.parameter_obj.polarization:
|
||||||
return param.polarization.name
|
return obj.parameter_obj.polarization.name
|
||||||
return "-"
|
return "-"
|
||||||
pol.short_description = "Поляризация"
|
pol.short_description = "Поляризация"
|
||||||
|
|
||||||
def freq_range(self, obj):
|
def freq_range(self, obj):
|
||||||
"""Отображает полосу частот из связанного параметра."""
|
"""Отображает полосу частот из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param:
|
return obj.parameter_obj.freq_range
|
||||||
return param.freq_range
|
|
||||||
return "-"
|
return "-"
|
||||||
freq_range.short_description = "Полоса, МГц"
|
freq_range.short_description = "Полоса, МГц"
|
||||||
freq_range.admin_order_field = "parameters_obj__freq_range"
|
freq_range.admin_order_field = "parameter_obj__freq_range"
|
||||||
|
|
||||||
def bod_velocity(self, obj):
|
def bod_velocity(self, obj):
|
||||||
"""Отображает символьную скорость из связанного параметра."""
|
"""Отображает символьную скорость из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param:
|
return obj.parameter_obj.bod_velocity
|
||||||
return param.bod_velocity
|
|
||||||
return "-"
|
return "-"
|
||||||
bod_velocity.short_description = "Сим. v, БОД"
|
bod_velocity.short_description = "Сим. v, БОД"
|
||||||
|
|
||||||
def modulation(self, obj):
|
def modulation(self, obj):
|
||||||
"""Отображает модуляцию из связанного параметра."""
|
"""Отображает модуляцию из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param and param.modulation:
|
if obj.parameter_obj.modulation:
|
||||||
return param.modulation.name
|
return obj.parameter_obj.modulation.name
|
||||||
return "-"
|
return "-"
|
||||||
modulation.short_description = "Модуляция"
|
modulation.short_description = "Модуляция"
|
||||||
|
|
||||||
def snr(self, obj):
|
def snr(self, obj):
|
||||||
"""Отображает отношение сигнал/шум из связанного параметра."""
|
"""Отображает отношение сигнал/шум из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||||
if param:
|
return obj.parameter_obj.snr
|
||||||
return param.snr
|
|
||||||
return "-"
|
return "-"
|
||||||
snr.short_description = "ОСШ"
|
snr.short_description = "ОСШ"
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,47 @@ class NewEventForm(forms.Form):
|
|||||||
'accept': '.xlsx,.xls'
|
'accept': '.xlsx,.xls'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FillLyngsatDataForm(forms.Form):
|
||||||
|
"""Форма для заполнения данных из Lyngsat"""
|
||||||
|
|
||||||
|
REGION_CHOICES = [
|
||||||
|
('europe', 'Европа'),
|
||||||
|
('asia', 'Азия'),
|
||||||
|
('america', 'Америка'),
|
||||||
|
('atlantic', 'Атлантика'),
|
||||||
|
]
|
||||||
|
|
||||||
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Satellite.objects.all().order_by('name'),
|
||||||
|
label="Выберите спутники",
|
||||||
|
widget=forms.SelectMultiple(attrs={
|
||||||
|
'class': 'form-select',
|
||||||
|
'size': '10'
|
||||||
|
}),
|
||||||
|
required=True,
|
||||||
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
|
||||||
|
)
|
||||||
|
|
||||||
|
regions = forms.MultipleChoiceField(
|
||||||
|
choices=REGION_CHOICES,
|
||||||
|
label="Выберите регионы",
|
||||||
|
widget=forms.SelectMultiple(attrs={
|
||||||
|
'class': 'form-select',
|
||||||
|
'size': '4'
|
||||||
|
}),
|
||||||
|
required=True,
|
||||||
|
initial=['europe', 'asia', 'america', 'atlantic'],
|
||||||
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
|
||||||
|
)
|
||||||
class ParameterForm(forms.ModelForm):
|
class ParameterForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||||
|
|
||||||
|
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
|
||||||
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Parameter
|
model = Parameter
|
||||||
fields = [
|
fields = [
|
||||||
@@ -115,22 +155,92 @@ class ParameterForm(forms.ModelForm):
|
|||||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
'bod_velocity', 'modulation', 'snr', 'standard'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'id_satellite': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
'id_satellite': forms.Select(attrs={
|
||||||
'frequency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
'class': 'form-select',
|
||||||
'freq_range': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
'required': True
|
||||||
'bod_velocity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
}),
|
||||||
'snr': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
'frequency': forms.NumberInput(attrs={
|
||||||
'polarization': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
'class': 'form-control',
|
||||||
'modulation': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
'step': '0.000001',
|
||||||
'standard': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
'min': '0',
|
||||||
|
'max': '50000',
|
||||||
|
'placeholder': 'Введите частоту в МГц'
|
||||||
|
}),
|
||||||
|
'freq_range': forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'step': '0.000001',
|
||||||
|
'min': '0',
|
||||||
|
'max': '1000',
|
||||||
|
'placeholder': 'Введите полосу частот в МГц'
|
||||||
|
}),
|
||||||
|
'bod_velocity': forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'step': '0.001',
|
||||||
|
'min': '0',
|
||||||
|
'placeholder': 'Введите символьную скорость в БОД'
|
||||||
|
}),
|
||||||
|
'snr': forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'step': '0.001',
|
||||||
|
'min': '-50',
|
||||||
|
'max': '100',
|
||||||
|
'placeholder': 'Введите ОСШ в дБ'
|
||||||
|
}),
|
||||||
|
'polarization': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'modulation': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'standard': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'id_satellite': 'Спутник',
|
||||||
|
'frequency': 'Частота (МГц)',
|
||||||
|
'freq_range': 'Полоса частот (МГц)',
|
||||||
|
'polarization': 'Поляризация',
|
||||||
|
'bod_velocity': 'Символьная скорость (БОД)',
|
||||||
|
'modulation': 'Модуляция',
|
||||||
|
'snr': 'ОСШ (дБ)',
|
||||||
|
'standard': 'Стандарт',
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
|
||||||
|
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
|
||||||
|
'bod_velocity': 'Символьная скорость должна быть положительной',
|
||||||
|
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['id_satellite'].choices = [(s.id, s.name) for s in Satellite.objects.all()]
|
|
||||||
self.fields['polarization'].choices = [(p.id, p.name) for p in Polarization.objects.all()]
|
# Динамически загружаем choices для select полей
|
||||||
self.fields['modulation'].choices = [(m.id, m.name) for m in Modulation.objects.all()]
|
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
||||||
self.fields['standard'].choices = [(s.id, s.name) for s in Standard.objects.all()]
|
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
|
||||||
|
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name')
|
||||||
|
self.fields['standard'].queryset = Standard.objects.all().order_by('name')
|
||||||
|
|
||||||
|
# Делаем спутник обязательным полем
|
||||||
|
self.fields['id_satellite'].required = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""
|
||||||
|
Дополнительная валидация формы.
|
||||||
|
|
||||||
|
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
||||||
|
"""
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
frequency = cleaned_data.get('frequency')
|
||||||
|
freq_range = cleaned_data.get('freq_range')
|
||||||
|
bod_velocity = cleaned_data.get('bod_velocity')
|
||||||
|
|
||||||
|
# Проверка что частота больше полосы частот
|
||||||
|
if frequency and freq_range:
|
||||||
|
if freq_range > frequency:
|
||||||
|
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
|
||||||
|
|
||||||
|
# Проверка что символьная скорость соответствует полосе частот
|
||||||
|
if bod_velocity and freq_range:
|
||||||
|
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
||||||
|
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
class GeoForm(forms.ModelForm):
|
class GeoForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -143,9 +253,49 @@ class GeoForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ObjItemForm(forms.ModelForm):
|
class ObjItemForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Форма для создания и редактирования объектов (источников сигнала).
|
||||||
|
|
||||||
|
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
|
||||||
|
через ParameterForm с использованием OneToOne связи.
|
||||||
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Введите название объекта',
|
||||||
|
'maxlength': '100'
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
labels = {
|
||||||
|
'name': 'Название объекта',
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Уникальное название объекта/источника сигнала',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Делаем поле name необязательным, так как оно может быть пустым
|
||||||
|
self.fields['name'].required = False
|
||||||
|
|
||||||
|
def clean_name(self):
|
||||||
|
"""
|
||||||
|
Валидация поля name.
|
||||||
|
|
||||||
|
Проверяет что название не состоит только из пробелов.
|
||||||
|
"""
|
||||||
|
name = self.cleaned_data.get('name')
|
||||||
|
|
||||||
|
if name:
|
||||||
|
# Удаляем лишние пробелы
|
||||||
|
name = name.strip()
|
||||||
|
|
||||||
|
# Проверяем что после удаления пробелов что-то осталось
|
||||||
|
if not name:
|
||||||
|
raise forms.ValidationError('Название не может состоять только из пробелов')
|
||||||
|
|
||||||
|
return name
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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='Объект'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -243,11 +243,11 @@ class ObjItemQuerySet(models.QuerySet):
|
|||||||
"updated_by__user",
|
"updated_by__user",
|
||||||
"created_by__user",
|
"created_by__user",
|
||||||
"source_type_obj",
|
"source_type_obj",
|
||||||
).prefetch_related(
|
"parameter_obj",
|
||||||
"parameters_obj__id_satellite",
|
"parameter_obj__id_satellite",
|
||||||
"parameters_obj__polarization",
|
"parameter_obj__polarization",
|
||||||
"parameters_obj__modulation",
|
"parameter_obj__modulation",
|
||||||
"parameters_obj__standard",
|
"parameter_obj__standard",
|
||||||
)
|
)
|
||||||
|
|
||||||
def recent(self, days=30):
|
def recent(self, days=30):
|
||||||
@@ -449,8 +449,14 @@ class Parameter(models.Model):
|
|||||||
verbose_name="Стандарт",
|
verbose_name="Стандарт",
|
||||||
)
|
)
|
||||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||||||
objitems = models.ManyToManyField(
|
objitem = models.OneToOneField(
|
||||||
ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True
|
ObjItem,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="parameter_obj",
|
||||||
|
verbose_name="Объект",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Связанный объект"
|
||||||
)
|
)
|
||||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
|
|||||||
@@ -124,23 +124,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Map Views Card -->
|
<!-- Lyngsat Data Fill Card -->
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="card h-100 shadow-sm border-0">
|
<div class="card h-100 shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="bg-secondary bg-opacity-10 rounded-circle p-2 me-3">
|
<div class="bg-secondary bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-map text-secondary" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download text-secondary" viewBox="0 0 16 16">
|
||||||
<path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zM1.61 2.22l4.39.88v10.88l-4.39-.88zm9.18 10.88 4-.8V2.34l-4 .8z"/>
|
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
|
||||||
|
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title mb-0">Карты</h3>
|
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
|
||||||
</div>
|
|
||||||
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
|
||||||
<div class="mt-2">
|
|
||||||
<a href="{% url 'mapsapp:2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
|
||||||
<a href="{% url 'mapsapp:3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
|
||||||
|
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
|
||||||
|
Заполнить данные Lyngsat
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
118
dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html
Normal file
118
dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Заполнение данных Lyngsat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h3 class="mb-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
|
||||||
|
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
Заполнение данных из Lyngsat
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Alert messages -->
|
||||||
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
|
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время,
|
||||||
|
так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Satellites Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold">
|
||||||
|
{{ form.satellites.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.satellites }}
|
||||||
|
{% if form.satellites.help_text %}
|
||||||
|
<div class="form-text">{{ form.satellites.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.satellites.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.satellites.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regions Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="{{ form.regions.id_for_label }}" class="form-label fw-bold">
|
||||||
|
{{ form.regions.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.regions }}
|
||||||
|
{% if form.regions.help_text %}
|
||||||
|
<div class="form-text">{{ form.regions.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.regions.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.regions.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
|
||||||
|
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
|
||||||
|
</svg>
|
||||||
|
Назад
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
Заполнить данные
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Card -->
|
||||||
|
<div class="card mt-4 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Информация</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat.
|
||||||
|
Выберите один или несколько спутников и регионы для парсинга данных.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li>
|
||||||
|
<li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li>
|
||||||
|
<li>Существующие записи будут обновлены, новые - созданы</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Form validation
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
var forms = document.querySelectorAll('.needs-validation');
|
||||||
|
Array.prototype.slice.call(forms).forEach(function(form) {
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
241
dbapp/mainapp/templates/mainapp/lyngsat_task_status.html
Normal file
241
dbapp/mainapp/templates/mainapp/lyngsat_task_status.html
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Статус задачи Lyngsat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h3 class="mb-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
||||||
|
</svg>
|
||||||
|
Статус задачи заполнения данных Lyngsat
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if task_id %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span id="status-text">Загрузка статуса...</span>
|
||||||
|
<span id="progress-percent">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 25px;">
|
||||||
|
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar" style="width: 0%;"
|
||||||
|
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<span id="progress-text">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task State -->
|
||||||
|
<div id="task-state-container" class="alert alert-info" role="alert">
|
||||||
|
<strong>Состояние:</strong> <span id="task-state">Проверка...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Container (hidden by default) -->
|
||||||
|
<div id="results-container" class="d-none">
|
||||||
|
<h5 class="mt-4">Результаты обработки</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6>
|
||||||
|
<h3 class="card-title" id="result-satellites">-</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6>
|
||||||
|
<h3 class="card-title" id="result-sources">-</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-3 border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2 text-success">Создано записей</h6>
|
||||||
|
<h3 class="card-title text-success" id="result-created">-</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-3 border-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6>
|
||||||
|
<h3 class="card-title text-info" id="result-updated">-</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<div id="errors-container" class="d-none">
|
||||||
|
<h6 class="text-danger">Ошибки при обработке:</h6>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<ul id="errors-list" class="mb-0"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Container (hidden by default) -->
|
||||||
|
<div id="error-container" class="alert alert-danger d-none" role="alert">
|
||||||
|
<strong>Ошибка:</strong> <span id="error-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-between mt-4">
|
||||||
|
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
|
||||||
|
</svg>
|
||||||
|
Назад к форме
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn">
|
||||||
|
Перейти к действиям
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных.
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary">
|
||||||
|
Перейти к форме
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if task_id %}
|
||||||
|
<script>
|
||||||
|
let taskId = '{{ task_id }}';
|
||||||
|
let pollInterval;
|
||||||
|
let isCompleted = false;
|
||||||
|
|
||||||
|
function updateProgress(data) {
|
||||||
|
const statusText = document.getElementById('status-text');
|
||||||
|
const progressBar = document.getElementById('progress-bar');
|
||||||
|
const progressText = document.getElementById('progress-text');
|
||||||
|
const progressPercent = document.getElementById('progress-percent');
|
||||||
|
const taskState = document.getElementById('task-state');
|
||||||
|
const taskStateContainer = document.getElementById('task-state-container');
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
taskState.textContent = data.state;
|
||||||
|
|
||||||
|
if (data.state === 'PENDING') {
|
||||||
|
statusText.textContent = 'Задача в очереди...';
|
||||||
|
taskStateContainer.className = 'alert alert-info';
|
||||||
|
} else if (data.state === 'PROGRESS') {
|
||||||
|
const percent = data.percent || 0;
|
||||||
|
statusText.textContent = data.status || 'Обработка...';
|
||||||
|
progressBar.style.width = percent + '%';
|
||||||
|
progressBar.setAttribute('aria-valuenow', percent);
|
||||||
|
progressText.textContent = percent + '%';
|
||||||
|
progressPercent.textContent = percent + '%';
|
||||||
|
taskStateContainer.className = 'alert alert-info';
|
||||||
|
} else if (data.state === 'SUCCESS') {
|
||||||
|
statusText.textContent = 'Задача завершена успешно!';
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
progressBar.setAttribute('aria-valuenow', 100);
|
||||||
|
progressText.textContent = '100%';
|
||||||
|
progressPercent.textContent = '100%';
|
||||||
|
progressBar.classList.remove('progress-bar-animated');
|
||||||
|
progressBar.classList.add('bg-success');
|
||||||
|
taskStateContainer.className = 'alert alert-success';
|
||||||
|
|
||||||
|
// Show results
|
||||||
|
if (data.result) {
|
||||||
|
showResults(data.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCompleted = true;
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
} else if (data.state === 'FAILURE') {
|
||||||
|
statusText.textContent = 'Ошибка при выполнении задачи';
|
||||||
|
progressBar.classList.remove('progress-bar-animated');
|
||||||
|
progressBar.classList.add('bg-danger');
|
||||||
|
taskStateContainer.className = 'alert alert-danger';
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
const errorContainer = document.getElementById('error-container');
|
||||||
|
const errorText = document.getElementById('error-text');
|
||||||
|
errorText.textContent = data.error || 'Неизвестная ошибка';
|
||||||
|
errorContainer.classList.remove('d-none');
|
||||||
|
|
||||||
|
isCompleted = true;
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResults(result) {
|
||||||
|
const resultsContainer = document.getElementById('results-container');
|
||||||
|
resultsContainer.classList.remove('d-none');
|
||||||
|
|
||||||
|
document.getElementById('result-satellites').textContent = result.total_satellites || 0;
|
||||||
|
document.getElementById('result-sources').textContent = result.total_sources || 0;
|
||||||
|
document.getElementById('result-created').textContent = result.created || 0;
|
||||||
|
document.getElementById('result-updated').textContent = result.updated || 0;
|
||||||
|
|
||||||
|
// Show errors if any
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
const errorsContainer = document.getElementById('errors-container');
|
||||||
|
const errorsList = document.getElementById('errors-list');
|
||||||
|
errorsContainer.classList.remove('d-none');
|
||||||
|
|
||||||
|
errorsList.innerHTML = '';
|
||||||
|
result.errors.slice(0, 10).forEach(error => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = error;
|
||||||
|
errorsList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.errors.length > 10) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = `И еще ${result.errors.length - 10} ошибок...`;
|
||||||
|
li.className = 'text-muted';
|
||||||
|
errorsList.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTaskStatus() {
|
||||||
|
fetch(`/api/lyngsat-task-status/${taskId}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateProgress(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error checking task status:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
checkTaskStatus();
|
||||||
|
pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds
|
||||||
|
|
||||||
|
// Stop polling after 30 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isCompleted) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.';
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -116,37 +116,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ВЧ загрузки -->
|
<!-- ВЧ загрузка -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-section-header">
|
<div class="form-section-header">
|
||||||
<h4>ВЧ загрузка</h4>
|
<h4>ВЧ загрузка</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for param in object.parameters_obj.all %}
|
{% if object.parameter_obj %}
|
||||||
<div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Спутник:</label>
|
<label class="form-label">Спутник:</label>
|
||||||
<div class="readonly-field">{{ param.id_satellite.name|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Частота (МГц):</label>
|
<label class="form-label">Частота (МГц):</label>
|
||||||
<div class="readonly-field">{{ param.frequency|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Полоса (МГц):</label>
|
<label class="form-label">Полоса (МГц):</label>
|
||||||
<div class="readonly-field">{{ param.freq_range|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Поляризация:</label>
|
<label class="form-label">Поляризация:</label>
|
||||||
<div class="readonly-field">{{ param.polarization.name|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,34 +153,33 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Символьная скорость:</label>
|
<label class="form-label">Символьная скорость:</label>
|
||||||
<div class="readonly-field">{{ param.bod_velocity|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Модуляция:</label>
|
<label class="form-label">Модуляция:</label>
|
||||||
<div class="readonly-field">{{ param.modulation.name|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">ОСШ:</label>
|
<label class="form-label">ОСШ:</label>
|
||||||
<div class="readonly-field">{{ param.snr|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Стандарт:</label>
|
<label class="form-label">Стандарт:</label>
|
||||||
<div class="readonly-field">{{ param.standard.name|default:"-" }}</div>
|
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
{% empty %}
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p>Нет данных о ВЧ загрузке</p>
|
<p>Нет данных о ВЧ загрузке</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Блок с картой -->
|
<!-- Блок с картой -->
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
||||||
<div>
|
<div>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
{% if object %}
|
{% if object %}
|
||||||
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
|
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post" id="objitem-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Основная информация -->
|
<!-- Основная информация -->
|
||||||
@@ -124,53 +124,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ВЧ загрузки -->
|
<!-- ВЧ загрузка -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-section-header d-flex justify-content-between align-items-center">
|
<div class="form-section-header">
|
||||||
<h4>ВЧ загрузка</h4>
|
<h4>ВЧ загрузка</h4>
|
||||||
{% if not parameter_forms.forms.0.instance.pk %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="parameters-container">
|
<div id="parameters-container">
|
||||||
{% for param_form in parameter_forms %}
|
|
||||||
{% comment %} <div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% endcomment %}
|
|
||||||
<div class="dynamic-form-header">
|
|
||||||
{% if parameter_forms.forms|length > 1 %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.id_satellite %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.id_satellite %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.frequency %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.frequency %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.freq_range %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.freq_range %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.polarization %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.polarization %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.bod_velocity %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.bod_velocity %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.modulation %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.modulation %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.snr %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.snr %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=param_form.standard %}
|
{% include 'mainapp/components/_form_field.html' with field=parameter_form.standard %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% comment %} </div> {% endcomment %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -348,42 +336,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Динамическое добавление ВЧ загрузок
|
|
||||||
let parameterIndex = {{ parameter_forms|length }};
|
|
||||||
|
|
||||||
document.getElementById('add-parameter')?.addEventListener('click', function() {
|
|
||||||
const container = document.getElementById('parameters-container');
|
|
||||||
const template = document.querySelector('.dynamic-form');
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
const clone = template.cloneNode(true);
|
|
||||||
clone.querySelectorAll('[id]').forEach(el => {
|
|
||||||
el.id = el.id.replace(/-\d+-/g, `-${parameterIndex}-`);
|
|
||||||
});
|
|
||||||
clone.querySelectorAll('[name]').forEach(el => {
|
|
||||||
el.name = el.name.replace(/-\d+-/g, `-${parameterIndex}-`);
|
|
||||||
});
|
|
||||||
clone.querySelectorAll('[for]').forEach(el => {
|
|
||||||
el.htmlFor = el.htmlFor.replace(/-\d+-/g, `-${parameterIndex}-`);
|
|
||||||
});
|
|
||||||
clone.querySelector('.dynamic-form-header h5').textContent = `ВЧ загрузка #${parameterIndex + 1}`;
|
|
||||||
clone.dataset.parameterIndex = parameterIndex;
|
|
||||||
container.appendChild(clone);
|
|
||||||
parameterIndex++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаление ВЧ загрузок
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (e.target.classList.contains('remove-parameter')) {
|
|
||||||
if (document.querySelectorAll('.dynamic-form').length > 1) {
|
|
||||||
e.target.closest('.dynamic-form').remove();
|
|
||||||
} else {
|
|
||||||
alert('Должна быть хотя бы одна ВЧ загрузка');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Инициализация карты
|
// Инициализация карты
|
||||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||||
|
|||||||
@@ -25,4 +25,8 @@ urlpatterns = [
|
|||||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
|
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||||
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||||
|
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
||||||
|
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
|
path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
|
path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
||||||
]
|
]
|
||||||
@@ -163,16 +163,6 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
|||||||
source = stroka[1]["Объект наблюдения"]
|
source = stroka[1]["Объект наблюдения"]
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
|
|
||||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
|
||||||
id_satellite=sat,
|
|
||||||
polarization=polarization_obj,
|
|
||||||
frequency=freq,
|
|
||||||
freq_range=freq_line,
|
|
||||||
bod_velocity=v,
|
|
||||||
modulation=mod_obj,
|
|
||||||
snr=snr,
|
|
||||||
)
|
|
||||||
|
|
||||||
geo, _ = Geo.objects.get_or_create(
|
geo, _ = Geo.objects.get_or_create(
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
coords=geo_point,
|
coords=geo_point,
|
||||||
@@ -187,12 +177,38 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
|||||||
geo.save()
|
geo.save()
|
||||||
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||||
|
|
||||||
existing_obj_items = ObjItem.objects.filter(
|
# Check if ObjItem with same geo already exists
|
||||||
parameters_obj=vch_load_obj, geo_obj=geo
|
existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
|
||||||
)
|
if existing_obj_item:
|
||||||
if not existing_obj_items.exists():
|
# Check if parameter with same values exists for this object
|
||||||
|
if (
|
||||||
|
hasattr(existing_obj_item, 'parameter_obj') and
|
||||||
|
existing_obj_item.parameter_obj and
|
||||||
|
existing_obj_item.parameter_obj.id_satellite == sat and
|
||||||
|
existing_obj_item.parameter_obj.polarization == polarization_obj and
|
||||||
|
existing_obj_item.parameter_obj.frequency == freq and
|
||||||
|
existing_obj_item.parameter_obj.freq_range == freq_line and
|
||||||
|
existing_obj_item.parameter_obj.bod_velocity == v and
|
||||||
|
existing_obj_item.parameter_obj.modulation == mod_obj and
|
||||||
|
existing_obj_item.parameter_obj.snr == snr
|
||||||
|
):
|
||||||
|
# Skip creating duplicate
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create new ObjItem and Parameter
|
||||||
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
|
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
|
||||||
obj_item.parameters_obj.set([vch_load_obj])
|
|
||||||
|
vch_load_obj = Parameter.objects.create(
|
||||||
|
id_satellite=sat,
|
||||||
|
polarization=polarization_obj,
|
||||||
|
frequency=freq,
|
||||||
|
freq_range=freq_line,
|
||||||
|
bod_velocity=v,
|
||||||
|
modulation=mod_obj,
|
||||||
|
snr=snr,
|
||||||
|
objitem=obj_item
|
||||||
|
)
|
||||||
|
|
||||||
geo.objitem = obj_item
|
geo.objitem = obj_item
|
||||||
geo.save()
|
geo.save()
|
||||||
|
|
||||||
@@ -316,14 +332,6 @@ def get_points_from_csv(file_content, current_user=None):
|
|||||||
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
|
|
||||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
|
||||||
id_satellite=sat_obj,
|
|
||||||
polarization=pol_obj,
|
|
||||||
frequency=row["freq"],
|
|
||||||
freq_range=row["f_range"],
|
|
||||||
# defaults={'id_user_add': user_to_use}
|
|
||||||
)
|
|
||||||
|
|
||||||
geo_obj, _ = Geo.objects.get_or_create(
|
geo_obj, _ = Geo.objects.get_or_create(
|
||||||
timestamp=row["time"],
|
timestamp=row["time"],
|
||||||
coords=Point(row["lon"], row["lat"], srid=4326),
|
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||||
@@ -334,12 +342,32 @@ def get_points_from_csv(file_content, current_user=None):
|
|||||||
)
|
)
|
||||||
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||||
|
|
||||||
existing_obj_items = ObjItem.objects.filter(
|
# Check if ObjItem with same geo already exists
|
||||||
parameters_obj=vch_load_obj, geo_obj=geo_obj
|
existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
|
||||||
)
|
if existing_obj_item:
|
||||||
if not existing_obj_items.exists():
|
# Check if parameter with same values exists for this object
|
||||||
|
if (
|
||||||
|
hasattr(existing_obj_item, 'parameter_obj') and
|
||||||
|
existing_obj_item.parameter_obj and
|
||||||
|
existing_obj_item.parameter_obj.id_satellite == sat_obj and
|
||||||
|
existing_obj_item.parameter_obj.polarization == pol_obj and
|
||||||
|
existing_obj_item.parameter_obj.frequency == row["freq"] and
|
||||||
|
existing_obj_item.parameter_obj.freq_range == row["f_range"]
|
||||||
|
):
|
||||||
|
# Skip creating duplicate
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create new ObjItem and Parameter
|
||||||
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
|
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
|
||||||
obj_item.parameters_obj.set([vch_load_obj])
|
|
||||||
|
vch_load_obj = Parameter.objects.create(
|
||||||
|
id_satellite=sat_obj,
|
||||||
|
polarization=pol_obj,
|
||||||
|
frequency=row["freq"],
|
||||||
|
freq_range=row["f_range"],
|
||||||
|
objitem=obj_item
|
||||||
|
)
|
||||||
|
|
||||||
geo_obj.objitem = obj_item
|
geo_obj.objitem = obj_item
|
||||||
geo_obj.save()
|
geo_obj.save()
|
||||||
|
|
||||||
@@ -598,29 +626,22 @@ def parse_pagination_params(
|
|||||||
|
|
||||||
def get_first_param_subquery(field_name: str):
|
def get_first_param_subquery(field_name: str):
|
||||||
"""
|
"""
|
||||||
Создает подзапрос для получения первого параметра объекта.
|
Возвращает F() выражение для доступа к полю параметра через OneToOne связь.
|
||||||
|
|
||||||
Используется для аннотации queryset с полями из связанной модели Parameter.
|
После рефакторинга связи Parameter-ObjItem с ManyToMany на OneToOne,
|
||||||
Возвращает значение указанного поля из первого параметра объекта.
|
эта функция упрощена для возврата прямого F() выражения вместо подзапроса.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field_name (str): Имя поля модели Parameter для извлечения.
|
field_name (str): Имя поля модели Parameter для извлечения.
|
||||||
Может включать связанные поля через __ (например, 'id_satellite__name').
|
Может включать связанные поля через __ (например, 'id_satellite__name').
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Subquery: Django Subquery объект для использования в annotate().
|
F: Django F() объект для использования в annotate().
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from django.db.models import Subquery, OuterRef
|
>>> freq_expr = get_first_param_subquery('frequency')
|
||||||
>>> freq_subq = get_first_param_subquery('frequency')
|
>>> objects = ObjItem.objects.annotate(first_freq=freq_expr)
|
||||||
>>> objects = ObjItem.objects.annotate(first_freq=Subquery(freq_subq))
|
|
||||||
>>> for obj in objects:
|
>>> for obj in objects:
|
||||||
... print(obj.first_freq)
|
... print(obj.first_freq)
|
||||||
"""
|
"""
|
||||||
from django.db.models import OuterRef
|
return F(f"parameter_obj__{field_name}")
|
||||||
|
|
||||||
return (
|
|
||||||
Parameter.objects.filter(objitems=OuterRef("pk"))
|
|
||||||
.order_by("id")
|
|
||||||
.values(field_name)[:1]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
|
||||||
from django.contrib.gis.geos import Point
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import OuterRef, Prefetch, Subquery
|
from django.db.models import F
|
||||||
from django.forms import inlineformset_factory, modelformset_factory
|
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_GET
|
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
CreateView,
|
CreateView,
|
||||||
DeleteView,
|
DeleteView,
|
||||||
FormView,
|
FormView,
|
||||||
TemplateView,
|
|
||||||
UpdateView,
|
UpdateView,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,20 +37,20 @@ from .forms import (
|
|||||||
UploadFileForm,
|
UploadFileForm,
|
||||||
UploadVchLoad,
|
UploadVchLoad,
|
||||||
VchLinkForm,
|
VchLinkForm,
|
||||||
|
FillLyngsatDataForm,
|
||||||
)
|
)
|
||||||
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
||||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite
|
from .models import Geo, Modulation, ObjItem, Polarization, Satellite
|
||||||
from .utils import (
|
from .utils import (
|
||||||
add_satellite_list,
|
add_satellite_list,
|
||||||
compare_and_link_vch_load,
|
compare_and_link_vch_load,
|
||||||
fill_data_from_df,
|
fill_data_from_df,
|
||||||
get_first_param_subquery,
|
|
||||||
get_points_from_csv,
|
get_points_from_csv,
|
||||||
get_vch_load_from_html,
|
get_vch_load_from_html,
|
||||||
kub_report,
|
kub_report,
|
||||||
parse_pagination_params,
|
parse_pagination_params,
|
||||||
)
|
)
|
||||||
from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml
|
from mapsapp.utils import parse_transponders_from_xml
|
||||||
|
|
||||||
|
|
||||||
class AddSatellitesView(LoginRequiredMixin, View):
|
class AddSatellitesView(LoginRequiredMixin, View):
|
||||||
@@ -150,9 +144,12 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
|||||||
class GetLocationsView(LoginRequiredMixin, View):
|
class GetLocationsView(LoginRequiredMixin, View):
|
||||||
def get(self, request, sat_id):
|
def get(self, request, sat_id):
|
||||||
locations = (
|
locations = (
|
||||||
ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
|
ObjItem.objects.filter(parameter_obj__id_satellite=sat_id)
|
||||||
.select_related("geo_obj")
|
.select_related(
|
||||||
.prefetch_related("parameters_obj__polarization")
|
"geo_obj",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not locations.exists():
|
if not locations.exists():
|
||||||
@@ -163,11 +160,10 @@ class GetLocationsView(LoginRequiredMixin, View):
|
|||||||
if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
|
if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
params = list(loc.parameters_obj.all())
|
param = getattr(loc, 'parameter_obj', None)
|
||||||
if not params:
|
if not param:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
param = params[0]
|
|
||||||
features.append(
|
features.append(
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
@@ -220,11 +216,12 @@ class ShowMapView(RoleRequiredMixin, View):
|
|||||||
points = []
|
points = []
|
||||||
if ids:
|
if ids:
|
||||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
|
locations = ObjItem.objects.filter(id__in=id_list).select_related(
|
||||||
"parameters_obj__id_satellite",
|
"parameter_obj",
|
||||||
"parameters_obj__polarization",
|
"parameter_obj__id_satellite",
|
||||||
"parameters_obj__modulation",
|
"parameter_obj__polarization",
|
||||||
"parameters_obj__standard",
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
"geo_obj",
|
"geo_obj",
|
||||||
)
|
)
|
||||||
for obj in locations:
|
for obj in locations:
|
||||||
@@ -234,7 +231,9 @@ class ShowMapView(RoleRequiredMixin, View):
|
|||||||
or not obj.geo_obj.coords
|
or not obj.geo_obj.coords
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
param = obj.parameters_obj.get()
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
points.append(
|
points.append(
|
||||||
{
|
{
|
||||||
"name": f"{obj.name}",
|
"name": f"{obj.name}",
|
||||||
@@ -265,11 +264,12 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
|||||||
points = []
|
points = []
|
||||||
if ids:
|
if ids:
|
||||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
|
locations = ObjItem.objects.filter(id__in=id_list).select_related(
|
||||||
"parameters_obj__id_satellite",
|
"parameter_obj",
|
||||||
"parameters_obj__polarization",
|
"parameter_obj__id_satellite",
|
||||||
"parameters_obj__modulation",
|
"parameter_obj__polarization",
|
||||||
"parameters_obj__standard",
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
"geo_obj",
|
"geo_obj",
|
||||||
)
|
)
|
||||||
for obj in locations:
|
for obj in locations:
|
||||||
@@ -279,7 +279,9 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
|||||||
or not obj.geo_obj.coords
|
or not obj.geo_obj.coords
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
param = obj.parameters_obj.get()
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
points.append(
|
points.append(
|
||||||
{
|
{
|
||||||
"name": f"{obj.name}",
|
"name": f"{obj.name}",
|
||||||
@@ -429,7 +431,7 @@ class DeleteSelectedObjectsView(RoleRequiredMixin, View):
|
|||||||
class ObjItemListView(LoginRequiredMixin, View):
|
class ObjItemListView(LoginRequiredMixin, View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
satellites = (
|
satellites = (
|
||||||
Satellite.objects.filter(parameters__objitems__isnull=False)
|
Satellite.objects.filter(parameters__objitem__isnull=False)
|
||||||
.distinct()
|
.distinct()
|
||||||
.only("id", "name")
|
.only("id", "name")
|
||||||
.order_by("name")
|
.order_by("name")
|
||||||
@@ -472,32 +474,31 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
"geo_obj",
|
"geo_obj",
|
||||||
"updated_by__user",
|
"updated_by__user",
|
||||||
"created_by__user",
|
"created_by__user",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.filter(parameter_obj__id_satellite_id__in=selected_satellites)
|
||||||
"parameters_obj__id_satellite",
|
|
||||||
"parameters_obj__polarization",
|
|
||||||
"parameters_obj__modulation",
|
|
||||||
"parameters_obj__standard",
|
|
||||||
)
|
|
||||||
.filter(parameters_obj__id_satellite_id__in=selected_satellites)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
objects = ObjItem.objects.select_related(
|
objects = ObjItem.objects.select_related(
|
||||||
"geo_obj",
|
"geo_obj",
|
||||||
"updated_by__user",
|
"updated_by__user",
|
||||||
"created_by__user",
|
"created_by__user",
|
||||||
).prefetch_related(
|
"parameter_obj",
|
||||||
"parameters_obj__id_satellite",
|
"parameter_obj__id_satellite",
|
||||||
"parameters_obj__polarization",
|
"parameter_obj__polarization",
|
||||||
"parameters_obj__modulation",
|
"parameter_obj__modulation",
|
||||||
"parameters_obj__standard",
|
"parameter_obj__standard",
|
||||||
)
|
)
|
||||||
|
|
||||||
if freq_min is not None and freq_min.strip() != "":
|
if freq_min is not None and freq_min.strip() != "":
|
||||||
try:
|
try:
|
||||||
freq_min_val = float(freq_min)
|
freq_min_val = float(freq_min)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__frequency__gte=freq_min_val
|
parameter_obj__frequency__gte=freq_min_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
@@ -505,7 +506,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
try:
|
try:
|
||||||
freq_max_val = float(freq_max)
|
freq_max_val = float(freq_max)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__frequency__lte=freq_max_val
|
parameter_obj__frequency__lte=freq_max_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
@@ -514,7 +515,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
try:
|
try:
|
||||||
range_min_val = float(range_min)
|
range_min_val = float(range_min)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__freq_range__gte=range_min_val
|
parameter_obj__freq_range__gte=range_min_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
@@ -522,7 +523,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
try:
|
try:
|
||||||
range_max_val = float(range_max)
|
range_max_val = float(range_max)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__freq_range__lte=range_max_val
|
parameter_obj__freq_range__lte=range_max_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
@@ -530,13 +531,13 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
if snr_min is not None and snr_min.strip() != "":
|
if snr_min is not None and snr_min.strip() != "":
|
||||||
try:
|
try:
|
||||||
snr_min_val = float(snr_min)
|
snr_min_val = float(snr_min)
|
||||||
objects = objects.filter(parameters_obj__snr__gte=snr_min_val)
|
objects = objects.filter(parameter_obj__snr__gte=snr_min_val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if snr_max is not None and snr_max.strip() != "":
|
if snr_max is not None and snr_max.strip() != "":
|
||||||
try:
|
try:
|
||||||
snr_max_val = float(snr_max)
|
snr_max_val = float(snr_max)
|
||||||
objects = objects.filter(parameters_obj__snr__lte=snr_max_val)
|
objects = objects.filter(parameter_obj__snr__lte=snr_max_val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -544,7 +545,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
try:
|
try:
|
||||||
bod_min_val = float(bod_min)
|
bod_min_val = float(bod_min)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__bod_velocity__gte=bod_min_val
|
parameter_obj__bod_velocity__gte=bod_min_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
@@ -552,19 +553,19 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
try:
|
try:
|
||||||
bod_max_val = float(bod_max)
|
bod_max_val = float(bod_max)
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__bod_velocity__lte=bod_max_val
|
parameter_obj__bod_velocity__lte=bod_max_val
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if selected_modulations:
|
if selected_modulations:
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__modulation__id__in=selected_modulations
|
parameter_obj__modulation__id__in=selected_modulations
|
||||||
)
|
)
|
||||||
|
|
||||||
if selected_polarizations:
|
if selected_polarizations:
|
||||||
objects = objects.filter(
|
objects = objects.filter(
|
||||||
parameters_obj__polarization__id__in=selected_polarizations
|
parameter_obj__polarization__id__in=selected_polarizations
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_kupsat == "1":
|
if has_kupsat == "1":
|
||||||
@@ -609,22 +610,14 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
else:
|
else:
|
||||||
selected_sat_id = None
|
selected_sat_id = None
|
||||||
|
|
||||||
first_param_freq_subq = get_first_param_subquery("frequency")
|
|
||||||
first_param_range_subq = get_first_param_subquery("freq_range")
|
|
||||||
first_param_snr_subq = get_first_param_subquery("snr")
|
|
||||||
first_param_bod_subq = get_first_param_subquery("bod_velocity")
|
|
||||||
first_param_sat_name_subq = get_first_param_subquery("id_satellite__name")
|
|
||||||
first_param_pol_name_subq = get_first_param_subquery("polarization__name")
|
|
||||||
first_param_mod_name_subq = get_first_param_subquery("modulation__name")
|
|
||||||
|
|
||||||
objects = objects.annotate(
|
objects = objects.annotate(
|
||||||
first_param_freq=Subquery(first_param_freq_subq),
|
first_param_freq=F("parameter_obj__frequency"),
|
||||||
first_param_range=Subquery(first_param_range_subq),
|
first_param_range=F("parameter_obj__freq_range"),
|
||||||
first_param_snr=Subquery(first_param_snr_subq),
|
first_param_snr=F("parameter_obj__snr"),
|
||||||
first_param_bod=Subquery(first_param_bod_subq),
|
first_param_bod=F("parameter_obj__bod_velocity"),
|
||||||
first_param_sat_name=Subquery(first_param_sat_name_subq),
|
first_param_sat_name=F("parameter_obj__id_satellite__name"),
|
||||||
first_param_pol_name=Subquery(first_param_pol_name_subq),
|
first_param_pol_name=F("parameter_obj__polarization__name"),
|
||||||
first_param_mod_name=Subquery(first_param_mod_name_subq),
|
first_param_mod_name=F("parameter_obj__modulation__name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
valid_sort_fields = {
|
valid_sort_fields = {
|
||||||
@@ -664,11 +657,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
processed_objects = []
|
processed_objects = []
|
||||||
for obj in page_obj:
|
for obj in page_obj:
|
||||||
param = None
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
if hasattr(obj, "parameters_obj") and obj.parameters_obj.all():
|
|
||||||
param_list = list(obj.parameters_obj.all())
|
|
||||||
if param_list:
|
|
||||||
param = param_list[0]
|
|
||||||
|
|
||||||
geo_coords = "-"
|
geo_coords = "-"
|
||||||
geo_timestamp = "-"
|
geo_timestamp = "-"
|
||||||
@@ -874,40 +863,33 @@ class ObjItemFormView(
|
|||||||
# Сохраняем параметры возврата для кнопки "Назад"
|
# Сохраняем параметры возврата для кнопки "Назад"
|
||||||
context["return_params"] = self.request.GET.get('return_params', '')
|
context["return_params"] = self.request.GET.get('return_params', '')
|
||||||
|
|
||||||
ParameterFormSet = modelformset_factory(
|
# Работаем с одной формой параметра вместо formset
|
||||||
Parameter,
|
if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
|
||||||
form=ParameterForm,
|
context["parameter_form"] = ParameterForm(
|
||||||
extra=self.get_parameter_formset_extra(),
|
instance=self.object.parameter_obj, prefix="parameter"
|
||||||
can_delete=True,
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
context["parameter_form"] = ParameterForm(prefix="parameter")
|
||||||
|
|
||||||
if self.object:
|
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
parameter_queryset = self.object.parameters_obj.all()
|
|
||||||
context["parameter_forms"] = ParameterFormSet(
|
|
||||||
queryset=parameter_queryset, prefix="parameters"
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasattr(self.object, "geo_obj"):
|
|
||||||
context["geo_form"] = GeoForm(
|
context["geo_form"] = GeoForm(
|
||||||
instance=self.object.geo_obj, prefix="geo"
|
instance=self.object.geo_obj, prefix="geo"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
context["geo_form"] = GeoForm(prefix="geo")
|
context["geo_form"] = GeoForm(prefix="geo")
|
||||||
else:
|
|
||||||
context["parameter_forms"] = ParameterFormSet(
|
|
||||||
queryset=Parameter.objects.none(), prefix="parameters"
|
|
||||||
)
|
|
||||||
context["geo_form"] = GeoForm(prefix="geo")
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_parameter_formset_extra(self):
|
|
||||||
"""Возвращает количество дополнительных форм для параметров."""
|
|
||||||
return 0 if self.object else 1
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
context = self.get_context_data()
|
# Получаем форму параметра
|
||||||
parameter_forms = context["parameter_forms"]
|
if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
|
||||||
|
parameter_form = ParameterForm(
|
||||||
|
self.request.POST,
|
||||||
|
instance=self.object.parameter_obj,
|
||||||
|
prefix="parameter"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parameter_form = ParameterForm(self.request.POST, prefix="parameter")
|
||||||
|
|
||||||
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
|
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
|
||||||
@@ -919,17 +901,26 @@ class ObjItemFormView(
|
|||||||
self.set_user_fields()
|
self.set_user_fields()
|
||||||
self.object.save()
|
self.object.save()
|
||||||
|
|
||||||
# Сохраняем связанные параметры
|
# Сохраняем связанный параметр
|
||||||
if parameter_forms.is_valid():
|
if parameter_form.is_valid():
|
||||||
self.save_parameters(parameter_forms)
|
self.save_parameter(parameter_form)
|
||||||
|
else:
|
||||||
|
context = self.get_context_data()
|
||||||
|
context.update({
|
||||||
|
'form': form,
|
||||||
|
'parameter_form': parameter_form,
|
||||||
|
'geo_form': geo_form,
|
||||||
|
})
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
# Сохраняем геоданные
|
# Сохраняем геоданные
|
||||||
if geo_form.is_valid():
|
if geo_form.is_valid():
|
||||||
self.save_geo_data(geo_form)
|
self.save_geo_data(geo_form)
|
||||||
else:
|
else:
|
||||||
|
context = self.get_context_data()
|
||||||
context.update({
|
context.update({
|
||||||
'form': form,
|
'form': form,
|
||||||
'parameter_forms': parameter_forms,
|
'parameter_form': parameter_form,
|
||||||
'geo_form': geo_form,
|
'geo_form': geo_form,
|
||||||
})
|
})
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
@@ -940,51 +931,12 @@ class ObjItemFormView(
|
|||||||
"""Устанавливает поля пользователя для объекта."""
|
"""Устанавливает поля пользователя для объекта."""
|
||||||
raise NotImplementedError("Subclasses must implement set_user_fields()")
|
raise NotImplementedError("Subclasses must implement set_user_fields()")
|
||||||
|
|
||||||
def save_parameters(self, parameter_forms):
|
def save_parameter(self, parameter_form):
|
||||||
"""Сохраняет параметры объекта с проверкой дубликатов."""
|
"""Сохраняет параметр объекта через OneToOne связь."""
|
||||||
instances = parameter_forms.save(commit=False)
|
if parameter_form.is_valid():
|
||||||
|
instance = parameter_form.save(commit=False)
|
||||||
# Обрабатываем удаленные параметры
|
instance.objitem = self.object
|
||||||
for deleted_obj in parameter_forms.deleted_objects:
|
|
||||||
# Отвязываем параметр от объекта
|
|
||||||
deleted_obj.objitems.remove(self.object)
|
|
||||||
# Если параметр больше не связан ни с одним объектом, удаляем его
|
|
||||||
if not deleted_obj.objitems.exists():
|
|
||||||
deleted_obj.delete()
|
|
||||||
|
|
||||||
for instance in instances:
|
|
||||||
# Проверяем, существует ли уже такая ВЧ загрузка
|
|
||||||
existing_param = Parameter.objects.filter(
|
|
||||||
id_satellite=instance.id_satellite,
|
|
||||||
polarization=instance.polarization,
|
|
||||||
frequency=instance.frequency,
|
|
||||||
freq_range=instance.freq_range,
|
|
||||||
bod_velocity=instance.bod_velocity,
|
|
||||||
modulation=instance.modulation,
|
|
||||||
snr=instance.snr,
|
|
||||||
standard=instance.standard,
|
|
||||||
).exclude(pk=instance.pk if instance.pk else None).first()
|
|
||||||
|
|
||||||
if existing_param:
|
|
||||||
# Если найден дубликат, удаляем старую запись из объекта
|
|
||||||
if instance.pk:
|
|
||||||
# Отвязываем старый параметр от объекта
|
|
||||||
instance.objitems.remove(self.object)
|
|
||||||
# Если старый параметр больше не связан ни с одним объектом, удаляем его
|
|
||||||
if not instance.objitems.exists():
|
|
||||||
instance.delete()
|
|
||||||
# Используем существующий параметр
|
|
||||||
self.link_parameter_to_object(existing_param)
|
|
||||||
else:
|
|
||||||
# Сохраняем новый параметр
|
|
||||||
instance.save()
|
instance.save()
|
||||||
self.link_parameter_to_object(instance)
|
|
||||||
|
|
||||||
def link_parameter_to_object(self, parameter):
|
|
||||||
"""Связывает параметр с объектом."""
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Subclasses must implement link_parameter_to_object()"
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_geo_data(self, geo_form):
|
def save_geo_data(self, geo_form):
|
||||||
"""Сохраняет геоданные объекта."""
|
"""Сохраняет геоданные объекта."""
|
||||||
@@ -1019,11 +971,6 @@ class ObjItemUpdateView(ObjItemFormView):
|
|||||||
def set_user_fields(self):
|
def set_user_fields(self):
|
||||||
self.object.updated_by = self.request.user.customuser
|
self.object.updated_by = self.request.user.customuser
|
||||||
|
|
||||||
def link_parameter_to_object(self, parameter):
|
|
||||||
# Добавляем объект к параметру, если его там еще нет
|
|
||||||
if self.object not in parameter.objitems.all():
|
|
||||||
parameter.objitems.add(self.object)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjItemCreateView(ObjItemFormView, CreateView):
|
class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||||
"""Представление для создания ObjItem."""
|
"""Представление для создания ObjItem."""
|
||||||
@@ -1034,9 +981,6 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
|
|||||||
self.object.created_by = self.request.user.customuser
|
self.object.created_by = self.request.user.customuser
|
||||||
self.object.updated_by = self.request.user.customuser
|
self.object.updated_by = self.request.user.customuser
|
||||||
|
|
||||||
def link_parameter_to_object(self, parameter):
|
|
||||||
parameter.objitems.add(self.object)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
@@ -1066,11 +1010,11 @@ class ObjItemDetailView(LoginRequiredMixin, View):
|
|||||||
'geo_obj',
|
'geo_obj',
|
||||||
'updated_by__user',
|
'updated_by__user',
|
||||||
'created_by__user',
|
'created_by__user',
|
||||||
).prefetch_related(
|
'parameter_obj',
|
||||||
'parameters_obj__id_satellite',
|
'parameter_obj__id_satellite',
|
||||||
'parameters_obj__polarization',
|
'parameter_obj__polarization',
|
||||||
'parameters_obj__modulation',
|
'parameter_obj__modulation',
|
||||||
'parameters_obj__standard',
|
'parameter_obj__standard',
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not obj:
|
if not obj:
|
||||||
@@ -1086,3 +1030,97 @@ class ObjItemDetailView(LoginRequiredMixin, View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "mainapp/objitem_detail.html", context)
|
return render(request, "mainapp/objitem_detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""
|
||||||
|
Представление для заполнения данных из Lyngsat.
|
||||||
|
|
||||||
|
Позволяет выбрать спутники и регионы для парсинга данных с сайта Lyngsat.
|
||||||
|
Запускает асинхронную задачу Celery для обработки.
|
||||||
|
"""
|
||||||
|
template_name = "mainapp/fill_lyngsat_data.html"
|
||||||
|
form_class = FillLyngsatDataForm
|
||||||
|
success_url = reverse_lazy("mainapp:lyngsat_task_status")
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
satellites = form.cleaned_data["satellites"]
|
||||||
|
regions = form.cleaned_data["regions"]
|
||||||
|
|
||||||
|
# Получаем названия спутников
|
||||||
|
target_sats = [sat.name for sat in satellites]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from lyngsatapp.tasks import fill_lyngsat_data_task
|
||||||
|
|
||||||
|
# Запускаем асинхронную задачу
|
||||||
|
task = fill_lyngsat_data_task.delay(target_sats, regions)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
f"Задача запущена! ID задачи: {task.id}. "
|
||||||
|
"Вы будете перенаправлены на страницу отслеживания прогресса."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Перенаправляем на страницу статуса задачи
|
||||||
|
return redirect('mainapp:lyngsat_task_status', task_id=task.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Ошибка при запуске задачи: {str(e)}")
|
||||||
|
return redirect("mainapp:fill_lyngsat_data")
|
||||||
|
|
||||||
|
|
||||||
|
class LyngsatTaskStatusView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Представление для отслеживания статуса задачи заполнения данных Lyngsat.
|
||||||
|
"""
|
||||||
|
template_name = "mainapp/lyngsat_task_status.html"
|
||||||
|
|
||||||
|
def get(self, request, task_id=None):
|
||||||
|
context = {
|
||||||
|
'task_id': task_id
|
||||||
|
}
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
|
class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API для получения статуса задачи Celery.
|
||||||
|
"""
|
||||||
|
def get(self, request, task_id):
|
||||||
|
from celery.result import AsyncResult
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
task = AsyncResult(task_id)
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'task_id': task_id,
|
||||||
|
'state': task.state,
|
||||||
|
'result': None,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.state == 'PENDING':
|
||||||
|
response_data['status'] = 'Задача в очереди...'
|
||||||
|
elif task.state == 'PROGRESS':
|
||||||
|
response_data['status'] = task.info.get('status', '')
|
||||||
|
response_data['current'] = task.info.get('current', 0)
|
||||||
|
response_data['total'] = task.info.get('total', 1)
|
||||||
|
response_data['percent'] = int((task.info.get('current', 0) / task.info.get('total', 1)) * 100)
|
||||||
|
elif task.state == 'SUCCESS':
|
||||||
|
# Получаем результат из кеша
|
||||||
|
result = cache.get(f'lyngsat_task_{task_id}')
|
||||||
|
if result:
|
||||||
|
response_data['result'] = result
|
||||||
|
response_data['status'] = 'Задача завершена успешно'
|
||||||
|
else:
|
||||||
|
response_data['result'] = task.result
|
||||||
|
response_data['status'] = 'Задача завершена'
|
||||||
|
elif task.state == 'FAILURE':
|
||||||
|
response_data['status'] = 'Ошибка при выполнении задачи'
|
||||||
|
response_data['error'] = str(task.info)
|
||||||
|
else:
|
||||||
|
response_data['status'] = task.state
|
||||||
|
|
||||||
|
return JsonResponse(response_data)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pandas>=2.3.3
|
|||||||
psycopg>=3.2.10
|
psycopg>=3.2.10
|
||||||
psycopg2-binary>=2.9.11
|
psycopg2-binary>=2.9.11
|
||||||
redis>=6.4.0
|
redis>=6.4.0
|
||||||
|
celery>=5.4.0
|
||||||
|
django-celery-results>=2.5.1
|
||||||
requests>=2.32.5
|
requests>=2.32.5
|
||||||
reverse-geocoder>=1.5.1
|
reverse-geocoder>=1.5.1
|
||||||
scikit-learn>=1.7.2
|
scikit-learn>=1.7.2
|
||||||
|
|||||||
5
dbapp/start_celery_worker.sh
Executable file
5
dbapp/start_celery_worker.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to start Celery worker
|
||||||
|
|
||||||
|
echo "Starting Celery worker..."
|
||||||
|
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log
|
||||||
@@ -19,43 +19,72 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
web:
|
redis:
|
||||||
build:
|
image: redis:7-alpine
|
||||||
context: ./dbapp
|
container_name: redis-dev
|
||||||
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"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
# Монтируем только код приложения, не весь проект
|
- redis_data_dev:/data
|
||||||
- ./dbapp/dbapp:/app/dbapp
|
healthcheck:
|
||||||
- ./dbapp/mainapp:/app/mainapp
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
- ./dbapp/mapsapp:/app/mapsapp
|
interval: 10s
|
||||||
- ./dbapp/lyngsatapp:/app/lyngsatapp
|
timeout: 5s
|
||||||
- ./dbapp/static:/app/static
|
retries: 5
|
||||||
- ./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
|
||||||
|
|
||||||
|
flaresolverr:
|
||||||
|
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
container_name: flaresolverr-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8191:8191"
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- LOG_HTML=false
|
||||||
|
- CAPTCHA_SOLVER=none
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
# web:
|
||||||
|
# build:
|
||||||
|
# context: ./dbapp
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# container_name: django-app-dev
|
||||||
|
# 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:
|
||||||
|
# - "8000:8000"
|
||||||
|
# volumes:
|
||||||
|
# # Монтируем только код приложения, не весь проект
|
||||||
|
# - ./dbapp/dbapp:/app/dbapp
|
||||||
|
# - ./dbapp/mainapp:/app/mainapp
|
||||||
|
# - ./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:
|
||||||
|
# - app-network
|
||||||
|
|
||||||
# tileserver:
|
# tileserver:
|
||||||
# image: maptiler/tileserver-gl:latest
|
# image: maptiler/tileserver-gl:latest
|
||||||
# container_name: tileserver-gl-dev
|
# container_name: tileserver-gl-dev
|
||||||
@@ -72,9 +101,10 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data_dev:
|
postgres_data_dev:
|
||||||
static_volume_dev:
|
redis_data_dev:
|
||||||
media_volume_dev:
|
# static_volume_dev:
|
||||||
logs_volume_dev:
|
# media_volume_dev:
|
||||||
|
# logs_volume_dev:
|
||||||
# tileserver_config_dev:
|
# tileserver_config_dev:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Reference in New Issue
Block a user