Настроил сеелери, начал привязку lyngsat

This commit is contained in:
2025-11-11 17:23:36 +03:00
parent 65e6c9a323
commit 4f21c9d7c8
110 changed files with 34270 additions and 33631 deletions

View File

@@ -1,23 +1,23 @@
# Development Environment Variables # Development Environment Variables
# Django Settings # Django Settings
DEBUG=True DEBUG=True
ENVIRONMENT=development ENVIRONMENT=development
DJANGO_SETTINGS_MODULE=dbapp.settings.development DJANGO_SETTINGS_MODULE=dbapp.settings.development
SECRET_KEY=django-insecure-dev-key-only-for-development SECRET_KEY=django-insecure-dev-key-only-for-development
# Database Configuration # Database Configuration
DB_ENGINE=django.contrib.gis.db.backends.postgis DB_ENGINE=django.contrib.gis.db.backends.postgis
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=123456 DB_PASSWORD=123456
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
# Allowed Hosts # Allowed Hosts
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# PostgreSQL Configuration # PostgreSQL Configuration
POSTGRES_DB=geodb POSTGRES_DB=geodb
POSTGRES_USER=geralt POSTGRES_USER=geralt
POSTGRES_PASSWORD=123456 POSTGRES_PASSWORD=123456

View File

@@ -1,28 +1,28 @@
# Production Environment Variables # Production Environment Variables
# ВАЖНО: Измените все значения перед деплоем! # ВАЖНО: Измените все значения перед деплоем!
# Django Settings # Django Settings
DEBUG=False DEBUG=False
ENVIRONMENT=production ENVIRONMENT=production
DJANGO_SETTINGS_MODULE=dbapp.settings.production DJANGO_SETTINGS_MODULE=dbapp.settings.production
SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production
# Database Configuration # Database Configuration
DB_ENGINE=django.contrib.gis.db.backends.postgis DB_ENGINE=django.contrib.gis.db.backends.postgis
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
# Allowed Hosts (comma-separated) # Allowed Hosts (comma-separated)
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
# PostgreSQL Configuration # PostgreSQL Configuration
POSTGRES_DB=geodb POSTGRES_DB=geodb
POSTGRES_USER=geralt POSTGRES_USER=geralt
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
# Gunicorn Configuration # Gunicorn Configuration
GUNICORN_WORKERS=3 GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120 GUNICORN_TIMEOUT=120

68
.gitignore vendored
View File

@@ -1,35 +1,35 @@
# Python-generated files # Python-generated files
__pycache__/ __pycache__/
*.py[oc] *.py[oc]
build/ build/
dist/ dist/
wheels/ wheels/
*.egg-info *.egg-info
# Virtual environments # Virtual environments
.venv .venv
.hintrc .hintrc
.vscode .vscode
data.json data.json
# Environment files # Environment files
.env .env
.env.local .env.local
.env.*.local .env.*.local
# Django # Django
*.log *.log
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
staticfiles/ staticfiles/
media/ media/
django-leaflet django-leaflet
admin-interface admin-interface
Тестовые Тестовые
tiles tiles
.kiro .kiro
# Docker # Docker
# docker-* # docker-*
maplibre-gl-js-5.10.0.zip maplibre-gl-js-5.10.0.zip

View File

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

View File

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

View File

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

View File

