From 65e6c9a3231f245322c30c6658995310e327a549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Mon, 10 Nov 2025 23:28:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D1=83=20=D0=B4=D0=BB=D1=8F=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D1=81=20LyngSat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ASYNC_CHANGES_SUMMARY.md | 396 +++++++++++++++++ ASYNC_LYNGSAT_GUIDE.md | 420 ++++++++++++++++++ CHANGES_SUMMARY.md | 133 ++++++ DEPLOYMENT_INSTRUCTIONS.md | 102 +++++ INSTALLATION_GUIDE.md | 347 +++++++++++++++ LYNGSAT_FILL_GUIDE.md | 78 ++++ QUICKSTART_ASYNC.md | 117 +++++ dbapp/dbapp/__init__.py | 8 + dbapp/dbapp/celery.py | 24 + dbapp/dbapp/settings/base.py | 200 +++++---- dbapp/lyngsatapp/admin.py | 8 +- dbapp/lyngsatapp/migrations/0001_initial.py | 37 ++ dbapp/lyngsatapp/parser.py | 338 +++++++------- dbapp/lyngsatapp/tasks.py | 73 +++ dbapp/lyngsatapp/utils.py | 210 +++++++-- dbapp/mainapp/forms.py | 34 ++ dbapp/mainapp/templates/mainapp/actions.html | 18 +- .../templates/mainapp/fill_lyngsat_data.html | 118 +++++ .../mainapp/lyngsat_task_status.html | 241 ++++++++++ dbapp/mainapp/urls.py | 4 + dbapp/mainapp/views.py | 95 ++++ dbapp/requirements.txt | 2 + dbapp/start_celery_worker.sh | 5 + docker-compose.yaml | 30 ++ 24 files changed, 2730 insertions(+), 308 deletions(-) create mode 100644 ASYNC_CHANGES_SUMMARY.md create mode 100644 ASYNC_LYNGSAT_GUIDE.md create mode 100644 CHANGES_SUMMARY.md create mode 100644 DEPLOYMENT_INSTRUCTIONS.md create mode 100644 INSTALLATION_GUIDE.md create mode 100644 LYNGSAT_FILL_GUIDE.md create mode 100644 QUICKSTART_ASYNC.md create mode 100644 dbapp/dbapp/celery.py create mode 100644 dbapp/lyngsatapp/migrations/0001_initial.py create mode 100644 dbapp/lyngsatapp/tasks.py create mode 100644 dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html create mode 100644 dbapp/mainapp/templates/mainapp/lyngsat_task_status.html create mode 100755 dbapp/start_celery_worker.sh diff --git a/ASYNC_CHANGES_SUMMARY.md b/ASYNC_CHANGES_SUMMARY.md new file mode 100644 index 0000000..740b4f1 --- /dev/null +++ b/ASYNC_CHANGES_SUMMARY.md @@ -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//` - статус конкретной задачи + - Добавлено: `/api/lyngsat-task-status//` - 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// + +**Ответ при выполнении:** +```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/ diff --git a/ASYNC_LYNGSAT_GUIDE.md b/ASYNC_LYNGSAT_GUIDE.md new file mode 100644 index 0000000..e7b3ad9 --- /dev/null +++ b/ASYNC_LYNGSAT_GUIDE.md @@ -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//` + +**Ответ**: +```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//` +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/) diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md new file mode 100644 index 0000000..ca1ce29 --- /dev/null +++ b/CHANGES_SUMMARY.md @@ -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. Протестировать форму заполнения данных через веб-интерфейс + +## Примечания + +- Процесс заполнения может занять продолжительное время (несколько минут на спутник) +- Рекомендуется начинать с небольшого количества спутников +- Все ошибки логируются и отображаются пользователю +- Существующие записи обновляются, новые создаются diff --git a/DEPLOYMENT_INSTRUCTIONS.md b/DEPLOYMENT_INSTRUCTIONS.md new file mode 100644 index 0000000..9ba33b9 --- /dev/null +++ b/DEPLOYMENT_INSTRUCTIONS.md @@ -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` diff --git a/INSTALLATION_GUIDE.md b/INSTALLATION_GUIDE.md new file mode 100644 index 0000000..36e3fa0 --- /dev/null +++ b/INSTALLATION_GUIDE.md @@ -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 +# Запустите снова +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 в репозитории с описанием проблемы и логами diff --git a/LYNGSAT_FILL_GUIDE.md b/LYNGSAT_FILL_GUIDE.md new file mode 100644 index 0000000..7a3f67f --- /dev/null +++ b/LYNGSAT_FILL_GUIDE.md @@ -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` (заменена карточка с картами) + +## Примечания + +- Процесс может занять продолжительное время в зависимости от количества выбранных спутников +- Рекомендуется выбирать небольшое количество спутников для первого запуска +- Существующие записи будут обновлены, новые - созданы +- Все ошибки логируются и отображаются пользователю diff --git a/QUICKSTART_ASYNC.md b/QUICKSTART_ASYNC.md new file mode 100644 index 0000000..a5009ae --- /dev/null +++ b/QUICKSTART_ASYNC.md @@ -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 на наличие ошибок +- Обновите страницу diff --git a/dbapp/dbapp/__init__.py b/dbapp/dbapp/__init__.py index e69de29..0c8d2b7 100644 --- a/dbapp/dbapp/__init__.py +++ b/dbapp/dbapp/__init__.py @@ -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 diff --git a/dbapp/dbapp/celery.py b/dbapp/dbapp/celery.py new file mode 100644 index 0000000..cc1657c --- /dev/null +++ b/dbapp/dbapp/celery.py @@ -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}') diff --git a/dbapp/dbapp/settings/base.py b/dbapp/dbapp/settings/base.py index 0d77e49..c6b92ef 100644 --- a/dbapp/dbapp/settings/base.py +++ b/dbapp/dbapp/settings/base.py @@ -21,19 +21,21 @@ load_dotenv() BASE_DIR = Path(__file__).resolve().parent.parent # GDAL/GEOS configuration for Windows -if os.name == 'nt': +if os.name == "nt": OSGEO4W = r"C:\Program Files\OSGeo4W" assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W - os.environ['OSGEO4W_ROOT'] = OSGEO4W - os.environ['PROJ_LIB'] = os.path.join(OSGEO4W, r"share\proj") - os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH'] + os.environ["OSGEO4W_ROOT"] = OSGEO4W + os.environ["PROJ_LIB"] = os.path.join(OSGEO4W, r"share\proj") + os.environ["PATH"] = OSGEO4W + r"\bin;" + os.environ["PATH"] # ============================================================================ # SECURITY SETTINGS # ============================================================================ # 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! # This should be overridden in environment-specific settings @@ -49,38 +51,42 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ # Django Autocomplete Light (must be before admin) - 'dal', - 'dal_select2', - + "dal", + "dal_select2", # Admin interface customization - 'admin_interface', - 'colorfield', - + "admin_interface", + "colorfield", # Django GIS - 'django.contrib.gis', - + "django.contrib.gis", # Django core apps - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", # Third-party apps - 'leaflet', - 'dynamic_raw_id', - 'rangefilter', - 'django_admin_multiple_choice_list_filter', - 'more_admin_filters', - 'import_export', - + "leaflet", + "dynamic_raw_id", + "rangefilter", + "django_admin_multiple_choice_list_filter", + "more_admin_filters", + "import_export", # Project apps - 'mainapp', - 'mapsapp', + "mainapp", + "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 # If you need a custom user model, uncomment and configure: # AUTH_USER_MODEL = 'mainapp.CustomUser' @@ -90,17 +96,17 @@ INSTALLED_APPS = [ # ============================================================================ MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'dbapp.urls' +ROOT_URLCONF = "dbapp.urls" # ============================================================================ # TEMPLATES CONFIGURATION @@ -108,36 +114,36 @@ ROOT_URLCONF = 'dbapp.urls' TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - BASE_DIR / 'templates', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + BASE_DIR / "templates", ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'dbapp.wsgi.application' +WSGI_APPLICATION = "dbapp.wsgi.application" # ============================================================================ # DATABASE CONFIGURATION # ============================================================================ DATABASES = { - 'default': { - 'ENGINE': os.getenv('DB_ENGINE', 'django.contrib.gis.db.backends.postgis'), - 'NAME': os.getenv('DB_NAME', 'db'), - 'USER': os.getenv('DB_USER', 'user'), - 'PASSWORD': os.getenv('DB_PASSWORD', 'password'), - 'HOST': os.getenv('DB_HOST', 'localhost'), - 'PORT': os.getenv('DB_PORT', '5432'), + "default": { + "ENGINE": os.getenv("DB_ENGINE", "django.contrib.gis.db.backends.postgis"), + "NAME": os.getenv("DB_NAME", "db"), + "USER": os.getenv("DB_USER", "user"), + "PASSWORD": os.getenv("DB_PASSWORD", "password"), + "HOST": os.getenv("DB_HOST", "localhost"), + "PORT": os.getenv("DB_PORT", "5432"), } } @@ -148,16 +154,16 @@ DATABASES = { 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 # ============================================================================ -LANGUAGE_CODE = 'ru' +LANGUAGE_CODE = "ru" -TIME_ZONE = 'Europe/Moscow' +TIME_ZONE = "Europe/Moscow" USE_I18N = True @@ -177,18 +183,18 @@ USE_TZ = True # AUTHENTICATION CONFIGURATION # ============================================================================ -LOGIN_URL = 'login' -LOGIN_REDIRECT_URL = 'mainapp:home' -LOGOUT_REDIRECT_URL = 'mainapp:home' +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "mainapp:home" +LOGOUT_REDIRECT_URL = "mainapp:home" # ============================================================================ # STATIC FILES CONFIGURATION # ============================================================================ -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = [ - BASE_DIR.parent / 'static', + BASE_DIR.parent / "static", ] # STATIC_ROOT will be set in production.py @@ -198,7 +204,7 @@ STATICFILES_DIRS = [ # ============================================================================ # Default primary key field type -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # ============================================================================ # THIRD-PARTY APP CONFIGURATION @@ -210,17 +216,53 @@ SILENCED_SYSTEM_CHECKS = ["security.W019"] # Leaflet Configuration LEAFLET_CONFIG = { - 'ATTRIBUTION_PREFIX': '', - 'TILES': [ + "ATTRIBUTION_PREFIX": "", + "TILES": [ ( - 'Satellite', - 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', - {'attribution': '© Esri', 'maxZoom': 16} + "Satellite", + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + {"attribution": "© Esri", "maxZoom": 16}, ), ( - 'Streets', - 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - {'attribution': '© OpenStreetMap'} - ) + "Streets", + "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + { + "attribution": '© OpenStreetMap' + }, + ), ], -} \ No newline at end of file +} + + +# ============================================================================ +# 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 diff --git a/dbapp/lyngsatapp/admin.py b/dbapp/lyngsatapp/admin.py index 08f0a33..82835d8 100644 --- a/dbapp/lyngsatapp/admin.py +++ b/dbapp/lyngsatapp/admin.py @@ -3,6 +3,8 @@ from .models import LyngSat @admin.register(LyngSat) class LyngSatAdmin(admin.ModelAdmin): - list_display = ("mark", "timestamp") - search_fields = ("mark", ) - ordering = ("timestamp",) \ No newline at end of file + list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update") + search_fields = ("id_satellite__name", "channel_info") + list_filter = ("id_satellite", "polarization", "modulation", "standard") + ordering = ("-last_update",) + readonly_fields = ("last_update",) \ No newline at end of file diff --git a/dbapp/lyngsatapp/migrations/0001_initial.py b/dbapp/lyngsatapp/migrations/0001_initial.py new file mode 100644 index 0000000..f928f51 --- /dev/null +++ b/dbapp/lyngsatapp/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/dbapp/lyngsatapp/parser.py b/dbapp/lyngsatapp/parser.py index 3c1288b..0bf5289 100644 --- a/dbapp/lyngsatapp/parser.py +++ b/dbapp/lyngsatapp/parser.py @@ -4,76 +4,80 @@ from datetime import datetime import re import time + class LyngSatParser: """Парсер данных для LyngSat(Для работы нужен flaresolver)""" + def __init__( - self, + self, flaresolver_url: str = "http://localhost:8191/v1", regions: list[str] | None = None, target_sats: list[str] | None = None, ): self.flaresolver_url = flaresolver_url self.regions = regions - 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.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.BASE_URL = "https://www.lyngsat.com" - + def parse_metadata(self, metadata: str) -> dict: if not metadata or not metadata.strip(): return { - 'standard': None, - 'modulation': None, - 'symbol_rate': None, - 'fec': None + "standard": None, + "modulation": None, + "symbol_rate": None, + "fec": None, } - normalized = re.sub(r'\s+', '', metadata.strip()) - fec_match = re.search(r'([1-9]/[1-9])$', normalized) + normalized = re.sub(r"\s+", "", metadata.strip()) + fec_match = re.search(r"([1-9]/[1-9])$", normalized) fec = fec_match.group(1) if fec_match else None if fec_match: - core = normalized[:fec_match.start()] + core = normalized[: fec_match.start()] else: 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 - rest = core[len(standard):] if standard else core + rest = core[len(standard) :] if standard else core 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: modulation = mod_match.group(1) - rest = rest[len(modulation):] + rest = rest[len(modulation) :] symbol_rate = None - sr_match = re.search(r'(\d+)$', rest) + sr_match = re.search(r"(\d+)$", rest) if sr_match: try: symbol_rate = int(sr_match.group(1)) except ValueError: pass - + return { - 'standard': standard, - 'modulation': modulation, - 'symbol_rate': symbol_rate, - 'fec': fec + "standard": standard, + "modulation": modulation, + "symbol_rate": symbol_rate, + "fec": fec, } def extract_date(self, s: str) -> datetime | None: s = s.strip() - match = re.search(r'(\d{6})$', s) + match = re.search(r"(\d{6})$", s) if not match: return None yymmdd = match.group(1) try: - return datetime.strptime(yymmdd, '%y%m%d').date() + return datetime.strptime(yymmdd, "%y%m%d").date() except ValueError: return None def convert_polarization(self, polarization: str) -> str: """Преобразовать код поляризации в понятное название на русском""" polarization_map = { - 'V': 'Вертикальная', - 'H': 'Горизонтальная', - 'R': 'Правая', - 'L': 'Левая' + "V": "Вертикальная", + "H": "Горизонтальная", + "R": "Правая", + "L": "Левая", } return polarization_map.get(polarization.upper(), polarization) @@ -83,11 +87,7 @@ class LyngSatParser: regions = self.regions for region in regions: url = f"{self.BASE_URL}/{region}.html" - payload = { - "cmd": "request.get", - "url": url, - "maxTimeout": 60000 - } + payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000} response = requests.post(self.flaresolver_url, json=payload) if response.status_code != 200: continue @@ -95,7 +95,7 @@ class LyngSatParser: html_regions.append(html_content) print(f"Обработал страницу по {region}") return html_regions - + def get_satellite_urls(self, html_regions: list[str]): sat_names = [] sat_urls = [] @@ -104,19 +104,19 @@ class LyngSatParser: 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 = [] for table in tables: - trs.extend(table.find_all('tr')) + trs.extend(table.find_all("tr")) for tr in trs: - sat_name = tr.find('span').text + sat_name = tr.find("span").text if self.target_sats is not None: if sat_name.strip().lower() not in self.target_sats: continue try: - sat_url = tr.find_all('a')[2]['href'] + sat_url = tr.find_all("a")[2]["href"] except IndexError: - sat_url = tr.find_all('a')[0]['href'] + sat_url = tr.find_all("a")[0]["href"] sat_names.append(sat_name) sat_urls.append(sat_url) return sat_names, sat_urls @@ -128,60 +128,67 @@ class LyngSatParser: 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 = [] for table in tables: - trs.extend(table.find_all('tr')) + trs.extend(table.find_all("tr")) for tr in trs: - sat_name = tr.find('span').text + sat_name = tr.find("span").text if self.target_sats is not None: if sat_name.strip().lower() not in self.target_sats: continue try: - sat_url = tr.find_all('a')[2]['href'] + sat_url = tr.find_all("a")[2]["href"] except IndexError: - sat_url = tr.find_all('a')[0]['href'] - - update_date = tr.find_all('td')[-1].text - sat_response = requests.post(self.flaresolver_url, json={ - "cmd": "request.get", - "url": f"{self.BASE_URL}/{sat_url}", - "maxTimeout": 60000 - }) - html_content = sat_response.json().get("solution", {}).get("response", "") + sat_url = tr.find_all("a")[0]["href"] + + update_date = tr.find_all("td")[-1].text + sat_response = requests.post( + self.flaresolver_url, + json={ + "cmd": "request.get", + "url": f"{self.BASE_URL}/{sat_url}", + "maxTimeout": 60000, + }, + ) + html_content = ( + sat_response.json().get("solution", {}).get("response", "") + ) sat_page_data = self.get_satellite_content(html_content) sat_data[sat_name] = { "url": f"{self.BASE_URL}/{sat_url}", "update_date": datetime.strptime(update_date, "%y%m%d").date(), - "sources": sat_page_data + "sources": sat_page_data, } return sat_data - + def get_satellite_content(self, html_content: str) -> dict: 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] data = [] 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): - tds = tr.find_all('td') + tds = tr.find_all("td") if len(tds) < 9 or idx < 2: 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) meta = self.parse_metadata(tds[1].text) provider_name = tds[3].text last_update = self.extract_date(tds[-1].text) - data.append({ - "freq": freq, - "pol": polarization, - "metadata": meta, - "provider_name": provider_name, - "last_update": last_update - }) + data.append( + { + "freq": freq, + "pol": polarization, + "metadata": meta, + "provider_name": provider_name, + "last_update": last_update, + } + ) return data - + class KingOfSatParser: def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0): @@ -193,20 +200,22 @@ class KingOfSatParser: self.base_url = base_url self.max_satellites = max_satellites self.session = requests.Session() - 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' - }) - + 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" + } + ) + def convert_polarization(self, polarization): """Преобразовать код поляризации в понятное название на русском""" polarization_map = { - 'V': 'Вертикальная', - 'H': 'Горизонтальная', - 'R': 'Правая', - 'L': 'Левая' + "V": "Вертикальная", + "H": "Горизонтальная", + "R": "Правая", + "L": "Левая", } return polarization_map.get(polarization.upper(), polarization) - + def fetch_page(self, url): """Получить HTML страницу""" try: @@ -216,114 +225,112 @@ class KingOfSatParser: except Exception as e: print(f"Ошибка при получении страницы {url}: {e}") return None - + def parse_satellite_table(self, html_content): """Распарсить таблицу со спутниками""" - soup = BeautifulSoup(html_content, 'html.parser') + soup = BeautifulSoup(html_content, "html.parser") satellites = [] - table = soup.find('table') + table = soup.find("table") if not table: print("Таблица не найдена") return satellites - - rows = table.find_all('tr')[1:] - + + rows = table.find_all("tr")[1:] + for row in rows: - cols = row.find_all('td') + cols = row.find_all("td") if len(cols) < 13: continue - + try: 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: position_value = position_match.group(1) position_direction = position_match.group(2) position = f"{position_value}{position_direction}" else: position = None - + # Название спутника (2-я колонка) satellite_cell = cols[1] 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 = cols[2].text.strip() if not norad or norad == "-": norad = None - + ini_link = None 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: 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 - + if satellite_name and ini_link and position: - satellites.append({ - 'position': position, - 'name': satellite_name, - 'norad': norad, - 'ini_url': ini_link, - 'update_date': update_date - }) - + satellites.append( + { + "position": position, + "name": satellite_name, + "norad": norad, + "ini_url": ini_link, + "update_date": update_date, + } + ) + except Exception as e: print(f"Ошибка при обработке строки таблицы: {e}") continue - + return satellites - + def parse_ini_file(self, ini_content): """Распарсить содержимое .ini файла""" - data = { - 'metadata': {}, - 'sattype': {}, - 'dvb': {} - } - + data = {"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) # if metadata_match: # data['metadata']['downloaded'] = metadata_match.group(1) - + # Парсим секцию [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: sattype_content = sattype_match.group(1).strip() - for line in sattype_content.split('\n'): + for line in sattype_content.split("\n"): line = line.strip() - if '=' in line: - key, value = line.split('=', 1) - data['sattype'][key.strip()] = value.strip() - + if "=" in line: + key, value = line.split("=", 1) + data["sattype"][key.strip()] = value.strip() + # Парсим секцию [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: dvb_content = dvb_match.group(1).strip() - for line in dvb_content.split('\n'): + for line in dvb_content.split("\n"): line = line.strip() - if '=' in line: - key, value = line.split('=', 1) - params = [p.strip() for p in value.split(',')] - polarization = params[1] if len(params) > 1 else '' + if "=" in line: + key, value = line.split("=", 1) + params = [p.strip() for p in value.split(",")] + polarization = params[1] if len(params) > 1 else "" if polarization: polarization = self.convert_polarization(polarization) - - data['dvb'][key.strip()] = { - 'frequency': params[0] if len(params) > 0 else '', - 'polarization': polarization, - 'symbol_rate': params[2] if len(params) > 2 else '', - 'fec': params[3] if len(params) > 3 else '', - 'standard': params[4] if len(params) > 4 else '', - 'modulation': params[5] if len(params) > 5 else '' + + data["dvb"][key.strip()] = { + "frequency": params[0] if len(params) > 0 else "", + "polarization": polarization, + "symbol_rate": params[2] if len(params) > 2 else "", + "fec": params[3] if len(params) > 3 else "", + "standard": params[4] if len(params) > 4 else "", + "modulation": params[5] if len(params) > 5 else "", } - + return data - + def download_ini_file(self, url): """Скачать содержимое .ini файла""" try: @@ -333,71 +340,66 @@ class KingOfSatParser: except Exception as e: print(f"Ошибка при скачивании .ini файла {url}: {e}") return None - + def get_all_satellites_data(self): """Получить данные всех спутников с учетом ограничения max_satellites""" - html_content = self.fetch_page(self.base_url + '/satellites') + html_content = self.fetch_page(self.base_url + "/satellites") if not html_content: return [] - + satellites = self.parse_satellite_table(html_content) - + if self.max_satellites > 0 and len(satellites) > self.max_satellites: - satellites = satellites[:self.max_satellites] - + satellites = satellites[: self.max_satellites] + results = [] processed_count = 0 - + for satellite in satellites: 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: print(f"Не удалось скачать .ini файл для {satellite['name']}") continue - + parsed_ini = self.parse_ini_file(ini_content) - + result = { - 'satellite_name': satellite['name'], - 'position': satellite['position'], - 'norad': satellite['norad'], - 'update_date': satellite['update_date'], - 'ini_url': satellite['ini_url'], - 'ini_data': parsed_ini + "satellite_name": satellite["name"], + "position": satellite["position"], + "norad": satellite["norad"], + "update_date": satellite["update_date"], + "ini_url": satellite["ini_url"], + "ini_data": parsed_ini, } - + results.append(result) processed_count += 1 - + if self.max_satellites > 0 and processed_count >= self.max_satellites: break - - time.sleep(1) - + + time.sleep(1) + return results - + def create_satellite_dict(self, satellites_data): """Создать словарь с данными спутников""" satellite_dict = {} - + for data in satellites_data: key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}" satellite_dict[key] = { - 'name': data['satellite_name'], - 'position': data['position'], - 'norad': data['norad'], - 'update_date': data['update_date'], - 'ini_url': data['ini_url'], - 'transponders_count': len(data['ini_data']['dvb']), - 'transponders': data['ini_data']['dvb'], - 'sattype_info': data['ini_data']['sattype'], - 'metadata': data['ini_data']['metadata'] + "name": data["satellite_name"], + "position": data["position"], + "norad": data["norad"], + "update_date": data["update_date"], + "ini_url": data["ini_url"], + "transponders_count": len(data["ini_data"]["dvb"]), + "transponders": data["ini_data"]["dvb"], + "sattype_info": data["ini_data"]["sattype"], + "metadata": data["ini_data"]["metadata"], } - - 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)) \ No newline at end of file + return satellite_dict diff --git a/dbapp/lyngsatapp/tasks.py b/dbapp/lyngsatapp/tasks.py new file mode 100644 index 0000000..48c538c --- /dev/null +++ b/dbapp/lyngsatapp/tasks.py @@ -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 diff --git a/dbapp/lyngsatapp/utils.py b/dbapp/lyngsatapp/utils.py index e75a893..c4e13e3 100644 --- a/dbapp/lyngsatapp/utils.py +++ b/dbapp/lyngsatapp/utils.py @@ -1,58 +1,170 @@ +import logging from .parser import LyngSatParser from .models import LyngSat from mainapp.models import Polarization, Standard, Modulation, Satellite -def fill_lyngsat_data(target_sats: list[str]): - parser = LyngSatParser( - target_sats=target_sats, - ) - lyngsat_data = parser.get_satellites_data() - for sat_name, data in lyngsat_data.items(): - url = data['url'] - sources = data['sources'] - for source in sources: +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( + 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() + 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'] + sources = data['sources'] + stats['total_sources'] += len(sources) + + logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}") + + # Находим спутник в базе try: - freq = float(source['freq']) - except Exception as e: - freq = -1.0 - print("Беда с частотой") - last_update = source['last_update'] - fec = source['metadata']['fec'] - modulation = source['metadata']['modulation'] - standard = source['metadata']['standard'] - symbol_velocity = source['metadata']['symbol_rate'] - polarization = source['pol'] - channel_info = source['provider_name'] + 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: + freq = float(source['freq']) + except (ValueError, TypeError): + freq = -1.0 + 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'] + fec = source['metadata'].get('fec') + modulation_name = source['metadata'].get('modulation') + standard_name = source['metadata'].get('standard') + symbol_velocity = source['metadata'].get('symbol_rate') + polarization_name = source['pol'] + channel_info = source['provider_name'] - pol_obj, _ = Polarization.objects.get_or_create( - name=polarization - ) + # Создаем или получаем связанные объекты + pol_obj, _ = Polarization.objects.get_or_create( + name=polarization_name if polarization_name else "-" + ) - mod_obj, _ = Modulation.objects.get_or_create( - name=modulation - ) + mod_obj, _ = Modulation.objects.get_or_create( + name=modulation_name if modulation_name else "-" + ) - standard_obj, _ = Standard.objects.get_or_create( - name=standard - ) + standard_obj, _ = Standard.objects.get_or_create( + name=standard_name if standard_name else "-" + ) - sat_obj, _ = Satellite.objects.get( - name__contains=sat_name - ) - lyng_obj, _ = LyngSat.objects.get_or_create( - id_satellite=sat_obj, - frequency=freq, - polarization=pol_obj, - defaults={ - "modulation": mod_obj, - "standard": standard_obj, - "sym_velocity": symbol_velocity, - "channel_info": channel_info, - "last_update": last_update, - "fec": fec, - "url": url - } - ) - lyng_obj.objects.update_or_create() - # TODO: сделать карточку и форму для действий и выбора спутника - lyng_obj.save() + # Создаем или обновляем запись Lyngsat + lyng_obj, created = LyngSat.objects.update_or_create( + id_satellite=sat_obj, + frequency=freq, + polarization=pol_obj, + defaults={ + "modulation": mod_obj, + "standard": standard_obj, + "sym_velocity": symbol_velocity if symbol_velocity else 0, + "channel_info": channel_info[:20] if channel_info else "", + "last_update": last_update, + "fec": fec[:30] if fec else "", + "url": url + } + ) + + if created: + 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 diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 2658289..48eae25 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -107,6 +107,40 @@ class NewEventForm(forms.Form): '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): """ Форма для создания и редактирования параметров ВЧ загрузки. diff --git a/dbapp/mainapp/templates/mainapp/actions.html b/dbapp/mainapp/templates/mainapp/actions.html index 1c82af9..4cccf8f 100644 --- a/dbapp/mainapp/templates/mainapp/actions.html +++ b/dbapp/mainapp/templates/mainapp/actions.html @@ -124,23 +124,23 @@ - +
- - + + +
-

