Настроил сеелери, начал привязку lyngsat
This commit is contained in:
46
.env.dev
46
.env.dev
@@ -1,23 +1,23 @@
|
||||
# Development Environment Variables
|
||||
|
||||
# Django Settings
|
||||
DEBUG=True
|
||||
ENVIRONMENT=development
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||
SECRET_KEY=django-insecure-dev-key-only-for-development
|
||||
|
||||
# Database Configuration
|
||||
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
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=123456
|
||||
# Development Environment Variables
|
||||
|
||||
# Django Settings
|
||||
DEBUG=True
|
||||
ENVIRONMENT=development
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||
SECRET_KEY=django-insecure-dev-key-only-for-development
|
||||
|
||||
# Database Configuration
|
||||
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
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=123456
|
||||
|
||||
56
.env.prod
56
.env.prod
@@ -1,28 +1,28 @@
|
||||
# Production Environment Variables
|
||||
# ВАЖНО: Измените все значения перед деплоем!
|
||||
|
||||
# Django Settings
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||
SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production
|
||||
|
||||
# Database Configuration
|
||||
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts (comma-separated)
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||
|
||||
# Gunicorn Configuration
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
# Production Environment Variables
|
||||
# ВАЖНО: Измените все значения перед деплоем!
|
||||
|
||||
# Django Settings
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||
SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production
|
||||
|
||||
# Database Configuration
|
||||
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts (comma-separated)
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||
|
||||
# Gunicorn Configuration
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
|
||||
68
.gitignore
vendored
68
.gitignore
vendored
@@ -1,35 +1,35 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.hintrc
|
||||
.vscode
|
||||
data.json
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Django
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
staticfiles/
|
||||
media/
|
||||
|
||||
django-leaflet
|
||||
admin-interface
|
||||
Тестовые
|
||||
tiles
|
||||
.kiro
|
||||
|
||||
# Docker
|
||||
# docker-*
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.hintrc
|
||||
.vscode
|
||||
data.json
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Django
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
staticfiles/
|
||||
media/
|
||||
|
||||
django-leaflet
|
||||
admin-interface
|
||||
Тестовые
|
||||
tiles
|
||||
.kiro
|
||||
|
||||
# Docker
|
||||
# docker-*
|
||||
maplibre-gl-js-5.10.0.zip
|
||||
@@ -1,396 +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/
|
||||
# Сводка изменений: Асинхронная обработка данных Lyngsat
|
||||
|
||||
## Обзор
|
||||
|
||||
Реализована полная асинхронная обработка данных Lyngsat с использованием Celery, Redis и детальным логированием.
|
||||
|
||||
## Ключевые улучшения
|
||||
|
||||
### 1. ✅ Асинхронная обработка
|
||||
- Задачи выполняются в фоновом режиме
|
||||
- Веб-интерфейс не блокируется
|
||||
- Можно обрабатывать несколько задач одновременно
|
||||
|
||||
### 2. ✅ Отслеживание прогресса
|
||||
- Прогресс-бар в реальном времени
|
||||
- Текущий статус обработки
|
||||
- Процент выполнения
|
||||
|
||||
### 3. ✅ Детальное логирование
|
||||
- Логи на уровне задачи
|
||||
- Логи на уровне спутника
|
||||
- Логи на уровне источника
|
||||
- Все ошибки записываются в лог
|
||||
|
||||
### 4. ✅ Результаты и статистика
|
||||
- Количество обработанных спутников
|
||||
- Количество обработанных источников
|
||||
- Количество созданных/обновленных записей
|
||||
- Список всех ошибок
|
||||
|
||||
## Новые файлы
|
||||
|
||||
### Backend
|
||||
1. **dbapp/dbapp/celery.py** - конфигурация Celery
|
||||
2. **dbapp/dbapp/__init__.py** - инициализация Celery app
|
||||
3. **dbapp/lyngsatapp/tasks.py** - асинхронная задача заполнения данных
|
||||
4. **dbapp/start_celery_worker.sh** - скрипт запуска worker
|
||||
|
||||
### Frontend
|
||||
5. **dbapp/mainapp/templates/mainapp/lyngsat_task_status.html** - страница отслеживания прогресса
|
||||
|
||||
### Документация
|
||||
6. **ASYNC_LYNGSAT_GUIDE.md** - полное руководство
|
||||
7. **QUICKSTART_ASYNC.md** - быстрый старт
|
||||
8. **ASYNC_CHANGES_SUMMARY.md** - этот файл
|
||||
|
||||
## Измененные файлы
|
||||
|
||||
### Конфигурация
|
||||
1. **dbapp/requirements.txt**
|
||||
- Добавлено: `celery>=5.4.0`
|
||||
- Добавлено: `django-celery-results>=2.5.1`
|
||||
|
||||
2. **dbapp/dbapp/settings/base.py**
|
||||
- Добавлено: `django_celery_results` в INSTALLED_APPS
|
||||
- Добавлено: полная конфигурация Celery (брокер, результаты, таймауты, логирование)
|
||||
|
||||
3. **docker-compose.yaml**
|
||||
- Добавлено: сервис Redis
|
||||
- Добавлено: сервис FlareSolver
|
||||
- Добавлено: volume для Redis
|
||||
|
||||
### Backend логика
|
||||
4. **dbapp/lyngsatapp/utils.py**
|
||||
- Добавлено: параметр `task_id` для логирования
|
||||
- Добавлено: параметр `update_progress` для обновления прогресса
|
||||
- Добавлено: детальное логирование на всех уровнях
|
||||
- Добавлено: логирование каждые 10 источников
|
||||
- Улучшено: обработка ошибок с логированием
|
||||
|
||||
5. **dbapp/mainapp/views.py**
|
||||
- Изменено: `FillLyngsatDataView` теперь запускает асинхронную задачу
|
||||
- Добавлено: `LyngsatTaskStatusView` - страница отслеживания
|
||||
- Добавлено: `LyngsatTaskStatusAPIView` - API для проверки статуса
|
||||
|
||||
6. **dbapp/mainapp/urls.py**
|
||||
- Добавлено: `/lyngsat-task-status/` - страница статуса
|
||||
- Добавлено: `/lyngsat-task-status/<task_id>/` - статус конкретной задачи
|
||||
- Добавлено: `/api/lyngsat-task-status/<task_id>/` - API endpoint
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Архитектура
|
||||
|
||||
```
|
||||
User Request → Django View → Celery Task → Redis Broker
|
||||
↓
|
||||
Celery Worker
|
||||
↓
|
||||
┌───────────┴───────────┐
|
||||
↓ ↓
|
||||
LyngSat Parser PostgreSQL
|
||||
↓ ↓
|
||||
FlareSolver Save Results
|
||||
```
|
||||
|
||||
### Поток данных
|
||||
|
||||
1. **Пользователь отправляет форму**
|
||||
- Django view получает данные
|
||||
- Создается асинхронная задача Celery
|
||||
- Возвращается task_id
|
||||
- Перенаправление на страницу статуса
|
||||
|
||||
2. **Celery Worker обрабатывает задачу**
|
||||
- Логирует начало обработки
|
||||
- Вызывает `fill_lyngsat_data` с callback
|
||||
- Обновляет прогресс через `update_state`
|
||||
- Логирует каждый шаг
|
||||
- Сохраняет результат в кеш
|
||||
|
||||
3. **Страница статуса отслеживает прогресс**
|
||||
- JavaScript опрашивает API каждые 2 секунды
|
||||
- Обновляет прогресс-бар
|
||||
- Показывает текущий статус
|
||||
- Отображает результаты при завершении
|
||||
|
||||
### Логирование
|
||||
|
||||
#### Уровни логирования
|
||||
- **INFO**: Основные события (начало, завершение, прогресс)
|
||||
- **DEBUG**: Детальная информация (каждая запись)
|
||||
- **WARNING**: Некритичные ошибки (спутник не найден)
|
||||
- **ERROR**: Критичные ошибки (с traceback)
|
||||
|
||||
#### Формат логов
|
||||
```
|
||||
[Timestamp: Level/Process][Task Name(Task ID)] [Task ID] Message
|
||||
```
|
||||
|
||||
Пример:
|
||||
```
|
||||
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Начало обработки данных Lyngsat
|
||||
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Спутники: Astra 4A, Hotbird 13G
|
||||
[2024-01-15 10:30:46: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Получено данных по 2 спутникам
|
||||
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработка спутника 1/2: Astra 4A
|
||||
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Найдено 150 источников для Astra 4A
|
||||
[2024-01-15 10:31:05: DEBUG/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Создана запись для Astra 4A 11766.0 МГц
|
||||
[2024-01-15 10:31:10: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработано 10/150 источников для Astra 4A
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### GET /api/lyngsat-task-status/<task_id>/
|
||||
|
||||
**Ответ при выполнении:**
|
||||
```json
|
||||
{
|
||||
"task_id": "abc123",
|
||||
"state": "PROGRESS",
|
||||
"status": "Обработка Astra 4A...",
|
||||
"current": 1,
|
||||
"total": 2,
|
||||
"percent": 50
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ при успехе:**
|
||||
```json
|
||||
{
|
||||
"task_id": "abc123",
|
||||
"state": "SUCCESS",
|
||||
"status": "Задача завершена успешно",
|
||||
"result": {
|
||||
"total_satellites": 2,
|
||||
"total_sources": 300,
|
||||
"created": 250,
|
||||
"updated": 50,
|
||||
"errors": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ при ошибке:**
|
||||
```json
|
||||
{
|
||||
"task_id": "abc123",
|
||||
"state": "FAILURE",
|
||||
"status": "Ошибка при выполнении задачи",
|
||||
"error": "Connection timeout"
|
||||
}
|
||||
```
|
||||
|
||||
## Настройки Celery
|
||||
|
||||
### Основные параметры
|
||||
```python
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
||||
CELERY_TASK_TRACK_STARTED = True
|
||||
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 минут
|
||||
```
|
||||
|
||||
### Переменные окружения
|
||||
Можно переопределить через `.env`:
|
||||
```bash
|
||||
CELERY_BROKER_URL=redis://redis:6379/0
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
### Обязательные сервисы
|
||||
1. **Redis** - брокер сообщений Celery
|
||||
2. **FlareSolver** - обход Cloudflare
|
||||
3. **PostgreSQL** - хранение данных и результатов
|
||||
|
||||
### Python пакеты
|
||||
- `celery>=5.4.0` - асинхронная обработка
|
||||
- `django-celery-results>=2.5.1` - хранение результатов
|
||||
- `redis>=6.4.0` - клиент Redis
|
||||
|
||||
## Команды для работы
|
||||
|
||||
### Запуск сервисов
|
||||
```bash
|
||||
# Redis и FlareSolver
|
||||
docker-compose up -d redis flaresolverr
|
||||
|
||||
# Celery Worker
|
||||
celery -A dbapp worker --loglevel=info
|
||||
|
||||
# Celery Worker в фоне
|
||||
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log --detach
|
||||
```
|
||||
|
||||
### Мониторинг
|
||||
```bash
|
||||
# Просмотр логов
|
||||
tail -f dbapp/logs/celery_worker.log
|
||||
|
||||
# Flower (веб-интерфейс)
|
||||
pip install flower
|
||||
celery -A dbapp flower
|
||||
# Откройте http://localhost:5555
|
||||
```
|
||||
|
||||
### Отладка
|
||||
```bash
|
||||
# Проверка Redis
|
||||
redis-cli ping
|
||||
|
||||
# Проверка FlareSolver
|
||||
curl http://localhost:8191/v1
|
||||
|
||||
# Django shell
|
||||
python manage.py shell
|
||||
>>> from celery.result import AsyncResult
|
||||
>>> task = AsyncResult('task_id')
|
||||
>>> print(task.state, task.info)
|
||||
```
|
||||
|
||||
## Производственное развертывание
|
||||
|
||||
### Systemd сервис
|
||||
```bash
|
||||
sudo systemctl enable celery-worker
|
||||
sudo systemctl start celery-worker
|
||||
sudo systemctl status celery-worker
|
||||
```
|
||||
|
||||
### Supervisor
|
||||
```bash
|
||||
sudo supervisorctl start celery-worker
|
||||
sudo supervisorctl status celery-worker
|
||||
```
|
||||
|
||||
### Docker
|
||||
Можно добавить Celery worker в docker-compose.yaml:
|
||||
```yaml
|
||||
celery-worker:
|
||||
build: ./dbapp
|
||||
command: celery -A dbapp worker --loglevel=info
|
||||
depends_on:
|
||||
- redis
|
||||
- db
|
||||
environment:
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Проверка системы
|
||||
```bash
|
||||
# 1. Проверка Django
|
||||
python manage.py check
|
||||
|
||||
# 2. Проверка миграций
|
||||
python manage.py migrate --check
|
||||
|
||||
# 3. Проверка Celery
|
||||
celery -A dbapp inspect ping
|
||||
|
||||
# 4. Проверка Redis
|
||||
redis-cli ping
|
||||
|
||||
# 5. Проверка FlareSolver
|
||||
curl http://localhost:8191/v1
|
||||
```
|
||||
|
||||
### Тестовый запуск
|
||||
```python
|
||||
# Django shell
|
||||
python manage.py shell
|
||||
|
||||
from lyngsatapp.tasks import fill_lyngsat_data_task
|
||||
|
||||
# Запуск задачи
|
||||
task = fill_lyngsat_data_task.delay(['Astra 4A'], ['europe'])
|
||||
print(f"Task ID: {task.id}")
|
||||
|
||||
# Проверка статуса
|
||||
print(task.state)
|
||||
print(task.info)
|
||||
```
|
||||
|
||||
## Метрики и мониторинг
|
||||
|
||||
### Что отслеживать
|
||||
- Количество активных workers
|
||||
- Количество задач в очереди
|
||||
- Среднее время выполнения задачи
|
||||
- Количество ошибок
|
||||
- Использование памяти Redis
|
||||
|
||||
### Инструменты
|
||||
- **Flower** - веб-интерфейс для Celery
|
||||
- **Redis Commander** - GUI для Redis
|
||||
- **Prometheus + Grafana** - метрики и дашборды
|
||||
|
||||
## Безопасность
|
||||
|
||||
### Рекомендации
|
||||
1. Используйте пароль для Redis в production
|
||||
2. Ограничьте доступ к Redis только для localhost
|
||||
3. Используйте SSL для Redis в production
|
||||
4. Ограничьте время выполнения задач
|
||||
5. Логируйте все действия
|
||||
|
||||
### Пример конфигурации Redis с паролем
|
||||
```python
|
||||
CELERY_BROKER_URL = 'redis://:password@localhost:6379/0'
|
||||
```
|
||||
|
||||
## Масштабирование
|
||||
|
||||
### Горизонтальное масштабирование
|
||||
Запустите несколько workers:
|
||||
```bash
|
||||
# Worker 1
|
||||
celery -A dbapp worker --loglevel=info -n worker1@%h
|
||||
|
||||
# Worker 2
|
||||
celery -A dbapp worker --loglevel=info -n worker2@%h
|
||||
|
||||
# Worker 3
|
||||
celery -A dbapp worker --loglevel=info -n worker3@%h
|
||||
```
|
||||
|
||||
### Приоритеты задач
|
||||
Можно настроить разные очереди для разных типов задач:
|
||||
```python
|
||||
@shared_task(queue='high_priority')
|
||||
def urgent_task():
|
||||
pass
|
||||
|
||||
@shared_task(queue='low_priority')
|
||||
def background_task():
|
||||
pass
|
||||
```
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. ✅ Применить миграции
|
||||
2. ✅ Запустить Redis и FlareSolver
|
||||
3. ✅ Запустить Celery Worker
|
||||
4. ✅ Протестировать через веб-интерфейс
|
||||
5. ⏳ Настроить production окружение
|
||||
6. ⏳ Добавить периодические задачи (Celery Beat)
|
||||
7. ⏳ Настроить email уведомления
|
||||
8. ⏳ Настроить мониторинг (Flower)
|
||||
|
||||
## Заключение
|
||||
|
||||
Система асинхронной обработки данных Lyngsat обеспечивает:
|
||||
- ✅ Неблокирующий веб-интерфейс
|
||||
- ✅ Отслеживание прогресса в реальном времени
|
||||
- ✅ Детальное логирование всех операций
|
||||
- ✅ Масштабируемость (несколько workers)
|
||||
- ✅ Надежность (retry при ошибках)
|
||||
- ✅ Мониторинг и отладка
|
||||
- ✅ Production-ready решение
|
||||
|
||||
Для получения дополнительной помощи:
|
||||
- Полное руководство: `ASYNC_LYNGSAT_GUIDE.md`
|
||||
- Быстрый старт: `QUICKSTART_ASYNC.md`
|
||||
- Документация Celery: https://docs.celeryproject.org/
|
||||
|
||||
@@ -1,420 +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/)
|
||||
# Руководство по асинхронному заполнению данных Lyngsat
|
||||
|
||||
## Обзор
|
||||
|
||||
Система заполнения данных Lyngsat теперь работает асинхронно с использованием Celery. Это позволяет:
|
||||
- Не блокировать веб-интерфейс во время долгих операций
|
||||
- Отслеживать прогресс выполнения задачи в реальном времени
|
||||
- Просматривать детальные логи обработки
|
||||
- Получать уведомления о завершении задачи
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Django │─────▶│ Celery │─────▶│ Redis │
|
||||
│ Web App │ │ Worker │ │ Broker │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────┐
|
||||
└─────────────▶│ PostgreSQL │
|
||||
│ Database │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Установка и настройка
|
||||
|
||||
### 1. Установка зависимостей
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Новые зависимости:
|
||||
- `celery>=5.4.0` - асинхронная обработка задач
|
||||
- `django-celery-results>=2.5.1` - хранение результатов в БД
|
||||
|
||||
### 2. Применение миграций
|
||||
|
||||
```bash
|
||||
cd dbapp
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
Это создаст таблицы для хранения результатов Celery.
|
||||
|
||||
### 3. Запуск Redis
|
||||
|
||||
Redis используется как брокер сообщений для Celery.
|
||||
|
||||
#### Вариант 1: Docker Compose (рекомендуется)
|
||||
```bash
|
||||
docker-compose up -d redis
|
||||
```
|
||||
|
||||
#### Вариант 2: Локальная установка
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install redis-server
|
||||
sudo systemctl start redis
|
||||
|
||||
# macOS
|
||||
brew install redis
|
||||
brew services start redis
|
||||
|
||||
# Проверка
|
||||
redis-cli ping
|
||||
# Должно вернуть: PONG
|
||||
```
|
||||
|
||||
### 4. Запуск FlareSolver
|
||||
|
||||
FlareSolver необходим для обхода защиты Cloudflare.
|
||||
|
||||
```bash
|
||||
docker-compose up -d flaresolverr
|
||||
```
|
||||
|
||||
Или отдельно:
|
||||
```bash
|
||||
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
|
||||
```
|
||||
|
||||
### 5. Запуск Celery Worker
|
||||
|
||||
#### Вариант 1: Используя скрипт
|
||||
```bash
|
||||
cd dbapp
|
||||
./start_celery_worker.sh
|
||||
```
|
||||
|
||||
#### Вариант 2: Напрямую
|
||||
```bash
|
||||
cd dbapp
|
||||
celery -A dbapp worker --loglevel=info
|
||||
```
|
||||
|
||||
#### Вариант 3: В фоновом режиме (Linux/macOS)
|
||||
```bash
|
||||
cd dbapp
|
||||
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log --detach
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### 1. Запуск задачи через веб-интерфейс
|
||||
|
||||
1. Откройте страницу действий: `http://localhost:8000/actions/`
|
||||
2. Нажмите "Заполнить данные Lyngsat"
|
||||
3. Выберите спутники и регионы
|
||||
4. Нажмите "Заполнить данные"
|
||||
5. Вы будете перенаправлены на страницу отслеживания прогресса
|
||||
|
||||
### 2. Отслеживание прогресса
|
||||
|
||||
На странице статуса задачи вы увидите:
|
||||
- **Прогресс-бар** с процентом выполнения
|
||||
- **Текущий статус** (например, "Обработка Astra 4A...")
|
||||
- **Состояние задачи** (PENDING, PROGRESS, SUCCESS, FAILURE)
|
||||
- **Результаты** после завершения:
|
||||
- Количество обработанных спутников
|
||||
- Количество обработанных источников
|
||||
- Количество созданных записей
|
||||
- Количество обновленных записей
|
||||
- Список ошибок (если есть)
|
||||
|
||||
Страница автоматически обновляется каждые 2 секунды.
|
||||
|
||||
### 3. Просмотр логов
|
||||
|
||||
Логи Celery worker содержат детальную информацию о процессе:
|
||||
|
||||
```bash
|
||||
# Просмотр логов в реальном времени
|
||||
tail -f dbapp/logs/celery_worker.log
|
||||
|
||||
# Поиск по логам
|
||||
grep "Task" dbapp/logs/celery_worker.log
|
||||
grep "ERROR" dbapp/logs/celery_worker.log
|
||||
```
|
||||
|
||||
Формат логов:
|
||||
```
|
||||
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Начало обработки данных Lyngsat
|
||||
[2024-01-15 10:30:45: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Спутники: Astra 4A, Hotbird 13G
|
||||
[2024-01-15 10:30:46: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Получено данных по 2 спутникам
|
||||
[2024-01-15 10:31:00: INFO/MainProcess][lyngsatapp.fill_lyngsat_data_async(abc123)] [Task abc123] Обработка спутника 1/2: Astra 4A
|
||||
```
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Структура задачи
|
||||
|
||||
**Файл**: `dbapp/lyngsatapp/tasks.py`
|
||||
|
||||
```python
|
||||
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
|
||||
def fill_lyngsat_data_task(self, target_sats, regions=None):
|
||||
# Логирование начала
|
||||
# Обновление прогресса
|
||||
# Вызов функции заполнения
|
||||
# Сохранение результата в кеш
|
||||
# Обработка ошибок
|
||||
```
|
||||
|
||||
### Обновление прогресса
|
||||
|
||||
Функция `fill_lyngsat_data` теперь принимает callback `update_progress`:
|
||||
|
||||
```python
|
||||
def update_progress(current, total, status):
|
||||
self.update_state(
|
||||
state='PROGRESS',
|
||||
meta={
|
||||
'current': current,
|
||||
'total': total,
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### API для проверки статуса
|
||||
|
||||
**Endpoint**: `/api/lyngsat-task-status/<task_id>/`
|
||||
|
||||
**Ответ**:
|
||||
```json
|
||||
{
|
||||
"task_id": "abc123",
|
||||
"state": "PROGRESS",
|
||||
"status": "Обработка Astra 4A...",
|
||||
"current": 1,
|
||||
"total": 2,
|
||||
"percent": 50
|
||||
}
|
||||
```
|
||||
|
||||
### Логирование
|
||||
|
||||
Используется стандартный модуль `logging` Python:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(f"[Task {task_id}] Начало обработки")
|
||||
logger.debug(f"[Task {task_id}] Детальная информация")
|
||||
logger.warning(f"[Task {task_id}] Предупреждение")
|
||||
logger.error(f"[Task {task_id}] Ошибка", exc_info=True)
|
||||
```
|
||||
|
||||
## Настройки Celery
|
||||
|
||||
**Файл**: `dbapp/dbapp/settings/base.py`
|
||||
|
||||
```python
|
||||
# Брокер сообщений
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||
|
||||
# Хранение результатов
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
||||
|
||||
# Таймауты
|
||||
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 минут
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 минут
|
||||
|
||||
# Отслеживание прогресса
|
||||
CELERY_TASK_TRACK_STARTED = True
|
||||
```
|
||||
|
||||
## Мониторинг и отладка
|
||||
|
||||
### Flower - веб-интерфейс для мониторинга Celery
|
||||
|
||||
Установка:
|
||||
```bash
|
||||
pip install flower
|
||||
```
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
celery -A dbapp flower
|
||||
```
|
||||
|
||||
Откройте: `http://localhost:5555`
|
||||
|
||||
### Проверка статуса задачи через Django shell
|
||||
|
||||
```python
|
||||
python manage.py shell
|
||||
|
||||
from celery.result import AsyncResult
|
||||
|
||||
task_id = 'abc123'
|
||||
task = AsyncResult(task_id)
|
||||
|
||||
print(f"State: {task.state}")
|
||||
print(f"Info: {task.info}")
|
||||
print(f"Result: {task.result}")
|
||||
```
|
||||
|
||||
### Очистка старых результатов
|
||||
|
||||
```bash
|
||||
# Удалить результаты старше 1 дня
|
||||
python manage.py celery_results_cleanup --days=1
|
||||
```
|
||||
|
||||
## Решение проблем
|
||||
|
||||
### Проблема: Worker не запускается
|
||||
|
||||
**Решение**:
|
||||
1. Проверьте, что Redis запущен: `redis-cli ping`
|
||||
2. Проверьте настройки в `.env`: `CELERY_BROKER_URL`
|
||||
3. Проверьте логи: `tail -f logs/celery_worker.log`
|
||||
|
||||
### Проблема: Задача зависла в состоянии PENDING
|
||||
|
||||
**Решение**:
|
||||
1. Проверьте, что worker запущен: `ps aux | grep celery`
|
||||
2. Перезапустите worker
|
||||
3. Проверьте соединение с Redis
|
||||
|
||||
### Проблема: Задача завершается с ошибкой
|
||||
|
||||
**Решение**:
|
||||
1. Проверьте логи worker
|
||||
2. Проверьте, что FlareSolver запущен: `curl http://localhost:8191/v1`
|
||||
3. Проверьте, что спутники существуют в базе данных
|
||||
|
||||
### Проблема: Прогресс не обновляется
|
||||
|
||||
**Решение**:
|
||||
1. Откройте консоль браузера (F12) и проверьте ошибки
|
||||
2. Проверьте, что API endpoint доступен: `/api/lyngsat-task-status/<task_id>/`
|
||||
3. Очистите кеш браузера
|
||||
|
||||
## Производственное развертывание
|
||||
|
||||
### Systemd сервис для Celery Worker
|
||||
|
||||
Создайте файл `/etc/systemd/system/celery-worker.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Celery Worker for Django Lyngsat
|
||||
After=network.target redis.service
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/path/to/dbapp
|
||||
Environment="PATH=/path/to/venv/bin"
|
||||
ExecStart=/path/to/venv/bin/celery -A dbapp worker --loglevel=info --logfile=/var/log/celery/worker.log --detach
|
||||
ExecStop=/path/to/venv/bin/celery -A dbapp control shutdown
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable celery-worker
|
||||
sudo systemctl start celery-worker
|
||||
sudo systemctl status celery-worker
|
||||
```
|
||||
|
||||
### Supervisor (альтернатива)
|
||||
|
||||
Установка:
|
||||
```bash
|
||||
sudo apt-get install supervisor
|
||||
```
|
||||
|
||||
Конфигурация `/etc/supervisor/conf.d/celery.conf`:
|
||||
```ini
|
||||
[program:celery-worker]
|
||||
command=/path/to/venv/bin/celery -A dbapp worker --loglevel=info
|
||||
directory=/path/to/dbapp
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/celery/worker.log
|
||||
stderr_logfile=/var/log/celery/worker_error.log
|
||||
```
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
sudo supervisorctl start celery-worker
|
||||
```
|
||||
|
||||
## Дополнительные возможности
|
||||
|
||||
### Периодические задачи (Celery Beat)
|
||||
|
||||
Для автоматического обновления данных по расписанию:
|
||||
|
||||
1. Установите `django-celery-beat`:
|
||||
```bash
|
||||
pip install django-celery-beat
|
||||
```
|
||||
|
||||
2. Добавьте в `INSTALLED_APPS`:
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_celery_beat',
|
||||
]
|
||||
```
|
||||
|
||||
3. Примените миграции:
|
||||
```bash
|
||||
python manage.py migrate django_celery_beat
|
||||
```
|
||||
|
||||
4. Создайте периодическую задачу через админ-панель Django
|
||||
|
||||
5. Запустите beat scheduler:
|
||||
```bash
|
||||
celery -A dbapp beat --loglevel=info
|
||||
```
|
||||
|
||||
### Уведомления по email
|
||||
|
||||
Добавьте в задачу отправку email при завершении:
|
||||
|
||||
```python
|
||||
from django.core.mail import send_mail
|
||||
|
||||
@shared_task(bind=True)
|
||||
def fill_lyngsat_data_task(self, target_sats, regions=None):
|
||||
# ... обработка ...
|
||||
|
||||
# Отправка email
|
||||
send_mail(
|
||||
'Задача Lyngsat завершена',
|
||||
f'Обработано {stats["total_satellites"]} спутников',
|
||||
'noreply@example.com',
|
||||
['admin@example.com'],
|
||||
)
|
||||
```
|
||||
|
||||
## Заключение
|
||||
|
||||
Асинхронная обработка данных Lyngsat обеспечивает:
|
||||
- ✅ Неблокирующий веб-интерфейс
|
||||
- ✅ Отслеживание прогресса в реальном времени
|
||||
- ✅ Детальное логирование
|
||||
- ✅ Масштабируемость (можно запустить несколько workers)
|
||||
- ✅ Надежность (автоматический retry при ошибках)
|
||||
|
||||
Для получения дополнительной помощи обратитесь к документации:
|
||||
- [Celery Documentation](https://docs.celeryproject.org/)
|
||||
- [Django Celery Results](https://django-celery-results.readthedocs.io/)
|
||||
|
||||
@@ -1,133 +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. Протестировать форму заполнения данных через веб-интерфейс
|
||||
|
||||
## Примечания
|
||||
|
||||
- Процесс заполнения может занять продолжительное время (несколько минут на спутник)
|
||||
- Рекомендуется начинать с небольшого количества спутников
|
||||
- Все ошибки логируются и отображаются пользователю
|
||||
- Существующие записи обновляются, новые создаются
|
||||
# Сводка изменений: Модернизация функциональности Lyngsat
|
||||
|
||||
## Обзор
|
||||
|
||||
Реализована новая функциональность для заполнения данных о транспондерах спутников с сайта Lyngsat через веб-интерфейс.
|
||||
|
||||
## Основные изменения
|
||||
|
||||
### 1. Удалена карточка с картами 2D/3D
|
||||
- **Файл**: `dbapp/mainapp/templates/mainapp/actions.html`
|
||||
- **Изменение**: Заменена карточка "Карты" на карточку "Заполнение данных Lyngsat"
|
||||
|
||||
### 2. Создана новая форма для заполнения данных
|
||||
- **Файл**: `dbapp/mainapp/forms.py`
|
||||
- **Добавлено**: Класс `FillLyngsatDataForm` с полями:
|
||||
- `satellites` - мультивыбор спутников из базы данных
|
||||
- `regions` - мультивыбор регионов (Europe, Asia, America, Atlantic)
|
||||
|
||||
### 3. Создан новый view для обработки формы
|
||||
- **Файл**: `dbapp/mainapp/views.py`
|
||||
- **Добавлено**: Класс `FillLyngsatDataView` для обработки запросов
|
||||
- **Функциональность**:
|
||||
- Валидация формы
|
||||
- Вызов функции заполнения данных
|
||||
- Отображение статистики и ошибок
|
||||
|
||||
### 4. Добавлен новый URL
|
||||
- **Файл**: `dbapp/mainapp/urls.py`
|
||||
- **Добавлено**: `path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data')`
|
||||
|
||||
### 5. Создан новый шаблон
|
||||
- **Файл**: `dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html`
|
||||
- **Содержимое**:
|
||||
- Форма с мультивыбором спутников и регионов
|
||||
- Информационные блоки
|
||||
- Валидация на стороне клиента
|
||||
|
||||
### 6. Доработана функция fill_lyngsat_data
|
||||
- **Файл**: `dbapp/lyngsatapp/utils.py`
|
||||
- **Изменения**:
|
||||
- Добавлен параметр `regions` для выбора регионов
|
||||
- Реализовано частичное заполнение данных
|
||||
- Добавлена детальная статистика обработки:
|
||||
- Количество обработанных спутников
|
||||
- Количество обработанных источников
|
||||
- Количество созданных записей
|
||||
- Количество обновленных записей
|
||||
- Список ошибок
|
||||
- Улучшена обработка ошибок (процесс не прерывается при ошибке)
|
||||
- Добавлена валидация данных перед сохранением
|
||||
|
||||
### 7. Исправлен parser.py
|
||||
- **Файл**: `dbapp/lyngsatapp/parser.py`
|
||||
- **Изменение**: Удален тестовый код выполнения в конце файла
|
||||
|
||||
### 8. Добавлено приложение lyngsatapp в настройки
|
||||
- **Файл**: `dbapp/dbapp/settings/base.py`
|
||||
- **Изменение**: Добавлено `'lyngsatapp'` в `INSTALLED_APPS`
|
||||
|
||||
### 9. Исправлен admin для LyngSat
|
||||
- **Файл**: `dbapp/lyngsatapp/admin.py`
|
||||
- **Изменение**: Обновлены поля в `list_display`, `search_fields`, `ordering` в соответствии с моделью
|
||||
|
||||
### 10. Создана миграция для LyngSat
|
||||
- **Файл**: `dbapp/lyngsatapp/migrations/0001_initial.py`
|
||||
- **Содержимое**: Создание модели LyngSat
|
||||
|
||||
## Новые файлы
|
||||
|
||||
1. `dbapp/mainapp/templates/mainapp/fill_lyngsat_data.html` - шаблон формы
|
||||
2. `dbapp/lyngsatapp/migrations/0001_initial.py` - миграция базы данных
|
||||
3. `LYNGSAT_FILL_GUIDE.md` - руководство пользователя
|
||||
4. `CHANGES_SUMMARY.md` - этот файл
|
||||
|
||||
## Измененные файлы
|
||||
|
||||
1. `dbapp/mainapp/forms.py` - добавлена форма `FillLyngsatDataForm`
|
||||
2. `dbapp/mainapp/views.py` - добавлен view `FillLyngsatDataView`
|
||||
3. `dbapp/mainapp/urls.py` - добавлен URL для новой функциональности
|
||||
4. `dbapp/mainapp/templates/mainapp/actions.html` - заменена карточка
|
||||
5. `dbapp/lyngsatapp/utils.py` - доработана функция `fill_lyngsat_data`
|
||||
6. `dbapp/lyngsatapp/parser.py` - удален тестовый код
|
||||
7. `dbapp/lyngsatapp/admin.py` - исправлены поля админки
|
||||
8. `dbapp/dbapp/settings/base.py` - добавлено приложение в INSTALLED_APPS
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Зависимости
|
||||
- FlareSolver должен быть запущен на `http://localhost:8191`
|
||||
- Спутники должны быть предварительно добавлены в базу данных
|
||||
|
||||
### Модель данных
|
||||
Модель `LyngSat` содержит следующие поля:
|
||||
- `id_satellite` - связь со спутником
|
||||
- `frequency` - частота в МГц
|
||||
- `polarization` - поляризация сигнала
|
||||
- `modulation` - тип модуляции
|
||||
- `standard` - стандарт передачи
|
||||
- `sym_velocity` - символьная скорость
|
||||
- `last_update` - дата последнего обновления
|
||||
- `channel_info` - информация о канале
|
||||
- `fec` - коэффициент коррекции ошибок
|
||||
- `url` - ссылка на страницу Lyngsat
|
||||
|
||||
### Процесс работы
|
||||
1. Пользователь выбирает спутники и регионы
|
||||
2. Система подключается к Lyngsat через FlareSolver
|
||||
3. Парсит данные для каждого спутника
|
||||
4. Создает или обновляет записи в базе данных
|
||||
5. Возвращает статистику обработки
|
||||
|
||||
## Тестирование
|
||||
|
||||
Выполнены следующие проверки:
|
||||
- ✅ `python manage.py check` - нет ошибок
|
||||
- ✅ `python manage.py makemigrations` - миграция создана
|
||||
- ✅ Проверка диагностики кода - нет критических ошибок
|
||||
- ✅ Проверка импортов - все импорты корректны
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
Для полного тестирования необходимо:
|
||||
1. Применить миграции: `python manage.py migrate`
|
||||
2. Запустить FlareSolver: `docker run -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest`
|
||||
3. Добавить спутники в базу данных (если еще не добавлены)
|
||||
4. Протестировать форму заполнения данных через веб-интерфейс
|
||||
|
||||
## Примечания
|
||||
|
||||
- Процесс заполнения может занять продолжительное время (несколько минут на спутник)
|
||||
- Рекомендуется начинать с небольшого количества спутников
|
||||
- Все ошибки логируются и отображаются пользователю
|
||||
- Существующие записи обновляются, новые создаются
|
||||
|
||||
@@ -1,249 +1,249 @@
|
||||
# Чеклист для деплоя в Production
|
||||
|
||||
## Перед деплоем
|
||||
|
||||
### 1. Безопасность
|
||||
|
||||
- [ ] Сгенерирован новый `SECRET_KEY`
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
- [ ] Изменены все пароли в `.env`:
|
||||
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
|
||||
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
|
||||
|
||||
- [ ] Настроен `ALLOWED_HOSTS` в `.env`:
|
||||
```
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
- [ ] `DEBUG=False` в `.env`
|
||||
|
||||
### 2. База данных
|
||||
|
||||
- [ ] Проверены все миграции:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
|
||||
```
|
||||
|
||||
- [ ] Настроен backup БД (cron job):
|
||||
```bash
|
||||
0 2 * * * cd /path/to/project && make backup
|
||||
```
|
||||
|
||||
### 3. Статические файлы
|
||||
|
||||
- [ ] Проверена директория для статики:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### 4. SSL/HTTPS (опционально, но рекомендуется)
|
||||
|
||||
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
|
||||
- [ ] Сертификаты размещены в `nginx/ssl/`
|
||||
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
|
||||
- [ ] Обновлен `server_name` в `ssl.conf`
|
||||
|
||||
### 5. Nginx
|
||||
|
||||
- [ ] Проверена конфигурация Nginx:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||
```
|
||||
|
||||
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
|
||||
|
||||
### 6. Docker
|
||||
|
||||
- [ ] Проверен `.dockerignore` - исключены ненужные файлы
|
||||
- [ ] Проверен `.gitignore` - не коммитятся секреты
|
||||
|
||||
### 7. Переменные окружения
|
||||
|
||||
Проверьте `.env` файл:
|
||||
|
||||
```bash
|
||||
# Django
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||
SECRET_KEY=<ваш-длинный-секретный-ключ>
|
||||
|
||||
# Database
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=<сильный-пароль>
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=<тот-же-сильный-пароль>
|
||||
|
||||
# Gunicorn
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
```
|
||||
|
||||
## Деплой
|
||||
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd <project-directory>
|
||||
```
|
||||
|
||||
### 2. Настройка окружения
|
||||
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
nano .env # Отредактируйте все необходимые переменные
|
||||
```
|
||||
|
||||
### 3. Запуск контейнеров
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
### 4. Проверка статуса
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml ps
|
||||
docker-compose -f docker-compose.prod.yaml logs -f
|
||||
```
|
||||
|
||||
### 5. Создание суперпользователя
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 6. Проверка работоспособности
|
||||
|
||||
- [ ] Открыть http://yourdomain.com
|
||||
- [ ] Открыть http://yourdomain.com/admin
|
||||
- [ ] Проверить статические файлы
|
||||
- [ ] Проверить медиа файлы
|
||||
- [ ] Проверить TileServer GL: http://yourdomain.com:8080
|
||||
|
||||
## После деплоя
|
||||
|
||||
### 1. Мониторинг
|
||||
|
||||
- [ ] Настроить мониторинг логов:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||
```
|
||||
|
||||
- [ ] Проверить использование ресурсов:
|
||||
```bash
|
||||
docker stats
|
||||
```
|
||||
|
||||
### 2. Backup
|
||||
|
||||
- [ ] Настроить автоматический backup БД
|
||||
- [ ] Проверить восстановление из backup
|
||||
- [ ] Настроить backup медиа файлов
|
||||
|
||||
### 3. Обновления
|
||||
|
||||
- [ ] Документировать процесс обновления
|
||||
- [ ] Тестировать обновления на dev окружении
|
||||
|
||||
### 4. Безопасность
|
||||
|
||||
- [ ] Настроить firewall (UFW, iptables)
|
||||
- [ ] Ограничить доступ к портам:
|
||||
- Открыть: 80, 443
|
||||
- Закрыть: 5432, 8000 (доступ только внутри Docker сети)
|
||||
|
||||
- [ ] Настроить fail2ban (опционально)
|
||||
|
||||
### 5. Производительность
|
||||
|
||||
- [ ] Настроить кэширование (Redis, Memcached)
|
||||
- [ ] Оптимизировать количество Gunicorn workers
|
||||
- [ ] Настроить CDN для статики (опционально)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблема: Контейнеры не запускаются
|
||||
|
||||
```bash
|
||||
# Проверить логи
|
||||
docker-compose -f docker-compose.prod.yaml logs
|
||||
|
||||
# Проверить конфигурацию
|
||||
docker-compose -f docker-compose.prod.yaml config
|
||||
```
|
||||
|
||||
### Проблема: База данных недоступна
|
||||
|
||||
```bash
|
||||
# Проверить статус БД
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
|
||||
|
||||
# Проверить логи БД
|
||||
docker-compose -f docker-compose.prod.yaml logs db
|
||||
```
|
||||
|
||||
### Проблема: Статические файлы не загружаются
|
||||
|
||||
```bash
|
||||
# Пересобрать статику
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||
|
||||
# Проверить права доступа
|
||||
docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
|
||||
```
|
||||
|
||||
### Проблема: 502 Bad Gateway
|
||||
|
||||
```bash
|
||||
# Проверить, что Django запущен
|
||||
docker-compose -f docker-compose.prod.yaml ps web
|
||||
|
||||
# Проверить логи Gunicorn
|
||||
docker-compose -f docker-compose.prod.yaml logs web
|
||||
|
||||
# Проверить конфигурацию Nginx
|
||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Перезапуск сервисов
|
||||
docker-compose -f docker-compose.prod.yaml restart web
|
||||
docker-compose -f docker-compose.prod.yaml restart nginx
|
||||
|
||||
# Обновление кода
|
||||
git pull
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
# Backup БД
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||
|
||||
# Восстановление БД
|
||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
|
||||
|
||||
# Очистка старых образов
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
## Контакты для поддержки
|
||||
|
||||
- Документация: [DOCKER_README.md](DOCKER_README.md)
|
||||
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md)
|
||||
# Чеклист для деплоя в Production
|
||||
|
||||
## Перед деплоем
|
||||
|
||||
### 1. Безопасность
|
||||
|
||||
- [ ] Сгенерирован новый `SECRET_KEY`
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
- [ ] Изменены все пароли в `.env`:
|
||||
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
|
||||
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
|
||||
|
||||
- [ ] Настроен `ALLOWED_HOSTS` в `.env`:
|
||||
```
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
- [ ] `DEBUG=False` в `.env`
|
||||
|
||||
### 2. База данных
|
||||
|
||||
- [ ] Проверены все миграции:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
|
||||
```
|
||||
|
||||
- [ ] Настроен backup БД (cron job):
|
||||
```bash
|
||||
0 2 * * * cd /path/to/project && make backup
|
||||
```
|
||||
|
||||
### 3. Статические файлы
|
||||
|
||||
- [ ] Проверена директория для статики:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### 4. SSL/HTTPS (опционально, но рекомендуется)
|
||||
|
||||
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
|
||||
- [ ] Сертификаты размещены в `nginx/ssl/`
|
||||
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
|
||||
- [ ] Обновлен `server_name` в `ssl.conf`
|
||||
|
||||
### 5. Nginx
|
||||
|
||||
- [ ] Проверена конфигурация Nginx:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||
```
|
||||
|
||||
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
|
||||
|
||||
### 6. Docker
|
||||
|
||||
- [ ] Проверен `.dockerignore` - исключены ненужные файлы
|
||||
- [ ] Проверен `.gitignore` - не коммитятся секреты
|
||||
|
||||
### 7. Переменные окружения
|
||||
|
||||
Проверьте `.env` файл:
|
||||
|
||||
```bash
|
||||
# Django
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||
SECRET_KEY=<ваш-длинный-секретный-ключ>
|
||||
|
||||
# Database
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=<сильный-пароль>
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=geodb
|
||||
POSTGRES_USER=geralt
|
||||
POSTGRES_PASSWORD=<тот-же-сильный-пароль>
|
||||
|
||||
# Gunicorn
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
```
|
||||
|
||||
## Деплой
|
||||
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd <project-directory>
|
||||
```
|
||||
|
||||
### 2. Настройка окружения
|
||||
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
nano .env # Отредактируйте все необходимые переменные
|
||||
```
|
||||
|
||||
### 3. Запуск контейнеров
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
### 4. Проверка статуса
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml ps
|
||||
docker-compose -f docker-compose.prod.yaml logs -f
|
||||
```
|
||||
|
||||
### 5. Создание суперпользователя
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 6. Проверка работоспособности
|
||||
|
||||
- [ ] Открыть http://yourdomain.com
|
||||
- [ ] Открыть http://yourdomain.com/admin
|
||||
- [ ] Проверить статические файлы
|
||||
- [ ] Проверить медиа файлы
|
||||
- [ ] Проверить TileServer GL: http://yourdomain.com:8080
|
||||
|
||||
## После деплоя
|
||||
|
||||
### 1. Мониторинг
|
||||
|
||||
- [ ] Настроить мониторинг логов:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||
```
|
||||
|
||||
- [ ] Проверить использование ресурсов:
|
||||
```bash
|
||||
docker stats
|
||||
```
|
||||
|
||||
### 2. Backup
|
||||
|
||||
- [ ] Настроить автоматический backup БД
|
||||
- [ ] Проверить восстановление из backup
|
||||
- [ ] Настроить backup медиа файлов
|
||||
|
||||
### 3. Обновления
|
||||
|
||||
- [ ] Документировать процесс обновления
|
||||
- [ ] Тестировать обновления на dev окружении
|
||||
|
||||
### 4. Безопасность
|
||||
|
||||
- [ ] Настроить firewall (UFW, iptables)
|
||||
- [ ] Ограничить доступ к портам:
|
||||
- Открыть: 80, 443
|
||||
- Закрыть: 5432, 8000 (доступ только внутри Docker сети)
|
||||
|
||||
- [ ] Настроить fail2ban (опционально)
|
||||
|
||||
### 5. Производительность
|
||||
|
||||
- [ ] Настроить кэширование (Redis, Memcached)
|
||||
- [ ] Оптимизировать количество Gunicorn workers
|
||||
- [ ] Настроить CDN для статики (опционально)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблема: Контейнеры не запускаются
|
||||
|
||||
```bash
|
||||
# Проверить логи
|
||||
docker-compose -f docker-compose.prod.yaml logs
|
||||
|
||||
# Проверить конфигурацию
|
||||
docker-compose -f docker-compose.prod.yaml config
|
||||
```
|
||||
|
||||
### Проблема: База данных недоступна
|
||||
|
||||
```bash
|
||||
# Проверить статус БД
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
|
||||
|
||||
# Проверить логи БД
|
||||
docker-compose -f docker-compose.prod.yaml logs db
|
||||
```
|
||||
|
||||
### Проблема: Статические файлы не загружаются
|
||||
|
||||
```bash
|
||||
# Пересобрать статику
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||
|
||||
# Проверить права доступа
|
||||
docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
|
||||
```
|
||||
|
||||
### Проблема: 502 Bad Gateway
|
||||
|
||||
```bash
|
||||
# Проверить, что Django запущен
|
||||
docker-compose -f docker-compose.prod.yaml ps web
|
||||
|
||||
# Проверить логи Gunicorn
|
||||
docker-compose -f docker-compose.prod.yaml logs web
|
||||
|
||||
# Проверить конфигурацию Nginx
|
||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Перезапуск сервисов
|
||||
docker-compose -f docker-compose.prod.yaml restart web
|
||||
docker-compose -f docker-compose.prod.yaml restart nginx
|
||||
|
||||
# Обновление кода
|
||||
git pull
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
# Backup БД
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||
|
||||
# Восстановление БД
|
||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
|
||||
|
||||
# Очистка старых образов
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
## Контакты для поддержки
|
||||
|
||||
- Документация: [DOCKER_README.md](DOCKER_README.md)
|
||||
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md)
|
||||
|
||||
@@ -1,102 +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`
|
||||
# Инструкция по развертыванию изменений
|
||||
|
||||
## Шаг 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`
|
||||
|
||||
524
DOCKER_README.md
524
DOCKER_README.md
@@ -1,262 +1,262 @@
|
||||
# Docker Setup для Django + PostGIS + TileServer GL
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт запуска
|
||||
│ └── ...
|
||||
├── nginx/ # Конфигурация Nginx (только для prod)
|
||||
│ └── conf.d/
|
||||
│ └── default.conf
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
├── docker-compose.yaml # Development окружение
|
||||
├── docker-compose.prod.yaml # Production окружение
|
||||
├── .env.dev # Переменные для development
|
||||
└── .env.prod # Переменные для production
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Development
|
||||
|
||||
1. Скопируйте файл окружения:
|
||||
```bash
|
||||
cp .env.dev .env
|
||||
```
|
||||
|
||||
2. Запустите контейнеры:
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
3. Создайте суперпользователя:
|
||||
```bash
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
4. Приложение доступно:
|
||||
- Django: http://localhost:8000
|
||||
- TileServer GL: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
### Production
|
||||
|
||||
1. Скопируйте и настройте файл окружения:
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
|
||||
```
|
||||
|
||||
2. Запустите контейнеры:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
3. Создайте суперпользователя:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
4. Приложение доступно:
|
||||
- Nginx: http://localhost (порт 80)
|
||||
- Django (напрямую): http://localhost:8000
|
||||
- TileServer GL: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
## Основные команды
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Запуск
|
||||
docker-compose up -d
|
||||
|
||||
# Остановка
|
||||
docker-compose down
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f web
|
||||
|
||||
# Выполнение команд Django
|
||||
docker-compose exec web python manage.py migrate
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
docker-compose exec web python manage.py shell
|
||||
|
||||
# Пересборка после изменений в Dockerfile
|
||||
docker-compose up -d --build
|
||||
|
||||
# Полная очистка (включая volumes)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
# Запуск
|
||||
docker-compose -f docker-compose.prod.yaml up -d
|
||||
|
||||
# Остановка
|
||||
docker-compose -f docker-compose.prod.yaml down
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||
|
||||
# Выполнение команд Django
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# Пересборка
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
## Различия между Dev и Prod
|
||||
|
||||
### Development
|
||||
- Django development server (runserver)
|
||||
- DEBUG=True
|
||||
- Код монтируется как volume (изменения применяются сразу)
|
||||
- Без Nginx
|
||||
- Простые пароли (для локальной разработки)
|
||||
|
||||
### Production
|
||||
- Gunicorn WSGI server
|
||||
- DEBUG=False
|
||||
- Код копируется в образ (не монтируется)
|
||||
- Nginx как reverse proxy
|
||||
- Сильные пароли и SECRET_KEY
|
||||
- Сбор статики (collectstatic)
|
||||
- Оптимизированные настройки безопасности
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### Основные переменные (.env)
|
||||
|
||||
```bash
|
||||
# Django
|
||||
DEBUG=True/False
|
||||
ENVIRONMENT=development/production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
|
||||
SECRET_KEY=your-secret-key
|
||||
|
||||
# Database
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=your-password
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||
|
||||
# Gunicorn (только для production)
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
### Development
|
||||
- `postgres_data_dev` - данные PostgreSQL
|
||||
- `static_volume_dev` - статические файлы
|
||||
- `media_volume_dev` - медиа файлы
|
||||
- `logs_volume_dev` - логи
|
||||
- `./dbapp:/app` - код приложения (live reload)
|
||||
|
||||
### Production
|
||||
- `postgres_data_prod` - данные PostgreSQL
|
||||
- `static_volume_prod` - статические файлы
|
||||
- `media_volume_prod` - медиа файлы
|
||||
- `logs_volume_prod` - логи
|
||||
|
||||
## TileServer GL
|
||||
|
||||
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
|
||||
|
||||
Пример структуры:
|
||||
```
|
||||
tiles/
|
||||
├── config.json
|
||||
└── your-tiles.mbtiles
|
||||
```
|
||||
|
||||
## Backup и восстановление БД
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
# Development
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup.sql
|
||||
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||
```
|
||||
|
||||
### Восстановление
|
||||
```bash
|
||||
# Development
|
||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблемы с миграциями
|
||||
```bash
|
||||
docker-compose exec web python manage.py migrate --fake-initial
|
||||
```
|
||||
|
||||
### Проблемы с правами доступа
|
||||
```bash
|
||||
docker-compose exec -u root web chown -R app:app /app
|
||||
```
|
||||
|
||||
### Очистка всех данных
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
### Проверка логов
|
||||
```bash
|
||||
# Все сервисы
|
||||
docker-compose logs -f
|
||||
|
||||
# Конкретный сервис
|
||||
docker-compose logs -f web
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
## Безопасность для Production
|
||||
|
||||
1. **Измените SECRET_KEY** - используйте длинный случайный ключ
|
||||
2. **Измените пароли БД** - используйте сильные пароли
|
||||
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
|
||||
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
|
||||
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
|
||||
|
||||
## Генерация SECRET_KEY
|
||||
|
||||
```python
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
### Проверка статуса контейнеров
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Использование ресурсов
|
||||
```bash
|
||||
docker stats
|
||||
```
|
||||
|
||||
### Healthcheck
|
||||
```bash
|
||||
curl http://localhost:8000/admin/
|
||||
```
|
||||
# Docker Setup для Django + PostGIS + TileServer GL
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт запуска
|
||||
│ └── ...
|
||||
├── nginx/ # Конфигурация Nginx (только для prod)
|
||||
│ └── conf.d/
|
||||
│ └── default.conf
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
├── docker-compose.yaml # Development окружение
|
||||
├── docker-compose.prod.yaml # Production окружение
|
||||
├── .env.dev # Переменные для development
|
||||
└── .env.prod # Переменные для production
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Development
|
||||
|
||||
1. Скопируйте файл окружения:
|
||||
```bash
|
||||
cp .env.dev .env
|
||||
```
|
||||
|
||||
2. Запустите контейнеры:
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
3. Создайте суперпользователя:
|
||||
```bash
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
4. Приложение доступно:
|
||||
- Django: http://localhost:8000
|
||||
- TileServer GL: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
### Production
|
||||
|
||||
1. Скопируйте и настройте файл окружения:
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
|
||||
```
|
||||
|
||||
2. Запустите контейнеры:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
3. Создайте суперпользователя:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
4. Приложение доступно:
|
||||
- Nginx: http://localhost (порт 80)
|
||||
- Django (напрямую): http://localhost:8000
|
||||
- TileServer GL: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
## Основные команды
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Запуск
|
||||
docker-compose up -d
|
||||
|
||||
# Остановка
|
||||
docker-compose down
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f web
|
||||
|
||||
# Выполнение команд Django
|
||||
docker-compose exec web python manage.py migrate
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
docker-compose exec web python manage.py shell
|
||||
|
||||
# Пересборка после изменений в Dockerfile
|
||||
docker-compose up -d --build
|
||||
|
||||
# Полная очистка (включая volumes)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
# Запуск
|
||||
docker-compose -f docker-compose.prod.yaml up -d
|
||||
|
||||
# Остановка
|
||||
docker-compose -f docker-compose.prod.yaml down
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||
|
||||
# Выполнение команд Django
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# Пересборка
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
## Различия между Dev и Prod
|
||||
|
||||
### Development
|
||||
- Django development server (runserver)
|
||||
- DEBUG=True
|
||||
- Код монтируется как volume (изменения применяются сразу)
|
||||
- Без Nginx
|
||||
- Простые пароли (для локальной разработки)
|
||||
|
||||
### Production
|
||||
- Gunicorn WSGI server
|
||||
- DEBUG=False
|
||||
- Код копируется в образ (не монтируется)
|
||||
- Nginx как reverse proxy
|
||||
- Сильные пароли и SECRET_KEY
|
||||
- Сбор статики (collectstatic)
|
||||
- Оптимизированные настройки безопасности
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### Основные переменные (.env)
|
||||
|
||||
```bash
|
||||
# Django
|
||||
DEBUG=True/False
|
||||
ENVIRONMENT=development/production
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
|
||||
SECRET_KEY=your-secret-key
|
||||
|
||||
# Database
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=your-password
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||
|
||||
# Gunicorn (только для production)
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
### Development
|
||||
- `postgres_data_dev` - данные PostgreSQL
|
||||
- `static_volume_dev` - статические файлы
|
||||
- `media_volume_dev` - медиа файлы
|
||||
- `logs_volume_dev` - логи
|
||||
- `./dbapp:/app` - код приложения (live reload)
|
||||
|
||||
### Production
|
||||
- `postgres_data_prod` - данные PostgreSQL
|
||||
- `static_volume_prod` - статические файлы
|
||||
- `media_volume_prod` - медиа файлы
|
||||
- `logs_volume_prod` - логи
|
||||
|
||||
## TileServer GL
|
||||
|
||||
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
|
||||
|
||||
Пример структуры:
|
||||
```
|
||||
tiles/
|
||||
├── config.json
|
||||
└── your-tiles.mbtiles
|
||||
```
|
||||
|
||||
## Backup и восстановление БД
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
# Development
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup.sql
|
||||
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||
```
|
||||
|
||||
### Восстановление
|
||||
```bash
|
||||
# Development
|
||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблемы с миграциями
|
||||
```bash
|
||||
docker-compose exec web python manage.py migrate --fake-initial
|
||||
```
|
||||
|
||||
### Проблемы с правами доступа
|
||||
```bash
|
||||
docker-compose exec -u root web chown -R app:app /app
|
||||
```
|
||||
|
||||
### Очистка всех данных
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
### Проверка логов
|
||||
```bash
|
||||
# Все сервисы
|
||||
docker-compose logs -f
|
||||
|
||||
# Конкретный сервис
|
||||
docker-compose logs -f web
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
## Безопасность для Production
|
||||
|
||||
1. **Измените SECRET_KEY** - используйте длинный случайный ключ
|
||||
2. **Измените пароли БД** - используйте сильные пароли
|
||||
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
|
||||
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
|
||||
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
|
||||
|
||||
## Генерация SECRET_KEY
|
||||
|
||||
```python
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
### Проверка статуса контейнеров
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Использование ресурсов
|
||||
```bash
|
||||
docker stats
|
||||
```
|
||||
|
||||
### Healthcheck
|
||||
```bash
|
||||
curl http://localhost:8000/admin/
|
||||
```
|
||||
|
||||
614
DOCKER_SETUP.md
614
DOCKER_SETUP.md
@@ -1,307 +1,307 @@
|
||||
# Docker Setup - Полное руководство
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
|
||||
|
||||
**Основные компоненты:**
|
||||
- Django 5.2 с PostGIS
|
||||
- PostgreSQL 17 с расширением PostGIS 3.4
|
||||
- TileServer GL для работы с картографическими тайлами
|
||||
- Nginx (только для production)
|
||||
- Gunicorn WSGI сервер (production)
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Development
|
||||
```bash
|
||||
cp .env.dev .env
|
||||
make dev-up
|
||||
make createsuperuser
|
||||
```
|
||||
Откройте http://localhost:8000
|
||||
|
||||
### Production
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
# Отредактируйте .env (SECRET_KEY, пароли, домены)
|
||||
make prod-up
|
||||
make prod-createsuperuser
|
||||
```
|
||||
Откройте http://yourdomain.com
|
||||
|
||||
## 📁 Структура файлов
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт инициализации
|
||||
│ ├── .dockerignore # Исключения для Docker
|
||||
│ └── ...
|
||||
│
|
||||
├── nginx/ # Nginx конфигурация (prod)
|
||||
│ ├── conf.d/
|
||||
│ │ ├── default.conf # HTTP конфигурация
|
||||
│ │ └── ssl.conf.example # HTTPS конфигурация (пример)
|
||||
│ └── ssl/ # SSL сертификаты
|
||||
│
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
│ ├── README.md # Инструкция по настройке
|
||||
│ ├── config.json.example # Пример конфигурации
|
||||
│ └── .gitignore
|
||||
│
|
||||
├── docker-compose.yaml # Development окружение
|
||||
├── docker-compose.prod.yaml # Production окружение
|
||||
│
|
||||
├── .env.dev # Переменные для dev
|
||||
├── .env.prod # Переменные для prod (шаблон)
|
||||
│
|
||||
├── Makefile # Удобные команды
|
||||
├── generate_secret_key.py # Генератор SECRET_KEY
|
||||
│
|
||||
└── Документация:
|
||||
├── QUICKSTART.md # Быстрый старт
|
||||
├── DOCKER_README.md # Подробная документация
|
||||
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
|
||||
└── DOCKER_SETUP.md # Этот файл
|
||||
```
|
||||
|
||||
## 🔧 Конфигурация
|
||||
|
||||
### Dockerfile
|
||||
|
||||
**Один универсальный Dockerfile** для dev и prod:
|
||||
- Multi-stage build для оптимизации размера
|
||||
- Установка GDAL, PostGIS зависимостей
|
||||
- Использование uv для управления зависимостями
|
||||
- Non-root пользователь для безопасности
|
||||
- Healthcheck для мониторинга
|
||||
|
||||
### entrypoint.sh
|
||||
|
||||
Скрипт автоматически:
|
||||
- Ждет готовности PostgreSQL
|
||||
- Выполняет миграции
|
||||
- Собирает статику (только prod)
|
||||
- Запускает runserver (dev) или Gunicorn (prod)
|
||||
|
||||
Поведение определяется переменной `ENVIRONMENT`:
|
||||
- `development` → Django development server
|
||||
- `production` → Gunicorn WSGI server
|
||||
|
||||
### docker-compose.yaml (Development)
|
||||
|
||||
**Сервисы:**
|
||||
- `db` - PostgreSQL с PostGIS
|
||||
- `web` - Django приложение
|
||||
- `tileserver` - TileServer GL
|
||||
|
||||
**Особенности:**
|
||||
- Код монтируется как volume (live reload)
|
||||
- DEBUG=True
|
||||
- Django development server
|
||||
- Простые пароли для локальной разработки
|
||||
|
||||
### docker-compose.prod.yaml (Production)
|
||||
|
||||
**Сервисы:**
|
||||
- `db` - PostgreSQL с PostGIS
|
||||
- `web` - Django с Gunicorn
|
||||
- `tileserver` - TileServer GL
|
||||
- `nginx` - Reverse proxy
|
||||
|
||||
**Особенности:**
|
||||
- Код копируется в образ (не монтируется)
|
||||
- DEBUG=False
|
||||
- Gunicorn WSGI server
|
||||
- Nginx для статики и проксирования
|
||||
- Сильные пароли из .env
|
||||
- Сбор статики (collectstatic)
|
||||
|
||||
## 🔐 Безопасность
|
||||
|
||||
### Для Production обязательно:
|
||||
|
||||
1. **Сгенерируйте SECRET_KEY:**
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
2. **Измените пароли БД** в `.env`
|
||||
|
||||
3. **Настройте ALLOWED_HOSTS:**
|
||||
```
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
4. **Настройте SSL/HTTPS** (рекомендуется):
|
||||
- Получите сертификаты (Let's Encrypt)
|
||||
- Поместите в `nginx/ssl/`
|
||||
- Используйте `nginx/conf.d/ssl.conf.example`
|
||||
|
||||
5. **Ограничьте доступ к портам:**
|
||||
- Открыть: 80, 443
|
||||
- Закрыть: 5432, 8000
|
||||
|
||||
## 📊 Мониторинг
|
||||
|
||||
### Логи
|
||||
```bash
|
||||
# Development
|
||||
make dev-logs
|
||||
|
||||
# Production
|
||||
make prod-logs
|
||||
|
||||
# Конкретный сервис
|
||||
docker-compose logs -f web
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
### Статус
|
||||
```bash
|
||||
make status # Development
|
||||
make prod-status # Production
|
||||
docker stats # Использование ресурсов
|
||||
```
|
||||
|
||||
### Healthcheck
|
||||
```bash
|
||||
curl http://localhost:8000/admin/
|
||||
```
|
||||
|
||||
## 💾 Backup и восстановление
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
make backup
|
||||
# или
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Восстановление
|
||||
```bash
|
||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||
```
|
||||
|
||||
### Автоматический backup (cron)
|
||||
```bash
|
||||
# Добавьте в crontab
|
||||
0 2 * * * cd /path/to/project && make backup
|
||||
```
|
||||
|
||||
## 🔄 Обновление
|
||||
|
||||
### Development
|
||||
```bash
|
||||
git pull
|
||||
make dev-build
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
git pull
|
||||
make prod-build
|
||||
make prod-migrate
|
||||
```
|
||||
|
||||
## 🗺️ TileServer GL
|
||||
|
||||
Поместите `.mbtiles` файлы в директорию `tiles/`:
|
||||
|
||||
```bash
|
||||
tiles/
|
||||
├── world.mbtiles
|
||||
└── satellite.mbtiles
|
||||
```
|
||||
|
||||
Доступ: http://localhost:8080
|
||||
|
||||
Подробнее: [tiles/README.md](tiles/README.md)
|
||||
|
||||
## 🛠️ Makefile команды
|
||||
|
||||
### Development
|
||||
```bash
|
||||
make dev-up # Запустить
|
||||
make dev-down # Остановить
|
||||
make dev-build # Пересобрать
|
||||
make dev-logs # Логи
|
||||
make dev-restart # Перезапустить web
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
make prod-up # Запустить
|
||||
make prod-down # Остановить
|
||||
make prod-build # Пересобрать
|
||||
make prod-logs # Логи
|
||||
make prod-restart # Перезапустить web
|
||||
```
|
||||
|
||||
### Django
|
||||
```bash
|
||||
make shell # Django shell
|
||||
make migrate # Миграции
|
||||
make makemigrations # Создать миграции
|
||||
make createsuperuser # Создать суперпользователя
|
||||
make collectstatic # Собрать статику
|
||||
```
|
||||
|
||||
### Утилиты
|
||||
```bash
|
||||
make backup # Backup БД
|
||||
make status # Статус контейнеров
|
||||
make clean # Очистка (с volumes)
|
||||
make clean-all # Полная очистка
|
||||
```
|
||||
|
||||
## 📚 Дополнительная документация
|
||||
|
||||
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
|
||||
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
|
||||
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
|
||||
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
|
||||
|
||||
## ❓ Troubleshooting
|
||||
|
||||
### Контейнеры не запускаются
|
||||
```bash
|
||||
docker-compose logs
|
||||
docker-compose config
|
||||
```
|
||||
|
||||
### База данных недоступна
|
||||
```bash
|
||||
docker-compose exec db pg_isready -U geralt
|
||||
docker-compose logs db
|
||||
```
|
||||
|
||||
### Статические файлы не загружаются
|
||||
```bash
|
||||
docker-compose exec web python manage.py collectstatic --noinput
|
||||
docker-compose exec web ls -la /app/staticfiles
|
||||
```
|
||||
|
||||
### 502 Bad Gateway
|
||||
```bash
|
||||
docker-compose ps web
|
||||
docker-compose logs web
|
||||
docker-compose exec nginx nginx -t
|
||||
```
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
|
||||
2. ✅ Запустите development окружение
|
||||
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
|
||||
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
|
||||
5. ✅ Настройте SSL для production
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
При возникновении проблем:
|
||||
1. Проверьте логи: `make dev-logs` или `make prod-logs`
|
||||
2. Изучите документацию в этой директории
|
||||
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей
|
||||
# Docker Setup - Полное руководство
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
|
||||
|
||||
**Основные компоненты:**
|
||||
- Django 5.2 с PostGIS
|
||||
- PostgreSQL 17 с расширением PostGIS 3.4
|
||||
- TileServer GL для работы с картографическими тайлами
|
||||
- Nginx (только для production)
|
||||
- Gunicorn WSGI сервер (production)
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Development
|
||||
```bash
|
||||
cp .env.dev .env
|
||||
make dev-up
|
||||
make createsuperuser
|
||||
```
|
||||
Откройте http://localhost:8000
|
||||
|
||||
### Production
|
||||
```bash
|
||||
cp .env.prod .env
|
||||
# Отредактируйте .env (SECRET_KEY, пароли, домены)
|
||||
make prod-up
|
||||
make prod-createsuperuser
|
||||
```
|
||||
Откройте http://yourdomain.com
|
||||
|
||||
## 📁 Структура файлов
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт инициализации
|
||||
│ ├── .dockerignore # Исключения для Docker
|
||||
│ └── ...
|
||||
│
|
||||
├── nginx/ # Nginx конфигурация (prod)
|
||||
│ ├── conf.d/
|
||||
│ │ ├── default.conf # HTTP конфигурация
|
||||
│ │ └── ssl.conf.example # HTTPS конфигурация (пример)
|
||||
│ └── ssl/ # SSL сертификаты
|
||||
│
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
│ ├── README.md # Инструкция по настройке
|
||||
│ ├── config.json.example # Пример конфигурации
|
||||
│ └── .gitignore
|
||||
│
|
||||
├── docker-compose.yaml # Development окружение
|
||||
├── docker-compose.prod.yaml # Production окружение
|
||||
│
|
||||
├── .env.dev # Переменные для dev
|
||||
├── .env.prod # Переменные для prod (шаблон)
|
||||
│
|
||||
├── Makefile # Удобные команды
|
||||
├── generate_secret_key.py # Генератор SECRET_KEY
|
||||
│
|
||||
└── Документация:
|
||||
├── QUICKSTART.md # Быстрый старт
|
||||
├── DOCKER_README.md # Подробная документация
|
||||
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
|
||||
└── DOCKER_SETUP.md # Этот файл
|
||||
```
|
||||
|
||||
## 🔧 Конфигурация
|
||||
|
||||
### Dockerfile
|
||||
|
||||
**Один универсальный Dockerfile** для dev и prod:
|
||||
- Multi-stage build для оптимизации размера
|
||||
- Установка GDAL, PostGIS зависимостей
|
||||
- Использование uv для управления зависимостями
|
||||
- Non-root пользователь для безопасности
|
||||
- Healthcheck для мониторинга
|
||||
|
||||
### entrypoint.sh
|
||||
|
||||
Скрипт автоматически:
|
||||
- Ждет готовности PostgreSQL
|
||||
- Выполняет миграции
|
||||
- Собирает статику (только prod)
|
||||
- Запускает runserver (dev) или Gunicorn (prod)
|
||||
|
||||
Поведение определяется переменной `ENVIRONMENT`:
|
||||
- `development` → Django development server
|
||||
- `production` → Gunicorn WSGI server
|
||||
|
||||
### docker-compose.yaml (Development)
|
||||
|
||||
**Сервисы:**
|
||||
- `db` - PostgreSQL с PostGIS
|
||||
- `web` - Django приложение
|
||||
- `tileserver` - TileServer GL
|
||||
|
||||
**Особенности:**
|
||||
- Код монтируется как volume (live reload)
|
||||
- DEBUG=True
|
||||
- Django development server
|
||||
- Простые пароли для локальной разработки
|
||||
|
||||
### docker-compose.prod.yaml (Production)
|
||||
|
||||
**Сервисы:**
|
||||
- `db` - PostgreSQL с PostGIS
|
||||
- `web` - Django с Gunicorn
|
||||
- `tileserver` - TileServer GL
|
||||
- `nginx` - Reverse proxy
|
||||
|
||||
**Особенности:**
|
||||
- Код копируется в образ (не монтируется)
|
||||
- DEBUG=False
|
||||
- Gunicorn WSGI server
|
||||
- Nginx для статики и проксирования
|
||||
- Сильные пароли из .env
|
||||
- Сбор статики (collectstatic)
|
||||
|
||||
## 🔐 Безопасность
|
||||
|
||||
### Для Production обязательно:
|
||||
|
||||
1. **Сгенерируйте SECRET_KEY:**
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
2. **Измените пароли БД** в `.env`
|
||||
|
||||
3. **Настройте ALLOWED_HOSTS:**
|
||||
```
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
4. **Настройте SSL/HTTPS** (рекомендуется):
|
||||
- Получите сертификаты (Let's Encrypt)
|
||||
- Поместите в `nginx/ssl/`
|
||||
- Используйте `nginx/conf.d/ssl.conf.example`
|
||||
|
||||
5. **Ограничьте доступ к портам:**
|
||||
- Открыть: 80, 443
|
||||
- Закрыть: 5432, 8000
|
||||
|
||||
## 📊 Мониторинг
|
||||
|
||||
### Логи
|
||||
```bash
|
||||
# Development
|
||||
make dev-logs
|
||||
|
||||
# Production
|
||||
make prod-logs
|
||||
|
||||
# Конкретный сервис
|
||||
docker-compose logs -f web
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
### Статус
|
||||
```bash
|
||||
make status # Development
|
||||
make prod-status # Production
|
||||
docker stats # Использование ресурсов
|
||||
```
|
||||
|
||||
### Healthcheck
|
||||
```bash
|
||||
curl http://localhost:8000/admin/
|
||||
```
|
||||
|
||||
## 💾 Backup и восстановление
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
make backup
|
||||
# или
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Восстановление
|
||||
```bash
|
||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||
```
|
||||
|
||||
### Автоматический backup (cron)
|
||||
```bash
|
||||
# Добавьте в crontab
|
||||
0 2 * * * cd /path/to/project && make backup
|
||||
```
|
||||
|
||||
## 🔄 Обновление
|
||||
|
||||
### Development
|
||||
```bash
|
||||
git pull
|
||||
make dev-build
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
git pull
|
||||
make prod-build
|
||||
make prod-migrate
|
||||
```
|
||||
|
||||
## 🗺️ TileServer GL
|
||||
|
||||
Поместите `.mbtiles` файлы в директорию `tiles/`:
|
||||
|
||||
```bash
|
||||
tiles/
|
||||
├── world.mbtiles
|
||||
└── satellite.mbtiles
|
||||
```
|
||||
|
||||
Доступ: http://localhost:8080
|
||||
|
||||
Подробнее: [tiles/README.md](tiles/README.md)
|
||||
|
||||
## 🛠️ Makefile команды
|
||||
|
||||
### Development
|
||||
```bash
|
||||
make dev-up # Запустить
|
||||
make dev-down # Остановить
|
||||
make dev-build # Пересобрать
|
||||
make dev-logs # Логи
|
||||
make dev-restart # Перезапустить web
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
make prod-up # Запустить
|
||||
make prod-down # Остановить
|
||||
make prod-build # Пересобрать
|
||||
make prod-logs # Логи
|
||||
make prod-restart # Перезапустить web
|
||||
```
|
||||
|
||||
### Django
|
||||
```bash
|
||||
make shell # Django shell
|
||||
make migrate # Миграции
|
||||
make makemigrations # Создать миграции
|
||||
make createsuperuser # Создать суперпользователя
|
||||
make collectstatic # Собрать статику
|
||||
```
|
||||
|
||||
### Утилиты
|
||||
```bash
|
||||
make backup # Backup БД
|
||||
make status # Статус контейнеров
|
||||
make clean # Очистка (с volumes)
|
||||
make clean-all # Полная очистка
|
||||
```
|
||||
|
||||
## 📚 Дополнительная документация
|
||||
|
||||
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
|
||||
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
|
||||
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
|
||||
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
|
||||
|
||||
## ❓ Troubleshooting
|
||||
|
||||
### Контейнеры не запускаются
|
||||
```bash
|
||||
docker-compose logs
|
||||
docker-compose config
|
||||
```
|
||||
|
||||
### База данных недоступна
|
||||
```bash
|
||||
docker-compose exec db pg_isready -U geralt
|
||||
docker-compose logs db
|
||||
```
|
||||
|
||||
### Статические файлы не загружаются
|
||||
```bash
|
||||
docker-compose exec web python manage.py collectstatic --noinput
|
||||
docker-compose exec web ls -la /app/staticfiles
|
||||
```
|
||||
|
||||
### 502 Bad Gateway
|
||||
```bash
|
||||
docker-compose ps web
|
||||
docker-compose logs web
|
||||
docker-compose exec nginx nginx -t
|
||||
```
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
|
||||
2. ✅ Запустите development окружение
|
||||
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
|
||||
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
|
||||
5. ✅ Настройте SSL для production
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
При возникновении проблем:
|
||||
1. Проверьте логи: `make dev-logs` или `make prod-logs`
|
||||
2. Изучите документацию в этой директории
|
||||
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей
|
||||
|
||||
@@ -1,240 +1,240 @@
|
||||
# Обзор созданных файлов Docker Setup
|
||||
|
||||
## 🐳 Docker файлы
|
||||
|
||||
### `dbapp/Dockerfile`
|
||||
**Универсальный Dockerfile** для dev и prod окружений.
|
||||
- Multi-stage build для оптимизации
|
||||
- Установка GDAL, PostGIS, PostgreSQL клиента
|
||||
- Использование uv для управления зависимостями
|
||||
- Non-root пользователь для безопасности
|
||||
- Healthcheck для мониторинга
|
||||
|
||||
### `dbapp/entrypoint.sh`
|
||||
**Скрипт инициализации контейнера.**
|
||||
- Ожидание готовности PostgreSQL
|
||||
- Автоматические миграции
|
||||
- Сбор статики (только prod)
|
||||
- Запуск runserver (dev) или Gunicorn (prod)
|
||||
|
||||
### `dbapp/.dockerignore`
|
||||
**Исключения для Docker build.**
|
||||
- Исключает ненужные файлы из образа
|
||||
- Уменьшает размер образа
|
||||
- Ускоряет сборку
|
||||
|
||||
## 🔧 Docker Compose файлы
|
||||
|
||||
### `docker-compose.yaml`
|
||||
**Development окружение.**
|
||||
- PostgreSQL с PostGIS
|
||||
- Django с development server
|
||||
- TileServer GL
|
||||
- Код монтируется как volume (live reload)
|
||||
- DEBUG=True
|
||||
|
||||
### `docker-compose.prod.yaml`
|
||||
**Production окружение.**
|
||||
- PostgreSQL с PostGIS
|
||||
- Django с Gunicorn
|
||||
- TileServer GL
|
||||
- Nginx reverse proxy
|
||||
- Код копируется в образ
|
||||
- DEBUG=False
|
||||
- Оптимизированные настройки
|
||||
|
||||
## 🌐 Nginx конфигурация
|
||||
|
||||
### `nginx/conf.d/default.conf`
|
||||
**HTTP конфигурация для production.**
|
||||
- Проксирование к Django
|
||||
- Раздача статики и медиа
|
||||
- Оптимизированные таймауты
|
||||
- Кэширование статики
|
||||
|
||||
### `nginx/conf.d/ssl.conf.example`
|
||||
**HTTPS конфигурация (пример).**
|
||||
- SSL/TLS настройки
|
||||
- Редирект с HTTP на HTTPS
|
||||
- Security headers
|
||||
- Оптимизированные SSL параметры
|
||||
|
||||
### `nginx/ssl/.gitkeep`
|
||||
**Директория для SSL сертификатов.**
|
||||
- Поместите сюда fullchain.pem и privkey.pem
|
||||
|
||||
## 🗺️ TileServer GL
|
||||
|
||||
### `tiles/README.md`
|
||||
**Инструкция по настройке TileServer GL.**
|
||||
- Как добавить тайлы
|
||||
- Примеры конфигурации
|
||||
- Использование в Django/Leaflet
|
||||
- Где взять тайлы
|
||||
|
||||
### `tiles/config.json.example`
|
||||
**Пример конфигурации TileServer GL.**
|
||||
- Настройки путей
|
||||
- Форматы и качество
|
||||
- Домены
|
||||
|
||||
### `tiles/.gitignore`
|
||||
**Исключения для git.**
|
||||
- Игнорирует большие .mbtiles файлы
|
||||
- Сохраняет примеры конфигурации
|
||||
|
||||
## 🔐 Переменные окружения
|
||||
|
||||
### `.env.dev`
|
||||
**Переменные для development.**
|
||||
- DEBUG=True
|
||||
- Простые пароли для локальной разработки
|
||||
- Настройки БД для dev
|
||||
|
||||
### `.env.prod`
|
||||
**Шаблон переменных для production.**
|
||||
- DEBUG=False
|
||||
- Требует изменения SECRET_KEY и паролей
|
||||
- Настройки для production
|
||||
|
||||
## 🛠️ Утилиты
|
||||
|
||||
### `Makefile`
|
||||
**Удобные команды для работы с Docker.**
|
||||
- `make dev-up` - запуск dev
|
||||
- `make prod-up` - запуск prod
|
||||
- `make migrate` - миграции
|
||||
- `make backup` - backup БД
|
||||
- И многое другое
|
||||
|
||||
### `generate_secret_key.py`
|
||||
**Генератор Django SECRET_KEY.**
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
### `QUICKSTART.md`
|
||||
**Быстрый старт.**
|
||||
- Минимальные команды для запуска
|
||||
- Development и Production
|
||||
- Основные команды
|
||||
|
||||
### `DOCKER_README.md`
|
||||
**Подробная документация.**
|
||||
- Полное описание структуры
|
||||
- Все команды с примерами
|
||||
- Troubleshooting
|
||||
- Backup и восстановление
|
||||
|
||||
### `DOCKER_SETUP.md`
|
||||
**Полное руководство.**
|
||||
- Обзор всей системы
|
||||
- Конфигурация
|
||||
- Безопасность
|
||||
- Мониторинг
|
||||
|
||||
### `DEPLOYMENT_CHECKLIST.md`
|
||||
**Чеклист для деплоя.**
|
||||
- Пошаговая инструкция
|
||||
- Проверка безопасности
|
||||
- Настройка production
|
||||
- Troubleshooting
|
||||
|
||||
### `FILES_OVERVIEW.md`
|
||||
**Этот файл.**
|
||||
- Описание всех созданных файлов
|
||||
- Назначение каждого файла
|
||||
|
||||
## 📝 Обновленные файлы
|
||||
|
||||
### `.gitignore`
|
||||
**Обновлен для Docker.**
|
||||
- Исключает .env файлы
|
||||
- Исключает логи и backup
|
||||
- Исключает временные файлы
|
||||
|
||||
## 🎯 Как использовать
|
||||
|
||||
### Для начала работы:
|
||||
1. Прочитайте **QUICKSTART.md**
|
||||
2. Выберите окружение (dev или prod)
|
||||
3. Скопируйте соответствующий .env файл
|
||||
4. Запустите с помощью Makefile
|
||||
|
||||
### Для деплоя:
|
||||
1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
|
||||
2. Следуйте чеклисту пошагово
|
||||
3. Используйте **DOCKER_README.md** для справки
|
||||
|
||||
### Для настройки TileServer:
|
||||
1. Прочитайте **tiles/README.md**
|
||||
2. Добавьте .mbtiles файлы
|
||||
3. Настройте config.json (опционально)
|
||||
|
||||
## 📊 Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── Docker конфигурация
|
||||
│ ├── dbapp/Dockerfile
|
||||
│ ├── dbapp/entrypoint.sh
|
||||
│ ├── dbapp/.dockerignore
|
||||
│ ├── docker-compose.yaml
|
||||
│ └── docker-compose.prod.yaml
|
||||
│
|
||||
├── Nginx
|
||||
│ ├── nginx/conf.d/default.conf
|
||||
│ ├── nginx/conf.d/ssl.conf.example
|
||||
│ └── nginx/ssl/.gitkeep
|
||||
│
|
||||
├── TileServer GL
|
||||
│ ├── tiles/README.md
|
||||
│ ├── tiles/config.json.example
|
||||
│ └── tiles/.gitignore
|
||||
│
|
||||
├── Переменные окружения
|
||||
│ ├── .env.dev
|
||||
│ └── .env.prod
|
||||
│
|
||||
├── Утилиты
|
||||
│ ├── Makefile
|
||||
│ └── generate_secret_key.py
|
||||
│
|
||||
└── Документация
|
||||
├── QUICKSTART.md
|
||||
├── DOCKER_README.md
|
||||
├── DOCKER_SETUP.md
|
||||
├── DEPLOYMENT_CHECKLIST.md
|
||||
└── FILES_OVERVIEW.md
|
||||
```
|
||||
|
||||
## ✅ Что было сделано
|
||||
|
||||
1. ✅ Создан универсальный Dockerfile (один для dev и prod)
|
||||
2. ✅ Настроен entrypoint.sh с автоматической инициализацией
|
||||
3. ✅ Созданы docker-compose.yaml для dev и prod
|
||||
4. ✅ Настроен Nginx для production
|
||||
5. ✅ Добавлена поддержка TileServer GL
|
||||
6. ✅ Созданы .env файлы для разных окружений
|
||||
7. ✅ Добавлен Makefile с удобными командами
|
||||
8. ✅ Написана подробная документация
|
||||
9. ✅ Создан чеклист для деплоя
|
||||
10. ✅ Добавлены утилиты (генератор SECRET_KEY)
|
||||
|
||||
## 🚀 Следующие шаги
|
||||
|
||||
1. Запустите development окружение
|
||||
2. Протестируйте все функции
|
||||
3. Подготовьте production окружение
|
||||
4. Следуйте DEPLOYMENT_CHECKLIST.md
|
||||
5. Настройте мониторинг и backup
|
||||
|
||||
## 💡 Полезные ссылки
|
||||
|
||||
- Django Documentation: https://docs.djangoproject.com/
|
||||
- Docker Documentation: https://docs.docker.com/
|
||||
- PostGIS Documentation: https://postgis.net/documentation/
|
||||
- TileServer GL: https://github.com/maptiler/tileserver-gl
|
||||
- Nginx Documentation: https://nginx.org/en/docs/
|
||||
# Обзор созданных файлов Docker Setup
|
||||
|
||||
## 🐳 Docker файлы
|
||||
|
||||
### `dbapp/Dockerfile`
|
||||
**Универсальный Dockerfile** для dev и prod окружений.
|
||||
- Multi-stage build для оптимизации
|
||||
- Установка GDAL, PostGIS, PostgreSQL клиента
|
||||
- Использование uv для управления зависимостями
|
||||
- Non-root пользователь для безопасности
|
||||
- Healthcheck для мониторинга
|
||||
|
||||
### `dbapp/entrypoint.sh`
|
||||
**Скрипт инициализации контейнера.**
|
||||
- Ожидание готовности PostgreSQL
|
||||
- Автоматические миграции
|
||||
- Сбор статики (только prod)
|
||||
- Запуск runserver (dev) или Gunicorn (prod)
|
||||
|
||||
### `dbapp/.dockerignore`
|
||||
**Исключения для Docker build.**
|
||||
- Исключает ненужные файлы из образа
|
||||
- Уменьшает размер образа
|
||||
- Ускоряет сборку
|
||||
|
||||
## 🔧 Docker Compose файлы
|
||||
|
||||
### `docker-compose.yaml`
|
||||
**Development окружение.**
|
||||
- PostgreSQL с PostGIS
|
||||
- Django с development server
|
||||
- TileServer GL
|
||||
- Код монтируется как volume (live reload)
|
||||
- DEBUG=True
|
||||
|
||||
### `docker-compose.prod.yaml`
|
||||
**Production окружение.**
|
||||
- PostgreSQL с PostGIS
|
||||
- Django с Gunicorn
|
||||
- TileServer GL
|
||||
- Nginx reverse proxy
|
||||
- Код копируется в образ
|
||||
- DEBUG=False
|
||||
- Оптимизированные настройки
|
||||
|
||||
## 🌐 Nginx конфигурация
|
||||
|
||||
### `nginx/conf.d/default.conf`
|
||||
**HTTP конфигурация для production.**
|
||||
- Проксирование к Django
|
||||
- Раздача статики и медиа
|
||||
- Оптимизированные таймауты
|
||||
- Кэширование статики
|
||||
|
||||
### `nginx/conf.d/ssl.conf.example`
|
||||
**HTTPS конфигурация (пример).**
|
||||
- SSL/TLS настройки
|
||||
- Редирект с HTTP на HTTPS
|
||||
- Security headers
|
||||
- Оптимизированные SSL параметры
|
||||
|
||||
### `nginx/ssl/.gitkeep`
|
||||
**Директория для SSL сертификатов.**
|
||||
- Поместите сюда fullchain.pem и privkey.pem
|
||||
|
||||
## 🗺️ TileServer GL
|
||||
|
||||
### `tiles/README.md`
|
||||
**Инструкция по настройке TileServer GL.**
|
||||
- Как добавить тайлы
|
||||
- Примеры конфигурации
|
||||
- Использование в Django/Leaflet
|
||||
- Где взять тайлы
|
||||
|
||||
### `tiles/config.json.example`
|
||||
**Пример конфигурации TileServer GL.**
|
||||
- Настройки путей
|
||||
- Форматы и качество
|
||||
- Домены
|
||||
|
||||
### `tiles/.gitignore`
|
||||
**Исключения для git.**
|
||||
- Игнорирует большие .mbtiles файлы
|
||||
- Сохраняет примеры конфигурации
|
||||
|
||||
## 🔐 Переменные окружения
|
||||
|
||||
### `.env.dev`
|
||||
**Переменные для development.**
|
||||
- DEBUG=True
|
||||
- Простые пароли для локальной разработки
|
||||
- Настройки БД для dev
|
||||
|
||||
### `.env.prod`
|
||||
**Шаблон переменных для production.**
|
||||
- DEBUG=False
|
||||
- Требует изменения SECRET_KEY и паролей
|
||||
- Настройки для production
|
||||
|
||||
## 🛠️ Утилиты
|
||||
|
||||
### `Makefile`
|
||||
**Удобные команды для работы с Docker.**
|
||||
- `make dev-up` - запуск dev
|
||||
- `make prod-up` - запуск prod
|
||||
- `make migrate` - миграции
|
||||
- `make backup` - backup БД
|
||||
- И многое другое
|
||||
|
||||
### `generate_secret_key.py`
|
||||
**Генератор Django SECRET_KEY.**
|
||||
```bash
|
||||
python generate_secret_key.py
|
||||
```
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
### `QUICKSTART.md`
|
||||
**Быстрый старт.**
|
||||
- Минимальные команды для запуска
|
||||
- Development и Production
|
||||
- Основные команды
|
||||
|
||||
### `DOCKER_README.md`
|
||||
**Подробная документация.**
|
||||
- Полное описание структуры
|
||||
- Все команды с примерами
|
||||
- Troubleshooting
|
||||
- Backup и восстановление
|
||||
|
||||
### `DOCKER_SETUP.md`
|
||||
**Полное руководство.**
|
||||
- Обзор всей системы
|
||||
- Конфигурация
|
||||
- Безопасность
|
||||
- Мониторинг
|
||||
|
||||
### `DEPLOYMENT_CHECKLIST.md`
|
||||
**Чеклист для деплоя.**
|
||||
- Пошаговая инструкция
|
||||
- Проверка безопасности
|
||||
- Настройка production
|
||||
- Troubleshooting
|
||||
|
||||
### `FILES_OVERVIEW.md`
|
||||
**Этот файл.**
|
||||
- Описание всех созданных файлов
|
||||
- Назначение каждого файла
|
||||
|
||||
## 📝 Обновленные файлы
|
||||
|
||||
### `.gitignore`
|
||||
**Обновлен для Docker.**
|
||||
- Исключает .env файлы
|
||||
- Исключает логи и backup
|
||||
- Исключает временные файлы
|
||||
|
||||
## 🎯 Как использовать
|
||||
|
||||
### Для начала работы:
|
||||
1. Прочитайте **QUICKSTART.md**
|
||||
2. Выберите окружение (dev или prod)
|
||||
3. Скопируйте соответствующий .env файл
|
||||
4. Запустите с помощью Makefile
|
||||
|
||||
### Для деплоя:
|
||||
1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
|
||||
2. Следуйте чеклисту пошагово
|
||||
3. Используйте **DOCKER_README.md** для справки
|
||||
|
||||
### Для настройки TileServer:
|
||||
1. Прочитайте **tiles/README.md**
|
||||
2. Добавьте .mbtiles файлы
|
||||
3. Настройте config.json (опционально)
|
||||
|
||||
## 📊 Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── Docker конфигурация
|
||||
│ ├── dbapp/Dockerfile
|
||||
│ ├── dbapp/entrypoint.sh
|
||||
│ ├── dbapp/.dockerignore
|
||||
│ ├── docker-compose.yaml
|
||||
│ └── docker-compose.prod.yaml
|
||||
│
|
||||
├── Nginx
|
||||
│ ├── nginx/conf.d/default.conf
|
||||
│ ├── nginx/conf.d/ssl.conf.example
|
||||
│ └── nginx/ssl/.gitkeep
|
||||
│
|
||||
├── TileServer GL
|
||||
│ ├── tiles/README.md
|
||||
│ ├── tiles/config.json.example
|
||||
│ └── tiles/.gitignore
|
||||
│
|
||||
├── Переменные окружения
|
||||
│ ├── .env.dev
|
||||
│ └── .env.prod
|
||||
│
|
||||
├── Утилиты
|
||||
│ ├── Makefile
|
||||
│ └── generate_secret_key.py
|
||||
│
|
||||
└── Документация
|
||||
├── QUICKSTART.md
|
||||
├── DOCKER_README.md
|
||||
├── DOCKER_SETUP.md
|
||||
├── DEPLOYMENT_CHECKLIST.md
|
||||
└── FILES_OVERVIEW.md
|
||||
```
|
||||
|
||||
## ✅ Что было сделано
|
||||
|
||||
1. ✅ Создан универсальный Dockerfile (один для dev и prod)
|
||||
2. ✅ Настроен entrypoint.sh с автоматической инициализацией
|
||||
3. ✅ Созданы docker-compose.yaml для dev и prod
|
||||
4. ✅ Настроен Nginx для production
|
||||
5. ✅ Добавлена поддержка TileServer GL
|
||||
6. ✅ Созданы .env файлы для разных окружений
|
||||
7. ✅ Добавлен Makefile с удобными командами
|
||||
8. ✅ Написана подробная документация
|
||||
9. ✅ Создан чеклист для деплоя
|
||||
10. ✅ Добавлены утилиты (генератор SECRET_KEY)
|
||||
|
||||
## 🚀 Следующие шаги
|
||||
|
||||
1. Запустите development окружение
|
||||
2. Протестируйте все функции
|
||||
3. Подготовьте production окружение
|
||||
4. Следуйте DEPLOYMENT_CHECKLIST.md
|
||||
5. Настройте мониторинг и backup
|
||||
|
||||
## 💡 Полезные ссылки
|
||||
|
||||
- Django Documentation: https://docs.djangoproject.com/
|
||||
- Docker Documentation: https://docs.docker.com/
|
||||
- PostGIS Documentation: https://postgis.net/documentation/
|
||||
- TileServer GL: https://github.com/maptiler/tileserver-gl
|
||||
- Nginx Documentation: https://nginx.org/en/docs/
|
||||
|
||||
@@ -1,347 +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 в репозитории с описанием проблемы и логами
|
||||
# Руководство по установке асинхронной системы 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 в репозитории с описанием проблемы и логами
|
||||
|
||||
@@ -1,78 +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` (заменена карточка с картами)
|
||||
|
||||
## Примечания
|
||||
|
||||
- Процесс может занять продолжительное время в зависимости от количества выбранных спутников
|
||||
- Рекомендуется выбирать небольшое количество спутников для первого запуска
|
||||
- Существующие записи будут обновлены, новые - созданы
|
||||
- Все ошибки логируются и отображаются пользователю
|
||||
# Руководство по заполнению данных 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` (заменена карточка с картами)
|
||||
|
||||
## Примечания
|
||||
|
||||
- Процесс может занять продолжительное время в зависимости от количества выбранных спутников
|
||||
- Рекомендуется выбирать небольшое количество спутников для первого запуска
|
||||
- Существующие записи будут обновлены, новые - созданы
|
||||
- Все ошибки логируются и отображаются пользователю
|
||||
|
||||
198
Makefile
198
Makefile
@@ -1,99 +1,99 @@
|
||||
.PHONY: help dev-up dev-down dev-build dev-logs prod-up prod-down prod-build prod-logs shell migrate createsuperuser clean
|
||||
|
||||
help:
|
||||
@echo "Доступные команды:"
|
||||
@echo " make dev-up - Запустить development окружение"
|
||||
@echo " make dev-down - Остановить development окружение"
|
||||
@echo " make dev-build - Пересобрать development контейнеры"
|
||||
@echo " make dev-logs - Показать логи development"
|
||||
@echo " make prod-up - Запустить production окружение"
|
||||
@echo " make prod-down - Остановить production окружение"
|
||||
@echo " make prod-build - Пересобрать production контейнеры"
|
||||
@echo " make prod-logs - Показать логи production"
|
||||
@echo " make shell - Открыть Django shell"
|
||||
@echo " make migrate - Выполнить миграции"
|
||||
@echo " make createsuperuser - Создать суперпользователя"
|
||||
@echo " make clean - Удалить все контейнеры и volumes"
|
||||
|
||||
# Development команды
|
||||
dev-up:
|
||||
docker-compose up -d
|
||||
|
||||
dev-down:
|
||||
docker-compose down
|
||||
|
||||
dev-build:
|
||||
docker-compose up -d --build
|
||||
|
||||
dev-logs:
|
||||
docker-compose logs -f
|
||||
|
||||
dev-restart:
|
||||
docker-compose restart web
|
||||
|
||||
# Production команды
|
||||
prod-up:
|
||||
docker-compose -f docker-compose.prod.yaml up -d
|
||||
|
||||
prod-down:
|
||||
docker-compose -f docker-compose.prod.yaml down
|
||||
|
||||
prod-build:
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
prod-logs:
|
||||
docker-compose -f docker-compose.prod.yaml logs -f
|
||||
|
||||
prod-restart:
|
||||
docker-compose -f docker-compose.prod.yaml restart web
|
||||
|
||||
# Django команды (для development по умолчанию)
|
||||
shell:
|
||||
docker-compose exec web python manage.py shell
|
||||
|
||||
migrate:
|
||||
docker-compose exec web python manage.py migrate
|
||||
|
||||
makemigrations:
|
||||
docker-compose exec web python manage.py makemigrations
|
||||
|
||||
createsuperuser:
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
|
||||
collectstatic:
|
||||
docker-compose exec web python manage.py collectstatic --noinput
|
||||
|
||||
# Для production
|
||||
prod-shell:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py shell
|
||||
|
||||
prod-migrate:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||
|
||||
prod-createsuperuser:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# Backup и восстановление
|
||||
backup:
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
restore:
|
||||
@read -p "Введите имя файла backup: " file; \
|
||||
docker-compose exec -T db psql -U geralt geodb < $$file
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
docker-compose down -v
|
||||
docker system prune -f
|
||||
|
||||
clean-all:
|
||||
docker-compose down -v
|
||||
docker-compose -f docker-compose.prod.yaml down -v
|
||||
docker system prune -af --volumes
|
||||
|
||||
# Проверка статуса
|
||||
status:
|
||||
docker-compose ps
|
||||
|
||||
prod-status:
|
||||
docker-compose -f docker-compose.prod.yaml ps
|
||||
.PHONY: help dev-up dev-down dev-build dev-logs prod-up prod-down prod-build prod-logs shell migrate createsuperuser clean
|
||||
|
||||
help:
|
||||
@echo "Доступные команды:"
|
||||
@echo " make dev-up - Запустить development окружение"
|
||||
@echo " make dev-down - Остановить development окружение"
|
||||
@echo " make dev-build - Пересобрать development контейнеры"
|
||||
@echo " make dev-logs - Показать логи development"
|
||||
@echo " make prod-up - Запустить production окружение"
|
||||
@echo " make prod-down - Остановить production окружение"
|
||||
@echo " make prod-build - Пересобрать production контейнеры"
|
||||
@echo " make prod-logs - Показать логи production"
|
||||
@echo " make shell - Открыть Django shell"
|
||||
@echo " make migrate - Выполнить миграции"
|
||||
@echo " make createsuperuser - Создать суперпользователя"
|
||||
@echo " make clean - Удалить все контейнеры и volumes"
|
||||
|
||||
# Development команды
|
||||
dev-up:
|
||||
docker-compose up -d
|
||||
|
||||
dev-down:
|
||||
docker-compose down
|
||||
|
||||
dev-build:
|
||||
docker-compose up -d --build
|
||||
|
||||
dev-logs:
|
||||
docker-compose logs -f
|
||||
|
||||
dev-restart:
|
||||
docker-compose restart web
|
||||
|
||||
# Production команды
|
||||
prod-up:
|
||||
docker-compose -f docker-compose.prod.yaml up -d
|
||||
|
||||
prod-down:
|
||||
docker-compose -f docker-compose.prod.yaml down
|
||||
|
||||
prod-build:
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
prod-logs:
|
||||
docker-compose -f docker-compose.prod.yaml logs -f
|
||||
|
||||
prod-restart:
|
||||
docker-compose -f docker-compose.prod.yaml restart web
|
||||
|
||||
# Django команды (для development по умолчанию)
|
||||
shell:
|
||||
docker-compose exec web python manage.py shell
|
||||
|
||||
migrate:
|
||||
docker-compose exec web python manage.py migrate
|
||||
|
||||
makemigrations:
|
||||
docker-compose exec web python manage.py makemigrations
|
||||
|
||||
createsuperuser:
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
|
||||
collectstatic:
|
||||
docker-compose exec web python manage.py collectstatic --noinput
|
||||
|
||||
# Для production
|
||||
prod-shell:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py shell
|
||||
|
||||
prod-migrate:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||
|
||||
prod-createsuperuser:
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# Backup и восстановление
|
||||
backup:
|
||||
docker-compose exec db pg_dump -U geralt geodb > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
restore:
|
||||
@read -p "Введите имя файла backup: " file; \
|
||||
docker-compose exec -T db psql -U geralt geodb < $$file
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
docker-compose down -v
|
||||
docker system prune -f
|
||||
|
||||
clean-all:
|
||||
docker-compose down -v
|
||||
docker-compose -f docker-compose.prod.yaml down -v
|
||||
docker system prune -af --volumes
|
||||
|
||||
# Проверка статуса
|
||||
status:
|
||||
docker-compose ps
|
||||
|
||||
prod-status:
|
||||
docker-compose -f docker-compose.prod.yaml ps
|
||||
|
||||
212
QUICKSTART.md
212
QUICKSTART.md
@@ -1,106 +1,106 @@
|
||||
# Быстрый старт с Docker
|
||||
|
||||
## Development (разработка)
|
||||
|
||||
```bash
|
||||
# 1. Скопировать переменные окружения
|
||||
cp .env.dev .env
|
||||
|
||||
# 2. Запустить контейнеры
|
||||
make dev-up
|
||||
# или
|
||||
docker-compose up -d --build
|
||||
|
||||
# 3. Создать суперпользователя
|
||||
make createsuperuser
|
||||
# или
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
|
||||
# 4. Открыть в браузере
|
||||
# Django: http://localhost:8000
|
||||
# Admin: http://localhost:8000/admin
|
||||
# TileServer: http://localhost:8080
|
||||
```
|
||||
|
||||
## Production (продакшн)
|
||||
|
||||
```bash
|
||||
# 1. Скопировать и настроить переменные
|
||||
cp .env.prod .env
|
||||
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
|
||||
|
||||
# 2. Запустить контейнеры
|
||||
make prod-up
|
||||
# или
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
# 3. Создать суперпользователя
|
||||
make prod-createsuperuser
|
||||
# или
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# 4. Открыть в браузере
|
||||
# Nginx: http://localhost
|
||||
# Django: http://localhost:8000
|
||||
# TileServer: http://localhost:8080
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Просмотр логов
|
||||
make dev-logs # development
|
||||
make prod-logs # production
|
||||
|
||||
# Остановка
|
||||
make dev-down # development
|
||||
make prod-down # production
|
||||
|
||||
# Перезапуск после изменений
|
||||
make dev-build # development
|
||||
make prod-build # production
|
||||
|
||||
# Django shell
|
||||
make shell # development
|
||||
make prod-shell # production
|
||||
|
||||
# Миграции
|
||||
make migrate # development
|
||||
make prod-migrate # production
|
||||
|
||||
# Backup БД
|
||||
make backup
|
||||
|
||||
# Статус контейнеров
|
||||
make status # development
|
||||
make prod-status # production
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт запуска
|
||||
│ ├── manage.py
|
||||
│ └── ...
|
||||
├── nginx/ # Nginx (только prod)
|
||||
│ └── conf.d/
|
||||
│ └── default.conf
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
│ ├── README.md
|
||||
│ └── config.json.example
|
||||
├── docker-compose.yaml # Development
|
||||
├── docker-compose.prod.yaml # Production
|
||||
├── .env.dev # Переменные dev
|
||||
├── .env.prod # Переменные prod
|
||||
├── Makefile # Команды для удобства
|
||||
└── DOCKER_README.md # Подробная документация
|
||||
```
|
||||
|
||||
## Что дальше?
|
||||
|
||||
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
|
||||
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
|
||||
3. Для production настройте SSL сертификаты в `nginx/ssl/`
|
||||
# Быстрый старт с Docker
|
||||
|
||||
## Development (разработка)
|
||||
|
||||
```bash
|
||||
# 1. Скопировать переменные окружения
|
||||
cp .env.dev .env
|
||||
|
||||
# 2. Запустить контейнеры
|
||||
make dev-up
|
||||
# или
|
||||
docker-compose up -d --build
|
||||
|
||||
# 3. Создать суперпользователя
|
||||
make createsuperuser
|
||||
# или
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
|
||||
# 4. Открыть в браузере
|
||||
# Django: http://localhost:8000
|
||||
# Admin: http://localhost:8000/admin
|
||||
# TileServer: http://localhost:8080
|
||||
```
|
||||
|
||||
## Production (продакшн)
|
||||
|
||||
```bash
|
||||
# 1. Скопировать и настроить переменные
|
||||
cp .env.prod .env
|
||||
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
|
||||
|
||||
# 2. Запустить контейнеры
|
||||
make prod-up
|
||||
# или
|
||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||
|
||||
# 3. Создать суперпользователя
|
||||
make prod-createsuperuser
|
||||
# или
|
||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||
|
||||
# 4. Открыть в браузере
|
||||
# Nginx: http://localhost
|
||||
# Django: http://localhost:8000
|
||||
# TileServer: http://localhost:8080
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Просмотр логов
|
||||
make dev-logs # development
|
||||
make prod-logs # production
|
||||
|
||||
# Остановка
|
||||
make dev-down # development
|
||||
make prod-down # production
|
||||
|
||||
# Перезапуск после изменений
|
||||
make dev-build # development
|
||||
make prod-build # production
|
||||
|
||||
# Django shell
|
||||
make shell # development
|
||||
make prod-shell # production
|
||||
|
||||
# Миграции
|
||||
make migrate # development
|
||||
make prod-migrate # production
|
||||
|
||||
# Backup БД
|
||||
make backup
|
||||
|
||||
# Статус контейнеров
|
||||
make status # development
|
||||
make prod-status # production
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── dbapp/ # Django приложение
|
||||
│ ├── Dockerfile # Универсальный Dockerfile
|
||||
│ ├── entrypoint.sh # Скрипт запуска
|
||||
│ ├── manage.py
|
||||
│ └── ...
|
||||
├── nginx/ # Nginx (только prod)
|
||||
│ └── conf.d/
|
||||
│ └── default.conf
|
||||
├── tiles/ # Тайлы для TileServer GL
|
||||
│ ├── README.md
|
||||
│ └── config.json.example
|
||||
├── docker-compose.yaml # Development
|
||||
├── docker-compose.prod.yaml # Production
|
||||
├── .env.dev # Переменные dev
|
||||
├── .env.prod # Переменные prod
|
||||
├── Makefile # Команды для удобства
|
||||
└── DOCKER_README.md # Подробная документация
|
||||
```
|
||||
|
||||
## Что дальше?
|
||||
|
||||
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
|
||||
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
|
||||
3. Для production настройте SSL сертификаты в `nginx/ssl/`
|
||||
|
||||
@@ -1,117 +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 на наличие ошибок
|
||||
- Обновите страницу
|
||||
# Быстрый старт: Асинхронное заполнение данных 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 на наличие ошибок
|
||||
- Обновите страницу
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
/staticfiles/
|
||||
/media/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yaml
|
||||
.dockerignore
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
/staticfiles/
|
||||
/media/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yaml
|
||||
.dockerignore
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Production environment variables
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=123456
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
# Production environment variables
|
||||
DEBUG=False
|
||||
ENVIRONMENT=production
|
||||
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
|
||||
DB_NAME=geodb
|
||||
DB_USER=geralt
|
||||
DB_PASSWORD=123456
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
ALLOWED_HOSTS=localhost,yourdomain.com
|
||||
1
dbapp/.python-version
Normal file
1
dbapp/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13.7
|
||||
217
dbapp/CELERY_SETUP.md
Normal file
217
dbapp/CELERY_SETUP.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Celery Setup and Testing Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have Redis running (it's already configured in your docker-compose.yaml):
|
||||
|
||||
```bash
|
||||
# Start Redis and other services
|
||||
cd /home/vesemir/DataStorage
|
||||
docker-compose up -d redis
|
||||
```
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
Since we're using django-celery-results and celery-beat, you need to run migrations:
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
This will create the necessary tables for storing Celery results and managing periodic tasks.
|
||||
|
||||
## Running Celery
|
||||
|
||||
### 1. Start Celery Worker
|
||||
|
||||
```bash
|
||||
# From the dbapp directory
|
||||
cd /home/vesemir/DataStorage/dbapp
|
||||
|
||||
# Run with development settings
|
||||
python -m celery -A dbapp worker --loglevel=info
|
||||
|
||||
# Or with environment variable
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development celery -A dbapp worker --loglevel=info
|
||||
```
|
||||
|
||||
### 2. Start Celery Beat (for periodic tasks)
|
||||
|
||||
```bash
|
||||
# From the dbapp directory
|
||||
cd /home/vesemir/DataStorage/dbapp
|
||||
|
||||
# Run with development settings
|
||||
python -m celery -A dbapp beat --loglevel=info
|
||||
|
||||
# Or with environment variable
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development celery -A dbapp beat --loglevel=info
|
||||
```
|
||||
|
||||
### 3. Start Flower (Optional - for monitoring)
|
||||
|
||||
```bash
|
||||
# Install flower if not already installed
|
||||
pip install flower
|
||||
|
||||
# Run flower to monitor tasks
|
||||
celery -A dbapp flower
|
||||
```
|
||||
|
||||
## Testing Celery
|
||||
|
||||
### Method 1: Using Django Shell
|
||||
|
||||
```bash
|
||||
cd /home/vesemir/DataStorage/dbapp
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
# In the Django shell
|
||||
from mainapp.tasks import test_celery_connection, add_numbers
|
||||
from lyngsatapp.tasks import fill_lyngsat_data_task
|
||||
|
||||
# Test simple connection
|
||||
result = test_celery_connection.delay("Test message!")
|
||||
print(result.id) # Task ID
|
||||
print(result.get(timeout=10)) # Wait for result and print
|
||||
|
||||
# Test addition
|
||||
result = add_numbers.delay(5, 7)
|
||||
print(result.get(timeout=10))
|
||||
|
||||
# Check task state
|
||||
print(result.state) # Should be 'SUCCESS'
|
||||
print(result.ready()) # Should be True
|
||||
print(result.successful()) # Should be True
|
||||
```
|
||||
|
||||
### Method 2: Using Django Management Command
|
||||
|
||||
Create a management command to test:
|
||||
|
||||
```bash
|
||||
mkdir -p dbapp/management/commands
|
||||
```
|
||||
|
||||
Create `/home/vesemir/DataStorage/dbapp/dbapp/management/commands/test_celery.py`:
|
||||
|
||||
```python
|
||||
from django.core.management.base import BaseCommand
|
||||
from mainapp.tasks import test_celery_connection, add_numbers
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Test Celery functionality'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Testing Celery connection...')
|
||||
|
||||
# Test simple task
|
||||
result = test_celery_connection.delay("Hello from test command!")
|
||||
self.stdout.write(f'Task ID: {result.id}')
|
||||
|
||||
# Wait for result
|
||||
task_result = result.get(timeout=10)
|
||||
self.stdout.write(self.style.SUCCESS(f'Task result: {task_result}'))
|
||||
|
||||
# Test math task
|
||||
math_result = add_numbers.delay(10, 20)
|
||||
sum_result = math_result.get(timeout=10)
|
||||
self.stdout.write(self.style.SUCCESS(f'10 + 20 = {sum_result}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('All tests passed!'))
|
||||
```
|
||||
|
||||
Then run:
|
||||
```bash
|
||||
python manage.py test_celery
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Error with Redis**: Make sure Redis is running
|
||||
```bash
|
||||
docker-compose up -d redis
|
||||
```
|
||||
|
||||
2. **Module Not Found Errors**: Ensure all dependencies are installed
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Settings Module Error**: Make sure DJANGO_SETTINGS_MODULE is set properly
|
||||
```bash
|
||||
export DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||
```
|
||||
|
||||
4. **Database Tables Missing**: Run migrations
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Check if Celery can connect to Redis:
|
||||
|
||||
```bash
|
||||
# Test Redis connection
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
Check Celery configuration:
|
||||
```python
|
||||
# In Django shell
|
||||
from django.conf import settings
|
||||
print(settings.CELERY_BROKER_URL)
|
||||
print(settings.CELERY_RESULT_BACKEND)
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Make sure your `.env` file contains:
|
||||
|
||||
```
|
||||
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||
```
|
||||
|
||||
## Running in Production
|
||||
|
||||
For production, ensure you have:
|
||||
|
||||
1. A production Redis instance
|
||||
2. Proper security settings
|
||||
3. Daemonized Celery workers
|
||||
|
||||
Example systemd service file for Celery worker (save as `/etc/systemd/system/celery.service`):
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Celery Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=www-data
|
||||
Group=www-data
|
||||
EnvironmentFile=/path/to/your/.env
|
||||
WorkingDirectory=/home/vesemir/DataStorage/dbapp
|
||||
ExecStart=/path/to/your/venv/bin/celery -A dbapp worker --loglevel=info --pidfile=/var/run/celery/worker.pid --logfile=/var/log/celery/worker.log
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
KillSignal=SIGTERM
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
114
dbapp/Dockerfile
114
dbapp/Dockerfile
@@ -1,57 +1,57 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gdal-bin \
|
||||
libgdal-dev \
|
||||
proj-bin \
|
||||
proj-data \
|
||||
libproj-dev \
|
||||
libproj25 \
|
||||
libgeos-dev \
|
||||
libgeos-c1v5 \
|
||||
build-essential \
|
||||
postgresql-client \
|
||||
libpq-dev \
|
||||
libpq5 \
|
||||
netcat-openbsd \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Upgrade pip
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# Copy requirements file
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /app/staticfiles /app/logs /app/media
|
||||
|
||||
# Set permissions for entrypoint
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd --create-home --shell /bin/bash app && \
|
||||
chown -R app:app /app
|
||||
|
||||
USER app
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gdal-bin \
|
||||
libgdal-dev \
|
||||
proj-bin \
|
||||
proj-data \
|
||||
libproj-dev \
|
||||
libproj25 \
|
||||
libgeos-dev \
|
||||
libgeos-c1v5 \
|
||||
build-essential \
|
||||
postgresql-client \
|
||||
libpq-dev \
|
||||
libpq5 \
|
||||
netcat-openbsd \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Upgrade pip
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# Copy requirements file
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /app/staticfiles /app/logs /app/media
|
||||
|
||||
# Set permissions for entrypoint
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd --create-home --shell /bin/bash app && \
|
||||
chown -R app:app /app
|
||||
|
||||
USER app
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# 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
|
||||
# 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:
|
||||
pass
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
ASGI config for dbapp project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
"""
|
||||
ASGI config for dbapp project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
|
||||
@@ -4,8 +4,8 @@ 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')
|
||||
# Use the environment variable to determine the settings module
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.getenv('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development'))
|
||||
|
||||
app = Celery('dbapp')
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
"""
|
||||
Settings module initialization.
|
||||
|
||||
Automatically determines the environment and loads appropriate settings.
|
||||
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
|
||||
Defaults to 'development' if not set.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Determine the environment from DJANGO_ENVIRONMENT variable
|
||||
# Defaults to 'development' for safety
|
||||
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
|
||||
|
||||
if ENVIRONMENT == 'production':
|
||||
from .production import *
|
||||
print("Loading production settings...")
|
||||
else:
|
||||
from .development import *
|
||||
"""
|
||||
Settings module initialization.
|
||||
|
||||
Automatically determines the environment and loads appropriate settings.
|
||||
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
|
||||
Defaults to 'development' if not set.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Determine the environment from DJANGO_ENVIRONMENT variable
|
||||
# Defaults to 'development' for safety
|
||||
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
|
||||
|
||||
if ENVIRONMENT == 'production':
|
||||
from .production import *
|
||||
print("Loading production settings...")
|
||||
else:
|
||||
from .development import *
|
||||
print("Loading development settings...")
|
||||
@@ -73,22 +73,13 @@ INSTALLED_APPS = [
|
||||
"django_admin_multiple_choice_list_filter",
|
||||
"more_admin_filters",
|
||||
"import_export",
|
||||
"django_celery_results",
|
||||
# Project apps
|
||||
"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'
|
||||
|
||||
# ============================================================================
|
||||
@@ -240,7 +231,7 @@ LEAFLET_CONFIG = {
|
||||
|
||||
# Celery Configuration Options
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||
CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_RESULT_BACKEND = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") # Use Redis for results
|
||||
CELERY_CACHE_BACKEND = "default"
|
||||
|
||||
# Celery Task Configuration
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
"""
|
||||
Development-specific settings.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# ============================================================================
|
||||
# ALLOWED HOSTS
|
||||
# ============================================================================
|
||||
|
||||
# Allow all hosts in development
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# ============================================================================
|
||||
# INSTALLED APPS - Development additions
|
||||
# ============================================================================
|
||||
|
||||
INSTALLED_APPS += [
|
||||
'debug_toolbar',
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# MIDDLEWARE - Development additions
|
||||
# ============================================================================
|
||||
|
||||
# Add debug toolbar middleware at the beginning
|
||||
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG TOOLBAR CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
INTERNAL_IPS = [
|
||||
'127.0.0.1',
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# EMAIL CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Use console backend for development
|
||||
"""
|
||||
Development-specific settings.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# ============================================================================
|
||||
# ALLOWED HOSTS
|
||||
# ============================================================================
|
||||
|
||||
# Allow all hosts in development
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# ============================================================================
|
||||
# INSTALLED APPS - Development additions
|
||||
# ============================================================================
|
||||
|
||||
INSTALLED_APPS += [
|
||||
'debug_toolbar',
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# MIDDLEWARE - Development additions
|
||||
# ============================================================================
|
||||
|
||||
# Add debug toolbar middleware at the beginning
|
||||
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG TOOLBAR CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
INTERNAL_IPS = [
|
||||
'127.0.0.1',
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# EMAIL CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Use console backend for development
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
@@ -1,135 +1,135 @@
|
||||
"""
|
||||
Production-specific settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from .base import *
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# ============================================================================
|
||||
# ALLOWED HOSTS
|
||||
# ============================================================================
|
||||
|
||||
# In production, specify allowed hosts explicitly from environment variable
|
||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
|
||||
# ============================================================================
|
||||
# SECURITY SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
# SSL/HTTPS settings
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Security headers
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
|
||||
# HSTS settings
|
||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
# Additional security settings
|
||||
SECURE_REDIRECT_EXEMPT = []
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# ============================================================================
|
||||
# TEMPLATE CACHING
|
||||
# ============================================================================
|
||||
|
||||
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",
|
||||
],
|
||||
"loaders": [
|
||||
(
|
||||
"django.template.loaders.cached.Loader",
|
||||
[
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# STATIC FILES CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
||||
|
||||
# ============================================================================
|
||||
# LOGGING CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"simple": {
|
||||
"format": "{levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"require_debug_false": {
|
||||
"()": "django.utils.log.RequireDebugFalse",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
},
|
||||
"file": {
|
||||
"level": "ERROR",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
"mail_admins": {
|
||||
"level": "ERROR",
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
"filters": ["require_debug_false"],
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console", "file"],
|
||||
"level": "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
"django.request": {
|
||||
"handlers": ["mail_admins", "file"],
|
||||
"level": "ERROR",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
"""
|
||||
Production-specific settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from .base import *
|
||||
|
||||
# ============================================================================
|
||||
# DEBUG CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# ============================================================================
|
||||
# ALLOWED HOSTS
|
||||
# ============================================================================
|
||||
|
||||
# In production, specify allowed hosts explicitly from environment variable
|
||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
|
||||
# ============================================================================
|
||||
# SECURITY SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
# SSL/HTTPS settings
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Security headers
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
|
||||
# HSTS settings
|
||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
# Additional security settings
|
||||
SECURE_REDIRECT_EXEMPT = []
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# ============================================================================
|
||||
# TEMPLATE CACHING
|
||||
# ============================================================================
|
||||
|
||||
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",
|
||||
],
|
||||
"loaders": [
|
||||
(
|
||||
"django.template.loaders.cached.Loader",
|
||||
[
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# STATIC FILES CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
||||
|
||||
# ============================================================================
|
||||
# LOGGING CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"simple": {
|
||||
"format": "{levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"require_debug_false": {
|
||||
"()": "django.utils.log.RequireDebugFalse",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
},
|
||||
"file": {
|
||||
"level": "ERROR",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
"mail_admins": {
|
||||
"level": "ERROR",
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
"filters": ["require_debug_false"],
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console", "file"],
|
||||
"level": "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
"django.request": {
|
||||
"handlers": ["mail_admins", "file"],
|
||||
"level": "ERROR",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
"""
|
||||
URL configuration for dbapp project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from mainapp import views
|
||||
from django.contrib.auth import views as auth_views
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls, name='admin'),
|
||||
path('', include('mainapp.urls', namespace='mainapp')),
|
||||
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||
# Authentication URLs
|
||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||
path('logout/', views.custom_logout, name='logout'),
|
||||
] + debug_toolbar_urls()
|
||||
"""
|
||||
URL configuration for dbapp project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from mainapp import views
|
||||
from django.contrib.auth import views as auth_views
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls, name='admin'),
|
||||
path('', include('mainapp.urls', namespace='mainapp')),
|
||||
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||
# Authentication URLs
|
||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||
path('logout/', views.custom_logout, name='logout'),
|
||||
] + debug_toolbar_urls()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
WSGI config for dbapp project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
"""
|
||||
WSGI config for dbapp project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Определяем окружение (по умолчанию production)
|
||||
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||
|
||||
echo "Starting in $ENVIRONMENT mode..."
|
||||
|
||||
# Ждем PostgreSQL
|
||||
echo "Waiting for PostgreSQL..."
|
||||
while ! nc -z $DB_HOST $DB_PORT; do
|
||||
sleep 0.1
|
||||
done
|
||||
echo "PostgreSQL started"
|
||||
|
||||
# Выполняем миграции
|
||||
echo "Running migrations..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
# Собираем статику (только для production)
|
||||
if [ "$ENVIRONMENT" = "production" ]; then
|
||||
echo "Collecting static files..."
|
||||
python manage.py collectstatic --noinput
|
||||
fi
|
||||
|
||||
# Запускаем сервер в зависимости от окружения
|
||||
if [ "$ENVIRONMENT" = "development" ]; then
|
||||
echo "Starting Django development server..."
|
||||
exec python manage.py runserver 0.0.0.0:8000
|
||||
else
|
||||
echo "Starting Gunicorn..."
|
||||
exec gunicorn --bind 0.0.0.0:8000 \
|
||||
--workers ${GUNICORN_WORKERS:-3} \
|
||||
--timeout ${GUNICORN_TIMEOUT:-120} \
|
||||
--reload \
|
||||
dbapp.wsgi:application
|
||||
fi
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Определяем окружение (по умолчанию production)
|
||||
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||
|
||||
echo "Starting in $ENVIRONMENT mode..."
|
||||
|
||||
# Ждем PostgreSQL
|
||||
echo "Waiting for PostgreSQL..."
|
||||
while ! nc -z $DB_HOST $DB_PORT; do
|
||||
sleep 0.1
|
||||
done
|
||||
echo "PostgreSQL started"
|
||||
|
||||
# Выполняем миграции
|
||||
echo "Running migrations..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
# Собираем статику (только для production)
|
||||
if [ "$ENVIRONMENT" = "production" ]; then
|
||||
echo "Collecting static files..."
|
||||
python manage.py collectstatic --noinput
|
||||
fi
|
||||
|
||||
# Запускаем сервер в зависимости от окружения
|
||||
if [ "$ENVIRONMENT" = "development" ]; then
|
||||
echo "Starting Django development server..."
|
||||
exec python manage.py runserver 0.0.0.0:8000
|
||||
else
|
||||
echo "Starting Gunicorn..."
|
||||
exec gunicorn --bind 0.0.0.0:8000 \
|
||||
--workers ${GUNICORN_WORKERS:-3} \
|
||||
--timeout ${GUNICORN_TIMEOUT:-120} \
|
||||
--reload \
|
||||
dbapp.wsgi:application
|
||||
fi
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from .models import LyngSat
|
||||
|
||||
@admin.register(LyngSat)
|
||||
class LyngSatAdmin(admin.ModelAdmin):
|
||||
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",)
|
||||
from django.contrib import admin
|
||||
from .models import LyngSat
|
||||
|
||||
@admin.register(LyngSat)
|
||||
class LyngSatAdmin(admin.ModelAdmin):
|
||||
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",)
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LyngsatappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'lyngsatapp'
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LyngsatappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'lyngsatapp'
|
||||
|
||||
@@ -1,37 +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',
|
||||
},
|
||||
),
|
||||
]
|
||||
# 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-11 13:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lyngsatapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lyngsat',
|
||||
name='last_update',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления'),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +1,37 @@
|
||||
from django.db import models
|
||||
from mainapp.models import (
|
||||
Satellite,
|
||||
Polarization,
|
||||
Modulation,
|
||||
Standard,
|
||||
get_default_polarization,
|
||||
get_default_modulation,
|
||||
get_default_standard
|
||||
)
|
||||
|
||||
class LyngSat(models.Model):
|
||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
|
||||
polarization = models.ForeignKey(
|
||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
|
||||
)
|
||||
modulation = models.ForeignKey(
|
||||
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
|
||||
)
|
||||
standard = models.ForeignKey(
|
||||
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
|
||||
)
|
||||
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
|
||||
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
||||
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Время")
|
||||
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
|
||||
fec = models.CharField(max_length=30, blank=True, null=True, verbose_name="Коэффициент коррекции ошибок")
|
||||
url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
|
||||
|
||||
def __str__(self):
|
||||
return f"Ист {self.frequency}, {self.polarization}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Источник LyngSat"
|
||||
verbose_name_plural = "Источники LyngSat"
|
||||
|
||||
|
||||
from django.db import models
|
||||
from mainapp.models import (
|
||||
Satellite,
|
||||
Polarization,
|
||||
Modulation,
|
||||
Standard,
|
||||
get_default_polarization,
|
||||
get_default_modulation,
|
||||
get_default_standard
|
||||
)
|
||||
|
||||
class LyngSat(models.Model):
|
||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
|
||||
polarization = models.ForeignKey(
|
||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
|
||||
)
|
||||
modulation = models.ForeignKey(
|
||||
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
|
||||
)
|
||||
standard = models.ForeignKey(
|
||||
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
|
||||
)
|
||||
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
|
||||
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
||||
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Дата посленего обновления")
|
||||
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
|
||||
fec = models.CharField(max_length=30, blank=True, null=True, verbose_name="Коэффициент коррекции ошибок")
|
||||
url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
|
||||
|
||||
def __str__(self):
|
||||
return f"Ист {self.frequency}, {self.polarization}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Источник LyngSat"
|
||||
verbose_name_plural = "Источники LyngSat"
|
||||
|
||||
|
||||
|
||||
@@ -1,405 +1,437 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
class LyngSatParser:
|
||||
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
|
||||
|
||||
def __init__(
|
||||
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.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,
|
||||
}
|
||||
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()]
|
||||
else:
|
||||
core = normalized
|
||||
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
|
||||
modulation = None
|
||||
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) :]
|
||||
symbol_rate = None
|
||||
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,
|
||||
}
|
||||
|
||||
def extract_date(self, s: str) -> datetime | None:
|
||||
s = s.strip()
|
||||
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()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_polarization(self, polarization: str) -> str:
|
||||
"""Преобразовать код поляризации в понятное название на русском"""
|
||||
polarization_map = {
|
||||
"V": "Вертикальная",
|
||||
"H": "Горизонтальная",
|
||||
"R": "Правая",
|
||||
"L": "Левая",
|
||||
}
|
||||
return polarization_map.get(polarization.upper(), polarization)
|
||||
|
||||
def get_region_pages(self, regions: list[str] | None = None) -> list[str]:
|
||||
html_regions = []
|
||||
if regions is None:
|
||||
regions = self.regions
|
||||
for region in regions:
|
||||
url = f"{self.BASE_URL}/{region}.html"
|
||||
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
|
||||
response = requests.post(self.flaresolver_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
continue
|
||||
html_content = response.json().get("solution", {}).get("response", "")
|
||||
html_regions.append(html_content)
|
||||
print(f"Обработал страницу по {region}")
|
||||
return html_regions
|
||||
|
||||
def get_satellite_urls(self, html_regions: list[str]):
|
||||
sat_names = []
|
||||
sat_urls = []
|
||||
for region_page in html_regions:
|
||||
soup = BeautifulSoup(region_page, "html.parser")
|
||||
|
||||
col_table = soup.find_all("div", class_="desktab")[0]
|
||||
|
||||
tables = col_table.find_next_sibling("table").find_all("table")
|
||||
trs = []
|
||||
for table in tables:
|
||||
trs.extend(table.find_all("tr"))
|
||||
for tr in trs:
|
||||
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"]
|
||||
except IndexError:
|
||||
sat_url = tr.find_all("a")[0]["href"]
|
||||
sat_names.append(sat_name)
|
||||
sat_urls.append(sat_url)
|
||||
return sat_names, sat_urls
|
||||
|
||||
def get_satellites_data(self) -> dict[dict]:
|
||||
sat_data = {}
|
||||
for region_page in self.get_region_pages(self.regions):
|
||||
soup = BeautifulSoup(region_page, "html.parser")
|
||||
|
||||
col_table = soup.find_all("div", class_="desktab")[0]
|
||||
|
||||
tables = col_table.find_next_sibling("table").find_all("table")
|
||||
trs = []
|
||||
for table in tables:
|
||||
trs.extend(table.find_all("tr"))
|
||||
for tr in trs:
|
||||
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"]
|
||||
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_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,
|
||||
}
|
||||
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")
|
||||
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")
|
||||
for idx, tr in enumerate(trs):
|
||||
tds = tr.find_all("td")
|
||||
if len(tds) < 9 or idx < 2:
|
||||
continue
|
||||
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,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class KingOfSatParser:
|
||||
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
|
||||
"""
|
||||
Инициализация парсера
|
||||
:param base_url: Базовый URL сайта
|
||||
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
|
||||
"""
|
||||
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"
|
||||
}
|
||||
)
|
||||
|
||||
def convert_polarization(self, polarization):
|
||||
"""Преобразовать код поляризации в понятное название на русском"""
|
||||
polarization_map = {
|
||||
"V": "Вертикальная",
|
||||
"H": "Горизонтальная",
|
||||
"R": "Правая",
|
||||
"L": "Левая",
|
||||
}
|
||||
return polarization_map.get(polarization.upper(), polarization)
|
||||
|
||||
def fetch_page(self, url):
|
||||
"""Получить HTML страницу"""
|
||||
try:
|
||||
response = self.session.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении страницы {url}: {e}")
|
||||
return None
|
||||
|
||||
def parse_satellite_table(self, html_content):
|
||||
"""Распарсить таблицу со спутниками"""
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
satellites = []
|
||||
table = soup.find("table")
|
||||
if not table:
|
||||
print("Таблица не найдена")
|
||||
return satellites
|
||||
|
||||
rows = table.find_all("tr")[1:]
|
||||
|
||||
for row in rows:
|
||||
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)
|
||||
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()
|
||||
|
||||
# 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)
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при обработке строки таблицы: {e}")
|
||||
continue
|
||||
|
||||
return satellites
|
||||
|
||||
def parse_ini_file(self, ini_content):
|
||||
"""Распарсить содержимое .ini файла"""
|
||||
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)
|
||||
if sattype_match:
|
||||
sattype_content = sattype_match.group(1).strip()
|
||||
for line in sattype_content.split("\n"):
|
||||
line = line.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)
|
||||
if dvb_match:
|
||||
dvb_content = dvb_match.group(1).strip()
|
||||
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 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 "",
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
def download_ini_file(self, url):
|
||||
"""Скачать содержимое .ini файла"""
|
||||
try:
|
||||
response = self.session.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
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")
|
||||
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]
|
||||
|
||||
results = []
|
||||
processed_count = 0
|
||||
|
||||
for satellite in satellites:
|
||||
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
results.append(result)
|
||||
processed_count += 1
|
||||
|
||||
if self.max_satellites > 0 and processed_count >= self.max_satellites:
|
||||
break
|
||||
|
||||
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"],
|
||||
}
|
||||
|
||||
return satellite_dict
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
import re
|
||||
import time
|
||||
|
||||
def parse_satellite_names(satellite_string: str) -> list[str]:
|
||||
slash_parts = [part.strip() for part in satellite_string.split('/')]
|
||||
all_names = []
|
||||
for part in slash_parts:
|
||||
main_match = re.match(r'^([^(]+)', part)
|
||||
if main_match:
|
||||
main_name = main_match.group(1).strip()
|
||||
if main_name:
|
||||
all_names.append(main_name)
|
||||
bracket_match = re.search(r'\(([^)]+)\)', part)
|
||||
if bracket_match:
|
||||
bracket_name = bracket_match.group(1).strip()
|
||||
if bracket_name:
|
||||
all_names.append(bracket_name)
|
||||
seen = set()
|
||||
result = []
|
||||
for name in all_names:
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
result.append(name.strip().lower())
|
||||
return result
|
||||
|
||||
|
||||
class LyngSatParser:
|
||||
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
|
||||
|
||||
def __init__(
|
||||
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.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,
|
||||
}
|
||||
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()]
|
||||
else:
|
||||
core = normalized
|
||||
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
|
||||
modulation = None
|
||||
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) :]
|
||||
symbol_rate = None
|
||||
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,
|
||||
}
|
||||
|
||||
def extract_date(self, s: str) -> datetime | None:
|
||||
s = s.strip()
|
||||
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()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_polarization(self, polarization: str) -> str:
|
||||
"""Преобразовать код поляризации в понятное название на русском"""
|
||||
polarization_map = {
|
||||
"V": "Вертикальная",
|
||||
"H": "Горизонтальная",
|
||||
"R": "Правая",
|
||||
"L": "Левая",
|
||||
}
|
||||
return polarization_map.get(polarization.upper(), polarization)
|
||||
|
||||
def get_region_pages(self, regions: list[str] | None = None) -> list[str]:
|
||||
html_regions = []
|
||||
if regions is None:
|
||||
regions = self.regions
|
||||
for region in regions:
|
||||
url = f"{self.BASE_URL}/{region}.html"
|
||||
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
|
||||
response = requests.post(self.flaresolver_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
continue
|
||||
html_content = response.json().get("solution", {}).get("response", "")
|
||||
html_regions.append(html_content)
|
||||
print(f"Обработал страницу по {region}")
|
||||
return html_regions
|
||||
|
||||
def get_satellite_urls(self, html_regions: list[str]):
|
||||
sat_names = []
|
||||
sat_urls = []
|
||||
for region_page in html_regions:
|
||||
soup = BeautifulSoup(region_page, "html.parser")
|
||||
|
||||
col_table = soup.find_all("div", class_="desktab")[0]
|
||||
|
||||
tables = col_table.find_next_sibling("table").find_all("table")
|
||||
trs = []
|
||||
for table in tables:
|
||||
trs.extend(table.find_all("tr"))
|
||||
for tr in trs:
|
||||
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"]
|
||||
except IndexError:
|
||||
sat_url = tr.find_all("a")[0]["href"]
|
||||
sat_names.append(sat_name)
|
||||
sat_urls.append(sat_url)
|
||||
return sat_names, sat_urls
|
||||
|
||||
def get_satellites_data(self) -> dict[dict]:
|
||||
sat_data = {}
|
||||
for region_page in self.get_region_pages(self.regions):
|
||||
soup = BeautifulSoup(region_page, "html.parser")
|
||||
|
||||
col_table = soup.find_all("div", class_="desktab")[0]
|
||||
|
||||
tables = col_table.find_next_sibling("table").find_all("table")
|
||||
trs = []
|
||||
for table in tables:
|
||||
trs.extend(table.find_all("tr"))
|
||||
for tr in trs:
|
||||
sat_name = tr.find("span").text.replace("ü", "u").strip().lower()
|
||||
if self.target_sats is not None:
|
||||
names = parse_satellite_names(sat_name)
|
||||
if len(names) == 1:
|
||||
sat_name = names[0]
|
||||
else:
|
||||
for name in names:
|
||||
if name in self.target_sats:
|
||||
sat_name = name
|
||||
if sat_name not in self.target_sats:
|
||||
continue
|
||||
try:
|
||||
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_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,
|
||||
}
|
||||
return sat_data
|
||||
|
||||
def get_satellite_content(self, html_content: str) -> list[dict]:
|
||||
data = []
|
||||
sat_soup = BeautifulSoup(html_content, "html.parser")
|
||||
try:
|
||||
big_table = sat_soup.find("table", class_="bigtable")
|
||||
all_tables = big_table.find_all("div", class_="desktab")[:-1]
|
||||
for table in all_tables:
|
||||
trs = table.find_next_sibling("table").find_all("tr")
|
||||
for idx, tr in enumerate(trs):
|
||||
tds = tr.find_all("td")
|
||||
if len(tds) < 9 or idx < 2:
|
||||
continue
|
||||
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,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return data if data else data[{}]
|
||||
|
||||
|
||||
class KingOfSatParser:
|
||||
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
|
||||
"""
|
||||
Инициализация парсера
|
||||
:param base_url: Базовый URL сайта
|
||||
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
|
||||
"""
|
||||
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"
|
||||
}
|
||||
)
|
||||
|
||||
def convert_polarization(self, polarization):
|
||||
"""Преобразовать код поляризации в понятное название на русском"""
|
||||
polarization_map = {
|
||||
"V": "Вертикальная",
|
||||
"H": "Горизонтальная",
|
||||
"R": "Правая",
|
||||
"L": "Левая",
|
||||
}
|
||||
return polarization_map.get(polarization.upper(), polarization)
|
||||
|
||||
def fetch_page(self, url):
|
||||
"""Получить HTML страницу"""
|
||||
try:
|
||||
response = self.session.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении страницы {url}: {e}")
|
||||
return None
|
||||
|
||||
def parse_satellite_table(self, html_content):
|
||||
"""Распарсить таблицу со спутниками"""
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
satellites = []
|
||||
table = soup.find("table")
|
||||
if not table:
|
||||
print("Таблица не найдена")
|
||||
return satellites
|
||||
|
||||
rows = table.find_all("tr")[1:]
|
||||
|
||||
for row in rows:
|
||||
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)
|
||||
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()
|
||||
|
||||
# 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)
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при обработке строки таблицы: {e}")
|
||||
continue
|
||||
|
||||
return satellites
|
||||
|
||||
def parse_ini_file(self, ini_content):
|
||||
"""Распарсить содержимое .ini файла"""
|
||||
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)
|
||||
if sattype_match:
|
||||
sattype_content = sattype_match.group(1).strip()
|
||||
for line in sattype_content.split("\n"):
|
||||
line = line.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)
|
||||
if dvb_match:
|
||||
dvb_content = dvb_match.group(1).strip()
|
||||
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 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 "",
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
def download_ini_file(self, url):
|
||||
"""Скачать содержимое .ini файла"""
|
||||
try:
|
||||
response = self.session.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
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")
|
||||
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]
|
||||
|
||||
results = []
|
||||
processed_count = 0
|
||||
|
||||
for satellite in satellites:
|
||||
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
results.append(result)
|
||||
processed_count += 1
|
||||
|
||||
if self.max_satellites > 0 and processed_count >= self.max_satellites:
|
||||
break
|
||||
|
||||
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"],
|
||||
}
|
||||
|
||||
return satellite_dict
|
||||
|
||||
@@ -1,73 +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
|
||||
"""
|
||||
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,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,170 +1,175 @@
|
||||
import logging
|
||||
from .parser import LyngSatParser
|
||||
from .models import LyngSat
|
||||
from mainapp.models import Polarization, Standard, Modulation, Satellite
|
||||
|
||||
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:
|
||||
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_name if polarization_name else "-"
|
||||
)
|
||||
|
||||
mod_obj, _ = Modulation.objects.get_or_create(
|
||||
name=modulation_name if modulation_name else "-"
|
||||
)
|
||||
|
||||
standard_obj, _ = Standard.objects.get_or_create(
|
||||
name=standard_name if standard_name else "-"
|
||||
)
|
||||
|
||||
# Создаем или обновляем запись 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
|
||||
import logging
|
||||
from .parser import LyngSatParser
|
||||
from .models import LyngSat
|
||||
from mainapp.models import Polarization, Standard, Modulation, Satellite
|
||||
|
||||
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(
|
||||
flaresolver_url="http://localhost:8191/v1",
|
||||
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:
|
||||
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_name if polarization_name else "-"
|
||||
)
|
||||
|
||||
mod_obj, _ = Modulation.objects.get_or_create(
|
||||
name=modulation_name if modulation_name else "-"
|
||||
)
|
||||
|
||||
standard_obj, _ = Standard.objects.get_or_create(
|
||||
name=standard_name if standard_name else "-"
|
||||
)
|
||||
|
||||
# Создаем или обновляем запись 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
|
||||
|
||||
|
||||
def link_lyngsat_to_sources():
|
||||
pass
|
||||
@@ -1,3 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,34 @@
|
||||
# Third-party imports
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
|
||||
|
||||
# Local imports
|
||||
from .models import ObjItem
|
||||
|
||||
def get_clusters(coords: list[tuple[float, float]]):
|
||||
coords = np.radians(coords)
|
||||
lat, lon = coords[:, 0], coords[:, 1]
|
||||
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
|
||||
# db = HDBSCAN()
|
||||
cluster_labels = db.fit_predict(coords)
|
||||
plt.figure(figsize=(10, 8))
|
||||
unique_labels = set(cluster_labels)
|
||||
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
|
||||
|
||||
for label, color in zip(unique_labels, colors):
|
||||
if label == -1:
|
||||
color = 'k'
|
||||
label_name = 'Шум'
|
||||
else:
|
||||
label_name = f'Кластер {label}'
|
||||
|
||||
mask = cluster_labels == label
|
||||
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
|
||||
|
||||
plt.xlabel('Долгота')
|
||||
plt.ylabel('Широта')
|
||||
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
plt.show()
|
||||
# Third-party imports
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
|
||||
|
||||
# Local imports
|
||||
from .models import ObjItem
|
||||
|
||||
def get_clusters(coords: list[tuple[float, float]]):
|
||||
coords = np.radians(coords)
|
||||
lat, lon = coords[:, 0], coords[:, 1]
|
||||
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
|
||||
# db = HDBSCAN()
|
||||
cluster_labels = db.fit_predict(coords)
|
||||
plt.figure(figsize=(10, 8))
|
||||
unique_labels = set(cluster_labels)
|
||||
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
|
||||
|
||||
for label, color in zip(unique_labels, colors):
|
||||
if label == -1:
|
||||
color = 'k'
|
||||
label_name = 'Шум'
|
||||
else:
|
||||
label_name = f'Кластер {label}'
|
||||
|
||||
mask = cluster_labels == label
|
||||
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
|
||||
|
||||
plt.xlabel('Долгота')
|
||||
plt.ylabel('Широта')
|
||||
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
plt.show()
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
# Django imports
|
||||
from django.contrib.admin import SimpleListFilter
|
||||
|
||||
# Local imports
|
||||
from .models import ObjItem
|
||||
|
||||
class GeoKupDistanceFilter(SimpleListFilter):
|
||||
title = 'Расстояние между гео и кубсатом'
|
||||
parameter_name = 'distance_geo_kup'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('small', 'Меньше 100 км'),
|
||||
('medium', '100-500 км'),
|
||||
('large', 'Больше 500 км'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'small':
|
||||
return queryset.filter(distance_coords_kup__lt=100)
|
||||
if self.value() == 'medium':
|
||||
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
|
||||
if self.value() == 'large':
|
||||
return queryset.filter(distance_coords_kup__gt=500)
|
||||
|
||||
|
||||
class GeoValidDistanceFilter(SimpleListFilter):
|
||||
title = 'Расстояние между гео и оперативным отделом'
|
||||
parameter_name = 'distance_geo_valid'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('small', 'Меньше 100 км'),
|
||||
('medium', '100-500 км'),
|
||||
('large', 'Больше 500 км'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'small':
|
||||
return queryset.filter(distance_coords_valid__lt=100)
|
||||
if self.value() == 'medium':
|
||||
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
|
||||
if self.value() == 'large':
|
||||
return queryset.filter(distance_coords_valid__gt=500)
|
||||
|
||||
class UniqueToggleFilter(SimpleListFilter):
|
||||
title = 'Уникальность по имени'
|
||||
parameter_name = 'name'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('unique', 'Только уникальные'),
|
||||
('all', 'Все'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'unique':
|
||||
return queryset.order_by('name').distinct('name')
|
||||
return queryset
|
||||
|
||||
class HasSigmaParameterFilter(SimpleListFilter):
|
||||
title = 'ВЧ sigma'
|
||||
parameter_name = 'has_sigma'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('yes', 'Заполнено'),
|
||||
('no', 'Пусто'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'yes':
|
||||
return queryset.filter(sigma_parameter__isnull=False)
|
||||
if self.value() == 'no':
|
||||
return queryset.filter(sigma_parameter__isnull=True)
|
||||
# Django imports
|
||||
from django.contrib.admin import SimpleListFilter
|
||||
|
||||
# Local imports
|
||||
from .models import ObjItem
|
||||
|
||||
class GeoKupDistanceFilter(SimpleListFilter):
|
||||
title = 'Расстояние между гео и кубсатом'
|
||||
parameter_name = 'distance_geo_kup'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('small', 'Меньше 100 км'),
|
||||
('medium', '100-500 км'),
|
||||
('large', 'Больше 500 км'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'small':
|
||||
return queryset.filter(distance_coords_kup__lt=100)
|
||||
if self.value() == 'medium':
|
||||
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
|
||||
if self.value() == 'large':
|
||||
return queryset.filter(distance_coords_kup__gt=500)
|
||||
|
||||
|
||||
class GeoValidDistanceFilter(SimpleListFilter):
|
||||
title = 'Расстояние между гео и оперативным отделом'
|
||||
parameter_name = 'distance_geo_valid'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('small', 'Меньше 100 км'),
|
||||
('medium', '100-500 км'),
|
||||
('large', 'Больше 500 км'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'small':
|
||||
return queryset.filter(distance_coords_valid__lt=100)
|
||||
if self.value() == 'medium':
|
||||
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
|
||||
if self.value() == 'large':
|
||||
return queryset.filter(distance_coords_valid__gt=500)
|
||||
|
||||
class UniqueToggleFilter(SimpleListFilter):
|
||||
title = 'Уникальность по имени'
|
||||
parameter_name = 'name'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('unique', 'Только уникальные'),
|
||||
('all', 'Все'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'unique':
|
||||
return queryset.order_by('name').distinct('name')
|
||||
return queryset
|
||||
|
||||
class HasSigmaParameterFilter(SimpleListFilter):
|
||||
title = 'ВЧ sigma'
|
||||
parameter_name = 'has_sigma'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('yes', 'Заполнено'),
|
||||
('no', 'Пусто'),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'yes':
|
||||
return queryset.filter(sigma_parameter__isnull=False)
|
||||
if self.value() == 'no':
|
||||
return queryset.filter(sigma_parameter__isnull=True)
|
||||
return queryset
|
||||
@@ -1,301 +1,301 @@
|
||||
# Django imports
|
||||
from django import forms
|
||||
|
||||
# Local imports
|
||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-file-input'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class LoadExcelData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите Excel файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.xlsx,.xls'
|
||||
})
|
||||
)
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
number_input = forms.IntegerField(
|
||||
label="Введите число объектов",
|
||||
min_value=0,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control'
|
||||
})
|
||||
)
|
||||
|
||||
class LoadCsvData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите CSV файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.csv'
|
||||
})
|
||||
)
|
||||
|
||||
class UploadVchLoad(UploadFileForm):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class VchLinkForm(forms.Form):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
# ku_range = forms.ChoiceField(
|
||||
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||
# # coerce=lambda x: x == 'True',
|
||||
# widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
# label='Выбор диапазона'
|
||||
# )
|
||||
value1 = forms.FloatField(
|
||||
label="Первое число",
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите первое число'
|
||||
})
|
||||
)
|
||||
value2 = forms.FloatField(
|
||||
label="Второе число",
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите второе число'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class NewEventForm(forms.Form):
|
||||
# sat_choice = forms.ModelChoiceField(
|
||||
# queryset=Satellite.objects.all(),
|
||||
# label="Выберите спутник",
|
||||
# widget=forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# })
|
||||
# )
|
||||
# pol_choice = forms.ModelChoiceField(
|
||||
# queryset=Polarization.objects.all(),
|
||||
# label="Выберите поляризацию",
|
||||
# widget=forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# })
|
||||
# )
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'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):
|
||||
"""
|
||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||
|
||||
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Parameter
|
||||
fields = [
|
||||
'id_satellite', 'frequency', 'freq_range', 'polarization',
|
||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
||||
]
|
||||
widgets = {
|
||||
'id_satellite': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
'required': True
|
||||
}),
|
||||
'frequency': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.000001',
|
||||
'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):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Динамически загружаем choices для select полей
|
||||
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
||||
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 Meta:
|
||||
model = Geo
|
||||
fields = ['location', 'comment', 'is_average']
|
||||
widgets = {
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
class ObjItemForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования объектов (источников сигнала).
|
||||
|
||||
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
|
||||
через ParameterForm с использованием OneToOne связи.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ObjItem
|
||||
fields = ['name']
|
||||
widgets = {
|
||||
'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('Название не может состоять только из пробелов')
|
||||
|
||||
# Django imports
|
||||
from django import forms
|
||||
|
||||
# Local imports
|
||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-file-input'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class LoadExcelData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите Excel файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.xlsx,.xls'
|
||||
})
|
||||
)
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
number_input = forms.IntegerField(
|
||||
label="Введите число объектов",
|
||||
min_value=0,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control'
|
||||
})
|
||||
)
|
||||
|
||||
class LoadCsvData(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите CSV файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'accept': '.csv'
|
||||
})
|
||||
)
|
||||
|
||||
class UploadVchLoad(UploadFileForm):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class VchLinkForm(forms.Form):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
# ku_range = forms.ChoiceField(
|
||||
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||
# # coerce=lambda x: x == 'True',
|
||||
# widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
# label='Выбор диапазона'
|
||||
# )
|
||||
value1 = forms.FloatField(
|
||||
label="Первое число",
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите первое число'
|
||||
})
|
||||
)
|
||||
value2 = forms.FloatField(
|
||||
label="Второе число",
|
||||
widget=forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите второе число'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class NewEventForm(forms.Form):
|
||||
# sat_choice = forms.ModelChoiceField(
|
||||
# queryset=Satellite.objects.all(),
|
||||
# label="Выберите спутник",
|
||||
# widget=forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# })
|
||||
# )
|
||||
# pol_choice = forms.ModelChoiceField(
|
||||
# queryset=Polarization.objects.all(),
|
||||
# label="Выберите поляризацию",
|
||||
# widget=forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# })
|
||||
# )
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-control',
|
||||
'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):
|
||||
"""
|
||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||
|
||||
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Parameter
|
||||
fields = [
|
||||
'id_satellite', 'frequency', 'freq_range', 'polarization',
|
||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
||||
]
|
||||
widgets = {
|
||||
'id_satellite': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
'required': True
|
||||
}),
|
||||
'frequency': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '0.000001',
|
||||
'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):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Динамически загружаем choices для select полей
|
||||
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
||||
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 Meta:
|
||||
model = Geo
|
||||
fields = ['location', 'comment', 'is_average']
|
||||
widgets = {
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
class ObjItemForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для создания и редактирования объектов (источников сигнала).
|
||||
|
||||
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
|
||||
через ParameterForm с использованием OneToOne связи.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ObjItem
|
||||
fields = ['name']
|
||||
widgets = {
|
||||
'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
|
||||
24
dbapp/mainapp/management/commands/test_celery.py
Normal file
24
dbapp/mainapp/management/commands/test_celery.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from mainapp.tasks import test_celery_connection, add_numbers
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Test Celery functionality'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Testing Celery connection...')
|
||||
|
||||
# Test simple task
|
||||
result = test_celery_connection.delay("Hello from test command!")
|
||||
self.stdout.write(f'Task ID: {result.id}')
|
||||
|
||||
# Wait for result
|
||||
task_result = result.get(timeout=10)
|
||||
self.stdout.write(self.style.SUCCESS(f'Task result: {task_result}'))
|
||||
|
||||
# Test math task
|
||||
math_result = add_numbers.delay(10, 20)
|
||||
sum_result = math_result.get(timeout=10)
|
||||
self.stdout.write(self.style.SUCCESS(f'10 + 20 = {sum_result}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('All tests passed!'))
|
||||
@@ -1,204 +1,204 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:36
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
import mainapp.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Mirror',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Зеркало',
|
||||
'verbose_name_plural': 'Зеркала',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Modulation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Модуляция',
|
||||
'verbose_name_plural': 'Модуляции',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Polarization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Поляризация',
|
||||
'verbose_name_plural': 'Поляризация',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Satellite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
|
||||
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Спутник',
|
||||
'verbose_name_plural': 'Спутники',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SigmaParMark',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
|
||||
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Отметка',
|
||||
'verbose_name_plural': 'Отметки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Standard',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Стандарт',
|
||||
'verbose_name_plural': 'Стандарты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Пользователь',
|
||||
'verbose_name_plural': 'Пользователи',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ObjItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Объект',
|
||||
'verbose_name_plural': 'Объекты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Parameter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ВЧ загрузка',
|
||||
'verbose_name_plural': 'ВЧ загрузки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SourceType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
|
||||
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Тип источника',
|
||||
'verbose_name_plural': 'Типы источников',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SigmaParameter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
|
||||
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
|
||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
|
||||
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
|
||||
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
|
||||
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
|
||||
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
|
||||
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
|
||||
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')),
|
||||
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ВЧ sigma',
|
||||
'verbose_name_plural': 'ВЧ sigma',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Geo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
|
||||
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
|
||||
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
|
||||
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
|
||||
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
|
||||
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')),
|
||||
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')),
|
||||
('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')),
|
||||
('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')),
|
||||
('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
|
||||
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Гео',
|
||||
'verbose_name_plural': 'Гео',
|
||||
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='parameter',
|
||||
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='parameter',
|
||||
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:36
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
import mainapp.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Mirror',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Зеркало',
|
||||
'verbose_name_plural': 'Зеркала',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Modulation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Модуляция',
|
||||
'verbose_name_plural': 'Модуляции',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Polarization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Поляризация',
|
||||
'verbose_name_plural': 'Поляризация',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Satellite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
|
||||
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Спутник',
|
||||
'verbose_name_plural': 'Спутники',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SigmaParMark',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
|
||||
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Отметка',
|
||||
'verbose_name_plural': 'Отметки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Standard',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Стандарт',
|
||||
'verbose_name_plural': 'Стандарты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Пользователь',
|
||||
'verbose_name_plural': 'Пользователи',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ObjItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Объект',
|
||||
'verbose_name_plural': 'Объекты',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Parameter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ВЧ загрузка',
|
||||
'verbose_name_plural': 'ВЧ загрузки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SourceType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
|
||||
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Тип источника',
|
||||
'verbose_name_plural': 'Типы источников',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SigmaParameter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
|
||||
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
|
||||
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
|
||||
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
|
||||
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
|
||||
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
|
||||
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
|
||||
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
|
||||
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')),
|
||||
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ВЧ sigma',
|
||||
'verbose_name_plural': 'ВЧ sigma',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Geo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
|
||||
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
|
||||
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
|
||||
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
|
||||
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
|
||||
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')),
|
||||
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')),
|
||||
('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')),
|
||||
('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')),
|
||||
('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')),
|
||||
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
|
||||
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Гео',
|
||||
'verbose_name_plural': 'Гео',
|
||||
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='parameter',
|
||||
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='parameter',
|
||||
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:56
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:56
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-31 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-10-31 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-01 07:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='geo',
|
||||
name='id_user_add',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='objitem',
|
||||
name='id_user_add',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='parameter',
|
||||
name='id_user_add',
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-11-01 07:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='geo',
|
||||
name='id_user_add',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='objitem',
|
||||
name='id_user_add',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='parameter',
|
||||
name='id_user_add',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-07 19:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-11-07 19:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,290 +1,290 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0005_alter_geo_objitem'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='customuser',
|
||||
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='geo',
|
||||
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mirror',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='modulation',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='objitem',
|
||||
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='polarization',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='satellite',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sigmaparmark',
|
||||
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sourcetype',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='standard',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='user',
|
||||
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='comment',
|
||||
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords_kupsat',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords_valid',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='distance_coords_kup',
|
||||
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='distance_kup_valid',
|
||||
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='is_average',
|
||||
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='location',
|
||||
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='mirrors',
|
||||
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mirror',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='modulation',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='snr',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='polarization',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='norad',
|
||||
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='datetime_begin',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='datetime_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='packets',
|
||||
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='power',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='snr',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='transfer',
|
||||
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparmark',
|
||||
name='mark',
|
||||
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparmark',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sourcetype',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sourcetype',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='standard',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='geo',
|
||||
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='geo',
|
||||
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0005_alter_geo_objitem'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='customuser',
|
||||
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='geo',
|
||||
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mirror',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='modulation',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='objitem',
|
||||
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='polarization',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='satellite',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sigmaparmark',
|
||||
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sourcetype',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='standard',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='user',
|
||||
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='comment',
|
||||
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords_kupsat',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='coords_valid',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='distance_coords_kup',
|
||||
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='distance_kup_valid',
|
||||
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='is_average',
|
||||
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='location',
|
||||
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='mirrors',
|
||||
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mirror',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='modulation',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='snr',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='polarization',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='norad',
|
||||
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='datetime_begin',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='datetime_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='packets',
|
||||
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='power',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='snr',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='transfer',
|
||||
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparmark',
|
||||
name='mark',
|
||||
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparmark',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sourcetype',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sourcetype',
|
||||
name='objitem',
|
||||
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='standard',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='geo',
|
||||
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='geo',
|
||||
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='objitem',
|
||||
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,23 +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='Объект'),
|
||||
),
|
||||
]
|
||||
# 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='Объект'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-11 13:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sourcetype',
|
||||
name='objitem',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='source_type_id',
|
||||
field=models.ForeignKey(blank=True, help_text='Тип источника сигнала', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_sourcetype', to='mainapp.sourcetype', verbose_name='Тип источника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='snr',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='bod_velocity',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='freq_range',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='power',
|
||||
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм'),
|
||||
),
|
||||
]
|
||||
@@ -1,229 +1,229 @@
|
||||
"""
|
||||
Переиспользуемые миксины для представлений mainapp.
|
||||
|
||||
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
|
||||
включая проверку прав доступа, обработку координат и сообщений.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Django imports
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
|
||||
class RoleRequiredMixin(UserPassesTestMixin):
|
||||
"""
|
||||
Mixin для проверки роли пользователя.
|
||||
|
||||
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
|
||||
|
||||
Attributes:
|
||||
required_roles (list): Список допустимых ролей для доступа.
|
||||
По умолчанию ['admin', 'moderator'].
|
||||
|
||||
Example:
|
||||
class MyView(RoleRequiredMixin, View):
|
||||
required_roles = ['admin', 'moderator']
|
||||
|
||||
def get(self, request):
|
||||
# Только пользователи с ролью admin или moderator могут получить доступ
|
||||
return render(request, 'template.html')
|
||||
"""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def test_func(self) -> bool:
|
||||
"""
|
||||
Проверяет, имеет ли пользователь требуемую роль.
|
||||
|
||||
Returns:
|
||||
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not hasattr(self.request.user, "customuser"):
|
||||
return False
|
||||
|
||||
return self.request.user.customuser.role in self.required_roles
|
||||
|
||||
|
||||
class CoordinateProcessingMixin:
|
||||
"""
|
||||
Mixin для обработки координат из POST данных форм.
|
||||
|
||||
Предоставляет методы для извлечения и обработки координат различных типов
|
||||
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||
|
||||
Example:
|
||||
class MyFormView(CoordinateProcessingMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
geo_instance = Geo()
|
||||
self.process_coordinates(geo_instance)
|
||||
geo_instance.save()
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||
"""
|
||||
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||
|
||||
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
|
||||
и устанавливает соответствующие поля объекта Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления координат.
|
||||
prefix (str): Префикс для полей формы (по умолчанию 'geo').
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- geo_longitude, geo_latitude: координаты геолокации
|
||||
- kupsat_longitude, kupsat_latitude: координаты кубсата
|
||||
- valid_longitude, valid_latitude: координаты оперативников
|
||||
"""
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_coords = self._extract_coordinates("geo")
|
||||
if geo_coords:
|
||||
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
||||
|
||||
# Обрабатываем координаты Кубсата
|
||||
kupsat_coords = self._extract_coordinates("kupsat")
|
||||
if kupsat_coords:
|
||||
geo_instance.coords_kupsat = Point(
|
||||
kupsat_coords[0], kupsat_coords[1], srid=4326
|
||||
)
|
||||
|
||||
# Обрабатываем координаты оперативников
|
||||
valid_coords = self._extract_coordinates("valid")
|
||||
if valid_coords:
|
||||
geo_instance.coords_valid = Point(
|
||||
valid_coords[0], valid_coords[1], srid=4326
|
||||
)
|
||||
|
||||
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||
"""
|
||||
Извлекает координаты указанного типа из POST данных.
|
||||
|
||||
Args:
|
||||
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
|
||||
если координаты не найдены или невалидны.
|
||||
"""
|
||||
longitude_key = f"{coord_type}_longitude"
|
||||
latitude_key = f"{coord_type}_latitude"
|
||||
|
||||
longitude = self.request.POST.get(longitude_key)
|
||||
latitude = self.request.POST.get(latitude_key)
|
||||
|
||||
if longitude and latitude:
|
||||
try:
|
||||
return (float(longitude), float(latitude))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def process_timestamp(self, geo_instance) -> None:
|
||||
"""
|
||||
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления timestamp.
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- timestamp_date: дата в формате YYYY-MM-DD
|
||||
- timestamp_time: время в формате HH:MM
|
||||
"""
|
||||
timestamp_date = self.request.POST.get("timestamp_date")
|
||||
timestamp_time = self.request.POST.get("timestamp_time")
|
||||
|
||||
if timestamp_date and timestamp_time:
|
||||
try:
|
||||
naive_datetime = datetime.strptime(
|
||||
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
geo_instance.timestamp = naive_datetime
|
||||
except ValueError:
|
||||
# Если формат даты/времени неверный, пропускаем
|
||||
pass
|
||||
|
||||
|
||||
class FormMessageMixin:
|
||||
"""
|
||||
Mixin для стандартизации сообщений об успехе и ошибках в формах.
|
||||
|
||||
Автоматически добавляет сообщения пользователю при успешной или неуспешной
|
||||
обработке формы.
|
||||
|
||||
Attributes:
|
||||
success_message (str): Сообщение при успешной обработке формы.
|
||||
error_message (str): Сообщение при ошибке обработки формы.
|
||||
|
||||
Example:
|
||||
class MyFormView(FormMessageMixin, FormView):
|
||||
success_message = "Данные успешно сохранены!"
|
||||
error_message = "Ошибка при сохранении данных"
|
||||
|
||||
def form_valid(self, form):
|
||||
# Автоматически добавит success_message
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
success_message = "Операция выполнена успешно"
|
||||
error_message = "Произошла ошибка при обработке формы"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Обрабатывает валидную форму и добавляет сообщение об успехе.
|
||||
|
||||
Args:
|
||||
form: Валидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_valid.
|
||||
"""
|
||||
if self.success_message:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""
|
||||
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
|
||||
|
||||
Args:
|
||||
form: Невалидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_invalid.
|
||||
"""
|
||||
if self.error_message:
|
||||
messages.error(self.request, self.error_message)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об успехе.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об успехе.
|
||||
"""
|
||||
return self.success_message
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об ошибке.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об ошибке.
|
||||
"""
|
||||
return self.error_message
|
||||
"""
|
||||
Переиспользуемые миксины для представлений mainapp.
|
||||
|
||||
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
|
||||
включая проверку прав доступа, обработку координат и сообщений.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Django imports
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
|
||||
class RoleRequiredMixin(UserPassesTestMixin):
|
||||
"""
|
||||
Mixin для проверки роли пользователя.
|
||||
|
||||
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
|
||||
|
||||
Attributes:
|
||||
required_roles (list): Список допустимых ролей для доступа.
|
||||
По умолчанию ['admin', 'moderator'].
|
||||
|
||||
Example:
|
||||
class MyView(RoleRequiredMixin, View):
|
||||
required_roles = ['admin', 'moderator']
|
||||
|
||||
def get(self, request):
|
||||
# Только пользователи с ролью admin или moderator могут получить доступ
|
||||
return render(request, 'template.html')
|
||||
"""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def test_func(self) -> bool:
|
||||
"""
|
||||
Проверяет, имеет ли пользователь требуемую роль.
|
||||
|
||||
Returns:
|
||||
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not hasattr(self.request.user, "customuser"):
|
||||
return False
|
||||
|
||||
return self.request.user.customuser.role in self.required_roles
|
||||
|
||||
|
||||
class CoordinateProcessingMixin:
|
||||
"""
|
||||
Mixin для обработки координат из POST данных форм.
|
||||
|
||||
Предоставляет методы для извлечения и обработки координат различных типов
|
||||
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||
|
||||
Example:
|
||||
class MyFormView(CoordinateProcessingMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
geo_instance = Geo()
|
||||
self.process_coordinates(geo_instance)
|
||||
geo_instance.save()
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||
"""
|
||||
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||
|
||||
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
|
||||
и устанавливает соответствующие поля объекта Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления координат.
|
||||
prefix (str): Префикс для полей формы (по умолчанию 'geo').
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- geo_longitude, geo_latitude: координаты геолокации
|
||||
- kupsat_longitude, kupsat_latitude: координаты кубсата
|
||||
- valid_longitude, valid_latitude: координаты оперативников
|
||||
"""
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_coords = self._extract_coordinates("geo")
|
||||
if geo_coords:
|
||||
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
||||
|
||||
# Обрабатываем координаты Кубсата
|
||||
kupsat_coords = self._extract_coordinates("kupsat")
|
||||
if kupsat_coords:
|
||||
geo_instance.coords_kupsat = Point(
|
||||
kupsat_coords[0], kupsat_coords[1], srid=4326
|
||||
)
|
||||
|
||||
# Обрабатываем координаты оперативников
|
||||
valid_coords = self._extract_coordinates("valid")
|
||||
if valid_coords:
|
||||
geo_instance.coords_valid = Point(
|
||||
valid_coords[0], valid_coords[1], srid=4326
|
||||
)
|
||||
|
||||
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||
"""
|
||||
Извлекает координаты указанного типа из POST данных.
|
||||
|
||||
Args:
|
||||
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
|
||||
если координаты не найдены или невалидны.
|
||||
"""
|
||||
longitude_key = f"{coord_type}_longitude"
|
||||
latitude_key = f"{coord_type}_latitude"
|
||||
|
||||
longitude = self.request.POST.get(longitude_key)
|
||||
latitude = self.request.POST.get(latitude_key)
|
||||
|
||||
if longitude and latitude:
|
||||
try:
|
||||
return (float(longitude), float(latitude))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def process_timestamp(self, geo_instance) -> None:
|
||||
"""
|
||||
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления timestamp.
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- timestamp_date: дата в формате YYYY-MM-DD
|
||||
- timestamp_time: время в формате HH:MM
|
||||
"""
|
||||
timestamp_date = self.request.POST.get("timestamp_date")
|
||||
timestamp_time = self.request.POST.get("timestamp_time")
|
||||
|
||||
if timestamp_date and timestamp_time:
|
||||
try:
|
||||
naive_datetime = datetime.strptime(
|
||||
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
geo_instance.timestamp = naive_datetime
|
||||
except ValueError:
|
||||
# Если формат даты/времени неверный, пропускаем
|
||||
pass
|
||||
|
||||
|
||||
class FormMessageMixin:
|
||||
"""
|
||||
Mixin для стандартизации сообщений об успехе и ошибках в формах.
|
||||
|
||||
Автоматически добавляет сообщения пользователю при успешной или неуспешной
|
||||
обработке формы.
|
||||
|
||||
Attributes:
|
||||
success_message (str): Сообщение при успешной обработке формы.
|
||||
error_message (str): Сообщение при ошибке обработки формы.
|
||||
|
||||
Example:
|
||||
class MyFormView(FormMessageMixin, FormView):
|
||||
success_message = "Данные успешно сохранены!"
|
||||
error_message = "Ошибка при сохранении данных"
|
||||
|
||||
def form_valid(self, form):
|
||||
# Автоматически добавит success_message
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
success_message = "Операция выполнена успешно"
|
||||
error_message = "Произошла ошибка при обработке формы"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Обрабатывает валидную форму и добавляет сообщение об успехе.
|
||||
|
||||
Args:
|
||||
form: Валидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_valid.
|
||||
"""
|
||||
if self.success_message:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""
|
||||
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
|
||||
|
||||
Args:
|
||||
form: Невалидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_invalid.
|
||||
"""
|
||||
if self.error_message:
|
||||
messages.error(self.request, self.error_message)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об успехе.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об успехе.
|
||||
"""
|
||||
return self.success_message
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об ошибке.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об ошибке.
|
||||
"""
|
||||
return self.error_message
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,76 +1,76 @@
|
||||
# Django imports
|
||||
from django.contrib.admin.filters import ChoicesFieldListFilter
|
||||
from django.forms import Media
|
||||
|
||||
|
||||
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
|
||||
"""
|
||||
A custom filter that maintains popup context when used in raw_id_fields modals.
|
||||
"""
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
|
||||
# Check if we're in a popup context
|
||||
self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
|
||||
|
||||
# Get all choices (related objects)
|
||||
self.lookup_choices = field.get_choices(include_blank=False)
|
||||
|
||||
def has_output(self):
|
||||
return len(self.lookup_choices) > 1
|
||||
|
||||
def value(self):
|
||||
return self.lookup_val
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
|
||||
|
||||
def choices(self, changelist):
|
||||
# If in popup, preserve the popup parameters in the filter URL
|
||||
popup_params = {}
|
||||
if self.is_popup:
|
||||
# Preserve popup parameters
|
||||
if '_popup' in changelist.params:
|
||||
popup_params['_popup'] = 1
|
||||
if 'pop' in changelist.params:
|
||||
popup_params['pop'] = changelist.params['pop']
|
||||
if '_to_field' in changelist.params:
|
||||
popup_params['_to_field'] = changelist.params['_to_field']
|
||||
|
||||
# Create the base URL with popup parameters
|
||||
all_params = changelist.get_filters_params()
|
||||
all_params.update(popup_params)
|
||||
|
||||
# Generate the URL for the filter
|
||||
url = changelist.get_query_string(all_params, [self.lookup_kwarg])
|
||||
|
||||
yield {
|
||||
'selected': self.lookup_val is None,
|
||||
'query_string': url,
|
||||
'display': 'All',
|
||||
}
|
||||
|
||||
# Add choices
|
||||
for lookup, title in self.lookup_choices:
|
||||
params = dict(all_params)
|
||||
params[self.lookup_kwarg] = lookup
|
||||
|
||||
# Remove the parameter if it's being set to the same value (for unselecting)
|
||||
if self.lookup_val == str(lookup):
|
||||
params.pop(self.lookup_kwarg, None)
|
||||
|
||||
# Add popup parameters to each choice URL
|
||||
choice_params = params.copy()
|
||||
choice_params.update(popup_params)
|
||||
|
||||
yield {
|
||||
'selected': str(lookup) == self.lookup_val,
|
||||
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
|
||||
'display': title,
|
||||
}
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
# Include necessary CSS/JS for dropdown functionality if needed
|
||||
# Django imports
|
||||
from django.contrib.admin.filters import ChoicesFieldListFilter
|
||||
from django.forms import Media
|
||||
|
||||
|
||||
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
|
||||
"""
|
||||
A custom filter that maintains popup context when used in raw_id_fields modals.
|
||||
"""
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
|
||||
# Check if we're in a popup context
|
||||
self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
|
||||
|
||||
# Get all choices (related objects)
|
||||
self.lookup_choices = field.get_choices(include_blank=False)
|
||||
|
||||
def has_output(self):
|
||||
return len(self.lookup_choices) > 1
|
||||
|
||||
def value(self):
|
||||
return self.lookup_val
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
|
||||
|
||||
def choices(self, changelist):
|
||||
# If in popup, preserve the popup parameters in the filter URL
|
||||
popup_params = {}
|
||||
if self.is_popup:
|
||||
# Preserve popup parameters
|
||||
if '_popup' in changelist.params:
|
||||
popup_params['_popup'] = 1
|
||||
if 'pop' in changelist.params:
|
||||
popup_params['pop'] = changelist.params['pop']
|
||||
if '_to_field' in changelist.params:
|
||||
popup_params['_to_field'] = changelist.params['_to_field']
|
||||
|
||||
# Create the base URL with popup parameters
|
||||
all_params = changelist.get_filters_params()
|
||||
all_params.update(popup_params)
|
||||
|
||||
# Generate the URL for the filter
|
||||
url = changelist.get_query_string(all_params, [self.lookup_kwarg])
|
||||
|
||||
yield {
|
||||
'selected': self.lookup_val is None,
|
||||
'query_string': url,
|
||||
'display': 'All',
|
||||
}
|
||||
|
||||
# Add choices
|
||||
for lookup, title in self.lookup_choices:
|
||||
params = dict(all_params)
|
||||
params[self.lookup_kwarg] = lookup
|
||||
|
||||
# Remove the parameter if it's being set to the same value (for unselecting)
|
||||
if self.lookup_val == str(lookup):
|
||||
params.pop(self.lookup_kwarg, None)
|
||||
|
||||
# Add popup parameters to each choice URL
|
||||
choice_params = params.copy()
|
||||
choice_params.update(popup_params)
|
||||
|
||||
yield {
|
||||
'selected': str(lookup) == self.lookup_val,
|
||||
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
|
||||
'display': title,
|
||||
}
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
# Include necessary CSS/JS for dropdown functionality if needed
|
||||
return Media()
|
||||
@@ -1,14 +1,14 @@
|
||||
# Django imports
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Local imports
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_or_update_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
CustomUser.objects.create(user=instance)
|
||||
# Django imports
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Local imports
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_or_update_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
CustomUser.objects.create(user=instance)
|
||||
instance.customuser.save()
|
||||
65
dbapp/mainapp/tasks.py
Normal file
65
dbapp/mainapp/tasks.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Simple test tasks for Celery functionality.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from celery import shared_task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(name='mainapp.test_celery_connection')
|
||||
def test_celery_connection(message="Hello from Celery!"):
|
||||
"""
|
||||
A simple test task to verify Celery is working.
|
||||
|
||||
Args:
|
||||
message (str): Message to return
|
||||
|
||||
Returns:
|
||||
str: Confirmation message with task completion time
|
||||
"""
|
||||
logger.info(f"Test task started with message: {message}")
|
||||
time.sleep(2) # Simulate some work
|
||||
result = f"Task completed! Received message: {message}"
|
||||
logger.info(f"Test task completed: {result}")
|
||||
return result
|
||||
|
||||
|
||||
@shared_task(name='mainapp.add_numbers')
|
||||
def add_numbers(x, y):
|
||||
"""
|
||||
A simple addition task to test Celery functionality.
|
||||
|
||||
Args:
|
||||
x (int): First number
|
||||
y (int): Second number
|
||||
|
||||
Returns:
|
||||
int: Sum of x and y
|
||||
"""
|
||||
logger.info(f"Adding {x} + {y}")
|
||||
result = x + y
|
||||
logger.info(f"Addition completed: {x} + {y} = {result}")
|
||||
return result
|
||||
|
||||
|
||||
@shared_task(name='mainapp.long_running_task')
|
||||
def long_running_task(duration=10):
|
||||
"""
|
||||
A task that runs for a specified duration to test long-running tasks.
|
||||
|
||||
Args:
|
||||
duration (int): Duration in seconds
|
||||
|
||||
Returns:
|
||||
str: Completion message
|
||||
"""
|
||||
logger.info(f"Starting long running task for {duration} seconds")
|
||||
for i in range(duration):
|
||||
time.sleep(1)
|
||||
logger.info(f"Long task progress: {i+1}/{duration}")
|
||||
|
||||
result = f"Long running task completed after {duration} seconds"
|
||||
logger.info(result)
|
||||
return result
|
||||
@@ -1,60 +1,60 @@
|
||||
{% extends "mapsapp/map2d_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Вынос точек{% endblock title %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
||||
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var overlays = [];
|
||||
|
||||
{% for group in groups %}
|
||||
var groupIndex = {{ forloop.counter0 }};
|
||||
var colorName = markerColors[groupIndex % markerColors.length];
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ group.name|escapejs }}";
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName);
|
||||
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: '{{ group.name|escapejs }}',
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
|
||||
// Используем именно tree-контрол
|
||||
L.control.layers.tree(baseLayers, overlays, {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
}).addTo(map);
|
||||
</script>
|
||||
{% extends "mapsapp/map2d_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Вынос точек{% endblock title %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
||||
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var overlays = [];
|
||||
|
||||
{% for group in groups %}
|
||||
var groupIndex = {{ forloop.counter0 }};
|
||||
var colorName = markerColors[groupIndex % markerColors.length];
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ group.name|escapejs }}";
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName);
|
||||
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: '{{ group.name|escapejs }}',
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
|
||||
// Используем именно tree-контрол
|
||||
L.control.layers.tree(baseLayers, overlays, {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
}).addTo(map);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
@@ -1,189 +1,189 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Действия{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4 fw-bold">Действия</h1>
|
||||
<p class="lead">Управление данными спутников</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert messages -->
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<!-- Main feature cards -->
|
||||
<div class="row g-4">
|
||||
<!-- Excel Data Upload Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-primary 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-file-earmark-excel text-primary" viewBox="0 0 16 16">
|
||||
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
|
||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Data Upload Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-success 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-file-earmark-text text-success" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0m0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
|
||||
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satellite List Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-info 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-satellite text-info" viewBox="0 0 16 16">
|
||||
<path d="M13.37 1.37c-2.75 0-5.4 1.13-7.29 3.02C4.13 6.33 3 8.98 3 11.73c0 2.75 1.13 5.4 3.02 7.29 1.94 1.94 4.54 3.02 7.29 3.02 2.75 0 5.4-1.13 7.29-3.02 1.94-1.94 3.02-4.54 3.02-7.29 0-2.75-1.13-5.4-3.02-7.29C18.77 2.5-2.75 1.37-5.5 1.37m-5.5 8.26c0-1.52.62-3.02 1.73-4.13 1.11-1.11 2.61-1.73 4.13-1.73 1.52 0 3.02.62 4.13 1.73 1.11 1.11 1.73 2.61 1.73 4.13 0 1.52-.62 3.02-1.73 4.13-1.11 1.11-2.61 1.73-4.13 1.73-1.52 0-3.02-.62-4.13-1.73-1.11-1.11-1.73-2.61-1.73-4.13"/>
|
||||
<path d="M6.63 6.63c.62-.62 1.45-.98 2.27-.98.82 0 1.65.36 2.27.98.62.62.98 1.45.98 2.27 0 .82-.36 1.65-.98 2.27-.62.62-1.45.98-2.27.98-.82 0-1.65-.36-2.27-.98-.62-.62-.98-1.45-.98-2.27 0-.82.36-1.65.98-2.27m2.27 1.02c-.26 0-.52.1-.71.29-.2.2-.29.46-.29.71 0 .26.1.52.29.71.2.2.46.29.71.29.26 0 .52-.1.71-.29.2-.2.29-.46.29-.71 0-.26-.1-.52-.29-.71-.19-.19-.45-.29-.71-.29"/>
|
||||
<path d="M5.13 5.13c.46-.46 1.08-.73 1.73-.73.65 0 1.27.27 1.73.73.46.46.73 1.08.73 1.73 0 .65-.27 1.27-.73 1.73-.46.46-1.08.73-1.73.73-.65 0-1.27-.27-1.73-.73-.46-.46-.73-1.08-.73-1.73 0-.65.27-1.27.73-1.73m1.73.58c-.15 0-.3.06-.42.18-.12.12-.18.27-.18.42 0 .15.06.3.18.42.12.12.27.18.42.18.15 0 .3-.06.42-.18.12-.12.18-.27.18-.42 0-.15-.06-.3-.18-.42-.12-.12-.27-.18-.42-.18"/>
|
||||
<path d="M8 3.5c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5"/>
|
||||
<path d="M10.5 8c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5s-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5"/>
|
||||
<path d="M8 12.5c-.28 0-.5.22-.5.5v1c0 .28.22.5.5.5s.5-.22.5-.5v-1c0-.28-.22-.5-.5-.5"/>
|
||||
<path d="M3.5 8c0 .28-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h1c.28 0 .5.22.5.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
||||
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
|
||||
Добавить список спутников
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transponders Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-warning 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-wifi text-warning" viewBox="0 0 16 16">
|
||||
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
|
||||
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
|
||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
|
||||
Добавить транспондеры
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VCH Load Data Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-danger 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-upload text-danger" 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 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
|
||||
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
|
||||
Добавить данные ВЧ загрузки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lyngsat Data Fill Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-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-cloud-download text-secondary" 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>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
|
||||
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
|
||||
Заполнить данные Lyngsat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculation Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-info 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-calculator text-info" viewBox="0 0 16 16">
|
||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v4h2V2a1 1 0 0 0-1-1M5 6v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm1 2v1h1V8zm0 2v1h1v-1zm0 2v1h1v-1zm-8-6v8H3V8zm2 0v8h1V8zm2 0v8h1V8zm2 0v8h1V8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
|
||||
</div>
|
||||
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
|
||||
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
|
||||
Открыть форму
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Event Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-success 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-plus-circle text-success" viewBox="0 0 16 16">
|
||||
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
|
||||
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
|
||||
Добавить событие
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Действия{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4 fw-bold">Действия</h1>
|
||||
<p class="lead">Управление данными спутников</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert messages -->
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<!-- Main feature cards -->
|
||||
<div class="row g-4">
|
||||
<!-- Excel Data Upload Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-primary 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-file-earmark-excel text-primary" viewBox="0 0 16 16">
|
||||
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
|
||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Data Upload Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-success 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-file-earmark-text text-success" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0m0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
|
||||
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satellite List Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-info 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-satellite text-info" viewBox="0 0 16 16">
|
||||
<path d="M13.37 1.37c-2.75 0-5.4 1.13-7.29 3.02C4.13 6.33 3 8.98 3 11.73c0 2.75 1.13 5.4 3.02 7.29 1.94 1.94 4.54 3.02 7.29 3.02 2.75 0 5.4-1.13 7.29-3.02 1.94-1.94 3.02-4.54 3.02-7.29 0-2.75-1.13-5.4-3.02-7.29C18.77 2.5-2.75 1.37-5.5 1.37m-5.5 8.26c0-1.52.62-3.02 1.73-4.13 1.11-1.11 2.61-1.73 4.13-1.73 1.52 0 3.02.62 4.13 1.73 1.11 1.11 1.73 2.61 1.73 4.13 0 1.52-.62 3.02-1.73 4.13-1.11 1.11-2.61 1.73-4.13 1.73-1.52 0-3.02-.62-4.13-1.73-1.11-1.11-1.73-2.61-1.73-4.13"/>
|
||||
<path d="M6.63 6.63c.62-.62 1.45-.98 2.27-.98.82 0 1.65.36 2.27.98.62.62.98 1.45.98 2.27 0 .82-.36 1.65-.98 2.27-.62.62-1.45.98-2.27.98-.82 0-1.65-.36-2.27-.98-.62-.62-.98-1.45-.98-2.27 0-.82.36-1.65.98-2.27m2.27 1.02c-.26 0-.52.1-.71.29-.2.2-.29.46-.29.71 0 .26.1.52.29.71.2.2.46.29.71.29.26 0 .52-.1.71-.29.2-.2.29-.46.29-.71 0-.26-.1-.52-.29-.71-.19-.19-.45-.29-.71-.29"/>
|
||||
<path d="M5.13 5.13c.46-.46 1.08-.73 1.73-.73.65 0 1.27.27 1.73.73.46.46.73 1.08.73 1.73 0 .65-.27 1.27-.73 1.73-.46.46-1.08.73-1.73.73-.65 0-1.27-.27-1.73-.73-.46-.46-.73-1.08-.73-1.73 0-.65.27-1.27.73-1.73m1.73.58c-.15 0-.3.06-.42.18-.12.12-.18.27-.18.42 0 .15.06.3.18.42.12.12.27.18.42.18.15 0 .3-.06.42-.18.12-.12.18-.27.18-.42 0-.15-.06-.3-.18-.42-.12-.12-.27-.18-.42-.18"/>
|
||||
<path d="M8 3.5c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5"/>
|
||||
<path d="M10.5 8c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5s-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5"/>
|
||||
<path d="M8 12.5c-.28 0-.5.22-.5.5v1c0 .28.22.5.5.5s.5-.22.5-.5v-1c0-.28-.22-.5-.5-.5"/>
|
||||
<path d="M3.5 8c0 .28-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h1c.28 0 .5.22.5.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
||||
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
|
||||
Добавить список спутников
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transponders Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-warning 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-wifi text-warning" viewBox="0 0 16 16">
|
||||
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
|
||||
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
|
||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
|
||||
Добавить транспондеры
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VCH Load Data Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-danger 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-upload text-danger" 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 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
|
||||
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
|
||||
Добавить данные ВЧ загрузки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lyngsat Data Fill Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-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-cloud-download text-secondary" 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>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
|
||||
</div>
|
||||
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
|
||||
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
|
||||
Заполнить данные Lyngsat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculation Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-info 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-calculator text-info" viewBox="0 0 16 16">
|
||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v4h2V2a1 1 0 0 0-1-1M5 6v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm1 2v1h1V8zm0 2v1h1v-1zm0 2v1h1v-1zm-8-6v8H3V8zm2 0v8h1V8zm2 0v8h1V8zm2 0v8h1V8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
|
||||
</div>
|
||||
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
|
||||
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
|
||||
Открыть форму
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Event Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-success 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-plus-circle text-success" viewBox="0 0 16 16">
|
||||
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
|
||||
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
|
||||
Добавить событие
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,34 +1,34 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных из CSV{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h2 class="mb-0">Загрузка данных из CSV</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных из CSV{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h2 class="mb-0">Загрузка данных из CSV</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,36 +1,36 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных из Excel{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h2 class="mb-0">Загрузка данных из Excel</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных из Excel{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h2 class="mb-0">Загрузка данных из Excel</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
|
||||
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,42 +1,42 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<title>{% block title %}Геолокация{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Дополнительные стили -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Навигационная панель -->
|
||||
{% include 'mainapp/components/_navbar.html' %}
|
||||
|
||||
<!-- Сообщения -->
|
||||
<div class="container mt-3">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
|
||||
|
||||
<!-- Дополнительные скрипты -->
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<title>{% block title %}Геолокация{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Дополнительные стили -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Навигационная панель -->
|
||||
{% include 'mainapp/components/_navbar.html' %}
|
||||
|
||||
<!-- Сообщения -->
|
||||
<div class="container mt-3">
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
|
||||
|
||||
<!-- Дополнительные скрипты -->
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,33 +1,33 @@
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для отображения полей формы
|
||||
Использование:
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.field_name %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
|
||||
{% endcomment %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
|
||||
{{ field.label }}
|
||||
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="form-check">
|
||||
{{ field }}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для отображения полей формы
|
||||
Использование:
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.field_name %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
|
||||
{% endcomment %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
|
||||
{{ field.label }}
|
||||
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="form-check">
|
||||
{{ field }}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для отображения сообщений Django
|
||||
Использование:
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
{% endcomment %}
|
||||
|
||||
{% if messages %}
|
||||
<div class="messages-container">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{% if message.tags == 'error' %}
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
{% elif message.tags == 'success' %}
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
{% elif message.tags == 'warning' %}
|
||||
<i class="bi bi-exclamation-circle-fill me-2"></i>
|
||||
{% elif message.tags == 'info' %}
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
{% endif %}
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для отображения сообщений Django
|
||||
Использование:
|
||||
{% include 'mainapp/components/_messages.html' %}
|
||||
{% endcomment %}
|
||||
|
||||
{% if messages %}
|
||||
<div class="messages-container">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{% if message.tags == 'error' %}
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
{% elif message.tags == 'success' %}
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
{% elif message.tags == 'warning' %}
|
||||
<i class="bi bi-exclamation-circle-fill me-2"></i>
|
||||
{% elif message.tags == 'info' %}
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
{% endif %}
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
{% comment %}
|
||||
Переиспользуемый компонент навигационной панели
|
||||
Использование:
|
||||
{% include 'mainapp/components/_navbar.html' %}
|
||||
{% endcomment %}
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
{% if user.is_authenticated %}
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
{% if user.first_name and user.last_name %}
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
{% elif user.get_full_name %}
|
||||
{{ user.get_full_name }}
|
||||
{% else %}
|
||||
{{ user.username }}
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'login' %}">Войти</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% comment %}
|
||||
Переиспользуемый компонент навигационной панели
|
||||
Использование:
|
||||
{% include 'mainapp/components/_navbar.html' %}
|
||||
{% endcomment %}
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
{% if user.is_authenticated %}
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
{% if user.first_name and user.last_name %}
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
{% elif user.get_full_name %}
|
||||
{{ user.get_full_name }}
|
||||
{% else %}
|
||||
{{ user.username }}
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'login' %}">Войти</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для заголовков таблиц с сортировкой
|
||||
Использование:
|
||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
|
||||
{% endcomment %}
|
||||
|
||||
<th scope="col">
|
||||
{% if sortable != False %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == field %}-{{ field }}{% elif sort == '-'|add:field %}{{ field }}{% else %}{{ field }}{% endif %}"
|
||||
class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
{{ label }}
|
||||
{% if sort == field %}
|
||||
<i class="bi bi-sort-up ms-1"></i>
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-white ms-1" title="Сбросить сортировку">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% elif sort == '-'|add:field %}
|
||||
<i class="bi bi-sort-down ms-1"></i>
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-white ms-1" title="Сбросить сортировку">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="bi bi-arrow-down-up ms-1"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% comment %}
|
||||
Переиспользуемый компонент для заголовков таблиц с сортировкой
|
||||
Использование:
|
||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
|
||||
{% endcomment %}
|
||||
|
||||
<th scope="col">
|
||||
{% if sortable != False %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == field %}-{{ field }}{% elif sort == '-'|add:field %}{{ field }}{% else %}{{ field }}{% endif %}"
|
||||
class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
{{ label }}
|
||||
{% if sort == field %}
|
||||
<i class="bi bi-sort-up ms-1"></i>
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-white ms-1" title="Сбросить сортировку">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% elif sort == '-'|add:field %}
|
||||
<i class="bi bi-sort-down ms-1"></i>
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-white ms-1" title="Сбросить сортировку">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="bi bi-arrow-down-up ms-1"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
</th>
|
||||
|
||||
@@ -1,118 +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 %}
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Привязка ВЧ{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h2 class="mb-0">Привязка ВЧ загрузки</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Введите допустимый разброс для частоты и полосы</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
|
||||
{{ form.ku_range }}
|
||||
{% if form.ku_range.errors %}
|
||||
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
|
||||
{{ form.value1 }}
|
||||
{% if form.value1.errors %}
|
||||
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
|
||||
{{ form.value2 }}
|
||||
{% if form.value2.errors %}
|
||||
<div class="text-danger mt-1">{{ form.value2.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
{% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
|
||||
<button type="submit" class="btn btn-info">Выполнить привязку</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Привязка ВЧ{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h2 class="mb-0">Привязка ВЧ загрузки</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Введите допустимый разброс для частоты и полосы</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
|
||||
{{ form.ku_range }}
|
||||
{% if form.ku_range.errors %}
|
||||
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
|
||||
{{ form.value1 }}
|
||||
{% if form.value1.errors %}
|
||||
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
|
||||
{{ form.value2 }}
|
||||
{% if form.value2.errors %}
|
||||
<div class="text-danger mt-1">{{ form.value2.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
{% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
|
||||
<button type="submit" class="btn btn-info">Выполнить привязку</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,241 +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 %}
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Удалить объект{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Удалить объект "{{ object }}"?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Удалить объект{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Удалить объект "{{ object }}"?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,473 +1,473 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
|
||||
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
||||
.btn-action { margin-right: 0.5rem; }
|
||||
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
||||
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
||||
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
|
||||
.form-check-input { margin-top: 0.25rem; }
|
||||
.datetime-group { display: flex; gap: 1rem; }
|
||||
.datetime-group > div { flex: 1; }
|
||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
||||
.map-container { margin-bottom: 1rem; }
|
||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
||||
.map-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-control-btn.active {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
.map-control-btn.edit {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
.map-control-btn.save {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
.map-control-btn.cancel {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
.leaflet-marker-icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Просмотр объекта: {{ object.name }}</h2>
|
||||
<div>
|
||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Основная информация</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название:</label>
|
||||
<div class="readonly-field">{{ object.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Создан пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата последнего изменения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Изменен пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВЧ загрузка -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>ВЧ загрузка</h4>
|
||||
</div>
|
||||
|
||||
{% if object.parameter_obj %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Частота (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Полоса (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Символьная скорость:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Стандарт:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<p>Нет данных о ВЧ загрузке</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Карта</h4>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Геоданные -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Геоданные</h4>
|
||||
</div>
|
||||
|
||||
{% if object.geo_obj %}
|
||||
<!-- Координаты геолокации -->
|
||||
<div class="coord-sync-group">
|
||||
<div class="coord-group-header">Координаты геолокации</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты Кубсата -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты Кубсата</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты оперативников -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты оперативников</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Местоположение:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата и время:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние гео-кубсат, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_coords_kup is not None %}
|
||||
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние гео-опер, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_coords_valid is not None %}
|
||||
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние кубсат-опер, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_kup_valid is not None %}
|
||||
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Нет данных о геолокации</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<!-- Подключаем Leaflet и его плагины -->
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация карты
|
||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Определяем цвета для маркеров
|
||||
const colors = {
|
||||
geo: 'blue',
|
||||
kupsat: 'red',
|
||||
valid: 'green'
|
||||
};
|
||||
|
||||
// Функция для создания иконки маркера
|
||||
function createMarkerIcon(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
|
||||
// Маркеры
|
||||
const markers = {};
|
||||
function createMarker(position, color, name) {
|
||||
const marker = L.marker(position, {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(color),
|
||||
title: name
|
||||
}).addTo(map);
|
||||
marker.bindPopup(name);
|
||||
return marker;
|
||||
}
|
||||
|
||||
// Получаем координаты из данных объекта
|
||||
{% if object.geo_obj and object.geo_obj.coords %}
|
||||
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
|
||||
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
|
||||
{% else %}
|
||||
const geoLat = 55.75;
|
||||
const geoLng = 37.62;
|
||||
{% endif %}
|
||||
|
||||
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
|
||||
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
|
||||
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
|
||||
{% else %}
|
||||
const kupsatLat = 55.75;
|
||||
const kupsatLng = 37.61;
|
||||
{% endif %}
|
||||
|
||||
{% if object.geo_obj and object.geo_obj.coords_valid %}
|
||||
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
|
||||
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
|
||||
{% else %}
|
||||
const validLat = 55.75;
|
||||
const validLng = 37.63;
|
||||
{% endif %}
|
||||
|
||||
// Создаем маркеры
|
||||
markers.geo = createMarker(
|
||||
[geoLat, geoLng],
|
||||
colors.geo,
|
||||
'Геолокация'
|
||||
);
|
||||
|
||||
markers.kupsat = createMarker(
|
||||
[kupsatLat, kupsatLng],
|
||||
colors.kupsat,
|
||||
'Кубсат'
|
||||
);
|
||||
|
||||
markers.valid = createMarker(
|
||||
[validLat, validLng],
|
||||
colors.valid,
|
||||
'Оперативник'
|
||||
);
|
||||
|
||||
// Центрируем карту на первом маркере
|
||||
if (map.hasLayer(markers.geo)) {
|
||||
map.setView(markers.geo.getLatLng(), 10);
|
||||
}
|
||||
|
||||
// Легенда
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
|
||||
legend.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.fontSize = '14px';
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.padding = '10px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||
div.innerHTML = `
|
||||
<h5>Легенда</h5>
|
||||
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
||||
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
|
||||
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
});
|
||||
</script>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
|
||||
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
||||
.btn-action { margin-right: 0.5rem; }
|
||||
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
||||
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
||||
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
|
||||
.form-check-input { margin-top: 0.25rem; }
|
||||
.datetime-group { display: flex; gap: 1rem; }
|
||||
.datetime-group > div { flex: 1; }
|
||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
||||
.map-container { margin-bottom: 1rem; }
|
||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
||||
.map-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-control-btn.active {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
.map-control-btn.edit {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
.map-control-btn.save {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
.map-control-btn.cancel {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
.leaflet-marker-icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Просмотр объекта: {{ object.name }}</h2>
|
||||
<div>
|
||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Основная информация</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название:</label>
|
||||
<div class="readonly-field">{{ object.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Создан пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата последнего изменения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Изменен пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВЧ загрузка -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>ВЧ загрузка</h4>
|
||||
</div>
|
||||
|
||||
{% if object.parameter_obj %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Частота (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Полоса (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Символьная скорость:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Стандарт:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<p>Нет данных о ВЧ загрузке</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Карта</h4>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Геоданные -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Геоданные</h4>
|
||||
</div>
|
||||
|
||||
{% if object.geo_obj %}
|
||||
<!-- Координаты геолокации -->
|
||||
<div class="coord-sync-group">
|
||||
<div class="coord-group-header">Координаты геолокации</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты Кубсата -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты Кубсата</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты оперативников -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты оперативников</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Местоположение:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата и время:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние гео-кубсат, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_coords_kup is not None %}
|
||||
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние гео-опер, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_coords_valid is not None %}
|
||||
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние кубсат-опер, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_kup_valid is not None %}
|
||||
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Нет данных о геолокации</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<!-- Подключаем Leaflet и его плагины -->
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация карты
|
||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Определяем цвета для маркеров
|
||||
const colors = {
|
||||
geo: 'blue',
|
||||
kupsat: 'red',
|
||||
valid: 'green'
|
||||
};
|
||||
|
||||
// Функция для создания иконки маркера
|
||||
function createMarkerIcon(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
|
||||
// Маркеры
|
||||
const markers = {};
|
||||
function createMarker(position, color, name) {
|
||||
const marker = L.marker(position, {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(color),
|
||||
title: name
|
||||
}).addTo(map);
|
||||
marker.bindPopup(name);
|
||||
return marker;
|
||||
}
|
||||
|
||||
// Получаем координаты из данных объекта
|
||||
{% if object.geo_obj and object.geo_obj.coords %}
|
||||
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
|
||||
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
|
||||
{% else %}
|
||||
const geoLat = 55.75;
|
||||
const geoLng = 37.62;
|
||||
{% endif %}
|
||||
|
||||
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
|
||||
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
|
||||
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
|
||||
{% else %}
|
||||
const kupsatLat = 55.75;
|
||||
const kupsatLng = 37.61;
|
||||
{% endif %}
|
||||
|
||||
{% if object.geo_obj and object.geo_obj.coords_valid %}
|
||||
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
|
||||
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
|
||||
{% else %}
|
||||
const validLat = 55.75;
|
||||
const validLng = 37.63;
|
||||
{% endif %}
|
||||
|
||||
// Создаем маркеры
|
||||
markers.geo = createMarker(
|
||||
[geoLat, geoLng],
|
||||
colors.geo,
|
||||
'Геолокация'
|
||||
);
|
||||
|
||||
markers.kupsat = createMarker(
|
||||
[kupsatLat, kupsatLng],
|
||||
colors.kupsat,
|
||||
'Кубсат'
|
||||
);
|
||||
|
||||
markers.valid = createMarker(
|
||||
[validLat, validLng],
|
||||
colors.valid,
|
||||
'Оперативник'
|
||||
);
|
||||
|
||||
// Центрируем карту на первом маркере
|
||||
if (map.hasLayer(markers.geo)) {
|
||||
map.setView(markers.geo.getLatLng(), 10);
|
||||
}
|
||||
|
||||
// Легенда
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
|
||||
legend.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.fontSize = '14px';
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.padding = '10px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||
div.innerHTML = `
|
||||
<h5>Легенда</h5>
|
||||
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
||||
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
|
||||
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,52 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Новое событие{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% comment%}
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>{% endcomment %}
|
||||
|
||||
{% comment %} <div class="mb-4">
|
||||
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
|
||||
{{ form.pol_choice }}
|
||||
{% if form.pol_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Выберите файл для загрузки</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Выполнить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Новое событие{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% comment%}
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>{% endcomment %}
|
||||
|
||||
{% comment %} <div class="mb-4">
|
||||
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
|
||||
{{ form.pol_choice }}
|
||||
{% if form.pol_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Выберите файл для загрузки</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Выполнить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,53 +1,53 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных транспондеров{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
|
||||
</div>
|
||||
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-warning">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных транспондеров{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
|
||||
</div>
|
||||
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-warning">Добавить в базу</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,56 +1,56 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных ВЧ загрузки{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-danger">Обработать файл</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Загрузка данных ВЧ загрузки{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
|
||||
{{ form.file }}
|
||||
{% if form.file.errors %}
|
||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||
{{ form.sat_choice }}
|
||||
{% if form.sat_choice.errors %}
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-danger">Обработать файл</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,3 @@
|
||||
"""
|
||||
Template tags для mainapp.
|
||||
"""
|
||||
"""
|
||||
Template tags для mainapp.
|
||||
"""
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
"""
|
||||
Пользовательские фильтры шаблонов для форматирования координат.
|
||||
|
||||
Этот модуль содержит фильтры Django для форматирования географических координат
|
||||
в читаемый вид в шаблонах.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from typing import Optional
|
||||
|
||||
# Django imports
|
||||
from django import template
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='format_coords')
|
||||
def format_coords(point: Optional[Point]) -> str:
|
||||
"""
|
||||
Форматирует объект Point в читаемую строку координат.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
str: Отформатированная строка координат в формате "XXN/S YYE/W"
|
||||
или "-" если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|format_coords }}
|
||||
|
||||
Результат:
|
||||
"55.75N 37.62E"
|
||||
"""
|
||||
if not point:
|
||||
return "-"
|
||||
|
||||
try:
|
||||
longitude = point.coords[0]
|
||||
latitude = point.coords[1]
|
||||
|
||||
lon_direction = "E" if longitude > 0 else "W"
|
||||
lat_direction = "N" if latitude > 0 else "S"
|
||||
|
||||
lon_value = abs(longitude)
|
||||
lat_value = abs(latitude)
|
||||
|
||||
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return "-"
|
||||
|
||||
|
||||
@register.filter(name='format_coords_decimal')
|
||||
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
|
||||
"""
|
||||
Форматирует объект Point в десятичные координаты с заданной точностью.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
precision (int): Количество знаков после запятой (по умолчанию 6).
|
||||
|
||||
Returns:
|
||||
str: Отформатированная строка координат в формате "lat, lon"
|
||||
или "-" если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|format_coords_decimal:4 }}
|
||||
|
||||
Результат:
|
||||
"55.7500, 37.6200"
|
||||
"""
|
||||
if not point:
|
||||
return "-"
|
||||
|
||||
try:
|
||||
longitude = point.coords[0]
|
||||
latitude = point.coords[1]
|
||||
|
||||
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
|
||||
return format_str.format(latitude, longitude)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return "-"
|
||||
|
||||
|
||||
@register.filter(name='coords_to_lat')
|
||||
def coords_to_lat(point: Optional[Point]) -> Optional[float]:
|
||||
"""
|
||||
Извлекает широту из объекта Point.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
float: Значение широты или None если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|coords_to_lat }}
|
||||
"""
|
||||
if not point:
|
||||
return None
|
||||
|
||||
try:
|
||||
return point.coords[1]
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
@register.filter(name='coords_to_lon')
|
||||
def coords_to_lon(point: Optional[Point]) -> Optional[float]:
|
||||
"""
|
||||
Извлекает долготу из объекта Point.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
float: Значение долготы или None если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|coords_to_lon }}
|
||||
"""
|
||||
if not point:
|
||||
return None
|
||||
|
||||
try:
|
||||
return point.coords[0]
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return None
|
||||
"""
|
||||
Пользовательские фильтры шаблонов для форматирования координат.
|
||||
|
||||
Этот модуль содержит фильтры Django для форматирования географических координат
|
||||
в читаемый вид в шаблонах.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from typing import Optional
|
||||
|
||||
# Django imports
|
||||
from django import template
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='format_coords')
|
||||
def format_coords(point: Optional[Point]) -> str:
|
||||
"""
|
||||
Форматирует объект Point в читаемую строку координат.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
str: Отформатированная строка координат в формате "XXN/S YYE/W"
|
||||
или "-" если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|format_coords }}
|
||||
|
||||
Результат:
|
||||
"55.75N 37.62E"
|
||||
"""
|
||||
if not point:
|
||||
return "-"
|
||||
|
||||
try:
|
||||
longitude = point.coords[0]
|
||||
latitude = point.coords[1]
|
||||
|
||||
lon_direction = "E" if longitude > 0 else "W"
|
||||
lat_direction = "N" if latitude > 0 else "S"
|
||||
|
||||
lon_value = abs(longitude)
|
||||
lat_value = abs(latitude)
|
||||
|
||||
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return "-"
|
||||
|
||||
|
||||
@register.filter(name='format_coords_decimal')
|
||||
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
|
||||
"""
|
||||
Форматирует объект Point в десятичные координаты с заданной точностью.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
precision (int): Количество знаков после запятой (по умолчанию 6).
|
||||
|
||||
Returns:
|
||||
str: Отформатированная строка координат в формате "lat, lon"
|
||||
или "-" если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|format_coords_decimal:4 }}
|
||||
|
||||
Результат:
|
||||
"55.7500, 37.6200"
|
||||
"""
|
||||
if not point:
|
||||
return "-"
|
||||
|
||||
try:
|
||||
longitude = point.coords[0]
|
||||
latitude = point.coords[1]
|
||||
|
||||
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
|
||||
return format_str.format(latitude, longitude)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return "-"
|
||||
|
||||
|
||||
@register.filter(name='coords_to_lat')
|
||||
def coords_to_lat(point: Optional[Point]) -> Optional[float]:
|
||||
"""
|
||||
Извлекает широту из объекта Point.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
float: Значение широты или None если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|coords_to_lat }}
|
||||
"""
|
||||
if not point:
|
||||
return None
|
||||
|
||||
try:
|
||||
return point.coords[1]
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
@register.filter(name='coords_to_lon')
|
||||
def coords_to_lon(point: Optional[Point]) -> Optional[float]:
|
||||
"""
|
||||
Извлекает долготу из объекта Point.
|
||||
|
||||
Args:
|
||||
point (Point): Объект Point из GeoDjango или None.
|
||||
|
||||
Returns:
|
||||
float: Значение долготы или None если point равен None.
|
||||
|
||||
Example:
|
||||
В шаблоне:
|
||||
{{ geo_obj.coords|coords_to_lon }}
|
||||
"""
|
||||
if not point:
|
||||
return None
|
||||
|
||||
try:
|
||||
return point.coords[0]
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
return None
|
||||
|
||||
@@ -1,179 +1,179 @@
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.gis.geos import Point
|
||||
from .models import CustomUser, Geo, ObjItem
|
||||
from .utils import format_coordinates, parse_pagination_params
|
||||
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
|
||||
from django.views import View
|
||||
|
||||
|
||||
class FormatCoordinatesTestCase(TestCase):
|
||||
"""Тесты для функции format_coordinates"""
|
||||
|
||||
def test_format_positive_coordinates(self):
|
||||
"""Тест форматирования положительных координат"""
|
||||
result = format_coordinates(37.62, 55.75)
|
||||
self.assertEqual(result, "55.75N 37.62E")
|
||||
|
||||
def test_format_negative_longitude(self):
|
||||
"""Тест форматирования с отрицательной долготой"""
|
||||
result = format_coordinates(-122.42, 37.77)
|
||||
self.assertEqual(result, "37.77N 122.42W")
|
||||
|
||||
def test_format_negative_latitude(self):
|
||||
"""Тест форматирования с отрицательной широтой"""
|
||||
result = format_coordinates(151.21, -33.87)
|
||||
self.assertEqual(result, "33.87S 151.21E")
|
||||
|
||||
def test_format_both_negative(self):
|
||||
"""Тест форматирования с обеими отрицательными координатами"""
|
||||
result = format_coordinates(-58.38, -34.60)
|
||||
self.assertEqual(result, "34.6S 58.38W")
|
||||
|
||||
|
||||
class ParsePaginationParamsTestCase(TestCase):
|
||||
"""Тесты для функции parse_pagination_params"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_default_values(self):
|
||||
"""Тест значений по умолчанию"""
|
||||
request = self.factory.get("/")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
self.assertEqual(per_page, 50)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Тест пользовательских значений"""
|
||||
request = self.factory.get("/?page=3&items_per_page=100")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 3)
|
||||
self.assertEqual(per_page, 100)
|
||||
|
||||
def test_invalid_page_number(self):
|
||||
"""Тест невалидного номера страницы"""
|
||||
request = self.factory.get("/?page=invalid")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
|
||||
def test_negative_page_number(self):
|
||||
"""Тест отрицательного номера страницы"""
|
||||
request = self.factory.get("/?page=-5")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
|
||||
def test_max_items_per_page_limit(self):
|
||||
"""Тест ограничения максимального количества элементов"""
|
||||
request = self.factory.get("/?items_per_page=20000")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(per_page, 10000)
|
||||
|
||||
|
||||
class RoleRequiredMixinTestCase(TestCase):
|
||||
"""Тесты для RoleRequiredMixin"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_admin_has_access(self):
|
||||
"""Тест что администратор имеет доступ"""
|
||||
user = User.objects.create_user(username="testuser", password="12345")
|
||||
# Get the automatically created CustomUser and set role to 'admin'
|
||||
custom_user = CustomUser.objects.get(user=user)
|
||||
custom_user.role = "admin"
|
||||
custom_user.save()
|
||||
|
||||
# Refresh user to get updated customuser
|
||||
user.refresh_from_db()
|
||||
|
||||
class TestView(RoleRequiredMixin, View):
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
view.request = request
|
||||
|
||||
self.assertTrue(view.test_func())
|
||||
|
||||
def test_user_without_role_denied(self):
|
||||
"""Тест что пользователь без роли не имеет доступа"""
|
||||
user_no_role = User.objects.create_user(username="norole", password="12345")
|
||||
# Get the automatically created CustomUser - default role is 'user'
|
||||
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
|
||||
self.assertEqual(custom_user_no_role.role, "user")
|
||||
|
||||
class TestView(RoleRequiredMixin, View):
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.get("/")
|
||||
request.user = user_no_role
|
||||
view.request = request
|
||||
|
||||
self.assertFalse(view.test_func())
|
||||
|
||||
|
||||
class CoordinateProcessingMixinTestCase(TestCase):
|
||||
"""Тесты для CoordinateProcessingMixin"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_extract_geo_coordinates(self):
|
||||
"""Тест извлечения координат геолокации"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
|
||||
)
|
||||
view.request = request
|
||||
|
||||
coords = view._extract_coordinates("geo")
|
||||
self.assertIsNotNone(coords)
|
||||
self.assertEqual(coords, (37.62, 55.75))
|
||||
|
||||
def test_extract_invalid_coordinates(self):
|
||||
"""Тест извлечения невалидных координат"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
|
||||
)
|
||||
view.request = request
|
||||
|
||||
coords = view._extract_coordinates("geo")
|
||||
self.assertIsNone(coords)
|
||||
|
||||
def test_process_coordinates(self):
|
||||
"""Тест обработки координат и применения к объекту Geo"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
{
|
||||
"geo_longitude": "37.62",
|
||||
"geo_latitude": "55.75",
|
||||
"kupsat_longitude": "37.63",
|
||||
"kupsat_latitude": "55.76",
|
||||
},
|
||||
)
|
||||
view.request = request
|
||||
|
||||
geo_instance = Geo()
|
||||
view.process_coordinates(geo_instance)
|
||||
|
||||
self.assertIsNotNone(geo_instance.coords)
|
||||
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
|
||||
self.assertIsNotNone(geo_instance.coords_kupsat)
|
||||
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.gis.geos import Point
|
||||
from .models import CustomUser, Geo, ObjItem
|
||||
from .utils import format_coordinates, parse_pagination_params
|
||||
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
|
||||
from django.views import View
|
||||
|
||||
|
||||
class FormatCoordinatesTestCase(TestCase):
|
||||
"""Тесты для функции format_coordinates"""
|
||||
|
||||
def test_format_positive_coordinates(self):
|
||||
"""Тест форматирования положительных координат"""
|
||||
result = format_coordinates(37.62, 55.75)
|
||||
self.assertEqual(result, "55.75N 37.62E")
|
||||
|
||||
def test_format_negative_longitude(self):
|
||||
"""Тест форматирования с отрицательной долготой"""
|
||||
result = format_coordinates(-122.42, 37.77)
|
||||
self.assertEqual(result, "37.77N 122.42W")
|
||||
|
||||
def test_format_negative_latitude(self):
|
||||
"""Тест форматирования с отрицательной широтой"""
|
||||
result = format_coordinates(151.21, -33.87)
|
||||
self.assertEqual(result, "33.87S 151.21E")
|
||||
|
||||
def test_format_both_negative(self):
|
||||
"""Тест форматирования с обеими отрицательными координатами"""
|
||||
result = format_coordinates(-58.38, -34.60)
|
||||
self.assertEqual(result, "34.6S 58.38W")
|
||||
|
||||
|
||||
class ParsePaginationParamsTestCase(TestCase):
|
||||
"""Тесты для функции parse_pagination_params"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_default_values(self):
|
||||
"""Тест значений по умолчанию"""
|
||||
request = self.factory.get("/")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
self.assertEqual(per_page, 50)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Тест пользовательских значений"""
|
||||
request = self.factory.get("/?page=3&items_per_page=100")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 3)
|
||||
self.assertEqual(per_page, 100)
|
||||
|
||||
def test_invalid_page_number(self):
|
||||
"""Тест невалидного номера страницы"""
|
||||
request = self.factory.get("/?page=invalid")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
|
||||
def test_negative_page_number(self):
|
||||
"""Тест отрицательного номера страницы"""
|
||||
request = self.factory.get("/?page=-5")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(page, 1)
|
||||
|
||||
def test_max_items_per_page_limit(self):
|
||||
"""Тест ограничения максимального количества элементов"""
|
||||
request = self.factory.get("/?items_per_page=20000")
|
||||
page, per_page = parse_pagination_params(request)
|
||||
self.assertEqual(per_page, 10000)
|
||||
|
||||
|
||||
class RoleRequiredMixinTestCase(TestCase):
|
||||
"""Тесты для RoleRequiredMixin"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_admin_has_access(self):
|
||||
"""Тест что администратор имеет доступ"""
|
||||
user = User.objects.create_user(username="testuser", password="12345")
|
||||
# Get the automatically created CustomUser and set role to 'admin'
|
||||
custom_user = CustomUser.objects.get(user=user)
|
||||
custom_user.role = "admin"
|
||||
custom_user.save()
|
||||
|
||||
# Refresh user to get updated customuser
|
||||
user.refresh_from_db()
|
||||
|
||||
class TestView(RoleRequiredMixin, View):
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
view.request = request
|
||||
|
||||
self.assertTrue(view.test_func())
|
||||
|
||||
def test_user_without_role_denied(self):
|
||||
"""Тест что пользователь без роли не имеет доступа"""
|
||||
user_no_role = User.objects.create_user(username="norole", password="12345")
|
||||
# Get the automatically created CustomUser - default role is 'user'
|
||||
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
|
||||
self.assertEqual(custom_user_no_role.role, "user")
|
||||
|
||||
class TestView(RoleRequiredMixin, View):
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.get("/")
|
||||
request.user = user_no_role
|
||||
view.request = request
|
||||
|
||||
self.assertFalse(view.test_func())
|
||||
|
||||
|
||||
class CoordinateProcessingMixinTestCase(TestCase):
|
||||
"""Тесты для CoordinateProcessingMixin"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_extract_geo_coordinates(self):
|
||||
"""Тест извлечения координат геолокации"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
|
||||
)
|
||||
view.request = request
|
||||
|
||||
coords = view._extract_coordinates("geo")
|
||||
self.assertIsNotNone(coords)
|
||||
self.assertEqual(coords, (37.62, 55.75))
|
||||
|
||||
def test_extract_invalid_coordinates(self):
|
||||
"""Тест извлечения невалидных координат"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
|
||||
)
|
||||
view.request = request
|
||||
|
||||
coords = view._extract_coordinates("geo")
|
||||
self.assertIsNone(coords)
|
||||
|
||||
def test_process_coordinates(self):
|
||||
"""Тест обработки координат и применения к объекту Geo"""
|
||||
|
||||
class TestView(CoordinateProcessingMixin, View):
|
||||
pass
|
||||
|
||||
view = TestView()
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
{
|
||||
"geo_longitude": "37.62",
|
||||
"geo_latitude": "55.75",
|
||||
"kupsat_longitude": "37.63",
|
||||
"kupsat_latitude": "55.76",
|
||||
},
|
||||
)
|
||||
view.request = request
|
||||
|
||||
geo_instance = Geo()
|
||||
view.process_coordinates(geo_instance)
|
||||
|
||||
self.assertIsNotNone(geo_instance.coords)
|
||||
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
|
||||
self.assertIsNotNone(geo_instance.coords_kupsat)
|
||||
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
|
||||
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
|
||||
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
|
||||
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
|
||||
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
|
||||
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
|
||||
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
|
||||
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
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>/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'),
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
|
||||
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
|
||||
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
|
||||
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
|
||||
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
|
||||
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
|
||||
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
|
||||
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
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>/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'),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
# Django imports
|
||||
from django.contrib import admin
|
||||
|
||||
# Third-party imports
|
||||
from import_export.admin import ImportExportActionModelAdmin
|
||||
from more_admin_filters import MultiSelectRelatedDropdownFilter
|
||||
from rangefilter.filters import NumericRangeFilterBuilder
|
||||
|
||||
# Local imports
|
||||
from .models import Transponders
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Base Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Базовый класс для всех admin моделей mapsapp.
|
||||
|
||||
Предоставляет общую функциональность:
|
||||
- Кнопки сохранения сверху и снизу
|
||||
- Настройка количества элементов на странице
|
||||
"""
|
||||
save_on_top = True
|
||||
list_per_page = 50
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
@admin.register(Transponders)
|
||||
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели Transponders.
|
||||
|
||||
Оптимизирована для работы с транспондерами:
|
||||
- Использует select_related для оптимизации запросов
|
||||
- Предоставляет фильтры по спутникам, поляризации и зоне
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
list_display = (
|
||||
"sat_id",
|
||||
"name",
|
||||
"zone_name",
|
||||
"downlink",
|
||||
"uplink",
|
||||
"frequency_range",
|
||||
"transfer",
|
||||
"polarization",
|
||||
)
|
||||
list_display_links = ("name",)
|
||||
list_select_related = ("polarization", "sat_id")
|
||||
|
||||
list_filter = (
|
||||
("polarization", MultiSelectRelatedDropdownFilter),
|
||||
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||
("downlink", NumericRangeFilterBuilder()),
|
||||
("uplink", NumericRangeFilterBuilder()),
|
||||
("frequency_range", NumericRangeFilterBuilder()),
|
||||
"zone_name",
|
||||
)
|
||||
|
||||
search_fields = ("name", "sat_id__name", "zone_name")
|
||||
ordering = ("name",)
|
||||
autocomplete_fields = ("sat_id", "polarization")
|
||||
# Django imports
|
||||
from django.contrib import admin
|
||||
|
||||
# Third-party imports
|
||||
from import_export.admin import ImportExportActionModelAdmin
|
||||
from more_admin_filters import MultiSelectRelatedDropdownFilter
|
||||
from rangefilter.filters import NumericRangeFilterBuilder
|
||||
|
||||
# Local imports
|
||||
from .models import Transponders
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Base Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Базовый класс для всех admin моделей mapsapp.
|
||||
|
||||
Предоставляет общую функциональность:
|
||||
- Кнопки сохранения сверху и снизу
|
||||
- Настройка количества элементов на странице
|
||||
"""
|
||||
save_on_top = True
|
||||
list_per_page = 50
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
@admin.register(Transponders)
|
||||
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""
|
||||
Админ-панель для модели Transponders.
|
||||
|
||||
Оптимизирована для работы с транспондерами:
|
||||
- Использует select_related для оптимизации запросов
|
||||
- Предоставляет фильтры по спутникам, поляризации и зоне
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
list_display = (
|
||||
"sat_id",
|
||||
"name",
|
||||
"zone_name",
|
||||
"downlink",
|
||||
"uplink",
|
||||
"frequency_range",
|
||||
"transfer",
|
||||
"polarization",
|
||||
)
|
||||
list_display_links = ("name",)
|
||||
list_select_related = ("polarization", "sat_id")
|
||||
|
||||
list_filter = (
|
||||
("polarization", MultiSelectRelatedDropdownFilter),
|
||||
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||
("downlink", NumericRangeFilterBuilder()),
|
||||
("uplink", NumericRangeFilterBuilder()),
|
||||
("frequency_range", NumericRangeFilterBuilder()),
|
||||
"zone_name",
|
||||
)
|
||||
|
||||
search_fields = ("name", "sat_id__name", "zone_name")
|
||||
ordering = ("name",)
|
||||
autocomplete_fields = ("sat_id", "polarization")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MapsappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mapsapp'
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MapsappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mapsapp'
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:36
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
import django.db.models.functions.math
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transponders',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
|
||||
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
|
||||
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
|
||||
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
|
||||
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')),
|
||||
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Транспондер',
|
||||
'verbose_name_plural': 'Транспондеры',
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-10-31 13:36
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
import django.db.models.functions.math
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transponders',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
|
||||
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
|
||||
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
|
||||
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
|
||||
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')),
|
||||
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Транспондер',
|
||||
'verbose_name_plural': 'Транспондеры',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
|
||||
('mapsapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='transponders',
|
||||
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='downlink',
|
||||
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='frequency_range',
|
||||
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='polarization',
|
||||
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='sat_id',
|
||||
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='uplink',
|
||||
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='zone_name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='transponders',
|
||||
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='transponders',
|
||||
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
|
||||
('mapsapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='transponders',
|
||||
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='downlink',
|
||||
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='frequency_range',
|
||||
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='polarization',
|
||||
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='sat_id',
|
||||
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='uplink',
|
||||
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transponders',
|
||||
name='zone_name',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='transponders',
|
||||
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='transponders',
|
||||
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,117 +1,117 @@
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import ExpressionWrapper, F
|
||||
from django.db.models.functions import Abs
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Polarization, Satellite, get_default_polarization
|
||||
|
||||
|
||||
class Transponders(models.Model):
|
||||
"""
|
||||
Модель транспондера спутника.
|
||||
|
||||
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
|
||||
"""
|
||||
|
||||
# Основные поля
|
||||
name = models.CharField(
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Название транспондера",
|
||||
db_index=True,
|
||||
help_text="Название транспондера"
|
||||
)
|
||||
downlink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Downlink",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
help_text="Частота downlink в МГц (0-50000)"
|
||||
)
|
||||
frequency_range = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Полоса",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||
help_text="Полоса частот в МГц (0-1000)"
|
||||
)
|
||||
uplink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Uplink",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
help_text="Частота uplink в МГц (0-50000)"
|
||||
)
|
||||
zone_name = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Название зоны",
|
||||
db_index=True,
|
||||
help_text="Название зоны покрытия транспондера"
|
||||
)
|
||||
|
||||
# Связи
|
||||
polarization = models.ForeignKey(
|
||||
Polarization,
|
||||
default=get_default_polarization,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="tran_polarizations",
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Поляризация",
|
||||
help_text="Поляризация сигнала"
|
||||
)
|
||||
sat_id = models.ForeignKey(
|
||||
Satellite,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="tran_satellite",
|
||||
verbose_name="Спутник",
|
||||
db_index=True,
|
||||
help_text="Спутник, которому принадлежит транспондер"
|
||||
)
|
||||
|
||||
# Вычисляемые поля
|
||||
transfer = models.GeneratedField(
|
||||
expression=ExpressionWrapper(
|
||||
Abs(F('downlink') - F('uplink')),
|
||||
output_field=models.FloatField()
|
||||
),
|
||||
output_field=models.FloatField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Перенос"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""Валидация на уровне модели"""
|
||||
super().clean()
|
||||
|
||||
# Проверка что downlink и uplink заданы
|
||||
if self.downlink and self.uplink:
|
||||
# Обычно uplink выше downlink для спутниковой связи
|
||||
if self.uplink < self.downlink:
|
||||
raise ValidationError({
|
||||
'uplink': 'Частота uplink обычно выше частоты downlink'
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Транспондер"
|
||||
verbose_name_plural = "Транспондеры"
|
||||
ordering = ['sat_id', 'downlink']
|
||||
indexes = [
|
||||
models.Index(fields=['sat_id', 'downlink']),
|
||||
models.Index(fields=['sat_id', 'zone_name']),
|
||||
]
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import ExpressionWrapper, F
|
||||
from django.db.models.functions import Abs
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Polarization, Satellite, get_default_polarization
|
||||
|
||||
|
||||
class Transponders(models.Model):
|
||||
"""
|
||||
Модель транспондера спутника.
|
||||
|
||||
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
|
||||
"""
|
||||
|
||||
# Основные поля
|
||||
name = models.CharField(
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Название транспондера",
|
||||
db_index=True,
|
||||
help_text="Название транспондера"
|
||||
)
|
||||
downlink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Downlink",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
help_text="Частота downlink в МГц (0-50000)"
|
||||
)
|
||||
frequency_range = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Полоса",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||
help_text="Полоса частот в МГц (0-1000)"
|
||||
)
|
||||
uplink = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Uplink",
|
||||
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||
help_text="Частота uplink в МГц (0-50000)"
|
||||
)
|
||||
zone_name = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Название зоны",
|
||||
db_index=True,
|
||||
help_text="Название зоны покрытия транспондера"
|
||||
)
|
||||
|
||||
# Связи
|
||||
polarization = models.ForeignKey(
|
||||
Polarization,
|
||||
default=get_default_polarization,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="tran_polarizations",
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Поляризация",
|
||||
help_text="Поляризация сигнала"
|
||||
)
|
||||
sat_id = models.ForeignKey(
|
||||
Satellite,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="tran_satellite",
|
||||
verbose_name="Спутник",
|
||||
db_index=True,
|
||||
help_text="Спутник, которому принадлежит транспондер"
|
||||
)
|
||||
|
||||
# Вычисляемые поля
|
||||
transfer = models.GeneratedField(
|
||||
expression=ExpressionWrapper(
|
||||
Abs(F('downlink') - F('uplink')),
|
||||
output_field=models.FloatField()
|
||||
),
|
||||
output_field=models.FloatField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Перенос"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""Валидация на уровне модели"""
|
||||
super().clean()
|
||||
|
||||
# Проверка что downlink и uplink заданы
|
||||
if self.downlink and self.uplink:
|
||||
# Обычно uplink выше downlink для спутниковой связи
|
||||
if self.uplink < self.downlink:
|
||||
raise ValidationError({
|
||||
'uplink': 'Частота uplink обычно выше частоты downlink'
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Транспондер"
|
||||
verbose_name_plural = "Транспондеры"
|
||||
ordering = ['sat_id', 'downlink']
|
||||
indexes = [
|
||||
models.Index(fields=['sat_id', 'downlink']),
|
||||
models.Index(fields=['sat_id', 'zone_name']),
|
||||
]
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,83 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block title %}Карта{% endblock %}</title>
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Extra CSS -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
let map = L.map('map').setView([0, 0], 2);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
street_local.addTo(map);
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
|
||||
let imageBounds = [[-82, -180], [82, 180]];
|
||||
|
||||
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
|
||||
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block title %}Карта{% endblock %}</title>
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Extra CSS -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
let map = L.map('map').setView([0, 0], 2);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
street_local.addTo(map);
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
|
||||
let imageBounds = [[-82, -180], [82, 180]];
|
||||
|
||||
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
|
||||
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,118 +1,118 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<title>Cesium Map Editor</title>
|
||||
<!-- Cesium Library -->
|
||||
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
|
||||
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
|
||||
<!-- Панель инструментов -->
|
||||
<div class="toolbar">
|
||||
<!-- Группа 1: Режимы рисования -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Рисование</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
|
||||
<span>🔍</span> Выделение
|
||||
</button>
|
||||
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
|
||||
<span>📌</span> Маркер
|
||||
</button>
|
||||
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
|
||||
<span>⬢</span> Полигон
|
||||
</button>
|
||||
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
|
||||
<span>〰️</span> Линия
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 2: Импорт/Экспорт -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Импорт/экспорт всех объектов</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
|
||||
<span>📥</span> Импорт
|
||||
</button>
|
||||
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
|
||||
<span>📤</span> Экспорт
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 3: Действия -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Действия</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
|
||||
<span>🗑️</span> Удалить
|
||||
</button>
|
||||
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
|
||||
<span>🧹</span> Очистить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Строка состояния -->
|
||||
<div class="status-bar">
|
||||
<span id="modeStatus">Режим: Выделение</span>
|
||||
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
|
||||
<span id="hint">Нажмите ESC для отмены</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок выбора объектов из БД -->
|
||||
<div class="db-objects-panel">
|
||||
<div class="panel-title">Объекты из базы</div>
|
||||
<select id="objectSelector" class="object-select">
|
||||
<option value="">— Выберите объект —</option>
|
||||
{% for sat in sats %}
|
||||
<option value="{{sat.id}}">{{sat.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
|
||||
</div>
|
||||
|
||||
<div class="footprint-control">
|
||||
<div class="panel-title">Области покрытия</div>
|
||||
<div class="footprint-actions">
|
||||
<button id="showAllFootprints">Показать все</button>
|
||||
<button id="hideAllFootprints">Скрыть все</button>
|
||||
</div>
|
||||
<div id="footprintToggles"></div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для описания -->
|
||||
<div id="descriptionModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Добавить описание</h3>
|
||||
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
|
||||
<div class="modal-buttons">
|
||||
<button id="confirmDescription">Сохранить</button>
|
||||
<button id="cancelDescription">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exportModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Экспорт данных</h3>
|
||||
<p>Выберите формат для экспорта всех объектов:</p>
|
||||
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
|
||||
<button id="exportGeoJson">GeoJSON</button>
|
||||
<button id="exportKml">KML</button>
|
||||
<button id="cancelExport">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'mapsapp/main.js' %}"></script>
|
||||
</body>
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<title>Cesium Map Editor</title>
|
||||
<!-- Cesium Library -->
|
||||
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
|
||||
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
|
||||
<!-- Панель инструментов -->
|
||||
<div class="toolbar">
|
||||
<!-- Группа 1: Режимы рисования -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Рисование</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
|
||||
<span>🔍</span> Выделение
|
||||
</button>
|
||||
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
|
||||
<span>📌</span> Маркер
|
||||
</button>
|
||||
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
|
||||
<span>⬢</span> Полигон
|
||||
</button>
|
||||
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
|
||||
<span>〰️</span> Линия
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 2: Импорт/Экспорт -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Импорт/экспорт всех объектов</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
|
||||
<span>📥</span> Импорт
|
||||
</button>
|
||||
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
|
||||
<span>📤</span> Экспорт
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 3: Действия -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Действия</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
|
||||
<span>🗑️</span> Удалить
|
||||
</button>
|
||||
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
|
||||
<span>🧹</span> Очистить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Строка состояния -->
|
||||
<div class="status-bar">
|
||||
<span id="modeStatus">Режим: Выделение</span>
|
||||
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
|
||||
<span id="hint">Нажмите ESC для отмены</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок выбора объектов из БД -->
|
||||
<div class="db-objects-panel">
|
||||
<div class="panel-title">Объекты из базы</div>
|
||||
<select id="objectSelector" class="object-select">
|
||||
<option value="">— Выберите объект —</option>
|
||||
{% for sat in sats %}
|
||||
<option value="{{sat.id}}">{{sat.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
|
||||
</div>
|
||||
|
||||
<div class="footprint-control">
|
||||
<div class="panel-title">Области покрытия</div>
|
||||
<div class="footprint-actions">
|
||||
<button id="showAllFootprints">Показать все</button>
|
||||
<button id="hideAllFootprints">Скрыть все</button>
|
||||
</div>
|
||||
<div id="footprintToggles"></div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для описания -->
|
||||
<div id="descriptionModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Добавить описание</h3>
|
||||
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
|
||||
<div class="modal-buttons">
|
||||
<button id="confirmDescription">Сохранить</button>
|
||||
<button id="cancelDescription">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exportModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Экспорт данных</h3>
|
||||
<p>Выберите формат для экспорта всех объектов:</p>
|
||||
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
|
||||
<button id="exportGeoJson">GeoJSON</button>
|
||||
<button id="exportKml">KML</button>
|
||||
<button id="cancelExport">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'mapsapp/main.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'mapsapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
|
||||
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
|
||||
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
|
||||
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
|
||||
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'mapsapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
|
||||
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
|
||||
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
|
||||
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
|
||||
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
|
||||
]
|
||||
@@ -1,165 +1,165 @@
|
||||
# Standard library imports
|
||||
import json
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Polarization, Satellite
|
||||
|
||||
from .models import Transponders
|
||||
|
||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||
for pos, value in data.get('page', {}).get('positions').items():
|
||||
for name in value['satellites']:
|
||||
if name['other_names'] is None:
|
||||
name['other_names'] = ''
|
||||
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
|
||||
return pos, name['id']
|
||||
return '', ''
|
||||
|
||||
def get_footprint_data(position: str = 62) -> dict:
|
||||
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
||||
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
|
||||
"""Возвращает словарь с данными по всем спутникам на странице"""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
# Файл json на диске для достоверности
|
||||
with open('data.json', 'w') as jf:
|
||||
json.dump(data, jf, indent=2)
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
|
||||
names = []
|
||||
for beam in footprint_data:
|
||||
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
|
||||
names.append(
|
||||
{
|
||||
"name": beam['name'],
|
||||
"fullname": beam['fullname'][8:]
|
||||
}
|
||||
)
|
||||
return names
|
||||
|
||||
|
||||
def get_band_names(satellite_name: str) -> list[str]:
|
||||
data = get_all_page_data()
|
||||
pos, sat_id = search_satellite_on_page(data, satellite_name)
|
||||
footprints = get_footprint_data(pos)
|
||||
names = get_names_footprints_for_satellite(footprints, sat_id)
|
||||
return names
|
||||
|
||||
def parse_transponders_from_json(filepath: str):
|
||||
with open(filepath, encoding="utf-8") as jf:
|
||||
data = json.load(jf)
|
||||
for sat_name, trans_zone in data["satellites"].items():
|
||||
for zone, trans in trans_zone.items():
|
||||
for tran in trans:
|
||||
f_b, f_e = tran["freq"][0].split("-")
|
||||
f = round((float(f_b) + float(f_e))/2, 3)
|
||||
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
||||
tran_obj = Transponders.objects.create(
|
||||
name=tran["name"],
|
||||
frequency=f,
|
||||
frequency_range=f_range,
|
||||
zone_name=zone,
|
||||
polarization=Polarization.objects.get(name=tran["pol"]),
|
||||
sat_id=Satellite.objects.get(name__iexact=sat_name)
|
||||
)
|
||||
tran_obj.save()
|
||||
|
||||
|
||||
# Third-party imports (additional)
|
||||
from lxml import etree
|
||||
|
||||
def parse_transponders_from_xml(data_in: BytesIO):
|
||||
|
||||
tree = etree.parse(data_in)
|
||||
ns = {
|
||||
'i': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
|
||||
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
|
||||
}
|
||||
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
||||
for sat in satellites[:]:
|
||||
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
if name == 'X' or 'DONT USE' in name:
|
||||
continue
|
||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
||||
zones = {}
|
||||
for zone in beams:
|
||||
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
|
||||
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
|
||||
"name": zone_name,
|
||||
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
|
||||
}
|
||||
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
|
||||
for transponder in transponders:
|
||||
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
|
||||
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||
tr_data = zones[tr_id]
|
||||
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
|
||||
match tr_data['pol']:
|
||||
case 'Horizontal':
|
||||
pol = 'Горизонтальная'
|
||||
case 'Vertical':
|
||||
pol = 'Вертикальная'
|
||||
case 'CircularRight':
|
||||
pol = 'Правая'
|
||||
case 'CircularLeft':
|
||||
pol = 'Левая'
|
||||
case _:
|
||||
pol = '-'
|
||||
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
|
||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
"norad": int(norad[0]) if norad else -1
|
||||
})
|
||||
trans_obj, _ = Transponders.objects.get_or_create(
|
||||
polarization=pol_obj,
|
||||
downlink=(downlink_start+downlink_end)/2/1000000,
|
||||
uplink=(uplink_start+uplink_end)/2/1000000,
|
||||
frequency_range=abs(downlink_end-downlink_start)/1000000,
|
||||
name=tr_name,
|
||||
defaults={
|
||||
"zone_name": tr_data['name'],
|
||||
"sat_id": sat_obj,
|
||||
}
|
||||
)
|
||||
trans_obj.save()
|
||||
# Standard library imports
|
||||
import json
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Polarization, Satellite
|
||||
|
||||
from .models import Transponders
|
||||
|
||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||
for pos, value in data.get('page', {}).get('positions').items():
|
||||
for name in value['satellites']:
|
||||
if name['other_names'] is None:
|
||||
name['other_names'] = ''
|
||||
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
|
||||
return pos, name['id']
|
||||
return '', ''
|
||||
|
||||
def get_footprint_data(position: str = 62) -> dict:
|
||||
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
||||
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
|
||||
"""Возвращает словарь с данными по всем спутникам на странице"""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
# Файл json на диске для достоверности
|
||||
with open('data.json', 'w') as jf:
|
||||
json.dump(data, jf, indent=2)
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
|
||||
names = []
|
||||
for beam in footprint_data:
|
||||
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
|
||||
names.append(
|
||||
{
|
||||
"name": beam['name'],
|
||||
"fullname": beam['fullname'][8:]
|
||||
}
|
||||
)
|
||||
return names
|
||||
|
||||
|
||||
def get_band_names(satellite_name: str) -> list[str]:
|
||||
data = get_all_page_data()
|
||||
pos, sat_id = search_satellite_on_page(data, satellite_name)
|
||||
footprints = get_footprint_data(pos)
|
||||
names = get_names_footprints_for_satellite(footprints, sat_id)
|
||||
return names
|
||||
|
||||
def parse_transponders_from_json(filepath: str):
|
||||
with open(filepath, encoding="utf-8") as jf:
|
||||
data = json.load(jf)
|
||||
for sat_name, trans_zone in data["satellites"].items():
|
||||
for zone, trans in trans_zone.items():
|
||||
for tran in trans:
|
||||
f_b, f_e = tran["freq"][0].split("-")
|
||||
f = round((float(f_b) + float(f_e))/2, 3)
|
||||
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
||||
tran_obj = Transponders.objects.create(
|
||||
name=tran["name"],
|
||||
frequency=f,
|
||||
frequency_range=f_range,
|
||||
zone_name=zone,
|
||||
polarization=Polarization.objects.get(name=tran["pol"]),
|
||||
sat_id=Satellite.objects.get(name__iexact=sat_name)
|
||||
)
|
||||
tran_obj.save()
|
||||
|
||||
|
||||
# Third-party imports (additional)
|
||||
from lxml import etree
|
||||
|
||||
def parse_transponders_from_xml(data_in: BytesIO):
|
||||
|
||||
tree = etree.parse(data_in)
|
||||
ns = {
|
||||
'i': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
|
||||
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
|
||||
}
|
||||
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
||||
for sat in satellites[:]:
|
||||
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
if name == 'X' or 'DONT USE' in name:
|
||||
continue
|
||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
||||
zones = {}
|
||||
for zone in beams:
|
||||
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
|
||||
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
|
||||
"name": zone_name,
|
||||
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
|
||||
}
|
||||
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
|
||||
for transponder in transponders:
|
||||
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
|
||||
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||
tr_data = zones[tr_id]
|
||||
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
|
||||
match tr_data['pol']:
|
||||
case 'Horizontal':
|
||||
pol = 'Горизонтальная'
|
||||
case 'Vertical':
|
||||
pol = 'Вертикальная'
|
||||
case 'CircularRight':
|
||||
pol = 'Правая'
|
||||
case 'CircularLeft':
|
||||
pol = 'Левая'
|
||||
case _:
|
||||
pol = '-'
|
||||
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
|
||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
"norad": int(norad[0]) if norad else -1
|
||||
})
|
||||
trans_obj, _ = Transponders.objects.get_or_create(
|
||||
polarization=pol_obj,
|
||||
downlink=(downlink_start+downlink_end)/2/1000000,
|
||||
uplink=(uplink_start+uplink_end)/2/1000000,
|
||||
frequency_range=abs(downlink_end-downlink_start)/1000000,
|
||||
name=tr_name,
|
||||
defaults={
|
||||
"zone_name": tr_data['name'],
|
||||
"sat_id": sat_obj,
|
||||
}
|
||||
)
|
||||
trans_obj.save()
|
||||
|
||||
@@ -1,148 +1,148 @@
|
||||
# Standard library imports
|
||||
from typing import Any, Dict
|
||||
|
||||
# Django imports
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Satellite
|
||||
from .models import Transponders
|
||||
from .utils import get_band_names
|
||||
|
||||
|
||||
class CesiumMapView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Представление для отображения 3D карты с использованием Cesium.
|
||||
|
||||
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
|
||||
"""
|
||||
template_name = 'mapsapp/map3d.html'
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Оптимизированный запрос - загружаем только необходимые поля
|
||||
context['sats'] = Satellite.objects.filter(
|
||||
parameters__objitems__isnull=False
|
||||
).distinct().only('id', 'name').order_by('name')
|
||||
return context
|
||||
|
||||
class GetFootprintsView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения зон покрытия (footprints) спутника.
|
||||
|
||||
Возвращает список названий зон покрытия для указанного спутника.
|
||||
"""
|
||||
def get(self, request, sat_id):
|
||||
try:
|
||||
# Оптимизированный запрос - загружаем только поле name
|
||||
sat_name = Satellite.objects.only('name').get(id=sat_id).name
|
||||
footprint_names = get_band_names(sat_name)
|
||||
|
||||
return JsonResponse(footprint_names, safe=False)
|
||||
except Satellite.DoesNotExist:
|
||||
return JsonResponse({"error": "Спутник не найден"}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
class TileProxyView(View):
|
||||
"""
|
||||
Прокси для загрузки тайлов карты покрытия спутников.
|
||||
|
||||
Кэширует тайлы на 7 дней для улучшения производительности.
|
||||
"""
|
||||
# Константы
|
||||
TILE_BASE_URL = "https://static.satbeams.com/tiles"
|
||||
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
|
||||
REQUEST_TIMEOUT = 10 # секунд
|
||||
|
||||
@method_decorator(require_GET)
|
||||
@method_decorator(cache_page(CACHE_DURATION))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, footprint_name, z, x, y):
|
||||
# Валидация имени footprint
|
||||
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
||||
return HttpResponse("Invalid footprint name", status=400)
|
||||
|
||||
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
|
||||
if resp.status_code == 200:
|
||||
response = HttpResponse(resp.content, content_type='image/png')
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
|
||||
return response
|
||||
else:
|
||||
return HttpResponseNotFound("Tile not found")
|
||||
except requests.Timeout:
|
||||
return HttpResponse("Request timeout", status=504)
|
||||
except requests.RequestException as e:
|
||||
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||
|
||||
class LeafletMapView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Представление для отображения 2D карты с использованием Leaflet.
|
||||
|
||||
Отображает спутники и транспондеры на интерактивной 2D карте.
|
||||
"""
|
||||
template_name = 'mapsapp/map2d.html'
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Оптимизированные запросы - загружаем только необходимые поля
|
||||
context['sats'] = Satellite.objects.filter(
|
||||
parameters__objitems__isnull=False
|
||||
).distinct().only('id', 'name').order_by('name')
|
||||
|
||||
context['trans'] = Transponders.objects.select_related(
|
||||
'sat_id', 'polarization'
|
||||
).only(
|
||||
'id', 'name', 'sat_id__name', 'polarization__name',
|
||||
'downlink', 'frequency_range', 'zone_name'
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения транспондеров спутника.
|
||||
|
||||
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
|
||||
"""
|
||||
def get(self, request, sat_id):
|
||||
# Оптимизированный запрос с select_related и only
|
||||
trans = Transponders.objects.filter(
|
||||
sat_id=sat_id
|
||||
).select_related('polarization').only(
|
||||
'name', 'downlink', 'frequency_range',
|
||||
'zone_name', 'polarization__name'
|
||||
)
|
||||
|
||||
if not trans.exists():
|
||||
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
|
||||
|
||||
# Используем list comprehension для лучшей производительности
|
||||
output = [
|
||||
{
|
||||
"name": tran.name,
|
||||
"frequency": tran.downlink,
|
||||
"frequency_range": tran.frequency_range,
|
||||
"zone_name": tran.zone_name,
|
||||
"polarization": tran.polarization.name if tran.polarization else "-"
|
||||
}
|
||||
for tran in trans
|
||||
]
|
||||
|
||||
# Standard library imports
|
||||
from typing import Any, Dict
|
||||
|
||||
# Django imports
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Satellite
|
||||
from .models import Transponders
|
||||
from .utils import get_band_names
|
||||
|
||||
|
||||
class CesiumMapView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Представление для отображения 3D карты с использованием Cesium.
|
||||
|
||||
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
|
||||
"""
|
||||
template_name = 'mapsapp/map3d.html'
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Оптимизированный запрос - загружаем только необходимые поля
|
||||
context['sats'] = Satellite.objects.filter(
|
||||
parameters__objitems__isnull=False
|
||||
).distinct().only('id', 'name').order_by('name')
|
||||
return context
|
||||
|
||||
class GetFootprintsView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения зон покрытия (footprints) спутника.
|
||||
|
||||
Возвращает список названий зон покрытия для указанного спутника.
|
||||
"""
|
||||
def get(self, request, sat_id):
|
||||
try:
|
||||
# Оптимизированный запрос - загружаем только поле name
|
||||
sat_name = Satellite.objects.only('name').get(id=sat_id).name
|
||||
footprint_names = get_band_names(sat_name)
|
||||
|
||||
return JsonResponse(footprint_names, safe=False)
|
||||
except Satellite.DoesNotExist:
|
||||
return JsonResponse({"error": "Спутник не найден"}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
class TileProxyView(View):
|
||||
"""
|
||||
Прокси для загрузки тайлов карты покрытия спутников.
|
||||
|
||||
Кэширует тайлы на 7 дней для улучшения производительности.
|
||||
"""
|
||||
# Константы
|
||||
TILE_BASE_URL = "https://static.satbeams.com/tiles"
|
||||
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
|
||||
REQUEST_TIMEOUT = 10 # секунд
|
||||
|
||||
@method_decorator(require_GET)
|
||||
@method_decorator(cache_page(CACHE_DURATION))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, footprint_name, z, x, y):
|
||||
# Валидация имени footprint
|
||||
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
||||
return HttpResponse("Invalid footprint name", status=400)
|
||||
|
||||
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
|
||||
if resp.status_code == 200:
|
||||
response = HttpResponse(resp.content, content_type='image/png')
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
|
||||
return response
|
||||
else:
|
||||
return HttpResponseNotFound("Tile not found")
|
||||
except requests.Timeout:
|
||||
return HttpResponse("Request timeout", status=504)
|
||||
except requests.RequestException as e:
|
||||
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||
|
||||
class LeafletMapView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Представление для отображения 2D карты с использованием Leaflet.
|
||||
|
||||
Отображает спутники и транспондеры на интерактивной 2D карте.
|
||||
"""
|
||||
template_name = 'mapsapp/map2d.html'
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Оптимизированные запросы - загружаем только необходимые поля
|
||||
context['sats'] = Satellite.objects.filter(
|
||||
parameters__objitems__isnull=False
|
||||
).distinct().only('id', 'name').order_by('name')
|
||||
|
||||
context['trans'] = Transponders.objects.select_related(
|
||||
'sat_id', 'polarization'
|
||||
).only(
|
||||
'id', 'name', 'sat_id__name', 'polarization__name',
|
||||
'downlink', 'frequency_range', 'zone_name'
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
|
||||
"""
|
||||
API для получения транспондеров спутника.
|
||||
|
||||
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
|
||||
"""
|
||||
def get(self, request, sat_id):
|
||||
# Оптимизированный запрос с select_related и only
|
||||
trans = Transponders.objects.filter(
|
||||
sat_id=sat_id
|
||||
).select_related('polarization').only(
|
||||
'name', 'downlink', 'frequency_range',
|
||||
'zone_name', 'polarization__name'
|
||||
)
|
||||
|
||||
if not trans.exists():
|
||||
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
|
||||
|
||||
# Используем list comprehension для лучшей производительности
|
||||
output = [
|
||||
{
|
||||
"name": tran.name,
|
||||
"frequency": tran.downlink,
|
||||
"frequency_range": tran.frequency_range,
|
||||
"zone_name": tran.zone_name,
|
||||
"polarization": tran.polarization.name if tran.polarization else "-"
|
||||
}
|
||||
for tran in trans
|
||||
]
|
||||
|
||||
return JsonResponse(output, safe=False)
|
||||
@@ -8,11 +8,14 @@ dependencies = [
|
||||
"aiosqlite>=0.21.0",
|
||||
"bcrypt>=5.0.0",
|
||||
"beautifulsoup4>=4.14.2",
|
||||
"celery>=5.5.3",
|
||||
"django>=5.2.7",
|
||||
"django-admin-interface>=0.30.1",
|
||||
"django-admin-multiple-choice-list-filter>=0.1.1",
|
||||
"django-admin-rangefilter>=0.13.3",
|
||||
"django-autocomplete-light>=3.12.1",
|
||||
"django-celery-beat>=2.6.0",
|
||||
"django-celery-results>=2.5.1",
|
||||
"django-daisy>=1.1.2",
|
||||
"django-debug-toolbar>=6.0.0",
|
||||
"django-dynamic-raw-id>=4.4",
|
||||
@@ -21,6 +24,7 @@ dependencies = [
|
||||
"django-map-widgets>=0.5.1",
|
||||
"django-more-admin-filters>=1.13",
|
||||
"dotenv>=0.9.9",
|
||||
"flower>=2.0.1",
|
||||
"geopy>=2.4.1",
|
||||
"gunicorn>=23.0.0",
|
||||
"lxml>=6.0.2",
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
aiosqlite>=0.21.0
|
||||
bcrypt>=5.0.0
|
||||
beautifulsoup4>=4.14.2
|
||||
django>=5.2.7
|
||||
django-admin-interface>=0.30.1
|
||||
django-admin-multiple-choice-list-filter>=0.1.1
|
||||
django-admin-rangefilter>=0.13.3
|
||||
django-autocomplete-light>=3.12.1
|
||||
django-daisy>=1.1.2
|
||||
django-debug-toolbar>=6.0.0
|
||||
django-dynamic-raw-id>=4.4
|
||||
django-import-export>=4.3.10
|
||||
django-leaflet>=0.32.0
|
||||
django-map-widgets>=0.5.1
|
||||
django-more-admin-filters>=1.13
|
||||
dotenv>=0.9.9
|
||||
geopy>=2.4.1
|
||||
gunicorn>=23.0.0
|
||||
lxml>=6.0.2
|
||||
matplotlib>=3.10.7
|
||||
numpy>=2.3.3
|
||||
openpyxl>=3.1.5
|
||||
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
|
||||
selenium>=4.38.0
|
||||
setuptools>=80.9.0
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Script to start Celery worker
|
||||
|
||||
echo "Starting Celery worker..."
|
||||
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log
|
||||
#!/bin/bash
|
||||
# Script to start Celery worker
|
||||
|
||||
echo "Starting Celery worker..."
|
||||
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"measure": "Messung",
|
||||
"measureDistancesAndAreas": "Messung von Abständen und Flächen",
|
||||
"createNewMeasurement": "Eine neue Messung durchführen",
|
||||
"startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",
|
||||
"finishMeasurement": "Messung beenden",
|
||||
"lastPoint": "Letzter Punkt",
|
||||
"area": "Fläche",
|
||||
"perimeter": "Rand",
|
||||
"pointLocation": "Lage des Punkts",
|
||||
"areaMeasurement": "Gemessene Fläche",
|
||||
"linearMeasurement": "Gemessener Abstand",
|
||||
"pathDistance": "Abstand entlang des Pfads",
|
||||
"centerOnArea": "Auf diese Fläche zentrieren",
|
||||
"centerOnLine": "Auf diesen Linienzug zentrieren",
|
||||
"centerOnLocation": "Auf diesen Ort zentrieren",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"acres": "Morgen",
|
||||
"feet": "Fuß",
|
||||
"kilometers": "Kilometer",
|
||||
"hectares": "Hektar",
|
||||
"meters": "Meter",
|
||||
"miles": "Meilen",
|
||||
"sqfeet": "Quadratfuß",
|
||||
"sqmeters": "Quadratmeter",
|
||||
"sqmiles": "Quadratmeilen",
|
||||
"decPoint": ",",
|
||||
"thousandsSep": "."
|
||||
}
|
||||
{
|
||||
"measure": "Messung",
|
||||
"measureDistancesAndAreas": "Messung von Abständen und Flächen",
|
||||
"createNewMeasurement": "Eine neue Messung durchführen",
|
||||
"startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",
|
||||
"finishMeasurement": "Messung beenden",
|
||||
"lastPoint": "Letzter Punkt",
|
||||
"area": "Fläche",
|
||||
"perimeter": "Rand",
|
||||
"pointLocation": "Lage des Punkts",
|
||||
"areaMeasurement": "Gemessene Fläche",
|
||||
"linearMeasurement": "Gemessener Abstand",
|
||||
"pathDistance": "Abstand entlang des Pfads",
|
||||
"centerOnArea": "Auf diese Fläche zentrieren",
|
||||
"centerOnLine": "Auf diesen Linienzug zentrieren",
|
||||
"centerOnLocation": "Auf diesen Ort zentrieren",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"acres": "Morgen",
|
||||
"feet": "Fuß",
|
||||
"kilometers": "Kilometer",
|
||||
"hectares": "Hektar",
|
||||
"meters": "Meter",
|
||||
"miles": "Meilen",
|
||||
"sqfeet": "Quadratfuß",
|
||||
"sqmeters": "Quadratmeter",
|
||||
"sqmiles": "Quadratmeilen",
|
||||
"decPoint": ",",
|
||||
"thousandsSep": "."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user