@@ -1,249 +1,249 @@
# Чеклист для деплоя в Production # Чеклист для деплоя в Production
## Перед деплоем ## Перед деплоем
### 1. Безопасность ### 1. Безопасность
- [ ] Сгенерирован новый `SECRET_KEY` - [ ] Сгенерирован новый `SECRET_KEY`
```bash ```bash
python generate_secret_key.py python generate_secret_key.py
``` ```
- [ ] Изменены все пароли в `.env`: - [ ] Изменены все пароли в `.env`:
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL - [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD` - [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
- [ ] Настроен `ALLOWED_HOSTS` в `.env`: - [ ] Настроен `ALLOWED_HOSTS` в `.env`:
``` ```
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
``` ```
- [ ] `DEBUG=False` в `.env` - [ ] `DEBUG=False` в `.env`
### 2. База данных ### 2. База данных
- [ ] Проверены все миграции: - [ ] Проверены все миграции:
```bash ```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
``` ```
- [ ] Настроен backup БД (cron job): - [ ] Настроен backup БД (cron job):
```bash ```bash
0 2 * * * cd /path/to/project && make backup 0 2 * * * cd /path/to/project && make backup
``` ```
### 3. Статические файлы ### 3. Статические файлы
- [ ] Проверена директория для статики: - [ ] Проверена директория для статики:
```bash ```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
``` ```
### 4. SSL/HTTPS (опционально, но рекомендуется) ### 4. SSL/HTTPS (опционально, но рекомендуется)
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot) - [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
- [ ] Сертификаты размещены в `nginx/ssl/` - [ ] Сертификаты размещены в `nginx/ssl/`
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf` - [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
- [ ] Обновлен `server_name` в `ssl.conf` - [ ] Обновлен `server_name` в `ssl.conf`
### 5. Nginx ### 5. Nginx
- [ ] Проверена конфигурация Nginx: - [ ] Проверена конфигурация Nginx:
```bash ```bash
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
``` ```
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf` - [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
### 6. Docker ### 6. Docker
- [ ] Проверен `.dockerignore` - исключены ненужные файлы - [ ] Проверен `.dockerignore` - исключены ненужные файлы
- [ ] Проверен `.gitignore` - не коммитятся секреты - [ ] Проверен `.gitignore` - не коммитятся секреты
### 7. Переменные окружения ### 7. Переменные окружения
Проверьте `.env` файл: Проверьте `.env` файл:
```bash ```bash
# Django # Django
DEBUG=False DEBUG=False
ENVIRONMENT=production ENVIRONMENT=production
DJANGO_SETTINGS_MODULE=dbapp.settings.production DJANGO_SETTINGS_MODULE=dbapp.settings.production
SECRET_KEY=<ваш-длинный-секретный-ключ> SECRET_KEY=<ваш-длинный-секретный-ключ>
# Database # Database
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=<сильный-пароль> DB_PASSWORD=<сильный-пароль>
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
# Allowed Hosts # Allowed Hosts
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
# PostgreSQL # PostgreSQL
POSTGRES_DB=geodb POSTGRES_DB=geodb
POSTGRES_USER=geralt POSTGRES_USER=geralt
POSTGRES_PASSWORD=<тот-же-сильный-пароль> POSTGRES_PASSWORD=<тот-же-сильный-пароль>
# Gunicorn # Gunicorn
GUNICORN_WORKERS=3 GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120 GUNICORN_TIMEOUT=120
``` ```
## Деплой ## Деплой
### 1. Клонирование репозитория ### 1. Клонирование репозитория
```bash ```bash
git clone <your-repo-url> git clone <your-repo-url>
cd <project-directory> cd <project-directory>
``` ```
### 2. Настройка окружения ### 2. Настройка окружения
```bash ```bash
cp .env.prod .env cp .env.prod .env
nano .env # Отредактируйте все необходимые переменные nano .env # Отредактируйте все необходимые переменные
``` ```
### 3. Запуск контейнеров ### 3. Запуск контейнеров
```bash ```bash
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
``` ```
### 4. Проверка статуса ### 4. Проверка статуса
```bash ```bash
docker-compose -f docker-compose.prod.yaml ps docker-compose -f docker-compose.prod.yaml ps
docker-compose -f docker-compose.prod.yaml logs -f docker-compose -f docker-compose.prod.yaml logs -f
``` ```
### 5. Создание суперпользователя ### 5. Создание суперпользователя
```bash ```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
``` ```
### 6. Проверка работоспособности ### 6. Проверка работоспособности
- [ ] Открыть http://yourdomain.com - [ ] Открыть http://yourdomain.com
- [ ] Открыть http://yourdomain.com/admin - [ ] Открыть http://yourdomain.com/admin
- [ ] Проверить статические файлы - [ ] Проверить статические файлы
- [ ] Проверить медиа файлы - [ ] Проверить медиа файлы
- [ ] Проверить TileServer GL: http://yourdomain.com:8080 - [ ] Проверить TileServer GL: http://yourdomain.com:8080
## После деплоя ## После деплоя
### 1. Мониторинг ### 1. Мониторинг
- [ ] Настроить мониторинг логов: - [ ] Настроить мониторинг логов:
```bash ```bash
docker-compose -f docker-compose.prod.yaml logs -f web docker-compose -f docker-compose.prod.yaml logs -f web
``` ```
- [ ] Проверить использование ресурсов: - [ ] Проверить использование ресурсов:
```bash ```bash
docker stats docker stats
``` ```
### 2. Backup ### 2. Backup
- [ ] Настроить автоматический backup БД - [ ] Настроить автоматический backup БД
- [ ] Проверить восстановление из backup - [ ] Проверить восстановление из backup
- [ ] Настроить backup медиа файлов - [ ] Настроить backup медиа файлов
### 3. Обновления ### 3. Обновления
- [ ] Документировать процесс обновления - [ ] Документировать процесс обновления
- [ ] Тестировать обновления на dev окружении - [ ] Тестировать обновления на dev окружении
### 4. Безопасность ### 4. Безопасность
- [ ] Настроить firewall (UFW, iptables) - [ ] Настроить firewall (UFW, iptables)
- [ ] Ограничить доступ к портам: - [ ] Ограничить доступ к портам:
- Открыть: 80, 443 - Открыть: 80, 443
- Закрыть: 5432, 8000 (доступ только внутри Docker сети) - Закрыть: 5432, 8000 (доступ только внутри Docker сети)
- [ ] Настроить fail2ban (опционально) - [ ] Настроить fail2ban (опционально)
### 5. Производительность ### 5. Производительность
- [ ] Настроить кэширование (Redis, Memcached) - [ ] Настроить кэширование (Redis, Memcached)
- [ ] Оптимизировать количество Gunicorn workers - [ ] Оптимизировать количество Gunicorn workers
- [ ] Настроить CDN для статики (опционально) - [ ] Настроить CDN для статики (опционально)
## Troubleshooting ## Troubleshooting
### Проблема: Контейнеры не запускаются ### Проблема: Контейнеры не запускаются
```bash ```bash
# Проверить логи # Проверить логи
docker-compose -f docker-compose.prod.yaml logs docker-compose -f docker-compose.prod.yaml logs
# Проверить конфигурацию # Проверить конфигурацию
docker-compose -f docker-compose.prod.yaml config docker-compose -f docker-compose.prod.yaml config
``` ```
### Проблема: База данных недоступна ### Проблема: База данных недоступна
```bash ```bash
# Проверить статус БД # Проверить статус БД
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
# Проверить логи БД # Проверить логи БД
docker-compose -f docker-compose.prod.yaml logs db docker-compose -f docker-compose.prod.yaml logs db
``` ```
### Проблема: Статические файлы не загружаются ### Проблема: Статические файлы не загружаются
```bash ```bash
# Пересобрать статику # Пересобрать статику
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput 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 docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
``` ```
### Проблема: 502 Bad Gateway ### Проблема: 502 Bad Gateway
```bash ```bash
# Проверить, что Django запущен # Проверить, что Django запущен
docker-compose -f docker-compose.prod.yaml ps web docker-compose -f docker-compose.prod.yaml ps web
# Проверить логи Gunicorn # Проверить логи Gunicorn
docker-compose -f docker-compose.prod.yaml logs web docker-compose -f docker-compose.prod.yaml logs web
# Проверить конфигурацию Nginx # Проверить конфигурацию Nginx
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
``` ```
## Полезные команды ## Полезные команды
```bash ```bash
# Перезапуск сервисов # Перезапуск сервисов
docker-compose -f docker-compose.prod.yaml restart web docker-compose -f docker-compose.prod.yaml restart web
docker-compose -f docker-compose.prod.yaml restart nginx docker-compose -f docker-compose.prod.yaml restart nginx
# Обновление кода # Обновление кода
git pull git pull
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
# Backup БД # 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 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 exec -T db psql -U geralt geodb < backup.sql
# Просмотр логов # Просмотр логов
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
# Очистка старых образов # Очистка старых образов
docker system prune -a docker system prune -a
``` ```
## Контакты для поддержки ## Контакты для поддержки
- Документация: [DOCKER_README.md](DOCKER_README.md) - Документация: [DOCKER_README.md](DOCKER_README.md)
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md) - Быстрый старт: [QUICKSTART.md](QUICKSTART.md)

View File

@@ -1,102 +1,102 @@
# Инструкция по развертыванию изменений # Инструкция по развертыванию изменений
## Шаг 1: Применение миграций ## Шаг 1: Применение миграций
```bash ```bash
cd dbapp cd dbapp
python manage.py migrate python manage.py migrate
``` ```
Это создаст таблицу `lyngsatapp_lyngsat` в базе данных. Это создаст таблицу `lyngsatapp_lyngsat` в базе данных.
## Шаг 2: Запуск FlareSolver (если еще не запущен) ## Шаг 2: Запуск FlareSolver (если еще не запущен)
FlareSolver необходим для обхода защиты Cloudflare на сайте Lyngsat. FlareSolver необходим для обхода защиты Cloudflare на сайте Lyngsat.
### Вариант 1: Docker ### Вариант 1: Docker
```bash ```bash
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
``` ```
### Вариант 2: Docker Compose ### Вариант 2: Docker Compose
Добавьте в `docker-compose.yaml`: Добавьте в `docker-compose.yaml`:
```yaml ```yaml
services: services:
flaresolverr: flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr container_name: flaresolverr
ports: ports:
- "8191:8191" - "8191:8191"
restart: unless-stopped restart: unless-stopped
``` ```
Затем запустите: Затем запустите:
```bash ```bash
docker-compose up -d flaresolverr docker-compose up -d flaresolverr
``` ```
## Шаг 3: Проверка работоспособности ## Шаг 3: Проверка работоспособности
1. Запустите сервер разработки: 1. Запустите сервер разработки:
```bash ```bash
python manage.py runserver python manage.py runserver
``` ```
2. Откройте браузер и перейдите на: 2. Откройте браузер и перейдите на:
``` ```
http://localhost:8000/actions/ http://localhost:8000/actions/
``` ```
3. Найдите карточку "Заполнение данных Lyngsat" и нажмите на кнопку 3. Найдите карточку "Заполнение данных Lyngsat" и нажмите на кнопку
4. Выберите один-два спутника для тестирования 4. Выберите один-два спутника для тестирования
5. Выберите регионы (например, только Europe) 5. Выберите регионы (например, только Europe)
6. Нажмите "Заполнить данные" и дождитесь завершения 6. Нажмите "Заполнить данные" и дождитесь завершения
## Шаг 4: Проверка результатов ## Шаг 4: Проверка результатов
1. Перейдите в админ-панель Django: 1. Перейдите в админ-панель Django:
``` ```
http://localhost:8000/admin/ http://localhost:8000/admin/
``` ```
2. Откройте раздел "Lyngsatapp" → "Источники LyngSat" 2. Откройте раздел "Lyngsatapp" → "Источники LyngSat"
3. Проверьте, что данные загружены корректно 3. Проверьте, что данные загружены корректно
## Возможные проблемы и решения ## Возможные проблемы и решения
### Проблема: FlareSolver не отвечает ### Проблема: FlareSolver не отвечает
**Решение**: Проверьте, что FlareSolver запущен: **Решение**: Проверьте, что FlareSolver запущен:
```bash ```bash
curl http://localhost:8191/v1 curl http://localhost:8191/v1
``` ```
### Проблема: Спутники не найдены в базе ### Проблема: Спутники не найдены в базе
**Решение**: Убедитесь, что спутники добавлены в базу данных. Используйте функцию "Добавление списка спутников" на странице действий. **Решение**: Убедитесь, что спутники добавлены в базу данных. Используйте функцию "Добавление списка спутников" на странице действий.
### Проблема: Долгое выполнение ### Проблема: Долгое выполнение
**Решение**: Это нормально. Процесс может занять несколько минут на спутник. Начните с 1-2 спутников для тестирования. **Решение**: Это нормально. Процесс может занять несколько минут на спутник. Начните с 1-2 спутников для тестирования.
### Проблема: Ошибки при парсинге ### Проблема: Ошибки при парсинге
**Решение**: Проверьте логи. Некоторые ошибки (например, некорректные частоты) не критичны и не прерывают процесс. **Решение**: Проверьте логи. Некоторые ошибки (например, некорректные частоты) не критичны и не прерывают процесс.
## Откат изменений (если необходимо) ## Откат изменений (если необходимо)
Если нужно откатить изменения: Если нужно откатить изменения:
```bash ```bash
# Откатить миграцию # Откатить миграцию
python manage.py migrate lyngsatapp zero python manage.py migrate lyngsatapp zero
# Откатить изменения в коде # Откатить изменения в коде
git checkout HEAD -- dbapp/ git checkout HEAD -- dbapp/
``` ```
## Дополнительная информация ## Дополнительная информация
- Подробное руководство пользователя: `LYNGSAT_FILL_GUIDE.md` - Подробное руководство пользователя: `LYNGSAT_FILL_GUIDE.md`
- Сводка изменений: `CHANGES_SUMMARY.md` - Сводка изменений: `CHANGES_SUMMARY.md`
- Документация по проекту: `README.md` - Документация по проекту: `README.md`

View File

@@ -1,262 +1,262 @@
# Docker Setup для Django + PostGIS + TileServer GL # Docker Setup для Django + PostGIS + TileServer GL
## Структура проекта ## Структура проекта
``` ```
. .
├── dbapp/ # Django приложение ├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile │ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт запуска │ ├── entrypoint.sh # Скрипт запуска
│ └── ... │ └── ...
├── nginx/ # Конфигурация Nginx (только для prod) ├── nginx/ # Конфигурация Nginx (только для prod)
│ └── conf.d/ │ └── conf.d/
│ └── default.conf │ └── default.conf
├── tiles/ # Тайлы для TileServer GL ├── tiles/ # Тайлы для TileServer GL
├── docker-compose.yaml # Development окружение ├── docker-compose.yaml # Development окружение
├── docker-compose.prod.yaml # Production окружение ├── docker-compose.prod.yaml # Production окружение
├── .env.dev # Переменные для development ├── .env.dev # Переменные для development
└── .env.prod # Переменные для production └── .env.prod # Переменные для production
``` ```
## Быстрый старт ## Быстрый старт
### Development ### Development
1. Скопируйте файл окружения: 1. Скопируйте файл окружения:
```bash ```bash
cp .env.dev .env cp .env.dev .env
``` ```
2. Запустите контейнеры: 2. Запустите контейнеры:
```bash ```bash
docker-compose up -d --build docker-compose up -d --build
``` ```
3. Создайте суперпользователя: 3. Создайте суперпользователя:
```bash ```bash
docker-compose exec web python manage.py createsuperuser docker-compose exec web python manage.py createsuperuser
``` ```
4. Приложение доступно: 4. Приложение доступно:
- Django: http://localhost:8000 - Django: http://localhost:8000
- TileServer GL: http://localhost:8080 - TileServer GL: http://localhost:8080
- PostgreSQL: localhost:5432 - PostgreSQL: localhost:5432
### Production ### Production
1. Скопируйте и настройте файл окружения: 1. Скопируйте и настройте файл окружения:
```bash ```bash
cp .env.prod .env cp .env.prod .env
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS # Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
``` ```
2. Запустите контейнеры: 2. Запустите контейнеры:
```bash ```bash
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
``` ```
3. Создайте суперпользователя: 3. Создайте суперпользователя:
```bash ```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
``` ```
4. Приложение доступно: 4. Приложение доступно:
- Nginx: http://localhost (порт 80) - Nginx: http://localhost (порт 80)
- Django (напрямую): http://localhost:8000 - Django (напрямую): http://localhost:8000
- TileServer GL: http://localhost:8080 - TileServer GL: http://localhost:8080
- PostgreSQL: localhost:5432 - PostgreSQL: localhost:5432
## Основные команды ## Основные команды
### Development ### Development
```bash ```bash
# Запуск # Запуск
docker-compose up -d docker-compose up -d
# Остановка # Остановка
docker-compose down docker-compose down
# Просмотр логов # Просмотр логов
docker-compose logs -f web docker-compose logs -f web
# Выполнение команд Django # Выполнение команд Django
docker-compose exec web python manage.py migrate docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser docker-compose exec web python manage.py createsuperuser
docker-compose exec web python manage.py shell docker-compose exec web python manage.py shell
# Пересборка после изменений в Dockerfile # Пересборка после изменений в Dockerfile
docker-compose up -d --build docker-compose up -d --build
# Полная очистка (включая volumes) # Полная очистка (включая volumes)
docker-compose down -v docker-compose down -v
``` ```
### Production ### Production
```bash ```bash
# Запуск # Запуск
docker-compose -f docker-compose.prod.yaml up -d docker-compose -f docker-compose.prod.yaml up -d
# Остановка # Остановка
docker-compose -f docker-compose.prod.yaml down docker-compose -f docker-compose.prod.yaml down
# Просмотр логов # Просмотр логов
docker-compose -f docker-compose.prod.yaml logs -f web docker-compose -f docker-compose.prod.yaml logs -f web
# Выполнение команд Django # Выполнение команд 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 migrate
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
# Пересборка # Пересборка
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
``` ```
## Различия между Dev и Prod ## Различия между Dev и Prod
### Development ### Development
- Django development server (runserver) - Django development server (runserver)
- DEBUG=True - DEBUG=True
- Код монтируется как volume (изменения применяются сразу) - Код монтируется как volume (изменения применяются сразу)
- Без Nginx - Без Nginx
- Простые пароли (для локальной разработки) - Простые пароли (для локальной разработки)
### Production ### Production
- Gunicorn WSGI server - Gunicorn WSGI server
- DEBUG=False - DEBUG=False
- Код копируется в образ (не монтируется) - Код копируется в образ (не монтируется)
- Nginx как reverse proxy - Nginx как reverse proxy
- Сильные пароли и SECRET_KEY - Сильные пароли и SECRET_KEY
- Сбор статики (collectstatic) - Сбор статики (collectstatic)
- Оптимизированные настройки безопасности - Оптимизированные настройки безопасности
## Переменные окружения ## Переменные окружения
### Основные переменные (.env) ### Основные переменные (.env)
```bash ```bash
# Django # Django
DEBUG=True/False DEBUG=True/False
ENVIRONMENT=development/production ENVIRONMENT=development/production
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
SECRET_KEY=your-secret-key SECRET_KEY=your-secret-key
# Database # Database
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=your-password DB_PASSWORD=your-password
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
# Allowed Hosts # Allowed Hosts
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
# Gunicorn (только для production) # Gunicorn (только для production)
GUNICORN_WORKERS=3 GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120 GUNICORN_TIMEOUT=120
``` ```
## Volumes ## Volumes
### Development ### Development
- `postgres_data_dev` - данные PostgreSQL - `postgres_data_dev` - данные PostgreSQL
- `static_volume_dev` - статические файлы - `static_volume_dev` - статические файлы
- `media_volume_dev` - медиа файлы - `media_volume_dev` - медиа файлы
- `logs_volume_dev` - логи - `logs_volume_dev` - логи
- `./dbapp:/app` - код приложения (live reload) - `./dbapp:/app` - код приложения (live reload)
### Production ### Production
- `postgres_data_prod` - данные PostgreSQL - `postgres_data_prod` - данные PostgreSQL
- `static_volume_prod` - статические файлы - `static_volume_prod` - статические файлы
- `media_volume_prod` - медиа файлы - `media_volume_prod` - медиа файлы
- `logs_volume_prod` - логи - `logs_volume_prod` - логи
## TileServer GL ## TileServer GL
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`. Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
Пример структуры: Пример структуры:
``` ```
tiles/ tiles/
├── config.json ├── config.json
└── your-tiles.mbtiles └── your-tiles.mbtiles
``` ```
## Backup и восстановление БД ## Backup и восстановление БД
### Backup ### Backup
```bash ```bash
# Development # Development
docker-compose exec db pg_dump -U geralt geodb > backup.sql docker-compose exec db pg_dump -U geralt geodb > backup.sql
# Production # Production
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
``` ```
### Восстановление ### Восстановление
```bash ```bash
# Development # Development
docker-compose exec -T db psql -U geralt geodb < backup.sql docker-compose exec -T db psql -U geralt geodb < backup.sql
# Production # Production
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
``` ```
## Troubleshooting ## Troubleshooting
### Проблемы с миграциями ### Проблемы с миграциями
```bash ```bash
docker-compose exec web python manage.py migrate --fake-initial docker-compose exec web python manage.py migrate --fake-initial
``` ```
### Проблемы с правами доступа ### Проблемы с правами доступа
```bash ```bash
docker-compose exec -u root web chown -R app:app /app docker-compose exec -u root web chown -R app:app /app
``` ```
### Очистка всех данных ### Очистка всех данных
```bash ```bash
docker-compose down -v docker-compose down -v
docker system prune -a docker system prune -a
``` ```
### Проверка логов ### Проверка логов
```bash ```bash
# Все сервисы # Все сервисы
docker-compose logs -f docker-compose logs -f
# Конкретный сервис # Конкретный сервис
docker-compose logs -f web docker-compose logs -f web
docker-compose logs -f db docker-compose logs -f db
``` ```
## Безопасность для Production ## Безопасность для Production
1. **Измените SECRET_KEY** - используйте длинный случайный ключ 1. **Измените SECRET_KEY** - используйте длинный случайный ключ
2. **Измените пароли БД** - используйте сильные пароли 2. **Измените пароли БД** - используйте сильные пароли
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен 3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/` 4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу 5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
## Генерация SECRET_KEY ## Генерация SECRET_KEY
```python ```python
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
``` ```
## Мониторинг ## Мониторинг
### Проверка статуса контейнеров ### Проверка статуса контейнеров
```bash ```bash
docker-compose ps docker-compose ps
``` ```
### Использование ресурсов ### Использование ресурсов
```bash ```bash
docker stats docker stats
``` ```
### Healthcheck ### Healthcheck
```bash ```bash
curl http://localhost:8000/admin/ curl http://localhost:8000/admin/
``` ```

View File

@@ -1,307 +1,307 @@
# Docker Setup - Полное руководство # Docker Setup - Полное руководство
## 📋 Обзор ## 📋 Обзор
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL. Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
**Основные компоненты:** **Основные компоненты:**
- Django 5.2 с PostGIS - Django 5.2 с PostGIS
- PostgreSQL 17 с расширением PostGIS 3.4 - PostgreSQL 17 с расширением PostGIS 3.4
- TileServer GL для работы с картографическими тайлами - TileServer GL для работы с картографическими тайлами
- Nginx (только для production) - Nginx (только для production)
- Gunicorn WSGI сервер (production) - Gunicorn WSGI сервер (production)
## 🚀 Быстрый старт ## 🚀 Быстрый старт
### Development ### Development
```bash ```bash
cp .env.dev .env cp .env.dev .env
make dev-up make dev-up
make createsuperuser make createsuperuser
``` ```
Откройте http://localhost:8000 Откройте http://localhost:8000
### Production ### Production
```bash ```bash
cp .env.prod .env cp .env.prod .env
# Отредактируйте .env (SECRET_KEY, пароли, домены) # Отредактируйте .env (SECRET_KEY, пароли, домены)
make prod-up make prod-up
make prod-createsuperuser make prod-createsuperuser
``` ```
Откройте http://yourdomain.com Откройте http://yourdomain.com
## 📁 Структура файлов ## 📁 Структура файлов
``` ```
. .
├── dbapp/ # Django приложение ├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile │ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт инициализации │ ├── entrypoint.sh # Скрипт инициализации
│ ├── .dockerignore # Исключения для Docker │ ├── .dockerignore # Исключения для Docker
│ └── ... │ └── ...
├── nginx/ # Nginx конфигурация (prod) ├── nginx/ # Nginx конфигурация (prod)
│ ├── conf.d/ │ ├── conf.d/
│ │ ├── default.conf # HTTP конфигурация │ │ ├── default.conf # HTTP конфигурация
│ │ └── ssl.conf.example # HTTPS конфигурация (пример) │ │ └── ssl.conf.example # HTTPS конфигурация (пример)
│ └── ssl/ # SSL сертификаты │ └── ssl/ # SSL сертификаты
├── tiles/ # Тайлы для TileServer GL ├── tiles/ # Тайлы для TileServer GL
│ ├── README.md # Инструкция по настройке │ ├── README.md # Инструкция по настройке
│ ├── config.json.example # Пример конфигурации │ ├── config.json.example # Пример конфигурации
│ └── .gitignore │ └── .gitignore
├── docker-compose.yaml # Development окружение ├── docker-compose.yaml # Development окружение
├── docker-compose.prod.yaml # Production окружение ├── docker-compose.prod.yaml # Production окружение
├── .env.dev # Переменные для dev ├── .env.dev # Переменные для dev
├── .env.prod # Переменные для prod (шаблон) ├── .env.prod # Переменные для prod (шаблон)
├── Makefile # Удобные команды ├── Makefile # Удобные команды
├── generate_secret_key.py # Генератор SECRET_KEY ├── generate_secret_key.py # Генератор SECRET_KEY
└── Документация: └── Документация:
├── QUICKSTART.md # Быстрый старт ├── QUICKSTART.md # Быстрый старт
├── DOCKER_README.md # Подробная документация ├── DOCKER_README.md # Подробная документация
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя ├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
└── DOCKER_SETUP.md # Этот файл └── DOCKER_SETUP.md # Этот файл
``` ```
## 🔧 Конфигурация ## 🔧 Конфигурация
### Dockerfile ### Dockerfile
**Один универсальный Dockerfile** для dev и prod: **Один универсальный Dockerfile** для dev и prod:
- Multi-stage build для оптимизации размера - Multi-stage build для оптимизации размера
- Установка GDAL, PostGIS зависимостей - Установка GDAL, PostGIS зависимостей
- Использование uv для управления зависимостями - Использование uv для управления зависимостями
- Non-root пользователь для безопасности - Non-root пользователь для безопасности
- Healthcheck для мониторинга - Healthcheck для мониторинга
### entrypoint.sh ### entrypoint.sh
Скрипт автоматически: Скрипт автоматически:
- Ждет готовности PostgreSQL - Ждет готовности PostgreSQL
- Выполняет миграции - Выполняет миграции
- Собирает статику (только prod) - Собирает статику (только prod)
- Запускает runserver (dev) или Gunicorn (prod) - Запускает runserver (dev) или Gunicorn (prod)
Поведение определяется переменной `ENVIRONMENT`: Поведение определяется переменной `ENVIRONMENT`:
- `development` → Django development server - `development` → Django development server
- `production` → Gunicorn WSGI server - `production` → Gunicorn WSGI server
### docker-compose.yaml (Development) ### docker-compose.yaml (Development)
**Сервисы:** **Сервисы:**
- `db` - PostgreSQL с PostGIS - `db` - PostgreSQL с PostGIS
- `web` - Django приложение - `web` - Django приложение
- `tileserver` - TileServer GL - `tileserver` - TileServer GL
**Особенности:** **Особенности:**
- Код монтируется как volume (live reload) - Код монтируется как volume (live reload)
- DEBUG=True - DEBUG=True
- Django development server - Django development server
- Простые пароли для локальной разработки - Простые пароли для локальной разработки
### docker-compose.prod.yaml (Production) ### docker-compose.prod.yaml (Production)
**Сервисы:** **Сервисы:**
- `db` - PostgreSQL с PostGIS - `db` - PostgreSQL с PostGIS
- `web` - Django с Gunicorn - `web` - Django с Gunicorn
- `tileserver` - TileServer GL - `tileserver` - TileServer GL
- `nginx` - Reverse proxy - `nginx` - Reverse proxy
**Особенности:** **Особенности:**
- Код копируется в образ (не монтируется) - Код копируется в образ (не монтируется)
- DEBUG=False - DEBUG=False
- Gunicorn WSGI server - Gunicorn WSGI server
- Nginx для статики и проксирования - Nginx для статики и проксирования
- Сильные пароли из .env - Сильные пароли из .env
- Сбор статики (collectstatic) - Сбор статики (collectstatic)
## 🔐 Безопасность ## 🔐 Безопасность
### Для Production обязательно: ### Для Production обязательно:
1. **Сгенерируйте SECRET_KEY:** 1. **Сгенерируйте SECRET_KEY:**
```bash ```bash
python generate_secret_key.py python generate_secret_key.py
``` ```
2. **Измените пароли БД** в `.env` 2. **Измените пароли БД** в `.env`
3. **Настройте ALLOWED_HOSTS:** 3. **Настройте ALLOWED_HOSTS:**
``` ```
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
``` ```
4. **Настройте SSL/HTTPS** (рекомендуется): 4. **Настройте SSL/HTTPS** (рекомендуется):
- Получите сертификаты (Let's Encrypt) - Получите сертификаты (Let's Encrypt)
- Поместите в `nginx/ssl/` - Поместите в `nginx/ssl/`
- Используйте `nginx/conf.d/ssl.conf.example` - Используйте `nginx/conf.d/ssl.conf.example`
5. **Ограничьте доступ к портам:** 5. **Ограничьте доступ к портам:**
- Открыть: 80, 443 - Открыть: 80, 443
- Закрыть: 5432, 8000 - Закрыть: 5432, 8000
## 📊 Мониторинг ## 📊 Мониторинг
### Логи ### Логи
```bash ```bash
# Development # Development
make dev-logs make dev-logs
# Production # Production
make prod-logs make prod-logs
# Конкретный сервис # Конкретный сервис
docker-compose logs -f web docker-compose logs -f web
docker-compose logs -f db docker-compose logs -f db
``` ```
### Статус ### Статус
```bash ```bash
make status # Development make status # Development
make prod-status # Production make prod-status # Production
docker stats # Использование ресурсов docker stats # Использование ресурсов
``` ```
### Healthcheck ### Healthcheck
```bash ```bash
curl http://localhost:8000/admin/ curl http://localhost:8000/admin/
``` ```
## 💾 Backup и восстановление ## 💾 Backup и восстановление
### Backup ### Backup
```bash ```bash
make backup make backup
# или # или
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
``` ```
### Восстановление ### Восстановление
```bash ```bash
docker-compose exec -T db psql -U geralt geodb < backup.sql docker-compose exec -T db psql -U geralt geodb < backup.sql
``` ```
### Автоматический backup (cron) ### Автоматический backup (cron)
```bash ```bash
# Добавьте в crontab # Добавьте в crontab
0 2 * * * cd /path/to/project && make backup 0 2 * * * cd /path/to/project && make backup
``` ```
## 🔄 Обновление ## 🔄 Обновление
### Development ### Development
```bash ```bash
git pull git pull
make dev-build make dev-build
``` ```
### Production ### Production
```bash ```bash
git pull git pull
make prod-build make prod-build
make prod-migrate make prod-migrate
``` ```
## 🗺️ TileServer GL ## 🗺️ TileServer GL
Поместите `.mbtiles` файлы в директорию `tiles/`: Поместите `.mbtiles` файлы в директорию `tiles/`:
```bash ```bash
tiles/ tiles/
├── world.mbtiles ├── world.mbtiles
└── satellite.mbtiles └── satellite.mbtiles
``` ```
Доступ: http://localhost:8080 Доступ: http://localhost:8080
Подробнее: [tiles/README.md](tiles/README.md) Подробнее: [tiles/README.md](tiles/README.md)
## 🛠️ Makefile команды ## 🛠️ Makefile команды
### Development ### Development
```bash ```bash
make dev-up # Запустить make dev-up # Запустить
make dev-down # Остановить make dev-down # Остановить
make dev-build # Пересобрать make dev-build # Пересобрать
make dev-logs # Логи make dev-logs # Логи
make dev-restart # Перезапустить web make dev-restart # Перезапустить web
``` ```
### Production ### Production
```bash ```bash
make prod-up # Запустить make prod-up # Запустить
make prod-down # Остановить make prod-down # Остановить
make prod-build # Пересобрать make prod-build # Пересобрать
make prod-logs # Логи make prod-logs # Логи
make prod-restart # Перезапустить web make prod-restart # Перезапустить web
``` ```
### Django ### Django
```bash ```bash
make shell # Django shell make shell # Django shell
make migrate # Миграции make migrate # Миграции
make makemigrations # Создать миграции make makemigrations # Создать миграции
make createsuperuser # Создать суперпользователя make createsuperuser # Создать суперпользователя
make collectstatic # Собрать статику make collectstatic # Собрать статику
``` ```
### Утилиты ### Утилиты
```bash ```bash
make backup # Backup БД make backup # Backup БД
make status # Статус контейнеров make status # Статус контейнеров
make clean # Очистка (с volumes) make clean # Очистка (с volumes)
make clean-all # Полная очистка make clean-all # Полная очистка
``` ```
## 📚 Дополнительная документация ## 📚 Дополнительная документация
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых - **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker - **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя - **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL - **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
## ❓ Troubleshooting ## ❓ Troubleshooting
### Контейнеры не запускаются ### Контейнеры не запускаются
```bash ```bash
docker-compose logs docker-compose logs
docker-compose config docker-compose config
``` ```
### База данных недоступна ### База данных недоступна
```bash ```bash
docker-compose exec db pg_isready -U geralt docker-compose exec db pg_isready -U geralt
docker-compose logs db docker-compose logs db
``` ```
### Статические файлы не загружаются ### Статические файлы не загружаются
```bash ```bash
docker-compose exec web python manage.py collectstatic --noinput docker-compose exec web python manage.py collectstatic --noinput
docker-compose exec web ls -la /app/staticfiles docker-compose exec web ls -la /app/staticfiles
``` ```
### 502 Bad Gateway ### 502 Bad Gateway
```bash ```bash
docker-compose ps web docker-compose ps web
docker-compose logs web docker-compose logs web
docker-compose exec nginx nginx -t docker-compose exec nginx nginx -t
``` ```
## 🎯 Следующие шаги ## 🎯 Следующие шаги
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md) 1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
2. ✅ Запустите development окружение 2. ✅ Запустите development окружение
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем 3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md)) 4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
5. ✅ Настройте SSL для production 5. ✅ Настройте SSL для production
## 📞 Поддержка ## 📞 Поддержка
При возникновении проблем: При возникновении проблем:
1. Проверьте логи: `make dev-logs` или `make prod-logs` 1. Проверьте логи: `make dev-logs` или `make prod-logs`
2. Изучите документацию в этой директории 2. Изучите документацию в этой директории
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей 3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей

View File

@@ -1,240 +1,240 @@
# Обзор созданных файлов Docker Setup # Обзор созданных файлов Docker Setup
## 🐳 Docker файлы ## 🐳 Docker файлы
### `dbapp/Dockerfile` ### `dbapp/Dockerfile`
**Универсальный Dockerfile** для dev и prod окружений. **Универсальный Dockerfile** для dev и prod окружений.
- Multi-stage build для оптимизации - Multi-stage build для оптимизации
- Установка GDAL, PostGIS, PostgreSQL клиента - Установка GDAL, PostGIS, PostgreSQL клиента
- Использование uv для управления зависимостями - Использование uv для управления зависимостями
- Non-root пользователь для безопасности - Non-root пользователь для безопасности
- Healthcheck для мониторинга - Healthcheck для мониторинга
### `dbapp/entrypoint.sh` ### `dbapp/entrypoint.sh`
**Скрипт инициализации контейнера.** **Скрипт инициализации контейнера.**
- Ожидание готовности PostgreSQL - Ожидание готовности PostgreSQL
- Автоматические миграции - Автоматические миграции
- Сбор статики (только prod) - Сбор статики (только prod)
- Запуск runserver (dev) или Gunicorn (prod) - Запуск runserver (dev) или Gunicorn (prod)
### `dbapp/.dockerignore` ### `dbapp/.dockerignore`
**Исключения для Docker build.** **Исключения для Docker build.**
- Исключает ненужные файлы из образа - Исключает ненужные файлы из образа
- Уменьшает размер образа - Уменьшает размер образа
- Ускоряет сборку - Ускоряет сборку
## 🔧 Docker Compose файлы ## 🔧 Docker Compose файлы
### `docker-compose.yaml` ### `docker-compose.yaml`
**Development окружение.** **Development окружение.**
- PostgreSQL с PostGIS - PostgreSQL с PostGIS
- Django с development server - Django с development server
- TileServer GL - TileServer GL
- Код монтируется как volume (live reload) - Код монтируется как volume (live reload)
- DEBUG=True - DEBUG=True
### `docker-compose.prod.yaml` ### `docker-compose.prod.yaml`
**Production окружение.** **Production окружение.**
- PostgreSQL с PostGIS - PostgreSQL с PostGIS
- Django с Gunicorn - Django с Gunicorn
- TileServer GL - TileServer GL
- Nginx reverse proxy - Nginx reverse proxy
- Код копируется в образ - Код копируется в образ
- DEBUG=False - DEBUG=False
- Оптимизированные настройки - Оптимизированные настройки
## 🌐 Nginx конфигурация ## 🌐 Nginx конфигурация
### `nginx/conf.d/default.conf` ### `nginx/conf.d/default.conf`
**HTTP конфигурация для production.** **HTTP конфигурация для production.**
- Проксирование к Django - Проксирование к Django
- Раздача статики и медиа - Раздача статики и медиа
- Оптимизированные таймауты - Оптимизированные таймауты
- Кэширование статики - Кэширование статики
### `nginx/conf.d/ssl.conf.example` ### `nginx/conf.d/ssl.conf.example`
**HTTPS конфигурация (пример).** **HTTPS конфигурация (пример).**
- SSL/TLS настройки - SSL/TLS настройки
- Редирект с HTTP на HTTPS - Редирект с HTTP на HTTPS
- Security headers - Security headers
- Оптимизированные SSL параметры - Оптимизированные SSL параметры
### `nginx/ssl/.gitkeep` ### `nginx/ssl/.gitkeep`
**Директория для SSL сертификатов.** **Директория для SSL сертификатов.**
- Поместите сюда fullchain.pem и privkey.pem - Поместите сюда fullchain.pem и privkey.pem
## 🗺️ TileServer GL ## 🗺️ TileServer GL
### `tiles/README.md` ### `tiles/README.md`
**Инструкция по настройке TileServer GL.** **Инструкция по настройке TileServer GL.**
- Как добавить тайлы - Как добавить тайлы
- Примеры конфигурации - Примеры конфигурации
- Использование в Django/Leaflet - Использование в Django/Leaflet
- Где взять тайлы - Где взять тайлы
### `tiles/config.json.example` ### `tiles/config.json.example`
**Пример конфигурации TileServer GL.** **Пример конфигурации TileServer GL.**
- Настройки путей - Настройки путей
- Форматы и качество - Форматы и качество
- Домены - Домены
### `tiles/.gitignore` ### `tiles/.gitignore`
**Исключения для git.** **Исключения для git.**
- Игнорирует большие .mbtiles файлы - Игнорирует большие .mbtiles файлы
- Сохраняет примеры конфигурации - Сохраняет примеры конфигурации
## 🔐 Переменные окружения ## 🔐 Переменные окружения
### `.env.dev` ### `.env.dev`
**Переменные для development.** **Переменные для development.**
- DEBUG=True - DEBUG=True
- Простые пароли для локальной разработки - Простые пароли для локальной разработки
- Настройки БД для dev - Настройки БД для dev
### `.env.prod` ### `.env.prod`
**Шаблон переменных для production.** **Шаблон переменных для production.**
- DEBUG=False - DEBUG=False
- Требует изменения SECRET_KEY и паролей - Требует изменения SECRET_KEY и паролей
- Настройки для production - Настройки для production
## 🛠️ Утилиты ## 🛠️ Утилиты
### `Makefile` ### `Makefile`
**Удобные команды для работы с Docker.** **Удобные команды для работы с Docker.**
- `make dev-up` - запуск dev - `make dev-up` - запуск dev
- `make prod-up` - запуск prod - `make prod-up` - запуск prod
- `make migrate` - миграции - `make migrate` - миграции
- `make backup` - backup БД - `make backup` - backup БД
- И многое другое - И многое другое
### `generate_secret_key.py` ### `generate_secret_key.py`
**Генератор Django SECRET_KEY.** **Генератор Django SECRET_KEY.**
```bash ```bash
python generate_secret_key.py python generate_secret_key.py
``` ```
## 📚 Документация ## 📚 Документация
### `QUICKSTART.md` ### `QUICKSTART.md`
**Быстрый старт.** **Быстрый старт.**
- Минимальные команды для запуска - Минимальные команды для запуска
- Development и Production - Development и Production
- Основные команды - Основные команды
### `DOCKER_README.md` ### `DOCKER_README.md`
**Подробная документация.** **Подробная документация.**
- Полное описание структуры - Полное описание структуры
- Все команды с примерами - Все команды с примерами
- Troubleshooting - Troubleshooting
- Backup и восстановление - Backup и восстановление
### `DOCKER_SETUP.md` ### `DOCKER_SETUP.md`
**Полное руководство.** **Полное руководство.**
- Обзор всей системы - Обзор всей системы
- Конфигурация - Конфигурация
- Безопасность - Безопасность
- Мониторинг - Мониторинг
### `DEPLOYMENT_CHECKLIST.md` ### `DEPLOYMENT_CHECKLIST.md`
**Чеклист для деплоя.** **Чеклист для деплоя.**
- Пошаговая инструкция - Пошаговая инструкция
- Проверка безопасности - Проверка безопасности
- Настройка production - Настройка production
- Troubleshooting - Troubleshooting
### `FILES_OVERVIEW.md` ### `FILES_OVERVIEW.md`
**Этот файл.** **Этот файл.**
- Описание всех созданных файлов - Описание всех созданных файлов
- Назначение каждого файла - Назначение каждого файла
## 📝 Обновленные файлы ## 📝 Обновленные файлы
### `.gitignore` ### `.gitignore`
**Обновлен для Docker.** **Обновлен для Docker.**
- Исключает .env файлы - Исключает .env файлы
- Исключает логи и backup - Исключает логи и backup
- Исключает временные файлы - Исключает временные файлы
## 🎯 Как использовать ## 🎯 Как использовать
### Для начала работы: ### Для начала работы:
1. Прочитайте **QUICKSTART.md** 1. Прочитайте **QUICKSTART.md**
2. Выберите окружение (dev или prod) 2. Выберите окружение (dev или prod)
3. Скопируйте соответствующий .env файл 3. Скопируйте соответствующий .env файл
4. Запустите с помощью Makefile 4. Запустите с помощью Makefile
### Для деплоя: ### Для деплоя:
1. Прочитайте **DEPLOYMENT_CHECKLIST.md** 1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
2. Следуйте чеклисту пошагово 2. Следуйте чеклисту пошагово
3. Используйте **DOCKER_README.md** для справки 3. Используйте **DOCKER_README.md** для справки
### Для настройки TileServer: ### Для настройки TileServer:
1. Прочитайте **tiles/README.md** 1. Прочитайте **tiles/README.md**
2. Добавьте .mbtiles файлы 2. Добавьте .mbtiles файлы
3. Настройте config.json (опционально) 3. Настройте config.json (опционально)
## 📊 Структура проекта ## 📊 Структура проекта
``` ```
. .
├── Docker конфигурация ├── Docker конфигурация
│ ├── dbapp/Dockerfile │ ├── dbapp/Dockerfile
│ ├── dbapp/entrypoint.sh │ ├── dbapp/entrypoint.sh
│ ├── dbapp/.dockerignore │ ├── dbapp/.dockerignore
│ ├── docker-compose.yaml │ ├── docker-compose.yaml
│ └── docker-compose.prod.yaml │ └── docker-compose.prod.yaml
├── Nginx ├── Nginx
│ ├── nginx/conf.d/default.conf │ ├── nginx/conf.d/default.conf
│ ├── nginx/conf.d/ssl.conf.example │ ├── nginx/conf.d/ssl.conf.example
│ └── nginx/ssl/.gitkeep │ └── nginx/ssl/.gitkeep
├── TileServer GL ├── TileServer GL
│ ├── tiles/README.md │ ├── tiles/README.md
│ ├── tiles/config.json.example │ ├── tiles/config.json.example
│ └── tiles/.gitignore │ └── tiles/.gitignore
├── Переменные окружения ├── Переменные окружения
│ ├── .env.dev │ ├── .env.dev
│ └── .env.prod │ └── .env.prod
├── Утилиты ├── Утилиты
│ ├── Makefile │ ├── Makefile
│ └── generate_secret_key.py │ └── generate_secret_key.py
└── Документация └── Документация
├── QUICKSTART.md ├── QUICKSTART.md
├── DOCKER_README.md ├── DOCKER_README.md
├── DOCKER_SETUP.md ├── DOCKER_SETUP.md
├── DEPLOYMENT_CHECKLIST.md ├── DEPLOYMENT_CHECKLIST.md
└── FILES_OVERVIEW.md └── FILES_OVERVIEW.md
``` ```
## ✅ Что было сделано ## ✅ Что было сделано
1. ✅ Создан универсальный Dockerfile (один для dev и prod) 1. ✅ Создан универсальный Dockerfile (один для dev и prod)
2. ✅ Настроен entrypoint.sh с автоматической инициализацией 2. ✅ Настроен entrypoint.sh с автоматической инициализацией
3. ✅ Созданы docker-compose.yaml для dev и prod 3. ✅ Созданы docker-compose.yaml для dev и prod
4. ✅ Настроен Nginx для production 4. ✅ Настроен Nginx для production
5. ✅ Добавлена поддержка TileServer GL 5. ✅ Добавлена поддержка TileServer GL
6. ✅ Созданы .env файлы для разных окружений 6. ✅ Созданы .env файлы для разных окружений
7. ✅ Добавлен Makefile с удобными командами 7. ✅ Добавлен Makefile с удобными командами
8. ✅ Написана подробная документация 8. ✅ Написана подробная документация
9. ✅ Создан чеклист для деплоя 9. ✅ Создан чеклист для деплоя
10. ✅ Добавлены утилиты (генератор SECRET_KEY) 10. ✅ Добавлены утилиты (генератор SECRET_KEY)
## 🚀 Следующие шаги ## 🚀 Следующие шаги
1. Запустите development окружение 1. Запустите development окружение
2. Протестируйте все функции 2. Протестируйте все функции
3. Подготовьте production окружение 3. Подготовьте production окружение
4. Следуйте DEPLOYMENT_CHECKLIST.md 4. Следуйте DEPLOYMENT_CHECKLIST.md
5. Настройте мониторинг и backup 5. Настройте мониторинг и backup
## 💡 Полезные ссылки ## 💡 Полезные ссылки
- Django Documentation: https://docs.djangoproject.com/ - Django Documentation: https://docs.djangoproject.com/
- Docker Documentation: https://docs.docker.com/ - Docker Documentation: https://docs.docker.com/
- PostGIS Documentation: https://postgis.net/documentation/ - PostGIS Documentation: https://postgis.net/documentation/
- TileServer GL: https://github.com/maptiler/tileserver-gl - TileServer GL: https://github.com/maptiler/tileserver-gl
- Nginx Documentation: https://nginx.org/en/docs/ - Nginx Documentation: https://nginx.org/en/docs/

View File

@@ -1,347 +1,347 @@
# Руководство по установке асинхронной системы Lyngsat # Руководство по установке асинхронной системы Lyngsat
## Вариант 1: Полная установка с Celery (рекомендуется) ## Вариант 1: Полная установка с Celery (рекомендуется)
### Шаг 1: Установка зависимостей ### Шаг 1: Установка зависимостей
```bash ```bash
pip install -r dbapp/requirements.txt pip install -r dbapp/requirements.txt
``` ```
Это установит: Это установит:
- `celery>=5.4.0` - `celery>=5.4.0`
- `django-celery-results>=2.5.1` - `django-celery-results>=2.5.1`
- И все остальные зависимости - И все остальные зависимости
### Шаг 2: Применение миграций ### Шаг 2: Применение миграций
```bash ```bash
cd dbapp cd dbapp
python manage.py migrate python manage.py migrate
``` ```
Это создаст: Это создаст:
- Таблицу `lyngsatapp_lyngsat` для данных Lyngsat - Таблицу `lyngsatapp_lyngsat` для данных Lyngsat
- Таблицы `django_celery_results_*` для результатов Celery - Таблицы `django_celery_results_*` для результатов Celery
### Шаг 3: Запуск сервисов ### Шаг 3: Запуск сервисов
```bash ```bash
# Запуск Redis и FlareSolver # Запуск Redis и FlareSolver
docker-compose up -d redis flaresolverr docker-compose up -d redis flaresolverr
# Проверка # Проверка
redis-cli ping # Должно вернуть PONG redis-cli ping # Должно вернуть PONG
curl http://localhost:8191/v1 # Должно вернуть JSON curl http://localhost:8191/v1 # Должно вернуть JSON
``` ```
### Шаг 4: Запуск приложения ### Шаг 4: Запуск приложения
**Терминал 1 - Django:** **Терминал 1 - Django:**
```bash ```bash
cd dbapp cd dbapp
python manage.py runserver python manage.py runserver
``` ```
**Терминал 2 - Celery Worker:** **Терминал 2 - Celery Worker:**
```bash ```bash
cd dbapp cd dbapp
celery -A dbapp worker --loglevel=info celery -A dbapp worker --loglevel=info
``` ```
### Шаг 5: Тестирование ### Шаг 5: Тестирование
1. Откройте `http://localhost:8000/actions/` 1. Откройте `http://localhost:8000/actions/`
2. Нажмите "Заполнить данные Lyngsat" 2. Нажмите "Заполнить данные Lyngsat"
3. Выберите спутники и регионы 3. Выберите спутники и регионы
4. Наблюдайте за прогрессом! 4. Наблюдайте за прогрессом!
--- ---
## Вариант 2: Базовая установка без Celery ## Вариант 2: Базовая установка без Celery
Если вы не хотите использовать асинхронную обработку, система будет работать в синхронном режиме. Если вы не хотите использовать асинхронную обработку, система будет работать в синхронном режиме.
### Шаг 1: Установка базовых зависимостей ### Шаг 1: Установка базовых зависимостей
```bash ```bash
# Установите все зависимости кроме Celery # Установите все зависимости кроме Celery
pip install -r dbapp/requirements.txt --ignore-installed celery django-celery-results pip install -r dbapp/requirements.txt --ignore-installed celery django-celery-results
``` ```
Или вручную удалите из `requirements.txt`: Или вручную удалите из `requirements.txt`:
- `celery>=5.4.0` - `celery>=5.4.0`
- `django-celery-results>=2.5.1` - `django-celery-results>=2.5.1`
Затем: Затем:
```bash ```bash
pip install -r dbapp/requirements.txt pip install -r dbapp/requirements.txt
``` ```
### Шаг 2: Применение миграций ### Шаг 2: Применение миграций
```bash ```bash
cd dbapp cd dbapp
python manage.py migrate python manage.py migrate
``` ```
### Шаг 3: Запуск FlareSolver ### Шаг 3: Запуск FlareSolver
```bash ```bash
docker-compose up -d flaresolverr docker-compose up -d flaresolverr
``` ```
### Шаг 4: Запуск Django ### Шаг 4: Запуск Django
```bash ```bash
cd dbapp cd dbapp
python manage.py runserver python manage.py runserver
``` ```
### Ограничения базовой установки ### Ограничения базовой установки
⚠️ **Внимание**: В синхронном режиме: ⚠️ **Внимание**: В синхронном режиме:
- Веб-интерфейс будет заблокирован во время обработки - Веб-интерфейс будет заблокирован во время обработки
- Нет отслеживания прогресса в реальном времени - Нет отслеживания прогресса в реальном времени
- Нет детального логирования - Нет детального логирования
- Обработка может занять много времени - Обработка может занять много времени
--- ---
## Проверка установки ## Проверка установки
### Проверка Django ### Проверка Django
```bash ```bash
python dbapp/manage.py check python dbapp/manage.py check
# Должно вывести: System check identified no issues (0 silenced). # Должно вывести: System check identified no issues (0 silenced).
``` ```
### Проверка Celery (если установлен) ### Проверка Celery (если установлен)
```bash ```bash
celery -A dbapp inspect ping celery -A dbapp inspect ping
# Должно вывести: pong # Должно вывести: pong
``` ```
### Проверка Redis (если установлен) ### Проверка Redis (если установлен)
```bash ```bash
redis-cli ping redis-cli ping
# Должно вывести: PONG # Должно вывести: PONG
``` ```
### Проверка FlareSolver ### Проверка FlareSolver
```bash ```bash
curl http://localhost:8191/v1 curl http://localhost:8191/v1
# Должно вернуть JSON с информацией о сервисе # Должно вернуть JSON с информацией о сервисе
``` ```
--- ---
## Решение проблем при установке ## Решение проблем при установке
### Проблема: ModuleNotFoundError: No module named 'celery' ### Проблема: ModuleNotFoundError: No module named 'celery'
**Решение 1**: Установите Celery **Решение 1**: Установите Celery
```bash ```bash
pip install celery django-celery-results pip install celery django-celery-results
``` ```
**Решение 2**: Используйте базовую установку (см. Вариант 2) **Решение 2**: Используйте базовую установку (см. Вариант 2)
### Проблема: Redis connection refused ### Проблема: Redis connection refused
**Решение**: Запустите Redis **Решение**: Запустите Redis
```bash ```bash
docker-compose up -d redis docker-compose up -d redis
# или # или
sudo systemctl start redis sudo systemctl start redis
``` ```
### Проблема: FlareSolver не отвечает ### Проблема: FlareSolver не отвечает
**Решение**: Запустите FlareSolver **Решение**: Запустите FlareSolver
```bash ```bash
docker-compose up -d flaresolverr docker-compose up -d flaresolverr
# или # или
docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
``` ```
### Проблема: Миграции не применяются ### Проблема: Миграции не применяются
**Решение**: Проверьте подключение к базе данных **Решение**: Проверьте подключение к базе данных
```bash ```bash
# Проверьте .env файл # Проверьте .env файл
cat dbapp/.env cat dbapp/.env
# Проверьте PostgreSQL # Проверьте PostgreSQL
docker-compose up -d db docker-compose up -d db
docker-compose logs db docker-compose logs db
``` ```
--- ---
## Переменные окружения ## Переменные окружения
Создайте файл `dbapp/.env` (если еще не создан): Создайте файл `dbapp/.env` (если еще не создан):
```bash ```bash
# Database # Database
DB_ENGINE=django.contrib.gis.db.backends.postgis DB_ENGINE=django.contrib.gis.db.backends.postgis
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=123456 DB_PASSWORD=123456
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
# Django # Django
SECRET_KEY=your-secret-key-here SECRET_KEY=your-secret-key-here
DEBUG=True DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1 ALLOWED_HOSTS=localhost,127.0.0.1
# Celery (опционально) # Celery (опционально)
CELERY_BROKER_URL=redis://localhost:6379/0 CELERY_BROKER_URL=redis://localhost:6379/0
# FlareSolver # FlareSolver
FLARESOLVERR_URL=http://localhost:8191/v1 FLARESOLVERR_URL=http://localhost:8191/v1
``` ```
--- ---
## Следующие шаги ## Следующие шаги
После успешной установки: После успешной установки:
1. **Прочитайте документацию**: 1. **Прочитайте документацию**:
- `QUICKSTART_ASYNC.md` - быстрый старт - `QUICKSTART_ASYNC.md` - быстрый старт
- `ASYNC_LYNGSAT_GUIDE.md` - полное руководство - `ASYNC_LYNGSAT_GUIDE.md` - полное руководство
- `ASYNC_CHANGES_SUMMARY.md` - технические детали - `ASYNC_CHANGES_SUMMARY.md` - технические детали
2. **Настройте production окружение** (если необходимо): 2. **Настройте production окружение** (если необходимо):
- Настройте Systemd/Supervisor для Celery - Настройте Systemd/Supervisor для Celery
- Настройте Nginx/Apache - Настройте Nginx/Apache
- Настройте SSL - Настройте SSL
- Настройте мониторинг - Настройте мониторинг
3. **Добавьте данные**: 3. **Добавьте данные**:
- Добавьте спутники через админ-панель - Добавьте спутники через админ-панель
- Запустите заполнение данных Lyngsat - Запустите заполнение данных Lyngsat
4. **Настройте мониторинг**: 4. **Настройте мониторинг**:
- Установите Flower для мониторинга Celery - Установите Flower для мониторинга Celery
- Настройте логирование - Настройте логирование
- Настройте алерты - Настройте алерты
--- ---
## Дополнительные инструменты ## Дополнительные инструменты
### Flower - мониторинг Celery ### Flower - мониторинг Celery
```bash ```bash
pip install flower pip install flower
celery -A dbapp flower celery -A dbapp flower
# Откройте http://localhost:5555 # Откройте http://localhost:5555
``` ```
### Redis Commander - GUI для Redis ### Redis Commander - GUI для Redis
```bash ```bash
docker run -d -p 8081:8081 --name redis-commander \ docker run -d -p 8081:8081 --name redis-commander \
--env REDIS_HOSTS=local:localhost:6379 \ --env REDIS_HOSTS=local:localhost:6379 \
rediscommander/redis-commander rediscommander/redis-commander
# Откройте http://localhost:8081 # Откройте http://localhost:8081
``` ```
### pgAdmin - GUI для PostgreSQL ### pgAdmin - GUI для PostgreSQL
```bash ```bash
docker run -d -p 5050:80 --name pgadmin \ docker run -d -p 5050:80 --name pgadmin \
-e PGADMIN_DEFAULT_EMAIL=admin@admin.com \ -e PGADMIN_DEFAULT_EMAIL=admin@admin.com \
-e PGADMIN_DEFAULT_PASSWORD=admin \ -e PGADMIN_DEFAULT_PASSWORD=admin \
dpage/pgadmin4 dpage/pgadmin4
# Откройте http://localhost:5050 # Откройте http://localhost:5050
``` ```
--- ---
## Обновление системы ## Обновление системы
### Обновление зависимостей ### Обновление зависимостей
```bash ```bash
pip install --upgrade -r dbapp/requirements.txt pip install --upgrade -r dbapp/requirements.txt
``` ```
### Применение новых миграций ### Применение новых миграций
```bash ```bash
cd dbapp cd dbapp
python manage.py migrate python manage.py migrate
``` ```
### Перезапуск сервисов ### Перезапуск сервисов
```bash ```bash
# Перезапуск Docker контейнеров # Перезапуск Docker контейнеров
docker-compose restart docker-compose restart
# Перезапуск Celery Worker # Перезапуск Celery Worker
# Найдите PID процесса # Найдите PID процесса
ps aux | grep celery ps aux | grep celery
# Остановите процесс # Остановите процесс
kill <PID> kill <PID>
# Запустите снова # Запустите снова
celery -A dbapp worker --loglevel=info celery -A dbapp worker --loglevel=info
``` ```
--- ---
## Удаление системы ## Удаление системы
### Остановка сервисов ### Остановка сервисов
```bash ```bash
# Остановка Docker контейнеров # Остановка Docker контейнеров
docker-compose down docker-compose down
# Остановка Celery Worker # Остановка Celery Worker
pkill -f "celery worker" pkill -f "celery worker"
``` ```
### Удаление данных ### Удаление данных
```bash ```bash
# Удаление Docker volumes # Удаление Docker volumes
docker-compose down -v docker-compose down -v
# Удаление виртуального окружения # Удаление виртуального окружения
rm -rf dbapp/.venv rm -rf dbapp/.venv
# Удаление миграций (опционально) # Удаление миграций (опционально)
find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete
find dbapp -path "*/migrations/*.pyc" -delete find dbapp -path "*/migrations/*.pyc" -delete
``` ```
--- ---
## Поддержка ## Поддержка
Если у вас возникли проблемы: Если у вас возникли проблемы:
1. Проверьте логи: 1. Проверьте логи:
- Django: консоль где запущен runserver - Django: консоль где запущен runserver
- Celery: `dbapp/logs/celery_worker.log` - Celery: `dbapp/logs/celery_worker.log`
- Docker: `docker-compose logs` - Docker: `docker-compose logs`
2. Проверьте документацию: 2. Проверьте документацию:
- `ASYNC_LYNGSAT_GUIDE.md` - `ASYNC_LYNGSAT_GUIDE.md`
- `QUICKSTART_ASYNC.md` - `QUICKSTART_ASYNC.md`
- `ASYNC_CHANGES_SUMMARY.md` - `ASYNC_CHANGES_SUMMARY.md`
3. Проверьте статус сервисов: 3. Проверьте статус сервисов:
```bash ```bash
docker-compose ps docker-compose ps
ps aux | grep celery ps aux | grep celery
redis-cli ping redis-cli ping
``` ```
4. Создайте issue в репозитории с описанием проблемы и логами 4. Создайте issue в репозитории с описанием проблемы и логами

View File

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

198
Makefile
View File

@@ -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 .PHONY: help dev-up dev-down dev-build dev-logs prod-up prod-down prod-build prod-logs shell migrate createsuperuser clean
help: help:
@echo "Доступные команды:" @echo "Доступные команды:"
@echo " make dev-up - Запустить development окружение" @echo " make dev-up - Запустить development окружение"
@echo " make dev-down - Остановить development окружение" @echo " make dev-down - Остановить development окружение"
@echo " make dev-build - Пересобрать development контейнеры" @echo " make dev-build - Пересобрать development контейнеры"
@echo " make dev-logs - Показать логи development" @echo " make dev-logs - Показать логи development"
@echo " make prod-up - Запустить production окружение" @echo " make prod-up - Запустить production окружение"
@echo " make prod-down - Остановить production окружение" @echo " make prod-down - Остановить production окружение"
@echo " make prod-build - Пересобрать production контейнеры" @echo " make prod-build - Пересобрать production контейнеры"
@echo " make prod-logs - Показать логи production" @echo " make prod-logs - Показать логи production"
@echo " make shell - Открыть Django shell" @echo " make shell - Открыть Django shell"
@echo " make migrate - Выполнить миграции" @echo " make migrate - Выполнить миграции"
@echo " make createsuperuser - Создать суперпользователя" @echo " make createsuperuser - Создать суперпользователя"
@echo " make clean - Удалить все контейнеры и volumes" @echo " make clean - Удалить все контейнеры и volumes"
# Development команды # Development команды
dev-up: dev-up:
docker-compose up -d docker-compose up -d
dev-down: dev-down:
docker-compose down docker-compose down
dev-build: dev-build:
docker-compose up -d --build docker-compose up -d --build
dev-logs: dev-logs:
docker-compose logs -f docker-compose logs -f
dev-restart: dev-restart:
docker-compose restart web docker-compose restart web
# Production команды # Production команды
prod-up: prod-up:
docker-compose -f docker-compose.prod.yaml up -d docker-compose -f docker-compose.prod.yaml up -d
prod-down: prod-down:
docker-compose -f docker-compose.prod.yaml down docker-compose -f docker-compose.prod.yaml down
prod-build: prod-build:
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
prod-logs: prod-logs:
docker-compose -f docker-compose.prod.yaml logs -f docker-compose -f docker-compose.prod.yaml logs -f
prod-restart: prod-restart:
docker-compose -f docker-compose.prod.yaml restart web docker-compose -f docker-compose.prod.yaml restart web
# Django команды (для development по умолчанию) # Django команды (для development по умолчанию)
shell: shell:
docker-compose exec web python manage.py shell docker-compose exec web python manage.py shell
migrate: migrate:
docker-compose exec web python manage.py migrate docker-compose exec web python manage.py migrate
makemigrations: makemigrations:
docker-compose exec web python manage.py makemigrations docker-compose exec web python manage.py makemigrations
createsuperuser: createsuperuser:
docker-compose exec web python manage.py createsuperuser docker-compose exec web python manage.py createsuperuser
collectstatic: collectstatic:
docker-compose exec web python manage.py collectstatic --noinput docker-compose exec web python manage.py collectstatic --noinput
# Для production # Для production
prod-shell: prod-shell:
docker-compose -f docker-compose.prod.yaml exec web python manage.py shell docker-compose -f docker-compose.prod.yaml exec web python manage.py shell
prod-migrate: prod-migrate:
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 migrate
prod-createsuperuser: prod-createsuperuser:
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
# Backup и восстановление # Backup и восстановление
backup: backup:
docker-compose exec db pg_dump -U geralt geodb > backup_$(shell date +%Y%m%d_%H%M%S).sql docker-compose exec db pg_dump -U geralt geodb > backup_$(shell date +%Y%m%d_%H%M%S).sql
restore: restore:
@read -p "Введите имя файла backup: " file; \ @read -p "Введите имя файла backup: " file; \
docker-compose exec -T db psql -U geralt geodb < $$file docker-compose exec -T db psql -U geralt geodb < $$file
# Очистка # Очистка
clean: clean:
docker-compose down -v docker-compose down -v
docker system prune -f docker system prune -f
clean-all: clean-all:
docker-compose down -v docker-compose down -v
docker-compose -f docker-compose.prod.yaml down -v docker-compose -f docker-compose.prod.yaml down -v
docker system prune -af --volumes docker system prune -af --volumes
# Проверка статуса # Проверка статуса
status: status:
docker-compose ps docker-compose ps
prod-status: prod-status:
docker-compose -f docker-compose.prod.yaml ps docker-compose -f docker-compose.prod.yaml ps

View File

@@ -1,106 +1,106 @@
# Быстрый старт с Docker # Быстрый старт с Docker
## Development (разработка) ## Development (разработка)
```bash ```bash
# 1. Скопировать переменные окружения # 1. Скопировать переменные окружения
cp .env.dev .env cp .env.dev .env
# 2. Запустить контейнеры # 2. Запустить контейнеры
make dev-up make dev-up
# или # или
docker-compose up -d --build docker-compose up -d --build
# 3. Создать суперпользователя # 3. Создать суперпользователя
make createsuperuser make createsuperuser
# или # или
docker-compose exec web python manage.py createsuperuser docker-compose exec web python manage.py createsuperuser
# 4. Открыть в браузере # 4. Открыть в браузере
# Django: http://localhost:8000 # Django: http://localhost:8000
# Admin: http://localhost:8000/admin # Admin: http://localhost:8000/admin
# TileServer: http://localhost:8080 # TileServer: http://localhost:8080
``` ```
## Production (продакшн) ## Production (продакшн)
```bash ```bash
# 1. Скопировать и настроить переменные # 1. Скопировать и настроить переменные
cp .env.prod .env cp .env.prod .env
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
# 2. Запустить контейнеры # 2. Запустить контейнеры
make prod-up make prod-up
# или # или
docker-compose -f docker-compose.prod.yaml up -d --build docker-compose -f docker-compose.prod.yaml up -d --build
# 3. Создать суперпользователя # 3. Создать суперпользователя
make prod-createsuperuser make prod-createsuperuser
# или # или
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
# 4. Открыть в браузере # 4. Открыть в браузере
# Nginx: http://localhost # Nginx: http://localhost
# Django: http://localhost:8000 # Django: http://localhost:8000
# TileServer: http://localhost:8080 # TileServer: http://localhost:8080
``` ```
## Полезные команды ## Полезные команды
```bash ```bash
# Просмотр логов # Просмотр логов
make dev-logs # development make dev-logs # development
make prod-logs # production make prod-logs # production
# Остановка # Остановка
make dev-down # development make dev-down # development
make prod-down # production make prod-down # production
# Перезапуск после изменений # Перезапуск после изменений
make dev-build # development make dev-build # development
make prod-build # production make prod-build # production
# Django shell # Django shell
make shell # development make shell # development
make prod-shell # production make prod-shell # production
# Миграции # Миграции
make migrate # development make migrate # development
make prod-migrate # production make prod-migrate # production
# Backup БД # Backup БД
make backup make backup
# Статус контейнеров # Статус контейнеров
make status # development make status # development
make prod-status # production make prod-status # production
``` ```
## Структура проекта ## Структура проекта
``` ```
. .
├── dbapp/ # Django приложение ├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile │ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт запуска │ ├── entrypoint.sh # Скрипт запуска
│ ├── manage.py │ ├── manage.py
│ └── ... │ └── ...
├── nginx/ # Nginx (только prod) ├── nginx/ # Nginx (только prod)
│ └── conf.d/ │ └── conf.d/
│ └── default.conf │ └── default.conf
├── tiles/ # Тайлы для TileServer GL ├── tiles/ # Тайлы для TileServer GL
│ ├── README.md │ ├── README.md
│ └── config.json.example │ └── config.json.example
├── docker-compose.yaml # Development ├── docker-compose.yaml # Development
├── docker-compose.prod.yaml # Production ├── docker-compose.prod.yaml # Production
├── .env.dev # Переменные dev ├── .env.dev # Переменные dev
├── .env.prod # Переменные prod ├── .env.prod # Переменные prod
├── Makefile # Команды для удобства ├── Makefile # Команды для удобства
└── DOCKER_README.md # Подробная документация └── DOCKER_README.md # Подробная документация
``` ```
## Что дальше? ## Что дальше?
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации 1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md) 2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
3. Для production настройте SSL сертификаты в `nginx/ssl/` 3. Для production настройте SSL сертификаты в `nginx/ssl/`

View File

@@ -1,117 +1,117 @@
# Быстрый старт: Асинхронное заполнение данных Lyngsat # Быстрый старт: Асинхронное заполнение данных Lyngsat
## Минимальная настройка (5 минут) ## Минимальная настройка (5 минут)
### 1. Установите зависимости ### 1. Установите зависимости
```bash ```bash
pip install -r dbapp/requirements.txt pip install -r dbapp/requirements.txt
``` ```
### 2. Примените миграции ### 2. Примените миграции
```bash ```bash
cd dbapp cd dbapp
python manage.py migrate python manage.py migrate
``` ```
### 3. Запустите необходимые сервисы ### 3. Запустите необходимые сервисы
**Терминал 1 - Redis и FlareSolver:** **Терминал 1 - Redis и FlareSolver:**
```bash ```bash
docker-compose up -d redis flaresolverr docker-compose up -d redis flaresolverr
``` ```
**Терминал 2 - Django:** **Терминал 2 - Django:**
```bash ```bash
cd dbapp cd dbapp
python manage.py runserver python manage.py runserver
``` ```
**Терминал 3 - Celery Worker:** **Терминал 3 - Celery Worker:**
```bash ```bash
cd dbapp cd dbapp
celery -A dbapp worker --loglevel=info celery -A dbapp worker --loglevel=info
``` ```
### 4. Используйте систему ### 4. Используйте систему
1. Откройте браузер: `http://localhost:8000/actions/` 1. Откройте браузер: `http://localhost:8000/actions/`
2. Нажмите "Заполнить данные Lyngsat" 2. Нажмите "Заполнить данные Lyngsat"
3. Выберите 1-2 спутника для теста 3. Выберите 1-2 спутника для теста
4. Выберите регион (например, Europe) 4. Выберите регион (например, Europe)
5. Нажмите "Заполнить данные" 5. Нажмите "Заполнить данные"
6. Наблюдайте за прогрессом в реальном времени! 6. Наблюдайте за прогрессом в реальном времени!
## Проверка работоспособности ## Проверка работоспособности
### Redis ### Redis
```bash ```bash
redis-cli ping redis-cli ping
# Должно вернуть: PONG # Должно вернуть: PONG
``` ```
### FlareSolver ### FlareSolver
```bash ```bash
curl http://localhost:8191/v1 curl http://localhost:8191/v1
# Должно вернуть JSON с информацией о сервисе # Должно вернуть JSON с информацией о сервисе
``` ```
### Celery Worker ### Celery Worker
Проверьте вывод в терминале 3 - должны быть сообщения: Проверьте вывод в терминале 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] Connected to redis://localhost:6379/0
[2024-01-15 10:30:00,000: INFO/MainProcess] celery@hostname ready. [2024-01-15 10:30:00,000: INFO/MainProcess] celery@hostname ready.
``` ```
## Остановка сервисов ## Остановка сервисов
```bash ```bash
# Остановить Docker контейнеры # Остановить Docker контейнеры
docker-compose down docker-compose down
# Остановить Django (Ctrl+C в терминале 2) # Остановить Django (Ctrl+C в терминале 2)
# Остановить Celery Worker (Ctrl+C в терминале 3) # Остановить Celery Worker (Ctrl+C в терминале 3)
``` ```
## Просмотр логов ## Просмотр логов
```bash ```bash
# Логи Celery Worker (если запущен с --logfile) # Логи Celery Worker (если запущен с --logfile)
tail -f dbapp/logs/celery_worker.log tail -f dbapp/logs/celery_worker.log
# Логи Docker контейнеров # Логи Docker контейнеров
docker-compose logs -f redis docker-compose logs -f redis
docker-compose logs -f flaresolverr docker-compose logs -f flaresolverr
``` ```
## Что дальше? ## Что дальше?
- Прочитайте полную документацию: `ASYNC_LYNGSAT_GUIDE.md` - Прочитайте полную документацию: `ASYNC_LYNGSAT_GUIDE.md`
- Настройте production окружение - Настройте production окружение
- Добавьте периодические задачи - Добавьте периодические задачи
- Настройте email уведомления - Настройте email уведомления
## Решение проблем ## Решение проблем
**Worker не запускается:** **Worker не запускается:**
```bash ```bash
# Проверьте Redis # Проверьте Redis
redis-cli ping redis-cli ping
# Проверьте переменные окружения # Проверьте переменные окружения
echo $CELERY_BROKER_URL echo $CELERY_BROKER_URL
``` ```
**Задача не выполняется:** **Задача не выполняется:**
```bash ```bash
# Проверьте FlareSolver # Проверьте FlareSolver
curl http://localhost:8191/v1 curl http://localhost:8191/v1
# Проверьте логи worker # Проверьте логи worker
tail -f dbapp/logs/celery_worker.log tail -f dbapp/logs/celery_worker.log
``` ```
**Прогресс не обновляется:** **Прогресс не обновляется:**
- Откройте консоль браузера (F12) - Откройте консоль браузера (F12)
- Проверьте Network tab на наличие ошибок - Проверьте Network tab на наличие ошибок
- Обновите страницу - Обновите страницу

View File

@@ -1,60 +1,60 @@
# Git # Git
.git .git
.gitignore .gitignore
.gitattributes .gitattributes
# Python # Python
__pycache__ __pycache__
*.py[cod] *.py[cod]
*$py.class *$py.class
*.so *.so
.Python .Python
*.egg-info *.egg-info
dist/ dist/
build/ build/
*.egg *.egg
# Virtual environments # Virtual environments
venv/ venv/
env/ env/
ENV/ ENV/
.venv .venv
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
*~ *~
# Django # Django
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
/staticfiles/ /staticfiles/
/media/ /media/
# Environment # Environment
.env .env
.env.local .env.local
.env.*.local .env.*.local
# Testing # Testing
.pytest_cache/ .pytest_cache/
.coverage .coverage
htmlcov/ htmlcov/
.tox/ .tox/
# Documentation # Documentation
*.md *.md
docs/ docs/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Docker # Docker
Dockerfile* Dockerfile*
docker-compose*.yaml docker-compose*.yaml
.dockerignore .dockerignore

View File

@@ -1,10 +1,10 @@
# Production environment variables # Production environment variables
DEBUG=False DEBUG=False
ENVIRONMENT=production ENVIRONMENT=production
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
DB_NAME=geodb DB_NAME=geodb
DB_USER=geralt DB_USER=geralt
DB_PASSWORD=123456 DB_PASSWORD=123456
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
ALLOWED_HOSTS=localhost,yourdomain.com ALLOWED_HOSTS=localhost,yourdomain.com

1
dbapp/.python-version Normal file
View File

@@ -0,0 +1 @@
3.13.7

217
dbapp/CELERY_SETUP.md Normal file
View 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
```

View File

@@ -1,57 +1,57 @@
FROM python:3.13-slim FROM python:3.13-slim
# Install system dependencies # Install system dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
gdal-bin \ gdal-bin \
libgdal-dev \ libgdal-dev \
proj-bin \ proj-bin \
proj-data \ proj-data \
libproj-dev \ libproj-dev \
libproj25 \ libproj25 \
libgeos-dev \ libgeos-dev \
libgeos-c1v5 \ libgeos-c1v5 \
build-essential \ build-essential \
postgresql-client \ postgresql-client \
libpq-dev \ libpq-dev \
libpq5 \ libpq5 \
netcat-openbsd \ netcat-openbsd \
gcc \ gcc \
g++ \ g++ \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Set environment variables # Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1
# Set work directory # Set work directory
WORKDIR /app WORKDIR /app
# Upgrade pip # Upgrade pip
RUN pip install --upgrade pip RUN pip install --upgrade pip
# Copy requirements file # Copy requirements file
COPY requirements.txt ./ COPY requirements.txt ./
# Install dependencies # Install dependencies
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy project files # Copy project files
COPY . . COPY . .
# Create directories # Create directories
RUN mkdir -p /app/staticfiles /app/logs /app/media RUN mkdir -p /app/staticfiles /app/logs /app/media
# Set permissions for entrypoint # Set permissions for entrypoint
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
# Create non-root user # Create non-root user
RUN useradd --create-home --shell /bin/bash app && \ RUN useradd --create-home --shell /bin/bash app && \
chown -R app:app /app chown -R app:app /app
USER app USER app
# Expose port # Expose port
EXPOSE 8000 EXPOSE 8000
# Run entrypoint script # Run entrypoint script
ENTRYPOINT ["/app/entrypoint.sh"] ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -1,8 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
try: try:
from .celery import app as celery_app from .celery import app as celery_app
__all__ = ('celery_app',) __all__ = ('celery_app',)
except ImportError: except ImportError:
# Celery is not installed, skip initialization pass
pass

View File

@@ -1,16 +1,16 @@
""" """
ASGI config for dbapp project. ASGI config for dbapp project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
""" """
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@@ -4,8 +4,8 @@ Celery configuration for dbapp project.
import os import os
from celery import Celery from celery import Celery
# Set the default Django settings module for the 'celery' program. # Use the environment variable to determine the settings module
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development') os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.getenv('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development'))
app = Celery('dbapp') app = Celery('dbapp')

View File

@@ -1,25 +1,25 @@
""" """
Settings module initialization. Settings module initialization.
Automatically determines the environment and loads appropriate settings. Automatically determines the environment and loads appropriate settings.
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'. Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
Defaults to 'development' if not set. Defaults to 'development' if not set.
""" """
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
# Determine the environment from DJANGO_ENVIRONMENT variable # Determine the environment from DJANGO_ENVIRONMENT variable
# Defaults to 'development' for safety # Defaults to 'development' for safety
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower() ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
if ENVIRONMENT == 'production': if ENVIRONMENT == 'production':
from .production import * from .production import *
print("Loading production settings...") print("Loading production settings...")
else: else:
from .development import * from .development import *
print("Loading development settings...") print("Loading development settings...")

View File

@@ -73,22 +73,13 @@ INSTALLED_APPS = [
"django_admin_multiple_choice_list_filter", "django_admin_multiple_choice_list_filter",
"more_admin_filters", "more_admin_filters",
"import_export", "import_export",
"django_celery_results",
# Project apps # Project apps
"mainapp", "mainapp",
"mapsapp", "mapsapp",
"lyngsatapp", "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' # AUTH_USER_MODEL = 'mainapp.CustomUser'
# ============================================================================ # ============================================================================
@@ -240,7 +231,7 @@ LEAFLET_CONFIG = {
# Celery Configuration Options # Celery Configuration Options
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") 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_CACHE_BACKEND = "default"
# Celery Task Configuration # Celery Task Configuration

View File

@@ -1,48 +1,48 @@
""" """
Development-specific settings. Development-specific settings.
""" """
from .base import * from .base import *
# ============================================================================ # ============================================================================
# DEBUG CONFIGURATION # DEBUG CONFIGURATION
# ============================================================================ # ============================================================================
DEBUG = True DEBUG = True
# ============================================================================ # ============================================================================
# ALLOWED HOSTS # ALLOWED HOSTS
# ============================================================================ # ============================================================================
# Allow all hosts in development # Allow all hosts in development
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
# ============================================================================ # ============================================================================
# INSTALLED APPS - Development additions # INSTALLED APPS - Development additions
# ============================================================================ # ============================================================================
INSTALLED_APPS += [ INSTALLED_APPS += [
'debug_toolbar', 'debug_toolbar',
] ]
# ============================================================================ # ============================================================================
# MIDDLEWARE - Development additions # MIDDLEWARE - Development additions
# ============================================================================ # ============================================================================
# Add debug toolbar middleware at the beginning # Add debug toolbar middleware at the beginning
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
# ============================================================================ # ============================================================================
# DEBUG TOOLBAR CONFIGURATION # DEBUG TOOLBAR CONFIGURATION
# ============================================================================ # ============================================================================
INTERNAL_IPS = [ INTERNAL_IPS = [
'127.0.0.1', '127.0.0.1',
] ]
# ============================================================================ # ============================================================================
# EMAIL CONFIGURATION # EMAIL CONFIGURATION
# ============================================================================ # ============================================================================
# Use console backend for development # Use console backend for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

View File

@@ -1,135 +1,135 @@
""" """
Production-specific settings. Production-specific settings.
""" """
import os import os
from .base import * from .base import *
# ============================================================================ # ============================================================================
# DEBUG CONFIGURATION # DEBUG CONFIGURATION
# ============================================================================ # ============================================================================
DEBUG = False DEBUG = False
# ============================================================================ # ============================================================================
# ALLOWED HOSTS # ALLOWED HOSTS
# ============================================================================ # ============================================================================
# In production, specify allowed hosts explicitly from environment variable # In production, specify allowed hosts explicitly from environment variable
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
# ============================================================================ # ============================================================================
# SECURITY SETTINGS # SECURITY SETTINGS
# ============================================================================ # ============================================================================
# SSL/HTTPS settings # SSL/HTTPS settings
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
# Security headers # Security headers
SECURE_BROWSER_XSS_FILTER = True SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_CONTENT_TYPE_NOSNIFF = True
# HSTS settings # HSTS settings
SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True SECURE_HSTS_PRELOAD = True
# Additional security settings # Additional security settings
SECURE_REDIRECT_EXEMPT = [] SECURE_REDIRECT_EXEMPT = []
X_FRAME_OPTIONS = "DENY" X_FRAME_OPTIONS = "DENY"
# ============================================================================ # ============================================================================
# TEMPLATE CACHING # TEMPLATE CACHING
# ============================================================================ # ============================================================================
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [ "DIRS": [
BASE_DIR / "templates", BASE_DIR / "templates",
], ],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
], ],
"loaders": [ "loaders": [
( (
"django.template.loaders.cached.Loader", "django.template.loaders.cached.Loader",
[ [
"django.template.loaders.filesystem.Loader", "django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader", "django.template.loaders.app_directories.Loader",
], ],
), ),
], ],
}, },
}, },
] ]
# ============================================================================ # ============================================================================
# STATIC FILES CONFIGURATION # STATIC FILES CONFIGURATION
# ============================================================================ # ============================================================================
STATIC_ROOT = BASE_DIR.parent / "staticfiles" STATIC_ROOT = BASE_DIR.parent / "staticfiles"
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
# ============================================================================ # ============================================================================
# LOGGING CONFIGURATION # LOGGING CONFIGURATION
# ============================================================================ # ============================================================================
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
"formatters": { "formatters": {
"verbose": { "verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{", "style": "{",
}, },
"simple": { "simple": {
"format": "{levelname} {message}", "format": "{levelname} {message}",
"style": "{", "style": "{",
}, },
}, },
"filters": { "filters": {
"require_debug_false": { "require_debug_false": {
"()": "django.utils.log.RequireDebugFalse", "()": "django.utils.log.RequireDebugFalse",
}, },
}, },
"handlers": { "handlers": {
"console": { "console": {
"level": "INFO", "level": "INFO",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "simple", "formatter": "simple",
}, },
"file": { "file": {
"level": "ERROR", "level": "ERROR",
"class": "logging.FileHandler", "class": "logging.FileHandler",
"filename": BASE_DIR.parent / "logs" / "django_errors.log", "filename": BASE_DIR.parent / "logs" / "django_errors.log",
"formatter": "verbose", "formatter": "verbose",
}, },
"mail_admins": { "mail_admins": {
"level": "ERROR", "level": "ERROR",
"class": "django.utils.log.AdminEmailHandler", "class": "django.utils.log.AdminEmailHandler",
"filters": ["require_debug_false"], "filters": ["require_debug_false"],
"formatter": "verbose", "formatter": "verbose",
}, },
}, },
"loggers": { "loggers": {
"django": { "django": {
"handlers": ["console", "file"], "handlers": ["console", "file"],
"level": "INFO", "level": "INFO",
"propagate": True, "propagate": True,
}, },
"django.request": { "django.request": {
"handlers": ["mail_admins", "file"], "handlers": ["mail_admins", "file"],
"level": "ERROR", "level": "ERROR",
"propagate": False, "propagate": False,
}, },
}, },
} }

View File

@@ -1,30 +1,30 @@
""" """
URL configuration for dbapp project. URL configuration for dbapp project.
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/ https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples: Examples:
Function views Function views
1. Add an import: from my_app import views 1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home') 2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views Class-based views
1. Add an import: from other_app.views import Home 1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from mainapp import views from mainapp import views
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
path('', include('mainapp.urls', namespace='mainapp')), path('', include('mainapp.urls', namespace='mainapp')),
path('', include('mapsapp.urls', namespace='mapsapp')), path('', include('mapsapp.urls', namespace='mapsapp')),
# Authentication URLs # Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'), path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', views.custom_logout, name='logout'), path('logout/', views.custom_logout, name='logout'),
] + debug_toolbar_urls() ] + debug_toolbar_urls()

View File

@@ -1,16 +1,16 @@
""" """
WSGI config for dbapp project. WSGI config for dbapp project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -1,37 +1,37 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Определяем окружение (по умолчанию production) # Определяем окружение (по умолчанию production)
ENVIRONMENT=${ENVIRONMENT:-production} ENVIRONMENT=${ENVIRONMENT:-production}
echo "Starting in $ENVIRONMENT mode..." echo "Starting in $ENVIRONMENT mode..."
# Ждем PostgreSQL # Ждем PostgreSQL
echo "Waiting for PostgreSQL..." echo "Waiting for PostgreSQL..."
while ! nc -z $DB_HOST $DB_PORT; do while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1 sleep 0.1
done done
echo "PostgreSQL started" echo "PostgreSQL started"
# Выполняем миграции # Выполняем миграции
echo "Running migrations..." echo "Running migrations..."
python manage.py migrate --noinput python manage.py migrate --noinput
# Собираем статику (только для production) # Собираем статику (только для production)
if [ "$ENVIRONMENT" = "production" ]; then if [ "$ENVIRONMENT" = "production" ]; then
echo "Collecting static files..." echo "Collecting static files..."
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
fi fi
# Запускаем сервер в зависимости от окружения # Запускаем сервер в зависимости от окружения
if [ "$ENVIRONMENT" = "development" ]; then if [ "$ENVIRONMENT" = "development" ]; then
echo "Starting Django development server..." echo "Starting Django development server..."
exec python manage.py runserver 0.0.0.0:8000 exec python manage.py runserver 0.0.0.0:8000
else else
echo "Starting Gunicorn..." echo "Starting Gunicorn..."
exec gunicorn --bind 0.0.0.0:8000 \ exec gunicorn --bind 0.0.0.0:8000 \
--workers ${GUNICORN_WORKERS:-3} \ --workers ${GUNICORN_WORKERS:-3} \
--timeout ${GUNICORN_TIMEOUT:-120} \ --timeout ${GUNICORN_TIMEOUT:-120} \
--reload \ --reload \
dbapp.wsgi:application dbapp.wsgi:application
fi fi

View File

@@ -1,10 +1,10 @@
from django.contrib import admin from django.contrib import admin
from .models import LyngSat from .models import LyngSat
@admin.register(LyngSat) @admin.register(LyngSat)
class LyngSatAdmin(admin.ModelAdmin): class LyngSatAdmin(admin.ModelAdmin):
list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update") list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update")
search_fields = ("id_satellite__name", "channel_info") search_fields = ("id_satellite__name", "channel_info")
list_filter = ("id_satellite", "polarization", "modulation", "standard") list_filter = ("id_satellite", "polarization", "modulation", "standard")
ordering = ("-last_update",) ordering = ("-last_update",)
readonly_fields = ("last_update",) readonly_fields = ("last_update",)

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class LyngsatappConfig(AppConfig): class LyngsatappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'lyngsatapp' name = 'lyngsatapp'

View File

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

View File

@@ -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='Дата посленего обновления'),
),
]

View File

@@ -1,37 +1,37 @@
from django.db import models from django.db import models
from mainapp.models import ( from mainapp.models import (
Satellite, Satellite,
Polarization, Polarization,
Modulation, Modulation,
Standard, Standard,
get_default_polarization, get_default_polarization,
get_default_modulation, get_default_modulation,
get_default_standard get_default_standard
) )
class LyngSat(models.Model): class LyngSat(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True) id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация" Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
) )
modulation = models.ForeignKey( modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция" Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
) )
standard = models.ForeignKey( standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт" 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="Частота, МГц") frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
sym_velocity = 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="Время") last_update = models.DateTimeField(null=True, blank=True, verbose_name="Дата посленего обновления")
channel_info = models.CharField(max_length=20, blank=True, null=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="Коэффициент коррекции ошибок") fec = models.CharField(max_length=30, blank=True, null=True, verbose_name="Коэффициент коррекции ошибок")
url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу") url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
def __str__(self): def __str__(self):
return f"Ист {self.frequency}, {self.polarization}" return f"Ист {self.frequency}, {self.polarization}"
class Meta: class Meta:
verbose_name = "Источник LyngSat" verbose_name = "Источник LyngSat"
verbose_name_plural = "Источники LyngSat" verbose_name_plural = "Источники LyngSat"

View File

@@ -1,405 +1,437 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import datetime from datetime import datetime
import re import re
import time import time
def parse_satellite_names(satellite_string: str) -> list[str]:
class LyngSatParser: slash_parts = [part.strip() for part in satellite_string.split('/')]
"""Парсер данных для LyngSat(Для работы нужен flaresolver)""" all_names = []
for part in slash_parts:
def __init__( main_match = re.match(r'^([^(]+)', part)
self, if main_match:
flaresolver_url: str = "http://localhost:8191/v1", main_name = main_match.group(1).strip()
regions: list[str] | None = None, if main_name:
target_sats: list[str] | None = None, all_names.append(main_name)
): bracket_match = re.search(r'\(([^)]+)\)', part)
self.flaresolver_url = flaresolver_url if bracket_match:
self.regions = regions bracket_name = bracket_match.group(1).strip()
self.target_sats = ( if bracket_name:
list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None all_names.append(bracket_name)
) seen = set()
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"] result = []
self.BASE_URL = "https://www.lyngsat.com" for name in all_names:
if name not in seen:
def parse_metadata(self, metadata: str) -> dict: seen.add(name)
if not metadata or not metadata.strip(): result.append(name.strip().lower())
return { return result
"standard": None,
"modulation": None,
"symbol_rate": None, class LyngSatParser:
"fec": None, """Парсер данных для LyngSat(Для работы нужен flaresolver)"""
}
normalized = re.sub(r"\s+", "", metadata.strip()) def __init__(
fec_match = re.search(r"([1-9]/[1-9])$", normalized) self,
fec = fec_match.group(1) if fec_match else None flaresolver_url: str = "http://localhost:8191/v1",
if fec_match: regions: list[str] | None = None,
core = normalized[: fec_match.start()] target_sats: list[str] | None = None,
else: ):
core = normalized self.flaresolver_url = flaresolver_url
std_match = re.match(r"(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)", core) self.regions = regions
standard = std_match.group(1) if std_match else None self.target_sats = (
rest = core[len(standard) :] if standard else core list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
modulation = None )
mod_match = re.match(r"(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)", rest) self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
if mod_match: self.BASE_URL = "https://www.lyngsat.com"
modulation = mod_match.group(1)
rest = rest[len(modulation) :] def parse_metadata(self, metadata: str) -> dict:
symbol_rate = None if not metadata or not metadata.strip():
sr_match = re.search(r"(\d+)$", rest) return {
if sr_match: "standard": None,
try: "modulation": None,
symbol_rate = int(sr_match.group(1)) "symbol_rate": None,
except ValueError: "fec": None,
pass }
normalized = re.sub(r"\s+", "", metadata.strip())
return { fec_match = re.search(r"([1-9]/[1-9])$", normalized)
"standard": standard, fec = fec_match.group(1) if fec_match else None
"modulation": modulation, if fec_match:
"symbol_rate": symbol_rate, core = normalized[: fec_match.start()]
"fec": fec, else:
} core = normalized
std_match = re.match(r"(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)", core)
def extract_date(self, s: str) -> datetime | None: standard = std_match.group(1) if std_match else None
s = s.strip() rest = core[len(standard) :] if standard else core
match = re.search(r"(\d{6})$", s) modulation = None
if not match: mod_match = re.match(r"(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)", rest)
return None if mod_match:
yymmdd = match.group(1) modulation = mod_match.group(1)
try: rest = rest[len(modulation) :]
return datetime.strptime(yymmdd, "%y%m%d").date() symbol_rate = None
except ValueError: sr_match = re.search(r"(\d+)$", rest)
return None if sr_match:
try:
def convert_polarization(self, polarization: str) -> str: symbol_rate = int(sr_match.group(1))
"""Преобразовать код поляризации в понятное название на русском""" except ValueError:
polarization_map = { pass
"V": "Вертикальная",
"H": "Горизонтальная", return {
"R": "Правая", "standard": standard,
"L": "Левая", "modulation": modulation,
} "symbol_rate": symbol_rate,
return polarization_map.get(polarization.upper(), polarization) "fec": fec,
}
def get_region_pages(self, regions: list[str] | None = None) -> list[str]:
html_regions = [] def extract_date(self, s: str) -> datetime | None:
if regions is None: s = s.strip()
regions = self.regions match = re.search(r"(\d{6})$", s)
for region in regions: if not match:
url = f"{self.BASE_URL}/{region}.html" return None
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000} yymmdd = match.group(1)
response = requests.post(self.flaresolver_url, json=payload) try:
if response.status_code != 200: return datetime.strptime(yymmdd, "%y%m%d").date()
continue except ValueError:
html_content = response.json().get("solution", {}).get("response", "") return None
html_regions.append(html_content)
print(f"Обработал страницу по {region}") def convert_polarization(self, polarization: str) -> str:
return html_regions """Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
def get_satellite_urls(self, html_regions: list[str]): "V": "Вертикальная",
sat_names = [] "H": "Горизонтальная",
sat_urls = [] "R": "Правая",
for region_page in html_regions: "L": "Левая",
soup = BeautifulSoup(region_page, "html.parser") }
return polarization_map.get(polarization.upper(), polarization)
col_table = soup.find_all("div", class_="desktab")[0]
def get_region_pages(self, regions: list[str] | None = None) -> list[str]:
tables = col_table.find_next_sibling("table").find_all("table") html_regions = []
trs = [] if regions is None:
for table in tables: regions = self.regions
trs.extend(table.find_all("tr")) for region in regions:
for tr in trs: url = f"{self.BASE_URL}/{region}.html"
sat_name = tr.find("span").text payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
if self.target_sats is not None: response = requests.post(self.flaresolver_url, json=payload)
if sat_name.strip().lower() not in self.target_sats: if response.status_code != 200:
continue continue
try: html_content = response.json().get("solution", {}).get("response", "")
sat_url = tr.find_all("a")[2]["href"] html_regions.append(html_content)
except IndexError: print(f"Обработал страницу по {region}")
sat_url = tr.find_all("a")[0]["href"] return html_regions
sat_names.append(sat_name)
sat_urls.append(sat_url) def get_satellite_urls(self, html_regions: list[str]):
return sat_names, sat_urls sat_names = []
sat_urls = []
def get_satellites_data(self) -> dict[dict]: for region_page in html_regions:
sat_data = {} soup = BeautifulSoup(region_page, "html.parser")
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]
col_table = soup.find_all("div", class_="desktab")[0] tables = col_table.find_next_sibling("table").find_all("table")
trs = []
tables = col_table.find_next_sibling("table").find_all("table") for table in tables:
trs = [] trs.extend(table.find_all("tr"))
for table in tables: for tr in trs:
trs.extend(table.find_all("tr")) sat_name = tr.find("span").text
for tr in trs: if self.target_sats is not None:
sat_name = tr.find("span").text if sat_name.strip().lower() not in self.target_sats:
if self.target_sats is not None: continue
if sat_name.strip().lower() not in self.target_sats: try:
continue sat_url = tr.find_all("a")[2]["href"]
try: except IndexError:
sat_url = tr.find_all("a")[2]["href"] sat_url = tr.find_all("a")[0]["href"]
except IndexError: sat_names.append(sat_name)
sat_url = tr.find_all("a")[0]["href"] sat_urls.append(sat_url)
return sat_names, sat_urls
update_date = tr.find_all("td")[-1].text
sat_response = requests.post( def get_satellites_data(self) -> dict[dict]:
self.flaresolver_url, sat_data = {}
json={ for region_page in self.get_region_pages(self.regions):
"cmd": "request.get", soup = BeautifulSoup(region_page, "html.parser")
"url": f"{self.BASE_URL}/{sat_url}",
"maxTimeout": 60000, col_table = soup.find_all("div", class_="desktab")[0]
},
) tables = col_table.find_next_sibling("table").find_all("table")
html_content = ( trs = []
sat_response.json().get("solution", {}).get("response", "") for table in tables:
) trs.extend(table.find_all("tr"))
sat_page_data = self.get_satellite_content(html_content) for tr in trs:
sat_data[sat_name] = { sat_name = tr.find("span").text.replace("ü", "u").strip().lower()
"url": f"{self.BASE_URL}/{sat_url}", if self.target_sats is not None:
"update_date": datetime.strptime(update_date, "%y%m%d").date(), names = parse_satellite_names(sat_name)
"sources": sat_page_data, if len(names) == 1:
} sat_name = names[0]
return sat_data else:
for name in names:
def get_satellite_content(self, html_content: str) -> dict: if name in self.target_sats:
sat_soup = BeautifulSoup(html_content, "html.parser") sat_name = name
big_table = sat_soup.find("table", class_="bigtable") if sat_name not in self.target_sats:
all_tables = big_table.find_all("div", class_="desktab")[:-1] continue
data = [] try:
for table in all_tables: sat_url = tr.find_all("a")[2]["href"]
trs = table.find_next_sibling("table").find_all("tr") except IndexError:
for idx, tr in enumerate(trs): sat_url = tr.find_all("a")[0]["href"]
tds = tr.find_all("td")
if len(tds) < 9 or idx < 2: update_date = tr.find_all("td")[-1].text
continue sat_response = requests.post(
freq, polarization = tds[0].find("b").text.strip().split("\xa0") self.flaresolver_url,
polarization = self.convert_polarization(polarization) json={
meta = self.parse_metadata(tds[1].text) "cmd": "request.get",
provider_name = tds[3].text "url": f"{self.BASE_URL}/{sat_url}",
last_update = self.extract_date(tds[-1].text) "maxTimeout": 60000,
data.append( },
{ )
"freq": freq, html_content = (
"pol": polarization, sat_response.json().get("solution", {}).get("response", "")
"metadata": meta, )
"provider_name": provider_name, sat_page_data = self.get_satellite_content(html_content)
"last_update": last_update, sat_data[sat_name] = {
} "url": f"{self.BASE_URL}/{sat_url}",
) "update_date": datetime.strptime(update_date, "%y%m%d").date(),
return data "sources": sat_page_data,
}
return sat_data
class KingOfSatParser:
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0): def get_satellite_content(self, html_content: str) -> list[dict]:
""" data = []
Инициализация парсера sat_soup = BeautifulSoup(html_content, "html.parser")
:param base_url: Базовый URL сайта try:
:param max_satellites: Максимальное количество спутников для парсинга (0 - все) big_table = sat_soup.find("table", class_="bigtable")
""" all_tables = big_table.find_all("div", class_="desktab")[:-1]
self.base_url = base_url for table in all_tables:
self.max_satellites = max_satellites trs = table.find_next_sibling("table").find_all("tr")
self.session = requests.Session() for idx, tr in enumerate(trs):
self.session.headers.update( tds = tr.find_all("td")
{ if len(tds) < 9 or idx < 2:
"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" continue
} freq, polarization = tds[0].find("b").text.strip().split("\xa0")
) polarization = self.convert_polarization(polarization)
meta = self.parse_metadata(tds[1].text)
def convert_polarization(self, polarization): provider_name = tds[3].text
"""Преобразовать код поляризации в понятное название на русском""" last_update = self.extract_date(tds[-1].text)
polarization_map = { data.append(
"V": "Вертикальная", {
"H": "Горизонтальная", "freq": freq,
"R": "Правая", "pol": polarization,
"L": "Левая", "metadata": meta,
} "provider_name": provider_name,
return polarization_map.get(polarization.upper(), polarization) "last_update": last_update,
}
def fetch_page(self, url): )
"""Получить HTML страницу""" except Exception as e:
try: print(e)
response = self.session.get(url, timeout=30) return data if data else data[{}]
response.raise_for_status()
return response.text
except Exception as e: class KingOfSatParser:
print(f"Ошибка при получении страницы {url}: {e}") def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
return None """
Инициализация парсера
def parse_satellite_table(self, html_content): :param base_url: Базовый URL сайта
"""Распарсить таблицу со спутниками""" :param max_satellites: Максимальное количество спутников для парсинга (0 - все)
soup = BeautifulSoup(html_content, "html.parser") """
satellites = [] self.base_url = base_url
table = soup.find("table") self.max_satellites = max_satellites
if not table: self.session = requests.Session()
print("Таблица не найдена") self.session.headers.update(
return satellites {
"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"
rows = table.find_all("tr")[1:] }
)
for row in rows:
cols = row.find_all("td") def convert_polarization(self, polarization):
if len(cols) < 13: """Преобразовать код поляризации в понятное название на русском"""
continue polarization_map = {
"V": "Вертикальная",
try: "H": "Горизонтальная",
position_cell = cols[0].text.strip() "R": "Правая",
position_match = re.search(r"([\d\.]+)°([EW])", position_cell) "L": "Левая",
if position_match: }
position_value = position_match.group(1) return polarization_map.get(polarization.upper(), polarization)
position_direction = position_match.group(2)
position = f"{position_value}{position_direction}" def fetch_page(self, url):
else: """Получить HTML страницу"""
position = None try:
response = self.session.get(url, timeout=30)
# Название спутника (2-я колонка) response.raise_for_status()
satellite_cell = cols[1] return response.text
satellite_name = satellite_cell.get_text(strip=True) except Exception as e:
# Удаляем возможные лишние символы или пробелы print(f"Ошибка при получении страницы {url}: {e}")
satellite_name = re.sub(r"\s+", " ", satellite_name).strip() return None
# NORAD (3-я колонка) def parse_satellite_table(self, html_content):
norad = cols[2].text.strip() """Распарсить таблицу со спутниками"""
if not norad or norad == "-": soup = BeautifulSoup(html_content, "html.parser")
norad = None satellites = []
table = soup.find("table")
ini_link = None if not table:
ini_cell = cols[3] print("Таблица не найдена")
ini_img = ini_cell.find("img", src=lambda x: x and "disquette.gif" in x) return satellites
if ini_img and position:
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0" rows = table.find_all("tr")[1:]
update_date = cols[12].text.strip() if len(cols) > 12 else None for row in rows:
cols = row.find_all("td")
if satellite_name and ini_link and position: if len(cols) < 13:
satellites.append( continue
{
"position": position, try:
"name": satellite_name, position_cell = cols[0].text.strip()
"norad": norad, position_match = re.search(r"([\d\.]+)°([EW])", position_cell)
"ini_url": ini_link, if position_match:
"update_date": update_date, position_value = position_match.group(1)
} position_direction = position_match.group(2)
) position = f"{position_value}{position_direction}"
else:
except Exception as e: position = None
print(f"Ошибка при обработке строки таблицы: {e}")
continue # Название спутника (2-я колонка)
satellite_cell = cols[1]
return satellites satellite_name = satellite_cell.get_text(strip=True)
# Удаляем возможные лишние символы или пробелы
def parse_ini_file(self, ini_content): satellite_name = re.sub(r"\s+", " ", satellite_name).strip()
"""Распарсить содержимое .ini файла"""
data = {"metadata": {}, "sattype": {}, "dvb": {}} # NORAD (3-я колонка)
norad = cols[2].text.strip()
# # Извлекаем метаданные из комментариев if not norad or norad == "-":
# 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) norad = None
# if metadata_match:
# data['metadata']['downloaded'] = metadata_match.group(1) ini_link = None
ini_cell = cols[3]
# Парсим секцию [SATTYPE] ini_img = ini_cell.find("img", src=lambda x: x and "disquette.gif" in x)
sattype_match = re.search(r"\[SATTYPE\](.*?)\n\[", ini_content, re.DOTALL) if ini_img and position:
if sattype_match: ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
sattype_content = sattype_match.group(1).strip()
for line in sattype_content.split("\n"): update_date = cols[12].text.strip() if len(cols) > 12 else None
line = line.strip()
if "=" in line: if satellite_name and ini_link and position:
key, value = line.split("=", 1) satellites.append(
data["sattype"][key.strip()] = value.strip() {
"position": position,
# Парсим секцию [DVB] "name": satellite_name,
dvb_match = re.search(r"\[DVB\](.*?)(?:\n\[|$)", ini_content, re.DOTALL) "norad": norad,
if dvb_match: "ini_url": ini_link,
dvb_content = dvb_match.group(1).strip() "update_date": update_date,
for line in dvb_content.split("\n"): }
line = line.strip() )
if "=" in line:
key, value = line.split("=", 1) except Exception as e:
params = [p.strip() for p in value.split(",")] print(f"Ошибка при обработке строки таблицы: {e}")
polarization = params[1] if len(params) > 1 else "" continue
if polarization:
polarization = self.convert_polarization(polarization) return satellites
data["dvb"][key.strip()] = { def parse_ini_file(self, ini_content):
"frequency": params[0] if len(params) > 0 else "", """Распарсить содержимое .ini файла"""
"polarization": polarization, data = {"metadata": {}, "sattype": {}, "dvb": {}}
"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 "", # 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)
"modulation": params[5] if len(params) > 5 else "", # if metadata_match:
} # data['metadata']['downloaded'] = metadata_match.group(1)
return data # Парсим секцию [SATTYPE]
sattype_match = re.search(r"\[SATTYPE\](.*?)\n\[", ini_content, re.DOTALL)
def download_ini_file(self, url): if sattype_match:
"""Скачать содержимое .ini файла""" sattype_content = sattype_match.group(1).strip()
try: for line in sattype_content.split("\n"):
response = self.session.get(url, timeout=30) line = line.strip()
response.raise_for_status() if "=" in line:
return response.text key, value = line.split("=", 1)
except Exception as e: data["sattype"][key.strip()] = value.strip()
print(f"Ошибка при скачивании .ini файла {url}: {e}")
return None # Парсим секцию [DVB]
dvb_match = re.search(r"\[DVB\](.*?)(?:\n\[|$)", ini_content, re.DOTALL)
def get_all_satellites_data(self): if dvb_match:
"""Получить данные всех спутников с учетом ограничения max_satellites""" dvb_content = dvb_match.group(1).strip()
html_content = self.fetch_page(self.base_url + "/satellites") for line in dvb_content.split("\n"):
if not html_content: line = line.strip()
return [] if "=" in line:
key, value = line.split("=", 1)
satellites = self.parse_satellite_table(html_content) params = [p.strip() for p in value.split(",")]
polarization = params[1] if len(params) > 1 else ""
if self.max_satellites > 0 and len(satellites) > self.max_satellites: if polarization:
satellites = satellites[: self.max_satellites] polarization = self.convert_polarization(polarization)
results = [] data["dvb"][key.strip()] = {
processed_count = 0 "frequency": params[0] if len(params) > 0 else "",
"polarization": polarization,
for satellite in satellites: "symbol_rate": params[2] if len(params) > 2 else "",
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})") "fec": params[3] if len(params) > 3 else "",
"standard": params[4] if len(params) > 4 else "",
ini_content = self.download_ini_file(satellite["ini_url"]) "modulation": params[5] if len(params) > 5 else "",
if not ini_content: }
print(f"Не удалось скачать .ini файл для {satellite['name']}")
continue return data
parsed_ini = self.parse_ini_file(ini_content) def download_ini_file(self, url):
"""Скачать содержимое .ini файла"""
result = { try:
"satellite_name": satellite["name"], response = self.session.get(url, timeout=30)
"position": satellite["position"], response.raise_for_status()
"norad": satellite["norad"], return response.text
"update_date": satellite["update_date"], except Exception as e:
"ini_url": satellite["ini_url"], print(f"Ошибка при скачивании .ini файла {url}: {e}")
"ini_data": parsed_ini, return None
}
def get_all_satellites_data(self):
results.append(result) """Получить данные всех спутников с учетом ограничения max_satellites"""
processed_count += 1 html_content = self.fetch_page(self.base_url + "/satellites")
if not html_content:
if self.max_satellites > 0 and processed_count >= self.max_satellites: return []
break
satellites = self.parse_satellite_table(html_content)
time.sleep(1)
if self.max_satellites > 0 and len(satellites) > self.max_satellites:
return results satellites = satellites[: self.max_satellites]
def create_satellite_dict(self, satellites_data): results = []
"""Создать словарь с данными спутников""" processed_count = 0
satellite_dict = {}
for satellite in satellites:
for data in satellites_data: print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
satellite_dict[key] = { ini_content = self.download_ini_file(satellite["ini_url"])
"name": data["satellite_name"], if not ini_content:
"position": data["position"], print(f"Не удалось скачать .ini файл для {satellite['name']}")
"norad": data["norad"], continue
"update_date": data["update_date"],
"ini_url": data["ini_url"], parsed_ini = self.parse_ini_file(ini_content)
"transponders_count": len(data["ini_data"]["dvb"]),
"transponders": data["ini_data"]["dvb"], result = {
"sattype_info": data["ini_data"]["sattype"], "satellite_name": satellite["name"],
"metadata": data["ini_data"]["metadata"], "position": satellite["position"],
} "norad": satellite["norad"],
"update_date": satellite["update_date"],
return satellite_dict "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

View File

@@ -1,73 +1,73 @@
""" """
Celery tasks for Lyngsat data processing. Celery tasks for Lyngsat data processing.
""" """
import logging import logging
from celery import shared_task from celery import shared_task
from django.core.cache import cache from django.core.cache import cache
from .utils import fill_lyngsat_data from .utils import fill_lyngsat_data
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async') @shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
def fill_lyngsat_data_task(self, target_sats, regions=None): def fill_lyngsat_data_task(self, target_sats, regions=None):
""" """
Асинхронная задача для заполнения данных Lyngsat. Асинхронная задача для заполнения данных Lyngsat.
Args: Args:
target_sats: Список названий спутников для обработки target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все) regions: Список регионов для парсинга (по умолчанию все)
Returns: Returns:
dict: Статистика обработки dict: Статистика обработки
""" """
task_id = self.request.id task_id = self.request.id
logger.info(f"[Task {task_id}] Начало обработки данных Lyngsat") logger.info(f"[Task {task_id}] Начало обработки данных Lyngsat")
logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}") logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}")
logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}") logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}")
# Обновляем статус задачи # Обновляем статус задачи
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': 0, 'current': 0,
'total': len(target_sats), 'total': len(target_sats),
'status': 'Инициализация...' 'status': 'Инициализация...'
} }
) )
try: try:
# Вызываем функцию заполнения данных # Вызываем функцию заполнения данных
stats = fill_lyngsat_data( stats = fill_lyngsat_data(
target_sats=target_sats, target_sats=target_sats,
regions=regions, regions=regions,
task_id=task_id, task_id=task_id,
update_progress=lambda current, total, status: self.update_state( update_progress=lambda current, total, status: self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': current, 'current': current,
'total': total, 'total': total,
'status': status 'status': status
} }
) )
) )
logger.info(f"[Task {task_id}] Обработка завершена успешно") logger.info(f"[Task {task_id}] Обработка завершена успешно")
logger.info(f"[Task {task_id}] Статистика: {stats}") logger.info(f"[Task {task_id}] Статистика: {stats}")
# Сохраняем результат в кеш для отображения на странице # Сохраняем результат в кеш для отображения на странице
cache.set(f'lyngsat_task_{task_id}', stats, timeout=3600) cache.set(f'lyngsat_task_{task_id}', stats, timeout=3600)
return stats return stats
except Exception as e: except Exception as e:
logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True) logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True)
self.update_state( self.update_state(
state='FAILURE', state='FAILURE',
meta={ meta={
'error': str(e), 'error': str(e),
'status': 'Ошибка при обработке' 'status': 'Ошибка при обработке'
} }
) )
raise raise

View File

@@ -1,3 +1,3 @@
from django.test import TestCase from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@@ -1,170 +1,175 @@
import logging import logging
from .parser import LyngSatParser from .parser import LyngSatParser
from .models import LyngSat from .models import LyngSat
from mainapp.models import Polarization, Standard, Modulation, Satellite from mainapp.models import Polarization, Standard, Modulation, Satellite
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def fill_lyngsat_data( def fill_lyngsat_data(
target_sats: list[str], target_sats: list[str],
regions: list[str] = None, regions: list[str] = None,
task_id: str = None, task_id: str = None,
update_progress=None update_progress=None
): ):
""" """
Заполняет данные Lyngsat для указанных спутников и регионов. Заполняет данные Lyngsat для указанных спутников и регионов.
Args: Args:
target_sats: Список названий спутников для обработки target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все) regions: Список регионов для парсинга (по умолчанию все)
task_id: ID задачи Celery для логирования task_id: ID задачи Celery для логирования
update_progress: Функция для обновления прогресса (current, total, status) update_progress: Функция для обновления прогресса (current, total, status)
Returns: Returns:
dict: Статистика обработки с ключами: dict: Статистика обработки с ключами:
- total_satellites: общее количество спутников - total_satellites: общее количество спутников
- total_sources: общее количество источников - total_sources: общее количество источников
- created: количество созданных записей - created: количество созданных записей
- updated: количество обновленных записей - updated: количество обновленных записей
- errors: список ошибок - errors: список ошибок
""" """
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]" log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]"
stats = { stats = {
'total_satellites': 0, 'total_satellites': 0,
'total_sources': 0, 'total_sources': 0,
'created': 0, 'created': 0,
'updated': 0, 'updated': 0,
'errors': [] 'errors': []
} }
if regions is None: if regions is None:
regions = ["europe", "asia", "america", "atlantic"] regions = ["europe", "asia", "america", "atlantic"]
logger.info(f"{log_prefix} Начало парсинга данных") logger.info(f"{log_prefix} Начало парсинга данных")
logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}") logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}")
logger.info(f"{log_prefix} Регионы: {', '.join(regions)}") logger.info(f"{log_prefix} Регионы: {', '.join(regions)}")
if update_progress: if update_progress:
update_progress(0, len(target_sats), "Инициализация парсера...") update_progress(0, len(target_sats), "Инициализация парсера...")
try: try:
parser = LyngSatParser( parser = LyngSatParser(
target_sats=target_sats, flaresolver_url="http://localhost:8191/v1",
regions=regions target_sats=target_sats,
) regions=regions
)
logger.info(f"{log_prefix} Получение данных со спутников...")
if update_progress: logger.info(f"{log_prefix} Получение данных со спутников...")
update_progress(0, len(target_sats), "Получение данных со спутников...") if update_progress:
update_progress(0, len(target_sats), "Получение данных со спутников...")
lyngsat_data = parser.get_satellites_data()
stats['total_satellites'] = len(lyngsat_data) lyngsat_data = parser.get_satellites_data()
stats['total_satellites'] = len(lyngsat_data)
logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам")
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}") 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}...") if update_progress:
update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...")
url = data['url']
sources = data['sources'] url = data['url']
stats['total_sources'] += len(sources) sources = data['sources']
stats['total_sources'] += len(sources)
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе
try: # Находим спутник в базе
sat_obj = Satellite.objects.get(name__icontains=sat_name) try:
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})") sat_obj = Satellite.objects.get(name__icontains=sat_name)
except Satellite.DoesNotExist: logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
error_msg = f"Спутник '{sat_name}' не найден в базе данных" except Satellite.DoesNotExist:
logger.warning(f"{log_prefix} {error_msg}") error_msg = f"Спутник '{sat_name}' не найден в базе данных"
stats['errors'].append(error_msg) logger.warning(f"{log_prefix} {error_msg}")
continue stats['errors'].append(error_msg)
except Satellite.MultipleObjectsReturned: continue
error_msg = f"Найдено несколько спутников с именем '{sat_name}'" except Satellite.MultipleObjectsReturned:
logger.warning(f"{log_prefix} {error_msg}") error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
stats['errors'].append(error_msg) logger.warning(f"{log_prefix} {error_msg}")
continue stats['errors'].append(error_msg)
continue
for source_idx, source in enumerate(sources, 1):
try: for source_idx, source in enumerate(sources, 1):
# Парсим частоту try:
try: # Парсим частоту
freq = float(source['freq']) try:
except (ValueError, TypeError): freq = float(source['freq'])
freq = -1.0 except (ValueError, TypeError):
error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}" freq = -1.0
logger.debug(f"{log_prefix} {error_msg}") error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}"
stats['errors'].append(error_msg) logger.debug(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
last_update = source['last_update']
fec = source['metadata'].get('fec') last_update = source['last_update']
modulation_name = source['metadata'].get('modulation') fec = source['metadata'].get('fec')
standard_name = source['metadata'].get('standard') modulation_name = source['metadata'].get('modulation')
symbol_velocity = source['metadata'].get('symbol_rate') standard_name = source['metadata'].get('standard')
polarization_name = source['pol'] symbol_velocity = source['metadata'].get('symbol_rate')
channel_info = source['provider_name'] polarization_name = source['pol']
channel_info = source['provider_name']
# Создаем или получаем связанные объекты
pol_obj, _ = Polarization.objects.get_or_create( # Создаем или получаем связанные объекты
name=polarization_name if polarization_name else "-" 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 "-" 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 "-" standard_obj, _ = Standard.objects.get_or_create(
) name=standard_name if standard_name else "-"
)
# Создаем или обновляем запись Lyngsat
lyng_obj, created = LyngSat.objects.update_or_create( # Создаем или обновляем запись Lyngsat
id_satellite=sat_obj, lyng_obj, created = LyngSat.objects.update_or_create(
frequency=freq, id_satellite=sat_obj,
polarization=pol_obj, frequency=freq,
defaults={ polarization=pol_obj,
"modulation": mod_obj, defaults={
"standard": standard_obj, "modulation": mod_obj,
"sym_velocity": symbol_velocity if symbol_velocity else 0, "standard": standard_obj,
"channel_info": channel_info[:20] if channel_info else "", "sym_velocity": symbol_velocity if symbol_velocity else 0,
"last_update": last_update, "channel_info": channel_info[:20] if channel_info else "",
"fec": fec[:30] if fec else "", "last_update": last_update,
"url": url "fec": fec[:30] if fec else "",
} "url": url
) }
)
if created:
stats['created'] += 1 if created:
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц") stats['created'] += 1
else: logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц")
stats['updated'] += 1 else:
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц") stats['updated'] += 1
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц")
# Логируем прогресс каждые 10 источников
if source_idx % 10 == 0: # Логируем прогресс каждые 10 источников
logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}") 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)}" except Exception as e:
logger.error(f"{log_prefix} {error_msg}", exc_info=True) error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
stats['errors'].append(error_msg) logger.error(f"{log_prefix} {error_msg}", exc_info=True)
continue stats['errors'].append(error_msg)
continue
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
except Exception as e:
error_msg = f"Критическая ошибка: {str(e)}" except Exception as e:
logger.error(f"{log_prefix} {error_msg}", exc_info=True) error_msg = f"Критическая ошибка: {str(e)}"
stats['errors'].append(error_msg) 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'])}")
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}")
if update_progress:
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено") if update_progress:
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
return stats
return stats
def link_lyngsat_to_sources():
pass

View File

@@ -1,3 +1,3 @@
from django.shortcuts import render from django.shortcuts import render
# Create your views here. # Create your views here.

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
# Third-party imports # Third-party imports
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
# Local imports # Local imports
from .models import ObjItem from .models import ObjItem
def get_clusters(coords: list[tuple[float, float]]): def get_clusters(coords: list[tuple[float, float]]):
coords = np.radians(coords) coords = np.radians(coords)
lat, lon = coords[:, 0], coords[:, 1] lat, lon = coords[:, 0], coords[:, 1]
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine') db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
# db = HDBSCAN() # db = HDBSCAN()
cluster_labels = db.fit_predict(coords) cluster_labels = db.fit_predict(coords)
plt.figure(figsize=(10, 8)) plt.figure(figsize=(10, 8))
unique_labels = set(cluster_labels) unique_labels = set(cluster_labels)
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels))) colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
for label, color in zip(unique_labels, colors): for label, color in zip(unique_labels, colors):
if label == -1: if label == -1:
color = 'k' color = 'k'
label_name = 'Шум' label_name = 'Шум'
else: else:
label_name = f'Кластер {label}' label_name = f'Кластер {label}'
mask = cluster_labels == label mask = cluster_labels == label
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30) plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
plt.xlabel('Долгота') plt.xlabel('Долгота')
plt.ylabel('Широта') plt.ylabel('Широта')
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)') plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
plt.legend() plt.legend()
plt.grid(True) plt.grid(True)
plt.show() plt.show()

View File

@@ -1,76 +1,76 @@
# Django imports # Django imports
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
# Local imports # Local imports
from .models import ObjItem from .models import ObjItem
class GeoKupDistanceFilter(SimpleListFilter): class GeoKupDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и кубсатом' title = 'Расстояние между гео и кубсатом'
parameter_name = 'distance_geo_kup' parameter_name = 'distance_geo_kup'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('small', 'Меньше 100 км'), ('small', 'Меньше 100 км'),
('medium', '100-500 км'), ('medium', '100-500 км'),
('large', 'Больше 500 км'), ('large', 'Больше 500 км'),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'small': if self.value() == 'small':
return queryset.filter(distance_coords_kup__lt=100) return queryset.filter(distance_coords_kup__lt=100)
if self.value() == 'medium': if self.value() == 'medium':
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500) return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
if self.value() == 'large': if self.value() == 'large':
return queryset.filter(distance_coords_kup__gt=500) return queryset.filter(distance_coords_kup__gt=500)
class GeoValidDistanceFilter(SimpleListFilter): class GeoValidDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и оперативным отделом' title = 'Расстояние между гео и оперативным отделом'
parameter_name = 'distance_geo_valid' parameter_name = 'distance_geo_valid'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('small', 'Меньше 100 км'), ('small', 'Меньше 100 км'),
('medium', '100-500 км'), ('medium', '100-500 км'),
('large', 'Больше 500 км'), ('large', 'Больше 500 км'),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'small': if self.value() == 'small':
return queryset.filter(distance_coords_valid__lt=100) return queryset.filter(distance_coords_valid__lt=100)
if self.value() == 'medium': if self.value() == 'medium':
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500) return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
if self.value() == 'large': if self.value() == 'large':
return queryset.filter(distance_coords_valid__gt=500) return queryset.filter(distance_coords_valid__gt=500)
class UniqueToggleFilter(SimpleListFilter): class UniqueToggleFilter(SimpleListFilter):
title = 'Уникальность по имени' title = 'Уникальность по имени'
parameter_name = 'name' parameter_name = 'name'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('unique', 'Только уникальные'), ('unique', 'Только уникальные'),
('all', 'Все'), ('all', 'Все'),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'unique': if self.value() == 'unique':
return queryset.order_by('name').distinct('name') return queryset.order_by('name').distinct('name')
return queryset return queryset
class HasSigmaParameterFilter(SimpleListFilter): class HasSigmaParameterFilter(SimpleListFilter):
title = 'ВЧ sigma' title = 'ВЧ sigma'
parameter_name = 'has_sigma' parameter_name = 'has_sigma'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('yes', 'Заполнено'), ('yes', 'Заполнено'),
('no', 'Пусто'), ('no', 'Пусто'),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'yes': if self.value() == 'yes':
return queryset.filter(sigma_parameter__isnull=False) return queryset.filter(sigma_parameter__isnull=False)
if self.value() == 'no': if self.value() == 'no':
return queryset.filter(sigma_parameter__isnull=True) return queryset.filter(sigma_parameter__isnull=True)
return queryset return queryset

View File

@@ -1,301 +1,301 @@
# Django imports # Django imports
from django import forms from django import forms
# Local imports # Local imports
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
class UploadFileForm(forms.Form): class UploadFileForm(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите файл", label="Выберите файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={
'class': 'form-file-input' 'class': 'form-file-input'
}) })
) )
class LoadExcelData(forms.Form): class LoadExcelData(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите Excel файл", label="Выберите Excel файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={
'class': 'form-control', 'class': 'form-control',
'accept': '.xlsx,.xls' 'accept': '.xlsx,.xls'
}) })
) )
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={
'class': 'form-select' 'class': 'form-select'
}) })
) )
number_input = forms.IntegerField( number_input = forms.IntegerField(
label="Введите число объектов", label="Введите число объектов",
min_value=0, min_value=0,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control' 'class': 'form-control'
}) })
) )
class LoadCsvData(forms.Form): class LoadCsvData(forms.Form):
file = forms.FileField( file = forms.FileField(
label="Выберите CSV файл", label="Выберите CSV файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={
'class': 'form-control', 'class': 'form-control',
'accept': '.csv' 'accept': '.csv'
}) })
) )
class UploadVchLoad(UploadFileForm): class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={
'class': 'form-select' 'class': 'form-select'
}) })
) )
class VchLinkForm(forms.Form): class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
widget=forms.Select(attrs={ widget=forms.Select(attrs={
'class': 'form-select' 'class': 'form-select'
}) })
) )
# ku_range = forms.ChoiceField( # ku_range = forms.ChoiceField(
# choices=[(9750.0, '9750'), (10750.0, '10750')], # choices=[(9750.0, '9750'), (10750.0, '10750')],
# # coerce=lambda x: x == 'True', # # coerce=lambda x: x == 'True',
# widget=forms.Select(attrs={'class': 'form-select'}), # widget=forms.Select(attrs={'class': 'form-select'}),
# label='Выбор диапазона' # label='Выбор диапазона'
# ) # )
value1 = forms.FloatField( value1 = forms.FloatField(
label="Первое число", label="Первое число",
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите первое число' 'placeholder': 'Введите первое число'
}) })
) )
value2 = forms.FloatField( value2 = forms.FloatField(
label="Второе число", label="Второе число",
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите второе число' 'placeholder': 'Введите второе число'
}) })
) )
class NewEventForm(forms.Form): class NewEventForm(forms.Form):
# sat_choice = forms.ModelChoiceField( # sat_choice = forms.ModelChoiceField(
# queryset=Satellite.objects.all(), # queryset=Satellite.objects.all(),
# label="Выберите спутник", # label="Выберите спутник",
# widget=forms.Select(attrs={ # widget=forms.Select(attrs={
# 'class': 'form-select' # 'class': 'form-select'
# }) # })
# ) # )
# pol_choice = forms.ModelChoiceField( # pol_choice = forms.ModelChoiceField(
# queryset=Polarization.objects.all(), # queryset=Polarization.objects.all(),
# label="Выберите поляризацию", # label="Выберите поляризацию",
# widget=forms.Select(attrs={ # widget=forms.Select(attrs={
# 'class': 'form-select' # 'class': 'form-select'
# }) # })
# ) # )
file = forms.FileField( file = forms.FileField(
label="Выберите файл", label="Выберите файл",
widget=forms.FileInput(attrs={ widget=forms.FileInput(attrs={
'class': 'form-control', 'class': 'form-control',
'accept': '.xlsx,.xls' 'accept': '.xlsx,.xls'
}) })
) )
class FillLyngsatDataForm(forms.Form): class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat""" """Форма для заполнения данных из Lyngsat"""
REGION_CHOICES = [ REGION_CHOICES = [
('europe', 'Европа'), ('europe', 'Европа'),
('asia', 'Азия'), ('asia', 'Азия'),
('america', 'Америка'), ('america', 'Америка'),
('atlantic', 'Атлантика'), ('atlantic', 'Атлантика'),
] ]
satellites = forms.ModelMultipleChoiceField( satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'), queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники", label="Выберите спутники",
widget=forms.SelectMultiple(attrs={ widget=forms.SelectMultiple(attrs={
'class': 'form-select', 'class': 'form-select',
'size': '10' 'size': '10'
}), }),
required=True, required=True,
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников" help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
) )
regions = forms.MultipleChoiceField( regions = forms.MultipleChoiceField(
choices=REGION_CHOICES, choices=REGION_CHOICES,
label="Выберите регионы", label="Выберите регионы",
widget=forms.SelectMultiple(attrs={ widget=forms.SelectMultiple(attrs={
'class': 'form-select', 'class': 'form-select',
'size': '4' 'size': '4'
}), }),
required=True, required=True,
initial=['europe', 'asia', 'america', 'atlantic'], initial=['europe', 'asia', 'america', 'atlantic'],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов" help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
) )
class ParameterForm(forms.ModelForm): class ParameterForm(forms.ModelForm):
""" """
Форма для создания и редактирования параметров ВЧ загрузки. Форма для создания и редактирования параметров ВЧ загрузки.
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь. Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
""" """
class Meta: class Meta:
model = Parameter model = Parameter
fields = [ fields = [
'id_satellite', 'frequency', 'freq_range', 'polarization', 'id_satellite', 'frequency', 'freq_range', 'polarization',
'bod_velocity', 'modulation', 'snr', 'standard' 'bod_velocity', 'modulation', 'snr', 'standard'
] ]
widgets = { widgets = {
'id_satellite': forms.Select(attrs={ 'id_satellite': forms.Select(attrs={
'class': 'form-select', 'class': 'form-select',
'required': True 'required': True
}), }),
'frequency': forms.NumberInput(attrs={ 'frequency': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'step': '0.000001', 'step': '0.000001',
'min': '0', 'min': '0',
'max': '50000', 'max': '50000',
'placeholder': 'Введите частоту в МГц' 'placeholder': 'Введите частоту в МГц'
}), }),
'freq_range': forms.NumberInput(attrs={ 'freq_range': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'step': '0.000001', 'step': '0.000001',
'min': '0', 'min': '0',
'max': '1000', 'max': '1000',
'placeholder': 'Введите полосу частот в МГц' 'placeholder': 'Введите полосу частот в МГц'
}), }),
'bod_velocity': forms.NumberInput(attrs={ 'bod_velocity': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'step': '0.001', 'step': '0.001',
'min': '0', 'min': '0',
'placeholder': 'Введите символьную скорость в БОД' 'placeholder': 'Введите символьную скорость в БОД'
}), }),
'snr': forms.NumberInput(attrs={ 'snr': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'step': '0.001', 'step': '0.001',
'min': '-50', 'min': '-50',
'max': '100', 'max': '100',
'placeholder': 'Введите ОСШ в дБ' 'placeholder': 'Введите ОСШ в дБ'
}), }),
'polarization': forms.Select(attrs={'class': 'form-select'}), 'polarization': forms.Select(attrs={'class': 'form-select'}),
'modulation': forms.Select(attrs={'class': 'form-select'}), 'modulation': forms.Select(attrs={'class': 'form-select'}),
'standard': forms.Select(attrs={'class': 'form-select'}), 'standard': forms.Select(attrs={'class': 'form-select'}),
} }
labels = { labels = {
'id_satellite': 'Спутник', 'id_satellite': 'Спутник',
'frequency': 'Частота (МГц)', 'frequency': 'Частота (МГц)',
'freq_range': 'Полоса частот (МГц)', 'freq_range': 'Полоса частот (МГц)',
'polarization': 'Поляризация', 'polarization': 'Поляризация',
'bod_velocity': 'Символьная скорость (БОД)', 'bod_velocity': 'Символьная скорость (БОД)',
'modulation': 'Модуляция', 'modulation': 'Модуляция',
'snr': 'ОСШ (дБ)', 'snr': 'ОСШ (дБ)',
'standard': 'Стандарт', 'standard': 'Стандарт',
} }
help_texts = { help_texts = {
'frequency': 'Частота в диапазоне от 0 до 50000 МГц', 'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц', 'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
'bod_velocity': 'Символьная скорость должна быть положительной', 'bod_velocity': 'Символьная скорость должна быть положительной',
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ', 'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Динамически загружаем choices для select полей # Динамически загружаем choices для select полей
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name') self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
self.fields['polarization'].queryset = Polarization.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['modulation'].queryset = Modulation.objects.all().order_by('name')
self.fields['standard'].queryset = Standard.objects.all().order_by('name') self.fields['standard'].queryset = Standard.objects.all().order_by('name')
# Делаем спутник обязательным полем # Делаем спутник обязательным полем
self.fields['id_satellite'].required = True self.fields['id_satellite'].required = True
def clean(self): def clean(self):
""" """
Дополнительная валидация формы. Дополнительная валидация формы.
Проверяет соотношение между частотой, полосой частот и символьной скоростью. Проверяет соотношение между частотой, полосой частот и символьной скоростью.
""" """
cleaned_data = super().clean() cleaned_data = super().clean()
frequency = cleaned_data.get('frequency') frequency = cleaned_data.get('frequency')
freq_range = cleaned_data.get('freq_range') freq_range = cleaned_data.get('freq_range')
bod_velocity = cleaned_data.get('bod_velocity') bod_velocity = cleaned_data.get('bod_velocity')
# Проверка что частота больше полосы частот # Проверка что частота больше полосы частот
if frequency and freq_range: if frequency and freq_range:
if freq_range > frequency: if freq_range > frequency:
self.add_error('freq_range', 'Полоса частот не может быть больше частоты') self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
# Проверка что символьная скорость соответствует полосе частот # Проверка что символьная скорость соответствует полосе частот
if bod_velocity and freq_range: if bod_velocity and freq_range:
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот') self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
return cleaned_data return cleaned_data
class GeoForm(forms.ModelForm): class GeoForm(forms.ModelForm):
class Meta: class Meta:
model = Geo model = Geo
fields = ['location', 'comment', 'is_average'] fields = ['location', 'comment', 'is_average']
widgets = { widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}), 'location': forms.TextInput(attrs={'class': 'form-control'}),
'comment': forms.TextInput(attrs={'class': 'form-control'}), 'comment': forms.TextInput(attrs={'class': 'form-control'}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
} }
class ObjItemForm(forms.ModelForm): class ObjItemForm(forms.ModelForm):
""" """
Форма для создания и редактирования объектов (источников сигнала). Форма для создания и редактирования объектов (источников сигнала).
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
через ParameterForm с использованием OneToOne связи. через ParameterForm с использованием OneToOne связи.
""" """
class Meta: class Meta:
model = ObjItem model = ObjItem
fields = ['name'] fields = ['name']
widgets = { widgets = {
'name': forms.TextInput(attrs={ 'name': forms.TextInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите название объекта', 'placeholder': 'Введите название объекта',
'maxlength': '100' 'maxlength': '100'
}), }),
} }
labels = { labels = {
'name': 'Название объекта', 'name': 'Название объекта',
} }
help_texts = { help_texts = {
'name': 'Уникальное название объекта/источника сигнала', 'name': 'Уникальное название объекта/источника сигнала',
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Делаем поле name необязательным, так как оно может быть пустым # Делаем поле name необязательным, так как оно может быть пустым
self.fields['name'].required = False self.fields['name'].required = False
def clean_name(self): def clean_name(self):
""" """
Валидация поля name. Валидация поля name.
Проверяет что название не состоит только из пробелов. Проверяет что название не состоит только из пробелов.
""" """
name = self.cleaned_data.get('name') name = self.cleaned_data.get('name')
if name: if name:
# Удаляем лишние пробелы # Удаляем лишние пробелы
name = name.strip() name = name.strip()
# Проверяем что после удаления пробелов что-то осталось # Проверяем что после удаления пробелов что-то осталось
if not name: if not name:
raise forms.ValidationError('Название не может состоять только из пробелов') raise forms.ValidationError('Название не может состоять только из пробелов')
return name return name

View 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!'))

View File

@@ -1,204 +1,204 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36 # 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.fields
import django.contrib.gis.db.models.functions import django.contrib.gis.db.models.functions
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions import django.db.models.expressions
import mainapp.models import mainapp.models
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Mirror', name='Mirror',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')), ('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
], ],
options={ options={
'verbose_name': 'Зеркало', 'verbose_name': 'Зеркало',
'verbose_name_plural': 'Зеркала', 'verbose_name_plural': 'Зеркала',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Modulation', name='Modulation',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')), ('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
], ],
options={ options={
'verbose_name': 'Модуляция', 'verbose_name': 'Модуляция',
'verbose_name_plural': 'Модуляции', 'verbose_name_plural': 'Модуляции',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Polarization', name='Polarization',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')), ('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
], ],
options={ options={
'verbose_name': 'Поляризация', 'verbose_name': 'Поляризация',
'verbose_name_plural': 'Поляризация', 'verbose_name_plural': 'Поляризация',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Satellite', name='Satellite',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')), ('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')), ('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
], ],
options={ options={
'verbose_name': 'Спутник', 'verbose_name': 'Спутник',
'verbose_name_plural': 'Спутники', 'verbose_name_plural': 'Спутники',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SigmaParMark', name='SigmaParMark',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')), ('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')), ('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
], ],
options={ options={
'verbose_name': 'Отметка', 'verbose_name': 'Отметка',
'verbose_name_plural': 'Отметки', 'verbose_name_plural': 'Отметки',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Standard', name='Standard',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')), ('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
], ],
options={ options={
'verbose_name': 'Стандарт', 'verbose_name': 'Стандарт',
'verbose_name_plural': 'Стандарты', 'verbose_name_plural': 'Стандарты',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='CustomUser', name='CustomUser',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')), ('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)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'verbose_name': 'Пользователь', 'verbose_name': 'Пользователь',
'verbose_name_plural': 'Пользователи', 'verbose_name_plural': 'Пользователи',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ObjItem', name='ObjItem',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')), ('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='Пользователь')), ('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={ options={
'verbose_name': 'Объект', 'verbose_name': 'Объект',
'verbose_name_plural': 'Объекты', 'verbose_name_plural': 'Объекты',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Parameter', name='Parameter',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')), ('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('freq_range', models.FloatField(blank=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='Символьная скорость, БОД')), ('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', 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='Пользователь')), ('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='Модуляция')), ('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='Источники')), ('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='Поляризация')), ('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='Спутник')), ('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='Стандарт')), ('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
], ],
options={ options={
'verbose_name': 'ВЧ загрузка', 'verbose_name': 'ВЧ загрузка',
'verbose_name_plural': 'ВЧ загрузки', 'verbose_name_plural': 'ВЧ загрузки',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SourceType', name='SourceType',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')), ('name', models.CharField(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='Гео')), ('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')),
], ],
options={ options={
'verbose_name': 'Тип источника', 'verbose_name': 'Тип источника',
'verbose_name_plural': 'Типы источников', 'verbose_name_plural': 'Типы источников',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SigmaParameter', name='SigmaParameter',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')), ('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
('status', models.CharField(blank=True, max_length=20, null=True, 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='Частота, МГц')), ('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, МГц')), ('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')), ('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
('power', 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='Символьная скорость, БОД')), ('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', 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='Пакетность')), ('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
('datetime_begin', models.DateTimeField(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='Время окончания измерения')), ('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='Спутник')), ('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='Модуляция')), ('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='ВЧ')), ('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='Поляризация')), ('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='Отметка')), ('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='Стандарт')), ('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
], ],
options={ options={
'verbose_name': 'ВЧ sigma', 'verbose_name': 'ВЧ sigma',
'verbose_name_plural': 'ВЧ sigma', 'verbose_name_plural': 'ВЧ sigma',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Geo', name='Geo',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')), ('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')), ('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')), ('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
('comment', models.CharField(blank=True, max_length=255, 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_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='Координаты оперативников')), ('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='Усреднённое')), ('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_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_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='Расстояние между купсатом и оперативным отделом, км')), ('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='Пользователь')), ('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='Зеркала')), ('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='Гео')), ('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
], ],
options={ options={
'verbose_name': 'Гео', 'verbose_name': 'Гео',
'verbose_name_plural': 'Гео', 'verbose_name_plural': 'Гео',
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')], 'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
}, },
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='parameter', model_name='parameter',
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'), index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='parameter', model_name='parameter',
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'), index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
), ),
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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='Мощность, дБм'),
),
]

View File

@@ -1,229 +1,229 @@
""" """
Переиспользуемые миксины для представлений mainapp. Переиспользуемые миксины для представлений mainapp.
Этот модуль содержит миксины для стандартизации общей логики в представлениях, Этот модуль содержит миксины для стандартизации общей логики в представлениях,
включая проверку прав доступа, обработку координат и сообщений. включая проверку прав доступа, обработку координат и сообщений.
""" """
# Standard library imports # Standard library imports
from datetime import datetime from datetime import datetime
from typing import Optional, Tuple from typing import Optional, Tuple
# Django imports # Django imports
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
class RoleRequiredMixin(UserPassesTestMixin): class RoleRequiredMixin(UserPassesTestMixin):
""" """
Mixin для проверки роли пользователя. Mixin для проверки роли пользователя.
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению. Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
Attributes: Attributes:
required_roles (list): Список допустимых ролей для доступа. required_roles (list): Список допустимых ролей для доступа.
По умолчанию ['admin', 'moderator']. По умолчанию ['admin', 'moderator'].
Example: Example:
class MyView(RoleRequiredMixin, View): class MyView(RoleRequiredMixin, View):
required_roles = ['admin', 'moderator'] required_roles = ['admin', 'moderator']
def get(self, request): def get(self, request):
# Только пользователи с ролью admin или moderator могут получить доступ # Только пользователи с ролью admin или moderator могут получить доступ
return render(request, 'template.html') return render(request, 'template.html')
""" """
required_roles = ["admin", "moderator"] required_roles = ["admin", "moderator"]
def test_func(self) -> bool: def test_func(self) -> bool:
""" """
Проверяет, имеет ли пользователь требуемую роль. Проверяет, имеет ли пользователь требуемую роль.
Returns: Returns:
bool: True если пользователь имеет одну из требуемых ролей, иначе False. bool: True если пользователь имеет одну из требуемых ролей, иначе False.
""" """
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
return False return False
if not hasattr(self.request.user, "customuser"): if not hasattr(self.request.user, "customuser"):
return False return False
return self.request.user.customuser.role in self.required_roles return self.request.user.customuser.role in self.required_roles
class CoordinateProcessingMixin: class CoordinateProcessingMixin:
""" """
Mixin для обработки координат из POST данных форм. Mixin для обработки координат из POST данных форм.
Предоставляет методы для извлечения и обработки координат различных типов Предоставляет методы для извлечения и обработки координат различных типов
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo. (геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
Example: Example:
class MyFormView(CoordinateProcessingMixin, FormView): class MyFormView(CoordinateProcessingMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
geo_instance = Geo() geo_instance = Geo()
self.process_coordinates(geo_instance) self.process_coordinates(geo_instance)
geo_instance.save() geo_instance.save()
return super().form_valid(form) return super().form_valid(form)
""" """
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None: def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
""" """
Обрабатывает координаты из POST данных и применяет их к объекту Geo. Обрабатывает координаты из POST данных и применяет их к объекту Geo.
Извлекает координаты геолокации, кубсата и оперативников из POST запроса Извлекает координаты геолокации, кубсата и оперативников из POST запроса
и устанавливает соответствующие поля объекта Geo. и устанавливает соответствующие поля объекта Geo.
Args: Args:
geo_instance: Экземпляр модели Geo для обновления координат. geo_instance: Экземпляр модели Geo для обновления координат.
prefix (str): Префикс для полей формы (по умолчанию 'geo'). prefix (str): Префикс для полей формы (по умолчанию 'geo').
Note: Note:
Метод ожидает следующие поля в request.POST: Метод ожидает следующие поля в request.POST:
- geo_longitude, geo_latitude: координаты геолокации - geo_longitude, geo_latitude: координаты геолокации
- kupsat_longitude, kupsat_latitude: координаты кубсата - kupsat_longitude, kupsat_latitude: координаты кубсата
- valid_longitude, valid_latitude: координаты оперативников - valid_longitude, valid_latitude: координаты оперативников
""" """
# Обрабатываем координаты геолокации # Обрабатываем координаты геолокации
geo_coords = self._extract_coordinates("geo") geo_coords = self._extract_coordinates("geo")
if geo_coords: if geo_coords:
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326) geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
# Обрабатываем координаты Кубсата # Обрабатываем координаты Кубсата
kupsat_coords = self._extract_coordinates("kupsat") kupsat_coords = self._extract_coordinates("kupsat")
if kupsat_coords: if kupsat_coords:
geo_instance.coords_kupsat = Point( geo_instance.coords_kupsat = Point(
kupsat_coords[0], kupsat_coords[1], srid=4326 kupsat_coords[0], kupsat_coords[1], srid=4326
) )
# Обрабатываем координаты оперативников # Обрабатываем координаты оперативников
valid_coords = self._extract_coordinates("valid") valid_coords = self._extract_coordinates("valid")
if valid_coords: if valid_coords:
geo_instance.coords_valid = Point( geo_instance.coords_valid = Point(
valid_coords[0], valid_coords[1], srid=4326 valid_coords[0], valid_coords[1], srid=4326
) )
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]: def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
""" """
Извлекает координаты указанного типа из POST данных. Извлекает координаты указанного типа из POST данных.
Args: Args:
coord_type (str): Тип координат ('geo', 'kupsat', 'valid'). coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
Returns: Returns:
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None, Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
если координаты не найдены или невалидны. если координаты не найдены или невалидны.
""" """
longitude_key = f"{coord_type}_longitude" longitude_key = f"{coord_type}_longitude"
latitude_key = f"{coord_type}_latitude" latitude_key = f"{coord_type}_latitude"
longitude = self.request.POST.get(longitude_key) longitude = self.request.POST.get(longitude_key)
latitude = self.request.POST.get(latitude_key) latitude = self.request.POST.get(latitude_key)
if longitude and latitude: if longitude and latitude:
try: try:
return (float(longitude), float(latitude)) return (float(longitude), float(latitude))
except (ValueError, TypeError): except (ValueError, TypeError):
return None return None
return None return None
def process_timestamp(self, geo_instance) -> None: def process_timestamp(self, geo_instance) -> None:
""" """
Обрабатывает дату и время из POST данных и применяет к объекту Geo. Обрабатывает дату и время из POST данных и применяет к объекту Geo.
Args: Args:
geo_instance: Экземпляр модели Geo для обновления timestamp. geo_instance: Экземпляр модели Geo для обновления timestamp.
Note: Note:
Метод ожидает следующие поля в request.POST: Метод ожидает следующие поля в request.POST:
- timestamp_date: дата в формате YYYY-MM-DD - timestamp_date: дата в формате YYYY-MM-DD
- timestamp_time: время в формате HH:MM - timestamp_time: время в формате HH:MM
""" """
timestamp_date = self.request.POST.get("timestamp_date") timestamp_date = self.request.POST.get("timestamp_date")
timestamp_time = self.request.POST.get("timestamp_time") timestamp_time = self.request.POST.get("timestamp_time")
if timestamp_date and timestamp_time: if timestamp_date and timestamp_time:
try: try:
naive_datetime = datetime.strptime( naive_datetime = datetime.strptime(
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M" f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
) )
geo_instance.timestamp = naive_datetime geo_instance.timestamp = naive_datetime
except ValueError: except ValueError:
# Если формат даты/времени неверный, пропускаем # Если формат даты/времени неверный, пропускаем
pass pass
class FormMessageMixin: class FormMessageMixin:
""" """
Mixin для стандартизации сообщений об успехе и ошибках в формах. Mixin для стандартизации сообщений об успехе и ошибках в формах.
Автоматически добавляет сообщения пользователю при успешной или неуспешной Автоматически добавляет сообщения пользователю при успешной или неуспешной
обработке формы. обработке формы.
Attributes: Attributes:
success_message (str): Сообщение при успешной обработке формы. success_message (str): Сообщение при успешной обработке формы.
error_message (str): Сообщение при ошибке обработки формы. error_message (str): Сообщение при ошибке обработки формы.
Example: Example:
class MyFormView(FormMessageMixin, FormView): class MyFormView(FormMessageMixin, FormView):
success_message = "Данные успешно сохранены!" success_message = "Данные успешно сохранены!"
error_message = "Ошибка при сохранении данных" error_message = "Ошибка при сохранении данных"
def form_valid(self, form): def form_valid(self, form):
# Автоматически добавит success_message # Автоматически добавит success_message
return super().form_valid(form) return super().form_valid(form)
""" """
success_message = "Операция выполнена успешно" success_message = "Операция выполнена успешно"
error_message = "Произошла ошибка при обработке формы" error_message = "Произошла ошибка при обработке формы"
def form_valid(self, form): def form_valid(self, form):
""" """
Обрабатывает валидную форму и добавляет сообщение об успехе. Обрабатывает валидную форму и добавляет сообщение об успехе.
Args: Args:
form: Валидная форма Django. form: Валидная форма Django.
Returns: Returns:
HttpResponse: Результат обработки родительского метода form_valid. HttpResponse: Результат обработки родительского метода form_valid.
""" """
if self.success_message: if self.success_message:
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)
return super().form_valid(form) return super().form_valid(form)
def form_invalid(self, form): def form_invalid(self, form):
""" """
Обрабатывает невалидную форму и добавляет сообщение об ошибке. Обрабатывает невалидную форму и добавляет сообщение об ошибке.
Args: Args:
form: Невалидная форма Django. form: Невалидная форма Django.
Returns: Returns:
HttpResponse: Результат обработки родительского метода form_invalid. HttpResponse: Результат обработки родительского метода form_invalid.
""" """
if self.error_message: if self.error_message:
messages.error(self.request, self.error_message) messages.error(self.request, self.error_message)
return super().form_invalid(form) return super().form_invalid(form)
def get_success_message(self) -> str: def get_success_message(self) -> str:
""" """
Возвращает сообщение об успехе. Возвращает сообщение об успехе.
Может быть переопределен в подклассах для динамического формирования сообщения. Может быть переопределен в подклассах для динамического формирования сообщения.
Returns: Returns:
str: Сообщение об успехе. str: Сообщение об успехе.
""" """
return self.success_message return self.success_message
def get_error_message(self) -> str: def get_error_message(self) -> str:
""" """
Возвращает сообщение об ошибке. Возвращает сообщение об ошибке.
Может быть переопределен в подклассах для динамического формирования сообщения. Может быть переопределен в подклассах для динамического формирования сообщения.
Returns: Returns:
str: Сообщение об ошибке. str: Сообщение об ошибке.
""" """
return self.error_message return self.error_message

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,76 @@
# Django imports # Django imports
from django.contrib.admin.filters import ChoicesFieldListFilter from django.contrib.admin.filters import ChoicesFieldListFilter
from django.forms import Media from django.forms import Media
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter): class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
""" """
A custom filter that maintains popup context when used in raw_id_fields modals. 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): def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(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 # 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 self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
# Get all choices (related objects) # Get all choices (related objects)
self.lookup_choices = field.get_choices(include_blank=False) self.lookup_choices = field.get_choices(include_blank=False)
def has_output(self): def has_output(self):
return len(self.lookup_choices) > 1 return len(self.lookup_choices) > 1
def value(self): def value(self):
return self.lookup_val return self.lookup_val
def expected_parameters(self): def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull] return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, changelist): def choices(self, changelist):
# If in popup, preserve the popup parameters in the filter URL # If in popup, preserve the popup parameters in the filter URL
popup_params = {} popup_params = {}
if self.is_popup: if self.is_popup:
# Preserve popup parameters # Preserve popup parameters
if '_popup' in changelist.params: if '_popup' in changelist.params:
popup_params['_popup'] = 1 popup_params['_popup'] = 1
if 'pop' in changelist.params: if 'pop' in changelist.params:
popup_params['pop'] = changelist.params['pop'] popup_params['pop'] = changelist.params['pop']
if '_to_field' in changelist.params: if '_to_field' in changelist.params:
popup_params['_to_field'] = changelist.params['_to_field'] popup_params['_to_field'] = changelist.params['_to_field']
# Create the base URL with popup parameters # Create the base URL with popup parameters
all_params = changelist.get_filters_params() all_params = changelist.get_filters_params()
all_params.update(popup_params) all_params.update(popup_params)
# Generate the URL for the filter # Generate the URL for the filter
url = changelist.get_query_string(all_params, [self.lookup_kwarg]) url = changelist.get_query_string(all_params, [self.lookup_kwarg])
yield { yield {
'selected': self.lookup_val is None, 'selected': self.lookup_val is None,
'query_string': url, 'query_string': url,
'display': 'All', 'display': 'All',
} }
# Add choices # Add choices
for lookup, title in self.lookup_choices: for lookup, title in self.lookup_choices:
params = dict(all_params) params = dict(all_params)
params[self.lookup_kwarg] = lookup params[self.lookup_kwarg] = lookup
# Remove the parameter if it's being set to the same value (for unselecting) # Remove the parameter if it's being set to the same value (for unselecting)
if self.lookup_val == str(lookup): if self.lookup_val == str(lookup):
params.pop(self.lookup_kwarg, None) params.pop(self.lookup_kwarg, None)
# Add popup parameters to each choice URL # Add popup parameters to each choice URL
choice_params = params.copy() choice_params = params.copy()
choice_params.update(popup_params) choice_params.update(popup_params)
yield { yield {
'selected': str(lookup) == self.lookup_val, 'selected': str(lookup) == self.lookup_val,
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]), 'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
'display': title, 'display': title,
} }
@property @property
def media(self): def media(self):
# Include necessary CSS/JS for dropdown functionality if needed # Include necessary CSS/JS for dropdown functionality if needed
return Media() return Media()

View File

@@ -1,14 +1,14 @@
# Django imports # Django imports
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
# Local imports # Local imports
from .models import CustomUser from .models import CustomUser
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs): def create_or_update_user_profile(sender, instance, created, **kwargs):
if created: if created:
CustomUser.objects.create(user=instance) CustomUser.objects.create(user=instance)
instance.customuser.save() instance.customuser.save()

65
dbapp/mainapp/tasks.py Normal file
View 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

View File

@@ -1,60 +1,60 @@
{% extends "mapsapp/map2d_base.html" %} {% extends "mapsapp/map2d_base.html" %}
{% load static %} {% load static %}
{% block title %}Вынос точек{% endblock title %} {% block title %}Вынос точек{% endblock title %}
{% block extra_js %} {% block extra_js %}
<script> <script>
// Цвета для стандартных маркеров (из leaflet-color-markers) // Цвета для стандартных маркеров (из leaflet-color-markers)
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue']; var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
var getColorIcon = function(color) { var getColorIcon = function(color) {
return L.icon({ return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png', iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}', shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41], iconSize: [25, 41],
iconAnchor: [12, 41], iconAnchor: [12, 41],
popupAnchor: [1, -34], popupAnchor: [1, -34],
shadowSize: [41, 41] shadowSize: [41, 41]
}); });
}; };
var overlays = []; var overlays = [];
{% for group in groups %} {% for group in groups %}
var groupIndex = {{ forloop.counter0 }}; var groupIndex = {{ forloop.counter0 }};
var colorName = markerColors[groupIndex % markerColors.length]; var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName); var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup(); var groupLayer = L.layerGroup();
var subgroup = []; var subgroup = [];
{% for point_data in group.points %} {% for point_data in group.points %}
var pointName = "{{ group.name|escapejs }}"; var pointName = "{{ group.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], { var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon icon: groupIcon
}).bindPopup(pointName); }).bindPopup(pointName);
groupLayer.addLayer(marker); groupLayer.addLayer(marker);
subgroup.push({ subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}", label: "{{ forloop.counter }} - {{ point_data.frequency }}",
layer: marker layer: marker
}); });
{% endfor %} {% endfor %}
overlays.push({ overlays.push({
label: '{{ group.name|escapejs }}', label: '{{ group.name|escapejs }}',
selectAllCheckbox: true, selectAllCheckbox: true,
children: subgroup, children: subgroup,
layer: groupLayer layer: groupLayer
}); });
{% endfor %} {% endfor %}
// Используем именно tree-контрол // Используем именно tree-контрол
L.control.layers.tree(baseLayers, overlays, { L.control.layers.tree(baseLayers, overlays, {
collapsed: false, collapsed: false,
autoZIndex: true autoZIndex: true
}).addTo(map); }).addTo(map);
</script> </script>
{% endblock extra_js %} {% endblock extra_js %}

View File

@@ -1,189 +1,189 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Действия{% endblock %} {% block title %}Действия{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="text-center mb-5"> <div class="text-center mb-5">
<h1 class="display-4 fw-bold">Действия</h1> <h1 class="display-4 fw-bold">Действия</h1>
<p class="lead">Управление данными спутников</p> <p class="lead">Управление данными спутников</p>
</div> </div>
<!-- Alert messages --> <!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
<!-- Main feature cards --> <!-- Main feature cards -->
<div class="row g-4"> <div class="row g-4">
<!-- Excel Data Upload Card --> <!-- Excel Data Upload Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-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"> <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="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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Загрузка данных из Excel</h3> <h3 class="card-title mb-0">Загрузка данных из Excel</h3>
</div> </div>
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p> <p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary"> <a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
Перейти к загрузке данных Перейти к загрузке данных
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- CSV Data Upload Card --> <!-- CSV Data Upload Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-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"> <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="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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Загрузка данных из CSV</h3> <h3 class="card-title mb-0">Загрузка данных из CSV</h3>
</div> </div>
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p> <p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success"> <a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
Перейти к загрузке данных Перейти к загрузке данных
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Satellite List Card --> <!-- Satellite List Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-info bg-opacity-10 rounded-circle p-2 me-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"> <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="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="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="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="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="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="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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Добавление списка спутников</h3> <h3 class="card-title mb-0">Добавление списка спутников</h3>
</div> </div>
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p> <p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info"> <a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
Добавить список спутников Добавить список спутников
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Transponders Card --> <!-- Transponders Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-warning bg-opacity-10 rounded-circle p-2 me-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"> <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="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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Добавление транспондеров</h3> <h3 class="card-title mb-0">Добавление транспондеров</h3>
</div> </div>
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p> <p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning"> <a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
Добавить транспондеры Добавить транспондеры
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- VCH Load Data Card --> <!-- VCH Load Data Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-danger bg-opacity-10 rounded-circle p-2 me-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"> <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="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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3> <h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
</div> </div>
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p> <p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger"> <a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
Добавить данные ВЧ загрузки Добавить данные ВЧ загрузки
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Lyngsat Data Fill Card --> <!-- Lyngsat Data Fill Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-secondary bg-opacity-10 rounded-circle p-2 me-3"> <div class="bg-secondary bg-opacity-10 rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download text-secondary" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download text-secondary" viewBox="0 0 16 16">
<path d="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="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"/> <path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg> </svg>
</div> </div>
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3> <h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
</div> </div>
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p> <p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary"> <a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
Заполнить данные Lyngsat Заполнить данные Lyngsat
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Calculation Card --> <!-- Calculation Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-info bg-opacity-10 rounded-circle p-2 me-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"> <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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3> <h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
</div> </div>
<p class="card-text">Привязка ВЧ загрузки с sigma</p> <p class="card-text">Привязка ВЧ загрузки с sigma</p>
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info"> <a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
Открыть форму Открыть форму
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- New Event Card --> <!-- New Event Card -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-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"> <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"/> <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> </svg>
</div> </div>
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3> <h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
</div> </div>
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p> <p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success"> <a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
Добавить событие Добавить событие
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,34 +1,34 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из CSV{% endblock %} {% block title %}Загрузка данных из CSV{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-success text-white"> <div class="card-header bg-success text-white">
<h2 class="mb-0">Загрузка данных из CSV</h2> <h2 class="mb-0">Загрузка данных из CSV</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p> <p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Form fields with Bootstrap styling --> <!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %} {% include 'mainapp/components/_form_field.html' with field=form.file %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-success">Добавить в базу</button> <button type="submit" class="btn btn-success">Добавить в базу</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,36 +1,36 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из Excel{% endblock %} {% block title %}Загрузка данных из Excel{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h2 class="mb-0">Загрузка данных из Excel</h2> <h2 class="mb-0">Загрузка данных из Excel</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p> <p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Form fields with Bootstrap styling --> <!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %} {% 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.sat_choice %}
{% include 'mainapp/components/_form_field.html' with field=form.number_input %} {% include 'mainapp/components/_form_field.html' with field=form.number_input %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-primary">Добавить в базу</button> <button type="submit" class="btn btn-primary">Добавить в базу</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,42 +1,42 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>{% block title %}Геолокация{% endblock %}</title> <title>{% block title %}Геолокация{% endblock %}</title>
<!-- Bootstrap Icons --> <!-- Bootstrap Icons -->
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- Дополнительные стили --> <!-- Дополнительные стили -->
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<!-- Навигационная панель --> <!-- Навигационная панель -->
{% include 'mainapp/components/_navbar.html' %} {% include 'mainapp/components/_navbar.html' %}
<!-- Сообщения --> <!-- Сообщения -->
<div class="container mt-3"> <div class="container mt-3">
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
</div> </div>
<!-- Основной контент --> <!-- Основной контент -->
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}"> <main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script> <script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
<!-- Дополнительные скрипты --> <!-- Дополнительные скрипты -->
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,33 +1,33 @@
{% comment %} {% comment %}
Переиспользуемый компонент для отображения полей формы Переиспользуемый компонент для отображения полей формы
Использование: Использование:
{% include 'mainapp/components/_form_field.html' with field=form.field_name %} {% 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" %} {% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
{% endcomment %} {% endcomment %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}"> <label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
{{ field.label }} {{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %} {% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label> </label>
{% if field.field.widget.input_type == 'checkbox' %} {% if field.field.widget.input_type == 'checkbox' %}
<div class="form-check"> <div class="form-check">
{{ field }} {{ field }}
</div> </div>
{% else %} {% else %}
{{ field }} {{ field }}
{% endif %} {% endif %}
{% if field.errors %} {% if field.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{% for error in field.errors %} {% for error in field.errors %}
{{ error }} {{ error }}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if field.help_text %} {% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small> <small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,25 +1,25 @@
{% comment %} {% comment %}
Переиспользуемый компонент для отображения сообщений Django Переиспользуемый компонент для отображения сообщений Django
Использование: Использование:
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
{% endcomment %} {% endcomment %}
{% if messages %} {% if messages %}
<div class="messages-container"> <div class="messages-container">
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{% if message.tags == 'error' %} {% if message.tags == 'error' %}
<i class="bi bi-exclamation-triangle-fill me-2"></i> <i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif message.tags == 'success' %} {% elif message.tags == 'success' %}
<i class="bi bi-check-circle-fill me-2"></i> <i class="bi bi-check-circle-fill me-2"></i>
{% elif message.tags == 'warning' %} {% elif message.tags == 'warning' %}
<i class="bi bi-exclamation-circle-fill me-2"></i> <i class="bi bi-exclamation-circle-fill me-2"></i>
{% elif message.tags == 'info' %} {% elif message.tags == 'info' %}
<i class="bi bi-info-circle-fill me-2"></i> <i class="bi bi-info-circle-fill me-2"></i>
{% endif %} {% endif %}
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,57 +1,57 @@
{% comment %} {% comment %}
Переиспользуемый компонент навигационной панели Переиспользуемый компонент навигационной панели
Использование: Использование:
{% include 'mainapp/components/_navbar.html' %} {% include 'mainapp/components/_navbar.html' %}
{% endcomment %} {% endcomment %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container"> <div class="container">
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a> <a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a> <a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a> <a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a> <a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a> <a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a> <a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
{% if user.first_name and user.last_name %} {% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }} {{ user.first_name }} {{ user.last_name }}
{% elif user.get_full_name %} {% elif user.get_full_name %}
{{ user.get_full_name }} {{ user.get_full_name }}
{% else %} {% else %}
{{ user.username }} {{ user.username }}
{% endif %} {% endif %}
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li> <li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
{% else %} {% else %}
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Войти</a> <a class="nav-link" href="{% url 'login' %}">Войти</a>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</nav> </nav>

View File

@@ -1,32 +1,32 @@
{% comment %} {% comment %}
Переиспользуемый компонент для заголовков таблиц с сортировкой Переиспользуемый компонент для заголовков таблиц с сортировкой
Использование: Использование:
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %} {% 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 %} {% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
{% endcomment %} {% endcomment %}
<th scope="col"> <th scope="col">
{% if sortable != False %} {% 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 %}" <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"> class="text-white text-decoration-none d-inline-flex align-items-center">
{{ label }} {{ label }}
{% if sort == field %} {% if sort == field %}
<i class="bi bi-sort-up ms-1"></i> <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 %}" <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="Сбросить сортировку"> class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i> <i class="bi bi-x-lg"></i>
</a> </a>
{% elif sort == '-'|add:field %} {% elif sort == '-'|add:field %}
<i class="bi bi-sort-down ms-1"></i> <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 %}" <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="Сбросить сортировку"> class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i> <i class="bi bi-x-lg"></i>
</a> </a>
{% else %} {% else %}
<i class="bi bi-arrow-down-up ms-1"></i> <i class="bi bi-arrow-down-up ms-1"></i>
{% endif %} {% endif %}
</a> </a>
{% else %} {% else %}
{{ label }} {{ label }}
{% endif %} {% endif %}
</th> </th>

View File

@@ -1,118 +1,118 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Заполнение данных Lyngsat{% endblock %} {% block title %}Заполнение данных Lyngsat{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h3 class="mb-0"> <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"> <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="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"/> <path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg> </svg>
Заполнение данных из Lyngsat Заполнение данных из Lyngsat
</h3> </h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Alert messages --> <!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
<strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время, <strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время,
так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции. так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции.
</div> </div>
<form method="post" class="needs-validation" novalidate> <form method="post" class="needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
<!-- Satellites Selection --> <!-- Satellites Selection -->
<div class="mb-4"> <div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold"> <label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold">
{{ form.satellites.label }} {{ form.satellites.label }}
</label> </label>
{{ form.satellites }} {{ form.satellites }}
{% if form.satellites.help_text %} {% if form.satellites.help_text %}
<div class="form-text">{{ form.satellites.help_text }}</div> <div class="form-text">{{ form.satellites.help_text }}</div>
{% endif %} {% endif %}
{% if form.satellites.errors %} {% if form.satellites.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{{ form.satellites.errors }} {{ form.satellites.errors }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Regions Selection --> <!-- Regions Selection -->
<div class="mb-4"> <div class="mb-4">
<label for="{{ form.regions.id_for_label }}" class="form-label fw-bold"> <label for="{{ form.regions.id_for_label }}" class="form-label fw-bold">
{{ form.regions.label }} {{ form.regions.label }}
</label> </label>
{{ form.regions }} {{ form.regions }}
{% if form.regions.help_text %} {% if form.regions.help_text %}
<div class="form-text">{{ form.regions.help_text }}</div> <div class="form-text">{{ form.regions.help_text }}</div>
{% endif %} {% endif %}
{% if form.regions.errors %} {% if form.regions.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{{ form.regions.errors }} {{ form.regions.errors }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Buttons --> <!-- Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between"> <div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary"> <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"> <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"/> <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> </svg>
Назад Назад
</a> </a>
<button type="submit" class="btn btn-primary"> <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"> <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="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"/> <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> </svg>
Заполнить данные Заполнить данные
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Info Card --> <!-- Info Card -->
<div class="card mt-4 shadow-sm"> <div class="card mt-4 shadow-sm">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Информация</h5> <h5 class="card-title">Информация</h5>
<p class="card-text"> <p class="card-text">
Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat. Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat.
Выберите один или несколько спутников и регионы для парсинга данных. Выберите один или несколько спутников и регионы для парсинга данных.
</p> </p>
<ul> <ul>
<li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li> <li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li>
<li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li> <li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li>
<li>Существующие записи будут обновлены, новые - созданы</li> <li>Существующие записи будут обновлены, новые - созданы</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Form validation // Form validation
(function() { (function() {
'use strict'; 'use strict';
var forms = document.querySelectorAll('.needs-validation'); var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) { Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) { form.addEventListener('submit', function(event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
form.classList.add('was-validated'); form.classList.add('was-validated');
}, false); }, false);
}); });
})(); })();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,68 +1,68 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Привязка ВЧ{% endblock %} {% block title %}Привязка ВЧ{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-info text-white"> <div class="card-header bg-info text-white">
<h2 class="mb-0">Привязка ВЧ загрузки</h2> <h2 class="mb-0">Привязка ВЧ загрузки</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert"> <div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="card-text">Введите допустимый разброс для частоты и полосы</p> <p class="card-text">Введите допустимый разброс для частоты и полосы</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label> <label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }} {{ form.sat_choice }}
{% if form.sat_choice.errors %} {% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div> <div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %} {% endif %}
</div> </div>
{% comment %} <div class="mb-3"> {% comment %} <div class="mb-3">
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label> <label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
{{ form.ku_range }} {{ form.ku_range }}
{% if form.ku_range.errors %} {% if form.ku_range.errors %}
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div> <div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %} {% endif %}
</div> {% endcomment %} </div> {% endcomment %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label> <label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
{{ form.value1 }} {{ form.value1 }}
{% if form.value1.errors %} {% if form.value1.errors %}
<div class="text-danger mt-1">{{ form.value1.errors }}</div> <div class="text-danger mt-1">{{ form.value1.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label> <label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
{{ form.value2 }} {{ form.value2 }}
{% if form.value2.errors %} {% if form.value2.errors %}
<div class="text-danger mt-1">{{ form.value2.errors }}</div> <div class="text-danger mt-1">{{ form.value2.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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> <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 %} {% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
<button type="submit" class="btn btn-info">Выполнить привязку</button> <button type="submit" class="btn btn-info">Выполнить привязку</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,241 +1,241 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Статус задачи Lyngsat{% endblock %} {% block title %}Статус задачи Lyngsat{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h3 class="mb-0"> <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"> <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"/> <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> </svg>
Статус задачи заполнения данных Lyngsat Статус задачи заполнения данных Lyngsat
</h3> </h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if task_id %} {% if task_id %}
<div class="mb-3"> <div class="mb-3">
<strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code> <strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code>
</div> </div>
<!-- Progress Bar --> <!-- Progress Bar -->
<div class="mb-4"> <div class="mb-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span id="status-text">Загрузка статуса...</span> <span id="status-text">Загрузка статуса...</span>
<span id="progress-percent">0%</span> <span id="progress-percent">0%</span>
</div> </div>
<div class="progress" style="height: 25px;"> <div class="progress" style="height: 25px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%;" role="progressbar" style="width: 0%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span id="progress-text">0%</span> <span id="progress-text">0%</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Task State --> <!-- Task State -->
<div id="task-state-container" class="alert alert-info" role="alert"> <div id="task-state-container" class="alert alert-info" role="alert">
<strong>Состояние:</strong> <span id="task-state">Проверка...</span> <strong>Состояние:</strong> <span id="task-state">Проверка...</span>
</div> </div>
<!-- Results Container (hidden by default) --> <!-- Results Container (hidden by default) -->
<div id="results-container" class="d-none"> <div id="results-container" class="d-none">
<h5 class="mt-4">Результаты обработки</h5> <h5 class="mt-4">Результаты обработки</h5>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6> <h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6>
<h3 class="card-title" id="result-satellites">-</h3> <h3 class="card-title" id="result-satellites">-</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6> <h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6>
<h3 class="card-title" id="result-sources">-</h3> <h3 class="card-title" id="result-sources">-</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3 border-success"> <div class="card mb-3 border-success">
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle mb-2 text-success">Создано записей</h6> <h6 class="card-subtitle mb-2 text-success">Создано записей</h6>
<h3 class="card-title text-success" id="result-created">-</h3> <h3 class="card-title text-success" id="result-created">-</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3 border-info"> <div class="card mb-3 border-info">
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6> <h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6>
<h3 class="card-title text-info" id="result-updated">-</h3> <h3 class="card-title text-info" id="result-updated">-</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Errors --> <!-- Errors -->
<div id="errors-container" class="d-none"> <div id="errors-container" class="d-none">
<h6 class="text-danger">Ошибки при обработке:</h6> <h6 class="text-danger">Ошибки при обработке:</h6>
<div class="alert alert-warning"> <div class="alert alert-warning">
<ul id="errors-list" class="mb-0"></ul> <ul id="errors-list" class="mb-0"></ul>
</div> </div>
</div> </div>
</div> </div>
<!-- Error Container (hidden by default) --> <!-- Error Container (hidden by default) -->
<div id="error-container" class="alert alert-danger d-none" role="alert"> <div id="error-container" class="alert alert-danger d-none" role="alert">
<strong>Ошибка:</strong> <span id="error-text"></span> <strong>Ошибка:</strong> <span id="error-text"></span>
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between mt-4"> <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"> <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"> <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"/> <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> </svg>
Назад к форме Назад к форме
</a> </a>
<a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn"> <a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn">
Перейти к действиям Перейти к действиям
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных. ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных.
</div> </div>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary"> <a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary">
Перейти к форме Перейти к форме
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% if task_id %} {% if task_id %}
<script> <script>
let taskId = '{{ task_id }}'; let taskId = '{{ task_id }}';
let pollInterval; let pollInterval;
let isCompleted = false; let isCompleted = false;
function updateProgress(data) { function updateProgress(data) {
const statusText = document.getElementById('status-text'); const statusText = document.getElementById('status-text');
const progressBar = document.getElementById('progress-bar'); const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text'); const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent'); const progressPercent = document.getElementById('progress-percent');
const taskState = document.getElementById('task-state'); const taskState = document.getElementById('task-state');
const taskStateContainer = document.getElementById('task-state-container'); const taskStateContainer = document.getElementById('task-state-container');
// Update state // Update state
taskState.textContent = data.state; taskState.textContent = data.state;
if (data.state === 'PENDING') { if (data.state === 'PENDING') {
statusText.textContent = 'Задача в очереди...'; statusText.textContent = 'Задача в очереди...';
taskStateContainer.className = 'alert alert-info'; taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'PROGRESS') { } else if (data.state === 'PROGRESS') {
const percent = data.percent || 0; const percent = data.percent || 0;
statusText.textContent = data.status || 'Обработка...'; statusText.textContent = data.status || 'Обработка...';
progressBar.style.width = percent + '%'; progressBar.style.width = percent + '%';
progressBar.setAttribute('aria-valuenow', percent); progressBar.setAttribute('aria-valuenow', percent);
progressText.textContent = percent + '%'; progressText.textContent = percent + '%';
progressPercent.textContent = percent + '%'; progressPercent.textContent = percent + '%';
taskStateContainer.className = 'alert alert-info'; taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'SUCCESS') { } else if (data.state === 'SUCCESS') {
statusText.textContent = 'Задача завершена успешно!'; statusText.textContent = 'Задача завершена успешно!';
progressBar.style.width = '100%'; progressBar.style.width = '100%';
progressBar.setAttribute('aria-valuenow', 100); progressBar.setAttribute('aria-valuenow', 100);
progressText.textContent = '100%'; progressText.textContent = '100%';
progressPercent.textContent = '100%'; progressPercent.textContent = '100%';
progressBar.classList.remove('progress-bar-animated'); progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success'); progressBar.classList.add('bg-success');
taskStateContainer.className = 'alert alert-success'; taskStateContainer.className = 'alert alert-success';
// Show results // Show results
if (data.result) { if (data.result) {
showResults(data.result); showResults(data.result);
} }
isCompleted = true; isCompleted = true;
clearInterval(pollInterval); clearInterval(pollInterval);
} else if (data.state === 'FAILURE') { } else if (data.state === 'FAILURE') {
statusText.textContent = 'Ошибка при выполнении задачи'; statusText.textContent = 'Ошибка при выполнении задачи';
progressBar.classList.remove('progress-bar-animated'); progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-danger'); progressBar.classList.add('bg-danger');
taskStateContainer.className = 'alert alert-danger'; taskStateContainer.className = 'alert alert-danger';
// Show error // Show error
const errorContainer = document.getElementById('error-container'); const errorContainer = document.getElementById('error-container');
const errorText = document.getElementById('error-text'); const errorText = document.getElementById('error-text');
errorText.textContent = data.error || 'Неизвестная ошибка'; errorText.textContent = data.error || 'Неизвестная ошибка';
errorContainer.classList.remove('d-none'); errorContainer.classList.remove('d-none');
isCompleted = true; isCompleted = true;
clearInterval(pollInterval); clearInterval(pollInterval);
} }
} }
function showResults(result) { function showResults(result) {
const resultsContainer = document.getElementById('results-container'); const resultsContainer = document.getElementById('results-container');
resultsContainer.classList.remove('d-none'); resultsContainer.classList.remove('d-none');
document.getElementById('result-satellites').textContent = result.total_satellites || 0; document.getElementById('result-satellites').textContent = result.total_satellites || 0;
document.getElementById('result-sources').textContent = result.total_sources || 0; document.getElementById('result-sources').textContent = result.total_sources || 0;
document.getElementById('result-created').textContent = result.created || 0; document.getElementById('result-created').textContent = result.created || 0;
document.getElementById('result-updated').textContent = result.updated || 0; document.getElementById('result-updated').textContent = result.updated || 0;
// Show errors if any // Show errors if any
if (result.errors && result.errors.length > 0) { if (result.errors && result.errors.length > 0) {
const errorsContainer = document.getElementById('errors-container'); const errorsContainer = document.getElementById('errors-container');
const errorsList = document.getElementById('errors-list'); const errorsList = document.getElementById('errors-list');
errorsContainer.classList.remove('d-none'); errorsContainer.classList.remove('d-none');
errorsList.innerHTML = ''; errorsList.innerHTML = '';
result.errors.slice(0, 10).forEach(error => { result.errors.slice(0, 10).forEach(error => {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = error; li.textContent = error;
errorsList.appendChild(li); errorsList.appendChild(li);
}); });
if (result.errors.length > 10) { if (result.errors.length > 10) {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = `И еще ${result.errors.length - 10} ошибок...`; li.textContent = `И еще ${result.errors.length - 10} ошибок...`;
li.className = 'text-muted'; li.className = 'text-muted';
errorsList.appendChild(li); errorsList.appendChild(li);
} }
} }
} }
function checkTaskStatus() { function checkTaskStatus() {
fetch(`/api/lyngsat-task-status/${taskId}/`) fetch(`/api/lyngsat-task-status/${taskId}/`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
updateProgress(data); updateProgress(data);
}) })
.catch(error => { .catch(error => {
console.error('Error checking task status:', error); console.error('Error checking task status:', error);
}); });
} }
// Start polling // Start polling
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
checkTaskStatus(); checkTaskStatus();
pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds
// Stop polling after 30 minutes // Stop polling after 30 minutes
setTimeout(() => { setTimeout(() => {
if (!isCompleted) { if (!isCompleted) {
clearInterval(pollInterval); clearInterval(pollInterval);
document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.'; document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.';
} }
}, 30 * 60 * 1000); }, 30 * 60 * 1000);
}); });
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,25 +1,25 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Удалить объект{% endblock %} {% block title %}Удалить объект{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<h2>Удалить объект "{{ object }}"?</h2> <h2>Удалить объект "{{ object }}"?</h2>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p> <p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Удалить</button> <button type="submit" class="btn btn-danger">Удалить</button>
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,473 +1,473 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% load static %} {% load static %}
{% load static leaflet_tags %} {% load static leaflet_tags %}
{% load l10n %} {% load l10n %}
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %} {% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; } .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; } .form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.btn-action { margin-right: 0.5rem; } .btn-action { margin-right: 0.5rem; }
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; } .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 { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; } .coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
.form-check-input { margin-top: 0.25rem; } .form-check-input { margin-top: 0.25rem; }
.datetime-group { display: flex; gap: 1rem; } .datetime-group { display: flex; gap: 1rem; }
.datetime-group > div { flex: 1; } .datetime-group > div { flex: 1; }
#map { height: 500px; width: 100%; margin-bottom: 1rem; } #map { height: 500px; width: 100%; margin-bottom: 1rem; }
.map-container { margin-bottom: 1rem; } .map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; } .coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
.map-controls { .map-controls {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 1rem; margin-bottom: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.map-control-btn { .map-control-btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border: 1px solid #ced4da; border: 1px solid #ced4da;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;
} }
.map-control-btn.active { .map-control-btn.active {
background-color: #e9ecef; background-color: #e9ecef;
border-color: #dee2e6; border-color: #dee2e6;
} }
.map-control-btn.edit { .map-control-btn.edit {
background-color: #fff3cd; background-color: #fff3cd;
border-color: #ffeeba; border-color: #ffeeba;
} }
.map-control-btn.save { .map-control-btn.save {
background-color: #d1ecf1; background-color: #d1ecf1;
border-color: #bee5eb; border-color: #bee5eb;
} }
.map-control-btn.cancel { .map-control-btn.cancel {
background-color: #f8d7da; background-color: #f8d7da;
border-color: #f5c6cb; border-color: #f5c6cb;
} }
.leaflet-marker-icon { .leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3)); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center"> <div class="col-12 d-flex justify-content-between align-items-center">
<h2>Просмотр объекта: {{ object.name }}</h2> <h2>Просмотр объекта: {{ object.name }}</h2>
<div> <div>
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a> <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>
</div> </div>
<!-- Основная информация --> <!-- Основная информация -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Основная информация</h4> <h4>Основная информация</h4>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Название:</label> <label class="form-label">Название:</label>
<div class="readonly-field">{{ object.name|default:"-" }}</div> <div class="readonly-field">{{ object.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Дата создания:</label> <label class="form-label">Дата создания:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Создан пользователем:</label> <label class="form-label">Создан пользователем:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Дата последнего изменения:</label> <label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Изменен пользователем:</label> <label class="form-label">Изменен пользователем:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- ВЧ загрузка --> <!-- ВЧ загрузка -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>ВЧ загрузка</h4> <h4>ВЧ загрузка</h4>
</div> </div>
{% if object.parameter_obj %} {% if object.parameter_obj %}
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Спутник:</label> <label class="form-label">Спутник:</label>
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Частота (МГц):</label> <label class="form-label">Частота (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Полоса (МГц):</label> <label class="form-label">Полоса (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Поляризация:</label> <label class="form-label">Поляризация:</label>
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Символьная скорость:</label> <label class="form-label">Символьная скорость:</label>
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Модуляция:</label> <label class="form-label">Модуляция:</label>
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">ОСШ:</label> <label class="form-label">ОСШ:</label>
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Стандарт:</label> <label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
</div> </div>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="mb-3"> <div class="mb-3">
<p>Нет данных о ВЧ загрузке</p> <p>Нет данных о ВЧ загрузке</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Блок с картой --> <!-- Блок с картой -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Карта</h4> <h4>Карта</h4>
</div> </div>
<div class="map-container"> <div class="map-container">
<div id="map"></div> <div id="map"></div>
</div> </div>
</div> </div>
<!-- Геоданные --> <!-- Геоданные -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Геоданные</h4> <h4>Геоданные</h4>
</div> </div>
{% if object.geo_obj %} {% if object.geo_obj %}
<!-- Координаты геолокации --> <!-- Координаты геолокации -->
<div class="coord-sync-group"> <div class="coord-sync-group">
<div class="coord-group-header">Координаты геолокации</div> <div class="coord-group-header">Координаты геолокации</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Широта:</label> <label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Долгота:</label> <label class="form-label">Долгота:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Координаты Кубсата --> <!-- Координаты Кубсата -->
<div class="coord-group"> <div class="coord-group">
<div class="coord-group-header">Координаты Кубсата</div> <div class="coord-group-header">Координаты Кубсата</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Долгота:</label> <label class="form-label">Долгота:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Широта:</label> <label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Координаты оперативников --> <!-- Координаты оперативников -->
<div class="coord-group"> <div class="coord-group">
<div class="coord-group-header">Координаты оперативников</div> <div class="coord-group-header">Координаты оперативников</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Долгота:</label> <label class="form-label">Долгота:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Широта:</label> <label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Местоположение:</label> <label class="form-label">Местоположение:</label>
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div> <div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Комментарий:</label> <label class="form-label">Комментарий:</label>
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div> <div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Дата и время:</label> <label class="form-label">Дата и время:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %} {% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-check-label">Усредненное значение:</label> <label class="form-check-label">Усредненное значение:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %} {% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Расстояние гео-кубсат, км:</label> <label class="form-label">Расстояние гео-кубсат, км:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.distance_coords_kup is not None %} {% if object.geo_obj.distance_coords_kup is not None %}
{{ object.geo_obj.distance_coords_kup|floatformat:2 }} {{ object.geo_obj.distance_coords_kup|floatformat:2 }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Расстояние гео-опер, км:</label> <label class="form-label">Расстояние гео-опер, км:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.distance_coords_valid is not None %} {% if object.geo_obj.distance_coords_valid is not None %}
{{ object.geo_obj.distance_coords_valid|floatformat:2 }} {{ object.geo_obj.distance_coords_valid|floatformat:2 }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Расстояние кубсат-опер, км:</label> <label class="form-label">Расстояние кубсат-опер, км:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.distance_kup_valid is not None %} {% if object.geo_obj.distance_kup_valid is not None %}
{{ object.geo_obj.distance_kup_valid|floatformat:2 }} {{ object.geo_obj.distance_kup_valid|floatformat:2 }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% else %} {% else %}
<p>Нет данных о геолокации</p> <p>Нет данных о геолокации</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="d-flex justify-content-end mt-4"> <div class="d-flex justify-content-end mt-4">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% 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> <a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
{% endif %} {% endif %}
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a> <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>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
{{ block.super }} {{ block.super }}
<!-- Подключаем Leaflet и его плагины --> <!-- Подключаем Leaflet и его плагины -->
{% leaflet_js %} {% leaflet_js %}
{% leaflet_css %} {% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script> <script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты // Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5); const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map); }).addTo(map);
// Определяем цвета для маркеров // Определяем цвета для маркеров
const colors = { const colors = {
geo: 'blue', geo: 'blue',
kupsat: 'red', kupsat: 'red',
valid: 'green' valid: 'green'
}; };
// Функция для создания иконки маркера // Функция для создания иконки маркера
function createMarkerIcon(color) { function createMarkerIcon(color) {
return L.icon({ return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png', iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`, shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41], iconSize: [25, 41],
iconAnchor: [12, 41], iconAnchor: [12, 41],
popupAnchor: [1, -34], popupAnchor: [1, -34],
shadowSize: [41, 41] shadowSize: [41, 41]
}); });
} }
// Маркеры // Маркеры
const markers = {}; const markers = {};
function createMarker(position, color, name) { function createMarker(position, color, name) {
const marker = L.marker(position, { const marker = L.marker(position, {
draggable: false, draggable: false,
icon: createMarkerIcon(color), icon: createMarkerIcon(color),
title: name title: name
}).addTo(map); }).addTo(map);
marker.bindPopup(name); marker.bindPopup(name);
return marker; return marker;
} }
// Получаем координаты из данных объекта // Получаем координаты из данных объекта
{% if object.geo_obj and object.geo_obj.coords %} {% if object.geo_obj and object.geo_obj.coords %}
const geoLat = {{ object.geo_obj.coords.y|unlocalize }}; const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
const geoLng = {{ object.geo_obj.coords.x|unlocalize }}; const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
{% else %} {% else %}
const geoLat = 55.75; const geoLat = 55.75;
const geoLng = 37.62; const geoLng = 37.62;
{% endif %} {% endif %}
{% if object.geo_obj and object.geo_obj.coords_kupsat %} {% if object.geo_obj and object.geo_obj.coords_kupsat %}
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }}; const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }}; const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
{% else %} {% else %}
const kupsatLat = 55.75; const kupsatLat = 55.75;
const kupsatLng = 37.61; const kupsatLng = 37.61;
{% endif %} {% endif %}
{% if object.geo_obj and object.geo_obj.coords_valid %} {% if object.geo_obj and object.geo_obj.coords_valid %}
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }}; const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }}; const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
{% else %} {% else %}
const validLat = 55.75; const validLat = 55.75;
const validLng = 37.63; const validLng = 37.63;
{% endif %} {% endif %}
// Создаем маркеры // Создаем маркеры
markers.geo = createMarker( markers.geo = createMarker(
[geoLat, geoLng], [geoLat, geoLng],
colors.geo, colors.geo,
'Геолокация' 'Геолокация'
); );
markers.kupsat = createMarker( markers.kupsat = createMarker(
[kupsatLat, kupsatLng], [kupsatLat, kupsatLng],
colors.kupsat, colors.kupsat,
'Кубсат' 'Кубсат'
); );
markers.valid = createMarker( markers.valid = createMarker(
[validLat, validLng], [validLat, validLng],
colors.valid, colors.valid,
'Оперативник' 'Оперативник'
); );
// Центрируем карту на первом маркере // Центрируем карту на первом маркере
if (map.hasLayer(markers.geo)) { if (map.hasLayer(markers.geo)) {
map.setView(markers.geo.getLatLng(), 10); map.setView(markers.geo.getLatLng(), 10);
} }
// Легенда // Легенда
const legend = L.control({ position: 'bottomright' }); const legend = L.control({ position: 'bottomright' });
legend.onAdd = function() { legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend'); const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px'; div.style.fontSize = '14px';
div.style.backgroundColor = 'white'; div.style.backgroundColor = 'white';
div.style.padding = '10px'; div.style.padding = '10px';
div.style.borderRadius = '4px'; div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)'; div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = ` div.innerHTML = `
<h5>Легенда</h5> <h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div> <div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
<div><span style="color: red; 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> <div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
`; `;
return div; return div;
}; };
legend.addTo(map); legend.addTo(map);
}); });
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,52 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Новое событие{% endblock %} {% block title %}Новое событие{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm border-0"> <div class="card shadow-sm border-0">
<div class="card-header bg-success text-white"> <div class="card-header bg-success text-white">
<h2 class="mb-0">Формирование таблицы Кубсат</h2> <h2 class="mb-0">Формирование таблицы Кубсат</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% comment%} {% comment%}
<div class="mb-4"> <div class="mb-4">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label> <label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
{{ form.sat_choice }} {{ form.sat_choice }}
{% if form.sat_choice.errors %} {% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div> <div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %} {% endif %}
</div>{% endcomment %} </div>{% endcomment %}
{% comment %} <div class="mb-4"> {% comment %} <div class="mb-4">
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label> <label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
{{ form.pol_choice }} {{ form.pol_choice }}
{% if form.pol_choice.errors %} {% if form.pol_choice.errors %}
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div> <div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
{% endif %} {% endif %}
</div> {% endcomment %} </div> {% endcomment %}
<div class="mb-4"> <div class="mb-4">
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label> <label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
{{ form.file }} {{ form.file }}
{% if form.file.errors %} {% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div> <div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %} {% endif %}
<div class="form-text">Выберите файл для загрузки</div> <div class="form-text">Выберите файл для загрузки</div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
<button type="submit" class="btn btn-success">Выполнить</button> <button type="submit" class="btn btn-success">Выполнить</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,53 +1,53 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных транспондеров{% endblock %} {% block title %}Загрузка данных транспондеров{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-warning text-white"> <div class="card-header bg-warning text-white">
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2> <h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert"> <div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p> <p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label> <label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
{{ form.file }} {{ form.file }}
{% if form.file.errors %} {% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div> <div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %} {% endif %}
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div> <div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
</div> </div>
{% comment %} <div class="mb-3"> {% comment %} <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label> <label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }} {{ form.sat_choice }}
{% if form.sat_choice.errors %} {% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div> <div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %} {% endif %}
</div> {% endcomment %} </div> {% endcomment %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-warning">Добавить в базу</button> <button type="submit" class="btn btn-warning">Добавить в базу</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,56 +1,56 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных ВЧ загрузки{% endblock %} {% block title %}Загрузка данных ВЧ загрузки{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-danger text-white"> <div class="card-header bg-danger text-white">
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2> <h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert"> <div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p> <p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Form fields with Bootstrap styling --> <!-- Form fields with Bootstrap styling -->
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label> <label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
{{ form.file }} {{ form.file }}
{% if form.file.errors %} {% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div> <div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %} {% endif %}
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div> <div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label> <label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }} {{ form.sat_choice }}
{% if form.sat_choice.errors %} {% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div> <div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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> <a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-danger">Обработать файл</button> <button type="submit" class="btn btn-danger">Обработать файл</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,3 +1,3 @@
""" """
Template tags для mainapp. Template tags для mainapp.
""" """

View File

@@ -1,133 +1,133 @@
""" """
Пользовательские фильтры шаблонов для форматирования координат. Пользовательские фильтры шаблонов для форматирования координат.
Этот модуль содержит фильтры Django для форматирования географических координат Этот модуль содержит фильтры Django для форматирования географических координат
в читаемый вид в шаблонах. в читаемый вид в шаблонах.
""" """
# Standard library imports # Standard library imports
from typing import Optional from typing import Optional
# Django imports # Django imports
from django import template from django import template
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
register = template.Library() register = template.Library()
@register.filter(name='format_coords') @register.filter(name='format_coords')
def format_coords(point: Optional[Point]) -> str: def format_coords(point: Optional[Point]) -> str:
""" """
Форматирует объект Point в читаемую строку координат. Форматирует объект Point в читаемую строку координат.
Args: Args:
point (Point): Объект Point из GeoDjango или None. point (Point): Объект Point из GeoDjango или None.
Returns: Returns:
str: Отформатированная строка координат в формате "XXN/S YYE/W" str: Отформатированная строка координат в формате "XXN/S YYE/W"
или "-" если point равен None. или "-" если point равен None.
Example: Example:
В шаблоне: В шаблоне:
{{ geo_obj.coords|format_coords }} {{ geo_obj.coords|format_coords }}
Результат: Результат:
"55.75N 37.62E" "55.75N 37.62E"
""" """
if not point: if not point:
return "-" return "-"
try: try:
longitude = point.coords[0] longitude = point.coords[0]
latitude = point.coords[1] latitude = point.coords[1]
lon_direction = "E" if longitude > 0 else "W" lon_direction = "E" if longitude > 0 else "W"
lat_direction = "N" if latitude > 0 else "S" lat_direction = "N" if latitude > 0 else "S"
lon_value = abs(longitude) lon_value = abs(longitude)
lat_value = abs(latitude) lat_value = abs(latitude)
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}" return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
return "-" return "-"
@register.filter(name='format_coords_decimal') @register.filter(name='format_coords_decimal')
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str: def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
""" """
Форматирует объект Point в десятичные координаты с заданной точностью. Форматирует объект Point в десятичные координаты с заданной точностью.
Args: Args:
point (Point): Объект Point из GeoDjango или None. point (Point): Объект Point из GeoDjango или None.
precision (int): Количество знаков после запятой (по умолчанию 6). precision (int): Количество знаков после запятой (по умолчанию 6).
Returns: Returns:
str: Отформатированная строка координат в формате "lat, lon" str: Отформатированная строка координат в формате "lat, lon"
или "-" если point равен None. или "-" если point равен None.
Example: Example:
В шаблоне: В шаблоне:
{{ geo_obj.coords|format_coords_decimal:4 }} {{ geo_obj.coords|format_coords_decimal:4 }}
Результат: Результат:
"55.7500, 37.6200" "55.7500, 37.6200"
""" """
if not point: if not point:
return "-" return "-"
try: try:
longitude = point.coords[0] longitude = point.coords[0]
latitude = point.coords[1] latitude = point.coords[1]
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}" format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
return format_str.format(latitude, longitude) return format_str.format(latitude, longitude)
except (AttributeError, IndexError, TypeError, ValueError): except (AttributeError, IndexError, TypeError, ValueError):
return "-" return "-"
@register.filter(name='coords_to_lat') @register.filter(name='coords_to_lat')
def coords_to_lat(point: Optional[Point]) -> Optional[float]: def coords_to_lat(point: Optional[Point]) -> Optional[float]:
""" """
Извлекает широту из объекта Point. Извлекает широту из объекта Point.
Args: Args:
point (Point): Объект Point из GeoDjango или None. point (Point): Объект Point из GeoDjango или None.
Returns: Returns:
float: Значение широты или None если point равен None. float: Значение широты или None если point равен None.
Example: Example:
В шаблоне: В шаблоне:
{{ geo_obj.coords|coords_to_lat }} {{ geo_obj.coords|coords_to_lat }}
""" """
if not point: if not point:
return None return None
try: try:
return point.coords[1] return point.coords[1]
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
return None return None
@register.filter(name='coords_to_lon') @register.filter(name='coords_to_lon')
def coords_to_lon(point: Optional[Point]) -> Optional[float]: def coords_to_lon(point: Optional[Point]) -> Optional[float]:
""" """
Извлекает долготу из объекта Point. Извлекает долготу из объекта Point.
Args: Args:
point (Point): Объект Point из GeoDjango или None. point (Point): Объект Point из GeoDjango или None.
Returns: Returns:
float: Значение долготы или None если point равен None. float: Значение долготы или None если point равен None.
Example: Example:
В шаблоне: В шаблоне:
{{ geo_obj.coords|coords_to_lon }} {{ geo_obj.coords|coords_to_lon }}
""" """
if not point: if not point:
return None return None
try: try:
return point.coords[0] return point.coords[0]
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
return None return None

View File

@@ -1,179 +1,179 @@
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View from django.views import View
class FormatCoordinatesTestCase(TestCase): class FormatCoordinatesTestCase(TestCase):
"""Тесты для функции format_coordinates""" """Тесты для функции format_coordinates"""
def test_format_positive_coordinates(self): def test_format_positive_coordinates(self):
"""Тест форматирования положительных координат""" """Тест форматирования положительных координат"""
result = format_coordinates(37.62, 55.75) result = format_coordinates(37.62, 55.75)
self.assertEqual(result, "55.75N 37.62E") self.assertEqual(result, "55.75N 37.62E")
def test_format_negative_longitude(self): def test_format_negative_longitude(self):
"""Тест форматирования с отрицательной долготой""" """Тест форматирования с отрицательной долготой"""
result = format_coordinates(-122.42, 37.77) result = format_coordinates(-122.42, 37.77)
self.assertEqual(result, "37.77N 122.42W") self.assertEqual(result, "37.77N 122.42W")
def test_format_negative_latitude(self): def test_format_negative_latitude(self):
"""Тест форматирования с отрицательной широтой""" """Тест форматирования с отрицательной широтой"""
result = format_coordinates(151.21, -33.87) result = format_coordinates(151.21, -33.87)
self.assertEqual(result, "33.87S 151.21E") self.assertEqual(result, "33.87S 151.21E")
def test_format_both_negative(self): def test_format_both_negative(self):
"""Тест форматирования с обеими отрицательными координатами""" """Тест форматирования с обеими отрицательными координатами"""
result = format_coordinates(-58.38, -34.60) result = format_coordinates(-58.38, -34.60)
self.assertEqual(result, "34.6S 58.38W") self.assertEqual(result, "34.6S 58.38W")
class ParsePaginationParamsTestCase(TestCase): class ParsePaginationParamsTestCase(TestCase):
"""Тесты для функции parse_pagination_params""" """Тесты для функции parse_pagination_params"""
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
def test_default_values(self): def test_default_values(self):
"""Тест значений по умолчанию""" """Тест значений по умолчанию"""
request = self.factory.get("/") request = self.factory.get("/")
page, per_page = parse_pagination_params(request) page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1) self.assertEqual(page, 1)
self.assertEqual(per_page, 50) self.assertEqual(per_page, 50)
def test_custom_values(self): def test_custom_values(self):
"""Тест пользовательских значений""" """Тест пользовательских значений"""
request = self.factory.get("/?page=3&items_per_page=100") request = self.factory.get("/?page=3&items_per_page=100")
page, per_page = parse_pagination_params(request) page, per_page = parse_pagination_params(request)
self.assertEqual(page, 3) self.assertEqual(page, 3)
self.assertEqual(per_page, 100) self.assertEqual(per_page, 100)
def test_invalid_page_number(self): def test_invalid_page_number(self):
"""Тест невалидного номера страницы""" """Тест невалидного номера страницы"""
request = self.factory.get("/?page=invalid") request = self.factory.get("/?page=invalid")
page, per_page = parse_pagination_params(request) page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1) self.assertEqual(page, 1)
def test_negative_page_number(self): def test_negative_page_number(self):
"""Тест отрицательного номера страницы""" """Тест отрицательного номера страницы"""
request = self.factory.get("/?page=-5") request = self.factory.get("/?page=-5")
page, per_page = parse_pagination_params(request) page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1) self.assertEqual(page, 1)
def test_max_items_per_page_limit(self): def test_max_items_per_page_limit(self):
"""Тест ограничения максимального количества элементов""" """Тест ограничения максимального количества элементов"""
request = self.factory.get("/?items_per_page=20000") request = self.factory.get("/?items_per_page=20000")
page, per_page = parse_pagination_params(request) page, per_page = parse_pagination_params(request)
self.assertEqual(per_page, 10000) self.assertEqual(per_page, 10000)
class RoleRequiredMixinTestCase(TestCase): class RoleRequiredMixinTestCase(TestCase):
"""Тесты для RoleRequiredMixin""" """Тесты для RoleRequiredMixin"""
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
def test_admin_has_access(self): def test_admin_has_access(self):
"""Тест что администратор имеет доступ""" """Тест что администратор имеет доступ"""
user = User.objects.create_user(username="testuser", password="12345") user = User.objects.create_user(username="testuser", password="12345")
# Get the automatically created CustomUser and set role to 'admin' # Get the automatically created CustomUser and set role to 'admin'
custom_user = CustomUser.objects.get(user=user) custom_user = CustomUser.objects.get(user=user)
custom_user.role = "admin" custom_user.role = "admin"
custom_user.save() custom_user.save()
# Refresh user to get updated customuser # Refresh user to get updated customuser
user.refresh_from_db() user.refresh_from_db()
class TestView(RoleRequiredMixin, View): class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"] required_roles = ["admin", "moderator"]
view = TestView() view = TestView()
request = self.factory.get("/") request = self.factory.get("/")
request.user = user request.user = user
view.request = request view.request = request
self.assertTrue(view.test_func()) self.assertTrue(view.test_func())
def test_user_without_role_denied(self): def test_user_without_role_denied(self):
"""Тест что пользователь без роли не имеет доступа""" """Тест что пользователь без роли не имеет доступа"""
user_no_role = User.objects.create_user(username="norole", password="12345") user_no_role = User.objects.create_user(username="norole", password="12345")
# Get the automatically created CustomUser - default role is 'user' # Get the automatically created CustomUser - default role is 'user'
custom_user_no_role = CustomUser.objects.get(user=user_no_role) custom_user_no_role = CustomUser.objects.get(user=user_no_role)
self.assertEqual(custom_user_no_role.role, "user") self.assertEqual(custom_user_no_role.role, "user")
class TestView(RoleRequiredMixin, View): class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"] required_roles = ["admin", "moderator"]
view = TestView() view = TestView()
request = self.factory.get("/") request = self.factory.get("/")
request.user = user_no_role request.user = user_no_role
view.request = request view.request = request
self.assertFalse(view.test_func()) self.assertFalse(view.test_func())
class CoordinateProcessingMixinTestCase(TestCase): class CoordinateProcessingMixinTestCase(TestCase):
"""Тесты для CoordinateProcessingMixin""" """Тесты для CoordinateProcessingMixin"""
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
def test_extract_geo_coordinates(self): def test_extract_geo_coordinates(self):
"""Тест извлечения координат геолокации""" """Тест извлечения координат геолокации"""
class TestView(CoordinateProcessingMixin, View): class TestView(CoordinateProcessingMixin, View):
pass pass
view = TestView() view = TestView()
request = self.factory.post( request = self.factory.post(
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"} "/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
) )
view.request = request view.request = request
coords = view._extract_coordinates("geo") coords = view._extract_coordinates("geo")
self.assertIsNotNone(coords) self.assertIsNotNone(coords)
self.assertEqual(coords, (37.62, 55.75)) self.assertEqual(coords, (37.62, 55.75))
def test_extract_invalid_coordinates(self): def test_extract_invalid_coordinates(self):
"""Тест извлечения невалидных координат""" """Тест извлечения невалидных координат"""
class TestView(CoordinateProcessingMixin, View): class TestView(CoordinateProcessingMixin, View):
pass pass
view = TestView() view = TestView()
request = self.factory.post( request = self.factory.post(
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"} "/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
) )
view.request = request view.request = request
coords = view._extract_coordinates("geo") coords = view._extract_coordinates("geo")
self.assertIsNone(coords) self.assertIsNone(coords)
def test_process_coordinates(self): def test_process_coordinates(self):
"""Тест обработки координат и применения к объекту Geo""" """Тест обработки координат и применения к объекту Geo"""
class TestView(CoordinateProcessingMixin, View): class TestView(CoordinateProcessingMixin, View):
pass pass
view = TestView() view = TestView()
request = self.factory.post( request = self.factory.post(
"/", "/",
{ {
"geo_longitude": "37.62", "geo_longitude": "37.62",
"geo_latitude": "55.75", "geo_latitude": "55.75",
"kupsat_longitude": "37.63", "kupsat_longitude": "37.63",
"kupsat_latitude": "55.76", "kupsat_latitude": "55.76",
}, },
) )
view.request = request view.request = request
geo_instance = Geo() geo_instance = Geo()
view.process_coordinates(geo_instance) view.process_coordinates(geo_instance)
self.assertIsNotNone(geo_instance.coords) self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75)) self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
self.assertIsNotNone(geo_instance.coords_kupsat) self.assertIsNotNone(geo_instance.coords_kupsat)
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76)) self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))

View File

@@ -1,32 +1,32 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'mainapp' app_name = 'mainapp'
urlpatterns = [ urlpatterns = [
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth 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('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('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('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'), 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('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'), path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'), path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'), 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('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('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'), 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>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'), path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'), path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'), path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('lyngsat-task-status/<str:task_id>/', 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'), 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

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " "available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?" "forget to activate a virtual environment?"
) from exc ) from exc
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -1,67 +1,67 @@
# Django imports # Django imports
from django.contrib import admin from django.contrib import admin
# Third-party imports # Third-party imports
from import_export.admin import ImportExportActionModelAdmin from import_export.admin import ImportExportActionModelAdmin
from more_admin_filters import MultiSelectRelatedDropdownFilter from more_admin_filters import MultiSelectRelatedDropdownFilter
from rangefilter.filters import NumericRangeFilterBuilder from rangefilter.filters import NumericRangeFilterBuilder
# Local imports # Local imports
from .models import Transponders from .models import Transponders
# ============================================================================ # ============================================================================
# Base Admin Classes # Base Admin Classes
# ============================================================================ # ============================================================================
class BaseAdmin(admin.ModelAdmin): class BaseAdmin(admin.ModelAdmin):
""" """
Базовый класс для всех admin моделей mapsapp. Базовый класс для всех admin моделей mapsapp.
Предоставляет общую функциональность: Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу - Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице - Настройка количества элементов на странице
""" """
save_on_top = True save_on_top = True
list_per_page = 50 list_per_page = 50
# ============================================================================ # ============================================================================
# Admin Classes # Admin Classes
# ============================================================================ # ============================================================================
@admin.register(Transponders) @admin.register(Transponders)
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin): class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
""" """
Админ-панель для модели Transponders. Админ-панель для модели Transponders.
Оптимизирована для работы с транспондерами: Оптимизирована для работы с транспондерами:
- Использует select_related для оптимизации запросов - Использует select_related для оптимизации запросов
- Предоставляет фильтры по спутникам, поляризации и зоне - Предоставляет фильтры по спутникам, поляризации и зоне
- Поддерживает импорт/экспорт данных - Поддерживает импорт/экспорт данных
""" """
list_display = ( list_display = (
"sat_id", "sat_id",
"name", "name",
"zone_name", "zone_name",
"downlink", "downlink",
"uplink", "uplink",
"frequency_range", "frequency_range",
"transfer", "transfer",
"polarization", "polarization",
) )
list_display_links = ("name",) list_display_links = ("name",)
list_select_related = ("polarization", "sat_id") list_select_related = ("polarization", "sat_id")
list_filter = ( list_filter = (
("polarization", MultiSelectRelatedDropdownFilter), ("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter), ("sat_id", MultiSelectRelatedDropdownFilter),
("downlink", NumericRangeFilterBuilder()), ("downlink", NumericRangeFilterBuilder()),
("uplink", NumericRangeFilterBuilder()), ("uplink", NumericRangeFilterBuilder()),
("frequency_range", NumericRangeFilterBuilder()), ("frequency_range", NumericRangeFilterBuilder()),
"zone_name", "zone_name",
) )
search_fields = ("name", "sat_id__name", "zone_name") search_fields = ("name", "sat_id__name", "zone_name")
ordering = ("name",) ordering = ("name",)
autocomplete_fields = ("sat_id", "polarization") autocomplete_fields = ("sat_id", "polarization")

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class MapsappConfig(AppConfig): class MapsappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'mapsapp' name = 'mapsapp'

View File

@@ -1,37 +1,37 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36 # Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions import django.db.models.expressions
import django.db.models.functions.math import django.db.models.functions.math
import mainapp.models import mainapp.models
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('mainapp', '0001_initial'), ('mainapp', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Transponders', name='Transponders',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')), ('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')), ('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')), ('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')), ('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')), ('zone_name', models.CharField(blank=True, 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='Перенос')), ('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='Поляризация')), ('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='Спутник')), ('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
], ],
options={ options={
'verbose_name': 'Транспондер', 'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры', 'verbose_name_plural': 'Транспондеры',
}, },
), ),
] ]

View File

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

View File

@@ -1,117 +1,117 @@
# Django imports # Django imports
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import ExpressionWrapper, F from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs from django.db.models.functions import Abs
# Local imports # Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization from mainapp.models import Polarization, Satellite, get_default_polarization
class Transponders(models.Model): class Transponders(models.Model):
""" """
Модель транспондера спутника. Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации. Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
""" """
# Основные поля # Основные поля
name = models.CharField( name = models.CharField(
max_length=30, max_length=30,
null=True, null=True,
blank=True, blank=True,
verbose_name="Название транспондера", verbose_name="Название транспондера",
db_index=True, db_index=True,
help_text="Название транспондера" help_text="Название транспондера"
) )
downlink = models.FloatField( downlink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Downlink", verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)], validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)" help_text="Частота downlink в МГц (0-50000)"
) )
frequency_range = models.FloatField( frequency_range = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)], validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)" help_text="Полоса частот в МГц (0-1000)"
) )
uplink = models.FloatField( uplink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Uplink", verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)], validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)" help_text="Частота uplink в МГц (0-50000)"
) )
zone_name = models.CharField( zone_name = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
verbose_name="Название зоны", verbose_name="Название зоны",
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера" help_text="Название зоны покрытия транспондера"
) )
# Связи # Связи
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, Polarization,
default=get_default_polarization, default=get_default_polarization,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
related_name="tran_polarizations", related_name="tran_polarizations",
null=True, null=True,
blank=True, blank=True,
verbose_name="Поляризация", verbose_name="Поляризация",
help_text="Поляризация сигнала" help_text="Поляризация сигнала"
) )
sat_id = models.ForeignKey( sat_id = models.ForeignKey(
Satellite, Satellite,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="tran_satellite", related_name="tran_satellite",
verbose_name="Спутник", verbose_name="Спутник",
db_index=True, db_index=True,
help_text="Спутник, которому принадлежит транспондер" help_text="Спутник, которому принадлежит транспондер"
) )
# Вычисляемые поля # Вычисляемые поля
transfer = models.GeneratedField( transfer = models.GeneratedField(
expression=ExpressionWrapper( expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')), Abs(F('downlink') - F('uplink')),
output_field=models.FloatField() output_field=models.FloatField()
), ),
output_field=models.FloatField(), output_field=models.FloatField(),
db_persist=True, db_persist=True,
null=True, null=True,
blank=True, blank=True,
verbose_name="Перенос" verbose_name="Перенос"
) )
def clean(self): def clean(self):
"""Валидация на уровне модели""" """Валидация на уровне модели"""
super().clean() super().clean()
# Проверка что downlink и uplink заданы # Проверка что downlink и uplink заданы
if self.downlink and self.uplink: if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи # Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink: if self.uplink < self.downlink:
raise ValidationError({ raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink' 'uplink': 'Частота uplink обычно выше частоты downlink'
}) })
def __str__(self): def __str__(self):
if self.name: if self.name:
return self.name return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}" return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta: class Meta:
verbose_name = "Транспондер" verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры" verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink'] ordering = ['sat_id', 'downlink']
indexes = [ indexes = [
models.Index(fields=['sat_id', 'downlink']), models.Index(fields=['sat_id', 'downlink']),
models.Index(fields=['sat_id', 'zone_name']), models.Index(fields=['sat_id', 'zone_name']),
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,83 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>{% block title %}Карта{% endblock %}</title> <title>{% block title %}Карта{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" /> <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet"> <link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.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"> <link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<!-- Extra CSS --> <!-- Extra CSS -->
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
#map { #map {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="map"></div> <div id="map"></div>
{% block content %} {% block content %}
{% endblock %} {% endblock %}
<!-- Leaflet JavaScript --> <!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script> <script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script> <script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script> <script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script> <script>
let map = L.map('map').setView([0, 0], 2); let map = L.map('map').setView([0, 0], 2);
L.control.scale({ L.control.scale({
imperial: false, imperial: false,
metric: true}).addTo(map); metric: true}).addTo(map);
map.attributionControl.setPrefix(false); map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}); });
street.addTo(map); street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' attribution: 'Tiles &copy; 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', { const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19, maxZoom: 19,
attribution: 'Local Tiles' attribution: 'Local Tiles'
}); });
street_local.addTo(map); street_local.addTo(map);
const baseLayers = { const baseLayers = {
"Улицы": street, "Улицы": street,
"Спутник": satellite, "Спутник": satellite,
"Локально": street_local "Локально": street_local
}; };
L.control.layers(baseLayers).addTo(map); L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18); map.setMaxZoom(18);
map.setMinZoom(0); map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map); L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}'; {% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
let imageBounds = [[-82, -180], [82, 180]]; let imageBounds = [[-82, -180], [82, 180]];
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %} L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
</script> </script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,118 +1,118 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>Cesium Map Editor</title> <title>Cesium Map Editor</title>
<!-- Cesium Library --> <!-- Cesium Library -->
<script src="{% static 'cesium/Cesium.js' %}" defer></script> <script src="{% static 'cesium/Cesium.js' %}" defer></script>
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet"> <link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
<!-- Custom Styles --> <!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}"> <link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
</head> </head>
<body> <body>
<div id="cesiumContainer"></div> <div id="cesiumContainer"></div>
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" /> <input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
<!-- Панель инструментов --> <!-- Панель инструментов -->
<div class="toolbar"> <div class="toolbar">
<!-- Группа 1: Режимы рисования --> <!-- Группа 1: Режимы рисования -->
<div class="toolbar-section"> <div class="toolbar-section">
<div class="section-title">Рисование</div> <div class="section-title">Рисование</div>
<div class="toolbar-group"> <div class="toolbar-group">
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)"> <button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
<span>🔍</span> Выделение <span>🔍</span> Выделение
</button> </button>
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)"> <button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
<span>📌</span> Маркер <span>📌</span> Маркер
</button> </button>
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)"> <button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
<span></span> Полигон <span></span> Полигон
</button> </button>
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)"> <button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
<span>〰️</span> Линия <span>〰️</span> Линия
</button> </button>
</div> </div>
</div> </div>
<!-- Группа 2: Импорт/Экспорт --> <!-- Группа 2: Импорт/Экспорт -->
<div class="toolbar-section"> <div class="toolbar-section">
<div class="section-title">Импорт/экспорт всех объектов</div> <div class="section-title">Импорт/экспорт всех объектов</div>
<div class="toolbar-group"> <div class="toolbar-group">
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML"> <button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
<span>📥</span> Импорт <span>📥</span> Импорт
</button> </button>
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML"> <button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
<span>📤</span> Экспорт <span>📤</span> Экспорт
</button> </button>
</div> </div>
</div> </div>
<!-- Группа 3: Действия --> <!-- Группа 3: Действия -->
<div class="toolbar-section"> <div class="toolbar-section">
<div class="section-title">Действия</div> <div class="section-title">Действия</div>
<div class="toolbar-group"> <div class="toolbar-group">
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)"> <button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
<span>🗑️</span> Удалить <span>🗑️</span> Удалить
</button> </button>
<button id="clearAll" class="tool-btn danger" title="Очистить всё"> <button id="clearAll" class="tool-btn danger" title="Очистить всё">
<span>🧹</span> Очистить <span>🧹</span> Очистить
</button> </button>
</div> </div>
</div> </div>
<!-- Строка состояния --> <!-- Строка состояния -->
<div class="status-bar"> <div class="status-bar">
<span id="modeStatus">Режим: Выделение</span> <span id="modeStatus">Режим: Выделение</span>
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span> <span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
<span id="hint">Нажмите ESC для отмены</span> <span id="hint">Нажмите ESC для отмены</span>
</div> </div>
</div> </div>
<!-- Блок выбора объектов из БД --> <!-- Блок выбора объектов из БД -->
<div class="db-objects-panel"> <div class="db-objects-panel">
<div class="panel-title">Объекты из базы</div> <div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select"> <select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option> <option value="">— Выберите объект —</option>
{% for sat in sats %} {% for sat in sats %}
<option value="{{sat.id}}">{{sat.name}}</option> <option value="{{sat.id}}">{{sat.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button> <button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
</div> </div>
<div class="footprint-control"> <div class="footprint-control">
<div class="panel-title">Области покрытия</div> <div class="panel-title">Области покрытия</div>
<div class="footprint-actions"> <div class="footprint-actions">
<button id="showAllFootprints">Показать все</button> <button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button> <button id="hideAllFootprints">Скрыть все</button>
</div> </div>
<div id="footprintToggles"></div> <div id="footprintToggles"></div>
</div> </div>
<!-- Модальное окно для описания --> <!-- Модальное окно для описания -->
<div id="descriptionModal" class="modal"> <div id="descriptionModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h3>Добавить описание</h3> <h3>Добавить описание</h3>
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea> <textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
<div class="modal-buttons"> <div class="modal-buttons">
<button id="confirmDescription">Сохранить</button> <button id="confirmDescription">Сохранить</button>
<button id="cancelDescription">Отмена</button> <button id="cancelDescription">Отмена</button>
</div> </div>
</div> </div>
</div> </div>
<div id="exportModal" class="modal"> <div id="exportModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h3>Экспорт данных</h3> <h3>Экспорт данных</h3>
<p>Выберите формат для экспорта всех объектов:</p> <p>Выберите формат для экспорта всех объектов:</p>
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;"> <div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
<button id="exportGeoJson">GeoJSON</button> <button id="exportGeoJson">GeoJSON</button>
<button id="exportKml">KML</button> <button id="exportKml">KML</button>
<button id="cancelExport">Отмена</button> <button id="cancelExport">Отмена</button>
</div> </div>
</div> </div>
</div> </div>
<script src="{% static 'mapsapp/main.js' %}"></script> <script src="{% static 'mapsapp/main.js' %}"></script>
</body> </body>
</html> </html>

View File

@@ -1,3 +1,3 @@
from django.test import TestCase from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@@ -1,14 +1,14 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'mapsapp' app_name = 'mapsapp'
urlpatterns = [ urlpatterns = [
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'), path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'), path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"), 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('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'), path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
] ]

View File

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

View File

@@ -1,148 +1,148 @@
# Standard library imports # Standard library imports
from typing import Any, Dict from typing import Any, Dict
# Django imports # Django imports
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic import TemplateView from django.views.generic import TemplateView
# Third-party imports # Third-party imports
import requests import requests
# Local imports # Local imports
from mainapp.models import Satellite from mainapp.models import Satellite
from .models import Transponders from .models import Transponders
from .utils import get_band_names from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView): class CesiumMapView(LoginRequiredMixin, TemplateView):
""" """
Представление для отображения 3D карты с использованием Cesium. Представление для отображения 3D карты с использованием Cesium.
Отображает спутники и их зоны покрытия на интерактивной 3D карте. Отображает спутники и их зоны покрытия на интерактивной 3D карте.
""" """
template_name = 'mapsapp/map3d.html' template_name = 'mapsapp/map3d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля # Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter( context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name') ).distinct().only('id', 'name').order_by('name')
return context return context
class GetFootprintsView(LoginRequiredMixin, View): class GetFootprintsView(LoginRequiredMixin, View):
""" """
API для получения зон покрытия (footprints) спутника. API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника. Возвращает список названий зон покрытия для указанного спутника.
""" """
def get(self, request, sat_id): def get(self, request, sat_id):
try: try:
# Оптимизированный запрос - загружаем только поле name # Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name sat_name = Satellite.objects.only('name').get(id=sat_id).name
footprint_names = get_band_names(sat_name) footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False) return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist: except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404) return JsonResponse({"error": "Спутник не найден"}, status=404)
except Exception as e: except Exception as e:
return JsonResponse({"error": str(e)}, status=500) return JsonResponse({"error": str(e)}, status=500)
class TileProxyView(View): class TileProxyView(View):
""" """
Прокси для загрузки тайлов карты покрытия спутников. Прокси для загрузки тайлов карты покрытия спутников.
Кэширует тайлы на 7 дней для улучшения производительности. Кэширует тайлы на 7 дней для улучшения производительности.
""" """
# Константы # Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles" TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд REQUEST_TIMEOUT = 10 # секунд
@method_decorator(require_GET) @method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION)) @method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def get(self, request, footprint_name, z, x, y): def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint # Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum(): if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400) return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png" url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
try: try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT) resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200: if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png') response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*" response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}" response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response return response
else: else:
return HttpResponseNotFound("Tile not found") return HttpResponseNotFound("Tile not found")
except requests.Timeout: except requests.Timeout:
return HttpResponse("Request timeout", status=504) return HttpResponse("Request timeout", status=504)
except requests.RequestException as e: except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500) return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView): class LeafletMapView(LoginRequiredMixin, TemplateView):
""" """
Представление для отображения 2D карты с использованием Leaflet. Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте. Отображает спутники и транспондеры на интерактивной 2D карте.
""" """
template_name = 'mapsapp/map2d.html' template_name = 'mapsapp/map2d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля # Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter( context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name') ).distinct().only('id', 'name').order_by('name')
context['trans'] = Transponders.objects.select_related( context['trans'] = Transponders.objects.select_related(
'sat_id', 'polarization' 'sat_id', 'polarization'
).only( ).only(
'id', 'name', 'sat_id__name', 'polarization__name', 'id', 'name', 'sat_id__name', 'polarization__name',
'downlink', 'frequency_range', 'zone_name' 'downlink', 'frequency_range', 'zone_name'
) )
return context return context
class GetTransponderOnSatIdView(LoginRequiredMixin, View): class GetTransponderOnSatIdView(LoginRequiredMixin, View):
""" """
API для получения транспондеров спутника. API для получения транспондеров спутника.
Возвращает список транспондеров для указанного спутника с оптимизированными запросами. Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
""" """
def get(self, request, sat_id): def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only # Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter( trans = Transponders.objects.filter(
sat_id=sat_id sat_id=sat_id
).select_related('polarization').only( ).select_related('polarization').only(
'name', 'downlink', 'frequency_range', 'name', 'downlink', 'frequency_range',
'zone_name', 'polarization__name' 'zone_name', 'polarization__name'
) )
if not trans.exists(): if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404) return JsonResponse({'error': 'Объектов не найдено'}, status=404)
# Используем list comprehension для лучшей производительности # Используем list comprehension для лучшей производительности
output = [ output = [
{ {
"name": tran.name, "name": tran.name,
"frequency": tran.downlink, "frequency": tran.downlink,
"frequency_range": tran.frequency_range, "frequency_range": tran.frequency_range,
"zone_name": tran.zone_name, "zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-" "polarization": tran.polarization.name if tran.polarization else "-"
} }
for tran in trans for tran in trans
] ]
return JsonResponse(output, safe=False) return JsonResponse(output, safe=False)

View File

@@ -8,11 +8,14 @@ dependencies = [
"aiosqlite>=0.21.0", "aiosqlite>=0.21.0",
"bcrypt>=5.0.0", "bcrypt>=5.0.0",
"beautifulsoup4>=4.14.2", "beautifulsoup4>=4.14.2",
"celery>=5.5.3",
"django>=5.2.7", "django>=5.2.7",
"django-admin-interface>=0.30.1", "django-admin-interface>=0.30.1",
"django-admin-multiple-choice-list-filter>=0.1.1", "django-admin-multiple-choice-list-filter>=0.1.1",
"django-admin-rangefilter>=0.13.3", "django-admin-rangefilter>=0.13.3",
"django-autocomplete-light>=3.12.1", "django-autocomplete-light>=3.12.1",
"django-celery-beat>=2.6.0",
"django-celery-results>=2.5.1",
"django-daisy>=1.1.2", "django-daisy>=1.1.2",
"django-debug-toolbar>=6.0.0", "django-debug-toolbar>=6.0.0",
"django-dynamic-raw-id>=4.4", "django-dynamic-raw-id>=4.4",
@@ -21,6 +24,7 @@ dependencies = [
"django-map-widgets>=0.5.1", "django-map-widgets>=0.5.1",
"django-more-admin-filters>=1.13", "django-more-admin-filters>=1.13",
"dotenv>=0.9.9", "dotenv>=0.9.9",
"flower>=2.0.1",
"geopy>=2.4.1", "geopy>=2.4.1",
"gunicorn>=23.0.0", "gunicorn>=23.0.0",
"lxml>=6.0.2", "lxml>=6.0.2",

View File

@@ -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

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Script to start Celery worker # Script to start Celery worker
echo "Starting Celery worker..." echo "Starting Celery worker..."
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log

View File

@@ -1,30 +1,30 @@
{ {
"measure": "Messung", "measure": "Messung",
"measureDistancesAndAreas": "Messung von Abständen und Flächen", "measureDistancesAndAreas": "Messung von Abständen und Flächen",
"createNewMeasurement": "Eine neue Messung durchführen", "createNewMeasurement": "Eine neue Messung durchführen",
"startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.", "startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",
"finishMeasurement": "Messung beenden", "finishMeasurement": "Messung beenden",
"lastPoint": "Letzter Punkt", "lastPoint": "Letzter Punkt",
"area": "Fläche", "area": "Fläche",
"perimeter": "Rand", "perimeter": "Rand",
"pointLocation": "Lage des Punkts", "pointLocation": "Lage des Punkts",
"areaMeasurement": "Gemessene Fläche", "areaMeasurement": "Gemessene Fläche",
"linearMeasurement": "Gemessener Abstand", "linearMeasurement": "Gemessener Abstand",
"pathDistance": "Abstand entlang des Pfads", "pathDistance": "Abstand entlang des Pfads",
"centerOnArea": "Auf diese Fläche zentrieren", "centerOnArea": "Auf diese Fläche zentrieren",
"centerOnLine": "Auf diesen Linienzug zentrieren", "centerOnLine": "Auf diesen Linienzug zentrieren",
"centerOnLocation": "Auf diesen Ort zentrieren", "centerOnLocation": "Auf diesen Ort zentrieren",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"delete": "Löschen", "delete": "Löschen",
"acres": "Morgen", "acres": "Morgen",
"feet": "Fuß", "feet": "Fuß",
"kilometers": "Kilometer", "kilometers": "Kilometer",
"hectares": "Hektar", "hectares": "Hektar",
"meters": "Meter", "meters": "Meter",
"miles": "Meilen", "miles": "Meilen",
"sqfeet": "Quadratfuß", "sqfeet": "Quadratfuß",
"sqmeters": "Quadratmeter", "sqmeters": "Quadratmeter",
"sqmiles": "Quadratmeilen", "sqmiles": "Quadratmeilen",
"decPoint": ",", "decPoint": ",",
"thousandsSep": "." "thousandsSep": "."
} }

Some files were not shown because too many files have changed in this diff Show More