Карты

-
-

Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.

-
- 2D Карта - 3D Карта +

Заполнение данных Lyngsat

+

Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.

+ + Заполнить данные Lyngsat +
diff --git a/dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html b/dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html new file mode 100644 index 0000000..e90b9f8 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html @@ -0,0 +1,118 @@ +{% extends 'mainapp/base.html' %} + +{% block title %}Заполнение данных Lyngsat{% endblock %} + +{% block content %} +
+
+
+
+
+

+ + + + + Заполнение данных из Lyngsat +

+
+
+ + {% include 'mainapp/components/_messages.html' %} + + + +
+ {% csrf_token %} + + +
+ + {{ form.satellites }} + {% if form.satellites.help_text %} +
{{ form.satellites.help_text }}
+ {% endif %} + {% if form.satellites.errors %} +
+ {{ form.satellites.errors }} +
+ {% endif %} +
+ + +
+ + {{ form.regions }} + {% if form.regions.help_text %} +
{{ form.regions.help_text }}
+ {% endif %} + {% if form.regions.errors %} +
+ {{ form.regions.errors }} +
+ {% endif %} +
+ + +
+ + + + + Назад + + +
+
+
+
+ + +
+
+
Информация
+

+ Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat. + Выберите один или несколько спутников и регионы для парсинга данных. +

+
    +
  • Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры
  • +
  • Процесс может занять несколько минут в зависимости от количества выбранных спутников
  • +
  • Существующие записи будут обновлены, новые - созданы
  • +
+
+
+
+
+
+ + +{% endblock %} diff --git a/dbapp/mainapp/templates/mainapp/lyngsat_task_status.html b/dbapp/mainapp/templates/mainapp/lyngsat_task_status.html new file mode 100644 index 0000000..41f1c36 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/lyngsat_task_status.html @@ -0,0 +1,241 @@ +{% extends 'mainapp/base.html' %} + +{% block title %}Статус задачи Lyngsat{% endblock %} + +{% block content %} +
+
+
+
+
+

+ + + + Статус задачи заполнения данных Lyngsat +

+
+
+ {% if task_id %} +
+ ID задачи: {{ task_id }} +
+ + +
+
+ Загрузка статуса... + 0% +
+
+
+ 0% +
+
+
+ + + + + +
+
Результаты обработки
+
+
+
+
+
Обработано спутников
+

-

+
+
+
+
+
+
+
Обработано источников
+

-

+
+
+
+
+
+
+
Создано записей
+

-

+
+
+
+
+
+
+
Обновлено записей
+

-

+
+
+
+
+ + +
+
Ошибки при обработке:
+
+
    +
    +
    +
    + + + + + + + {% else %} + + + Перейти к форме + + {% endif %} +
    +
    +
    +
    +
    + +{% if task_id %} + +{% endif %} +{% endblock %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 3c70829..78aa97b 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -25,4 +25,8 @@ urlpatterns = [ path('object//edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'), path('object//', views.ObjItemDetailView.as_view(), name='objitem_detail'), path('object//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//', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), + path('api/lyngsat-task-status//', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'), ] \ No newline at end of file diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views.py index 76aa606..d40a7aa 100644 --- a/dbapp/mainapp/views.py +++ b/dbapp/mainapp/views.py @@ -37,6 +37,7 @@ from .forms import ( UploadFileForm, UploadVchLoad, VchLinkForm, + FillLyngsatDataForm, ) from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin from .models import Geo, Modulation, ObjItem, Polarization, Satellite @@ -1029,3 +1030,97 @@ class ObjItemDetailView(LoginRequiredMixin, View): } 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) diff --git a/dbapp/requirements.txt b/dbapp/requirements.txt index 3ce710d..9e41cc3 100644 --- a/dbapp/requirements.txt +++ b/dbapp/requirements.txt @@ -24,6 +24,8 @@ pandas>=2.3.3 psycopg>=3.2.10 psycopg2-binary>=2.9.11 redis>=6.4.0 +celery>=5.4.0 +django-celery-results>=2.5.1 requests>=2.32.5 reverse-geocoder>=1.5.1 scikit-learn>=1.7.2 diff --git a/dbapp/start_celery_worker.sh b/dbapp/start_celery_worker.sh new file mode 100755 index 0000000..d9f544a --- /dev/null +++ b/dbapp/start_celery_worker.sh @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml index 3fefe7c..4174fd6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,6 +19,35 @@ services: networks: - app-network + redis: + image: redis:7-alpine + container_name: redis-dev + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data_dev:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - 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 @@ -72,6 +101,7 @@ services: volumes: postgres_data_dev: + redis_data_dev: # static_volume_dev: # media_volume_dev: # logs_volume_dev: