Compare commits
2 Commits
ed9a79f94a
...
1c18ae96f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c18ae96f7 | |||
| a591b79656 |
@@ -1,249 +0,0 @@
|
|||||||
# Чеклист для деплоя в Production
|
|
||||||
|
|
||||||
## Перед деплоем
|
|
||||||
|
|
||||||
### 1. Безопасность
|
|
||||||
|
|
||||||
- [ ] Сгенерирован новый `SECRET_KEY`
|
|
||||||
```bash
|
|
||||||
python generate_secret_key.py
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Изменены все пароли в `.env`:
|
|
||||||
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
|
|
||||||
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
|
|
||||||
|
|
||||||
- [ ] Настроен `ALLOWED_HOSTS` в `.env`:
|
|
||||||
```
|
|
||||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] `DEBUG=False` в `.env`
|
|
||||||
|
|
||||||
### 2. База данных
|
|
||||||
|
|
||||||
- [ ] Проверены все миграции:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Настроен backup БД (cron job):
|
|
||||||
```bash
|
|
||||||
0 2 * * * cd /path/to/project && make backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Статические файлы
|
|
||||||
|
|
||||||
- [ ] Проверена директория для статики:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. SSL/HTTPS (опционально, но рекомендуется)
|
|
||||||
|
|
||||||
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
|
|
||||||
- [ ] Сертификаты размещены в `nginx/ssl/`
|
|
||||||
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
|
|
||||||
- [ ] Обновлен `server_name` в `ssl.conf`
|
|
||||||
|
|
||||||
### 5. Nginx
|
|
||||||
|
|
||||||
- [ ] Проверена конфигурация Nginx:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
|
|
||||||
|
|
||||||
### 6. Docker
|
|
||||||
|
|
||||||
- [ ] Проверен `.dockerignore` - исключены ненужные файлы
|
|
||||||
- [ ] Проверен `.gitignore` - не коммитятся секреты
|
|
||||||
|
|
||||||
### 7. Переменные окружения
|
|
||||||
|
|
||||||
Проверьте `.env` файл:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Django
|
|
||||||
DEBUG=False
|
|
||||||
ENVIRONMENT=production
|
|
||||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
|
||||||
SECRET_KEY=<ваш-длинный-секретный-ключ>
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_NAME=geodb
|
|
||||||
DB_USER=geralt
|
|
||||||
DB_PASSWORD=<сильный-пароль>
|
|
||||||
DB_HOST=db
|
|
||||||
DB_PORT=5432
|
|
||||||
|
|
||||||
# Allowed Hosts
|
|
||||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
|
||||||
|
|
||||||
# PostgreSQL
|
|
||||||
POSTGRES_DB=geodb
|
|
||||||
POSTGRES_USER=geralt
|
|
||||||
POSTGRES_PASSWORD=<тот-же-сильный-пароль>
|
|
||||||
|
|
||||||
# Gunicorn
|
|
||||||
GUNICORN_WORKERS=3
|
|
||||||
GUNICORN_TIMEOUT=120
|
|
||||||
```
|
|
||||||
|
|
||||||
## Деплой
|
|
||||||
|
|
||||||
### 1. Клонирование репозитория
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <your-repo-url>
|
|
||||||
cd <project-directory>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Настройка окружения
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.prod .env
|
|
||||||
nano .env # Отредактируйте все необходимые переменные
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Запуск контейнеров
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Проверка статуса
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml ps
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Создание суперпользователя
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Проверка работоспособности
|
|
||||||
|
|
||||||
- [ ] Открыть http://yourdomain.com
|
|
||||||
- [ ] Открыть http://yourdomain.com/admin
|
|
||||||
- [ ] Проверить статические файлы
|
|
||||||
- [ ] Проверить медиа файлы
|
|
||||||
- [ ] Проверить TileServer GL: http://yourdomain.com:8080
|
|
||||||
|
|
||||||
## После деплоя
|
|
||||||
|
|
||||||
### 1. Мониторинг
|
|
||||||
|
|
||||||
- [ ] Настроить мониторинг логов:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Проверить использование ресурсов:
|
|
||||||
```bash
|
|
||||||
docker stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Backup
|
|
||||||
|
|
||||||
- [ ] Настроить автоматический backup БД
|
|
||||||
- [ ] Проверить восстановление из backup
|
|
||||||
- [ ] Настроить backup медиа файлов
|
|
||||||
|
|
||||||
### 3. Обновления
|
|
||||||
|
|
||||||
- [ ] Документировать процесс обновления
|
|
||||||
- [ ] Тестировать обновления на dev окружении
|
|
||||||
|
|
||||||
### 4. Безопасность
|
|
||||||
|
|
||||||
- [ ] Настроить firewall (UFW, iptables)
|
|
||||||
- [ ] Ограничить доступ к портам:
|
|
||||||
- Открыть: 80, 443
|
|
||||||
- Закрыть: 5432, 8000 (доступ только внутри Docker сети)
|
|
||||||
|
|
||||||
- [ ] Настроить fail2ban (опционально)
|
|
||||||
|
|
||||||
### 5. Производительность
|
|
||||||
|
|
||||||
- [ ] Настроить кэширование (Redis, Memcached)
|
|
||||||
- [ ] Оптимизировать количество Gunicorn workers
|
|
||||||
- [ ] Настроить CDN для статики (опционально)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Проблема: Контейнеры не запускаются
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверить логи
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs
|
|
||||||
|
|
||||||
# Проверить конфигурацию
|
|
||||||
docker-compose -f docker-compose.prod.yaml config
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблема: База данных недоступна
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверить статус БД
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
|
|
||||||
|
|
||||||
# Проверить логи БД
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблема: Статические файлы не загружаются
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Пересобрать статику
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
# Проверить права доступа
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблема: 502 Bad Gateway
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверить, что Django запущен
|
|
||||||
docker-compose -f docker-compose.prod.yaml ps web
|
|
||||||
|
|
||||||
# Проверить логи Gunicorn
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs web
|
|
||||||
|
|
||||||
# Проверить конфигурацию Nginx
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
|
||||||
```
|
|
||||||
|
|
||||||
## Полезные команды
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Перезапуск сервисов
|
|
||||||
docker-compose -f docker-compose.prod.yaml restart web
|
|
||||||
docker-compose -f docker-compose.prod.yaml restart nginx
|
|
||||||
|
|
||||||
# Обновление кода
|
|
||||||
git pull
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
|
||||||
|
|
||||||
# Backup БД
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
|
||||||
|
|
||||||
# Восстановление БД
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
|
||||||
|
|
||||||
# Просмотр логов
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
|
|
||||||
|
|
||||||
# Очистка старых образов
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
## Контакты для поддержки
|
|
||||||
|
|
||||||
- Документация: [DOCKER_README.md](DOCKER_README.md)
|
|
||||||
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md)
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# Инструкция по развертыванию изменений
|
|
||||||
|
|
||||||
## Шаг 1: Применение миграций
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
python manage.py migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
Это создаст таблицу `lyngsatapp_lyngsat` в базе данных.
|
|
||||||
|
|
||||||
## Шаг 2: Запуск FlareSolver (если еще не запущен)
|
|
||||||
|
|
||||||
FlareSolver необходим для обхода защиты Cloudflare на сайте Lyngsat.
|
|
||||||
|
|
||||||
### Вариант 1: Docker
|
|
||||||
```bash
|
|
||||||
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вариант 2: Docker Compose
|
|
||||||
Добавьте в `docker-compose.yaml`:
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
flaresolverr:
|
|
||||||
image: ghcr.io/flaresolverr/flaresolverr:latest
|
|
||||||
container_name: flaresolverr
|
|
||||||
ports:
|
|
||||||
- "8191:8191"
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
Затем запустите:
|
|
||||||
```bash
|
|
||||||
docker-compose up -d flaresolverr
|
|
||||||
```
|
|
||||||
|
|
||||||
## Шаг 3: Проверка работоспособности
|
|
||||||
|
|
||||||
1. Запустите сервер разработки:
|
|
||||||
```bash
|
|
||||||
python manage.py runserver
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Откройте браузер и перейдите на:
|
|
||||||
```
|
|
||||||
http://localhost:8000/actions/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Найдите карточку "Заполнение данных Lyngsat" и нажмите на кнопку
|
|
||||||
|
|
||||||
4. Выберите один-два спутника для тестирования
|
|
||||||
|
|
||||||
5. Выберите регионы (например, только Europe)
|
|
||||||
|
|
||||||
6. Нажмите "Заполнить данные" и дождитесь завершения
|
|
||||||
|
|
||||||
## Шаг 4: Проверка результатов
|
|
||||||
|
|
||||||
1. Перейдите в админ-панель Django:
|
|
||||||
```
|
|
||||||
http://localhost:8000/admin/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Откройте раздел "Lyngsatapp" → "Источники LyngSat"
|
|
||||||
|
|
||||||
3. Проверьте, что данные загружены корректно
|
|
||||||
|
|
||||||
## Возможные проблемы и решения
|
|
||||||
|
|
||||||
### Проблема: FlareSolver не отвечает
|
|
||||||
**Решение**: Проверьте, что FlareSolver запущен:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8191/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблема: Спутники не найдены в базе
|
|
||||||
**Решение**: Убедитесь, что спутники добавлены в базу данных. Используйте функцию "Добавление списка спутников" на странице действий.
|
|
||||||
|
|
||||||
### Проблема: Долгое выполнение
|
|
||||||
**Решение**: Это нормально. Процесс может занять несколько минут на спутник. Начните с 1-2 спутников для тестирования.
|
|
||||||
|
|
||||||
### Проблема: Ошибки при парсинге
|
|
||||||
**Решение**: Проверьте логи. Некоторые ошибки (например, некорректные частоты) не критичны и не прерывают процесс.
|
|
||||||
|
|
||||||
## Откат изменений (если необходимо)
|
|
||||||
|
|
||||||
Если нужно откатить изменения:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Откатить миграцию
|
|
||||||
python manage.py migrate lyngsatapp zero
|
|
||||||
|
|
||||||
# Откатить изменения в коде
|
|
||||||
git checkout HEAD -- dbapp/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дополнительная информация
|
|
||||||
|
|
||||||
- Подробное руководство пользователя: `LYNGSAT_FILL_GUIDE.md`
|
|
||||||
- Сводка изменений: `CHANGES_SUMMARY.md`
|
|
||||||
- Документация по проекту: `README.md`
|
|
||||||
262
DOCKER_README.md
262
DOCKER_README.md
@@ -1,262 +0,0 @@
|
|||||||
# Docker Setup для Django + PostGIS + TileServer GL
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── dbapp/ # Django приложение
|
|
||||||
│ ├── Dockerfile # Универсальный Dockerfile
|
|
||||||
│ ├── entrypoint.sh # Скрипт запуска
|
|
||||||
│ └── ...
|
|
||||||
├── nginx/ # Конфигурация Nginx (только для prod)
|
|
||||||
│ └── conf.d/
|
|
||||||
│ └── default.conf
|
|
||||||
├── tiles/ # Тайлы для TileServer GL
|
|
||||||
├── docker-compose.yaml # Development окружение
|
|
||||||
├── docker-compose.prod.yaml # Production окружение
|
|
||||||
├── .env.dev # Переменные для development
|
|
||||||
└── .env.prod # Переменные для production
|
|
||||||
```
|
|
||||||
|
|
||||||
## Быстрый старт
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
1. Скопируйте файл окружения:
|
|
||||||
```bash
|
|
||||||
cp .env.dev .env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Запустите контейнеры:
|
|
||||||
```bash
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Создайте суперпользователя:
|
|
||||||
```bash
|
|
||||||
docker-compose exec web python manage.py createsuperuser
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Приложение доступно:
|
|
||||||
- Django: http://localhost:8000
|
|
||||||
- TileServer GL: http://localhost:8080
|
|
||||||
- PostgreSQL: localhost:5432
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
1. Скопируйте и настройте файл окружения:
|
|
||||||
```bash
|
|
||||||
cp .env.prod .env
|
|
||||||
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Запустите контейнеры:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Создайте суперпользователя:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Приложение доступно:
|
|
||||||
- Nginx: http://localhost (порт 80)
|
|
||||||
- Django (напрямую): http://localhost:8000
|
|
||||||
- TileServer GL: http://localhost:8080
|
|
||||||
- PostgreSQL: localhost:5432
|
|
||||||
|
|
||||||
## Основные команды
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Остановка
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Просмотр логов
|
|
||||||
docker-compose logs -f web
|
|
||||||
|
|
||||||
# Выполнение команд Django
|
|
||||||
docker-compose exec web python manage.py migrate
|
|
||||||
docker-compose exec web python manage.py createsuperuser
|
|
||||||
docker-compose exec web python manage.py shell
|
|
||||||
|
|
||||||
# Пересборка после изменений в Dockerfile
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Полная очистка (включая volumes)
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d
|
|
||||||
|
|
||||||
# Остановка
|
|
||||||
docker-compose -f docker-compose.prod.yaml down
|
|
||||||
|
|
||||||
# Просмотр логов
|
|
||||||
docker-compose -f docker-compose.prod.yaml logs -f web
|
|
||||||
|
|
||||||
# Выполнение команд Django
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
|
||||||
|
|
||||||
# Пересборка
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Различия между Dev и Prod
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- Django development server (runserver)
|
|
||||||
- DEBUG=True
|
|
||||||
- Код монтируется как volume (изменения применяются сразу)
|
|
||||||
- Без Nginx
|
|
||||||
- Простые пароли (для локальной разработки)
|
|
||||||
|
|
||||||
### Production
|
|
||||||
- Gunicorn WSGI server
|
|
||||||
- DEBUG=False
|
|
||||||
- Код копируется в образ (не монтируется)
|
|
||||||
- Nginx как reverse proxy
|
|
||||||
- Сильные пароли и SECRET_KEY
|
|
||||||
- Сбор статики (collectstatic)
|
|
||||||
- Оптимизированные настройки безопасности
|
|
||||||
|
|
||||||
## Переменные окружения
|
|
||||||
|
|
||||||
### Основные переменные (.env)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Django
|
|
||||||
DEBUG=True/False
|
|
||||||
ENVIRONMENT=development/production
|
|
||||||
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
|
|
||||||
SECRET_KEY=your-secret-key
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_NAME=geodb
|
|
||||||
DB_USER=geralt
|
|
||||||
DB_PASSWORD=your-password
|
|
||||||
DB_HOST=db
|
|
||||||
DB_PORT=5432
|
|
||||||
|
|
||||||
# Allowed Hosts
|
|
||||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
|
||||||
|
|
||||||
# Gunicorn (только для production)
|
|
||||||
GUNICORN_WORKERS=3
|
|
||||||
GUNICORN_TIMEOUT=120
|
|
||||||
```
|
|
||||||
|
|
||||||
## Volumes
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- `postgres_data_dev` - данные PostgreSQL
|
|
||||||
- `static_volume_dev` - статические файлы
|
|
||||||
- `media_volume_dev` - медиа файлы
|
|
||||||
- `logs_volume_dev` - логи
|
|
||||||
- `./dbapp:/app` - код приложения (live reload)
|
|
||||||
|
|
||||||
### Production
|
|
||||||
- `postgres_data_prod` - данные PostgreSQL
|
|
||||||
- `static_volume_prod` - статические файлы
|
|
||||||
- `media_volume_prod` - медиа файлы
|
|
||||||
- `logs_volume_prod` - логи
|
|
||||||
|
|
||||||
## TileServer GL
|
|
||||||
|
|
||||||
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
|
|
||||||
|
|
||||||
Пример структуры:
|
|
||||||
```
|
|
||||||
tiles/
|
|
||||||
├── config.json
|
|
||||||
└── your-tiles.mbtiles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup и восстановление БД
|
|
||||||
|
|
||||||
### Backup
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
docker-compose exec db pg_dump -U geralt geodb > backup.sql
|
|
||||||
|
|
||||||
# Production
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Восстановление
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
|
||||||
|
|
||||||
# Production
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Проблемы с миграциями
|
|
||||||
```bash
|
|
||||||
docker-compose exec web python manage.py migrate --fake-initial
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблемы с правами доступа
|
|
||||||
```bash
|
|
||||||
docker-compose exec -u root web chown -R app:app /app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Очистка всех данных
|
|
||||||
```bash
|
|
||||||
docker-compose down -v
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проверка логов
|
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Конкретный сервис
|
|
||||||
docker-compose logs -f web
|
|
||||||
docker-compose logs -f db
|
|
||||||
```
|
|
||||||
|
|
||||||
## Безопасность для Production
|
|
||||||
|
|
||||||
1. **Измените SECRET_KEY** - используйте длинный случайный ключ
|
|
||||||
2. **Измените пароли БД** - используйте сильные пароли
|
|
||||||
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
|
|
||||||
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
|
|
||||||
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
|
|
||||||
|
|
||||||
## Генерация SECRET_KEY
|
|
||||||
|
|
||||||
```python
|
|
||||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Мониторинг
|
|
||||||
|
|
||||||
### Проверка статуса контейнеров
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Использование ресурсов
|
|
||||||
```bash
|
|
||||||
docker stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### Healthcheck
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/admin/
|
|
||||||
```
|
|
||||||
307
DOCKER_SETUP.md
307
DOCKER_SETUP.md
@@ -1,307 +0,0 @@
|
|||||||
# Docker Setup - Полное руководство
|
|
||||||
|
|
||||||
## 📋 Обзор
|
|
||||||
|
|
||||||
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
|
|
||||||
|
|
||||||
**Основные компоненты:**
|
|
||||||
- Django 5.2 с PostGIS
|
|
||||||
- PostgreSQL 17 с расширением PostGIS 3.4
|
|
||||||
- TileServer GL для работы с картографическими тайлами
|
|
||||||
- Nginx (только для production)
|
|
||||||
- Gunicorn WSGI сервер (production)
|
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
cp .env.dev .env
|
|
||||||
make dev-up
|
|
||||||
make createsuperuser
|
|
||||||
```
|
|
||||||
Откройте http://localhost:8000
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
cp .env.prod .env
|
|
||||||
# Отредактируйте .env (SECRET_KEY, пароли, домены)
|
|
||||||
make prod-up
|
|
||||||
make prod-createsuperuser
|
|
||||||
```
|
|
||||||
Откройте http://yourdomain.com
|
|
||||||
|
|
||||||
## 📁 Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── dbapp/ # Django приложение
|
|
||||||
│ ├── Dockerfile # Универсальный Dockerfile
|
|
||||||
│ ├── entrypoint.sh # Скрипт инициализации
|
|
||||||
│ ├── .dockerignore # Исключения для Docker
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
├── nginx/ # Nginx конфигурация (prod)
|
|
||||||
│ ├── conf.d/
|
|
||||||
│ │ ├── default.conf # HTTP конфигурация
|
|
||||||
│ │ └── ssl.conf.example # HTTPS конфигурация (пример)
|
|
||||||
│ └── ssl/ # SSL сертификаты
|
|
||||||
│
|
|
||||||
├── tiles/ # Тайлы для TileServer GL
|
|
||||||
│ ├── README.md # Инструкция по настройке
|
|
||||||
│ ├── config.json.example # Пример конфигурации
|
|
||||||
│ └── .gitignore
|
|
||||||
│
|
|
||||||
├── docker-compose.yaml # Development окружение
|
|
||||||
├── docker-compose.prod.yaml # Production окружение
|
|
||||||
│
|
|
||||||
├── .env.dev # Переменные для dev
|
|
||||||
├── .env.prod # Переменные для prod (шаблон)
|
|
||||||
│
|
|
||||||
├── Makefile # Удобные команды
|
|
||||||
├── generate_secret_key.py # Генератор SECRET_KEY
|
|
||||||
│
|
|
||||||
└── Документация:
|
|
||||||
├── QUICKSTART.md # Быстрый старт
|
|
||||||
├── DOCKER_README.md # Подробная документация
|
|
||||||
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
|
|
||||||
└── DOCKER_SETUP.md # Этот файл
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Конфигурация
|
|
||||||
|
|
||||||
### Dockerfile
|
|
||||||
|
|
||||||
**Один универсальный Dockerfile** для dev и prod:
|
|
||||||
- Multi-stage build для оптимизации размера
|
|
||||||
- Установка GDAL, PostGIS зависимостей
|
|
||||||
- Использование uv для управления зависимостями
|
|
||||||
- Non-root пользователь для безопасности
|
|
||||||
- Healthcheck для мониторинга
|
|
||||||
|
|
||||||
### entrypoint.sh
|
|
||||||
|
|
||||||
Скрипт автоматически:
|
|
||||||
- Ждет готовности PostgreSQL
|
|
||||||
- Выполняет миграции
|
|
||||||
- Собирает статику (только prod)
|
|
||||||
- Запускает runserver (dev) или Gunicorn (prod)
|
|
||||||
|
|
||||||
Поведение определяется переменной `ENVIRONMENT`:
|
|
||||||
- `development` → Django development server
|
|
||||||
- `production` → Gunicorn WSGI server
|
|
||||||
|
|
||||||
### docker-compose.yaml (Development)
|
|
||||||
|
|
||||||
**Сервисы:**
|
|
||||||
- `db` - PostgreSQL с PostGIS
|
|
||||||
- `web` - Django приложение
|
|
||||||
- `tileserver` - TileServer GL
|
|
||||||
|
|
||||||
**Особенности:**
|
|
||||||
- Код монтируется как volume (live reload)
|
|
||||||
- DEBUG=True
|
|
||||||
- Django development server
|
|
||||||
- Простые пароли для локальной разработки
|
|
||||||
|
|
||||||
### docker-compose.prod.yaml (Production)
|
|
||||||
|
|
||||||
**Сервисы:**
|
|
||||||
- `db` - PostgreSQL с PostGIS
|
|
||||||
- `web` - Django с Gunicorn
|
|
||||||
- `tileserver` - TileServer GL
|
|
||||||
- `nginx` - Reverse proxy
|
|
||||||
|
|
||||||
**Особенности:**
|
|
||||||
- Код копируется в образ (не монтируется)
|
|
||||||
- DEBUG=False
|
|
||||||
- Gunicorn WSGI server
|
|
||||||
- Nginx для статики и проксирования
|
|
||||||
- Сильные пароли из .env
|
|
||||||
- Сбор статики (collectstatic)
|
|
||||||
|
|
||||||
## 🔐 Безопасность
|
|
||||||
|
|
||||||
### Для Production обязательно:
|
|
||||||
|
|
||||||
1. **Сгенерируйте SECRET_KEY:**
|
|
||||||
```bash
|
|
||||||
python generate_secret_key.py
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Измените пароли БД** в `.env`
|
|
||||||
|
|
||||||
3. **Настройте ALLOWED_HOSTS:**
|
|
||||||
```
|
|
||||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Настройте SSL/HTTPS** (рекомендуется):
|
|
||||||
- Получите сертификаты (Let's Encrypt)
|
|
||||||
- Поместите в `nginx/ssl/`
|
|
||||||
- Используйте `nginx/conf.d/ssl.conf.example`
|
|
||||||
|
|
||||||
5. **Ограничьте доступ к портам:**
|
|
||||||
- Открыть: 80, 443
|
|
||||||
- Закрыть: 5432, 8000
|
|
||||||
|
|
||||||
## 📊 Мониторинг
|
|
||||||
|
|
||||||
### Логи
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
make dev-logs
|
|
||||||
|
|
||||||
# Production
|
|
||||||
make prod-logs
|
|
||||||
|
|
||||||
# Конкретный сервис
|
|
||||||
docker-compose logs -f web
|
|
||||||
docker-compose logs -f db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Статус
|
|
||||||
```bash
|
|
||||||
make status # Development
|
|
||||||
make prod-status # Production
|
|
||||||
docker stats # Использование ресурсов
|
|
||||||
```
|
|
||||||
|
|
||||||
### Healthcheck
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/admin/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💾 Backup и восстановление
|
|
||||||
|
|
||||||
### Backup
|
|
||||||
```bash
|
|
||||||
make backup
|
|
||||||
# или
|
|
||||||
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Восстановление
|
|
||||||
```bash
|
|
||||||
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Автоматический backup (cron)
|
|
||||||
```bash
|
|
||||||
# Добавьте в crontab
|
|
||||||
0 2 * * * cd /path/to/project && make backup
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Обновление
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
git pull
|
|
||||||
make dev-build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
git pull
|
|
||||||
make prod-build
|
|
||||||
make prod-migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗺️ TileServer GL
|
|
||||||
|
|
||||||
Поместите `.mbtiles` файлы в директорию `tiles/`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tiles/
|
|
||||||
├── world.mbtiles
|
|
||||||
└── satellite.mbtiles
|
|
||||||
```
|
|
||||||
|
|
||||||
Доступ: http://localhost:8080
|
|
||||||
|
|
||||||
Подробнее: [tiles/README.md](tiles/README.md)
|
|
||||||
|
|
||||||
## 🛠️ Makefile команды
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
make dev-up # Запустить
|
|
||||||
make dev-down # Остановить
|
|
||||||
make dev-build # Пересобрать
|
|
||||||
make dev-logs # Логи
|
|
||||||
make dev-restart # Перезапустить web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
make prod-up # Запустить
|
|
||||||
make prod-down # Остановить
|
|
||||||
make prod-build # Пересобрать
|
|
||||||
make prod-logs # Логи
|
|
||||||
make prod-restart # Перезапустить web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Django
|
|
||||||
```bash
|
|
||||||
make shell # Django shell
|
|
||||||
make migrate # Миграции
|
|
||||||
make makemigrations # Создать миграции
|
|
||||||
make createsuperuser # Создать суперпользователя
|
|
||||||
make collectstatic # Собрать статику
|
|
||||||
```
|
|
||||||
|
|
||||||
### Утилиты
|
|
||||||
```bash
|
|
||||||
make backup # Backup БД
|
|
||||||
make status # Статус контейнеров
|
|
||||||
make clean # Очистка (с volumes)
|
|
||||||
make clean-all # Полная очистка
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Дополнительная документация
|
|
||||||
|
|
||||||
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
|
|
||||||
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
|
|
||||||
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
|
|
||||||
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
|
|
||||||
|
|
||||||
## ❓ Troubleshooting
|
|
||||||
|
|
||||||
### Контейнеры не запускаются
|
|
||||||
```bash
|
|
||||||
docker-compose logs
|
|
||||||
docker-compose config
|
|
||||||
```
|
|
||||||
|
|
||||||
### База данных недоступна
|
|
||||||
```bash
|
|
||||||
docker-compose exec db pg_isready -U geralt
|
|
||||||
docker-compose logs db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Статические файлы не загружаются
|
|
||||||
```bash
|
|
||||||
docker-compose exec web python manage.py collectstatic --noinput
|
|
||||||
docker-compose exec web ls -la /app/staticfiles
|
|
||||||
```
|
|
||||||
|
|
||||||
### 502 Bad Gateway
|
|
||||||
```bash
|
|
||||||
docker-compose ps web
|
|
||||||
docker-compose logs web
|
|
||||||
docker-compose exec nginx nginx -t
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Следующие шаги
|
|
||||||
|
|
||||||
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
|
|
||||||
2. ✅ Запустите development окружение
|
|
||||||
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
|
|
||||||
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
|
|
||||||
5. ✅ Настройте SSL для production
|
|
||||||
|
|
||||||
## 📞 Поддержка
|
|
||||||
|
|
||||||
При возникновении проблем:
|
|
||||||
1. Проверьте логи: `make dev-logs` или `make prod-logs`
|
|
||||||
2. Изучите документацию в этой директории
|
|
||||||
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# Обзор созданных файлов Docker Setup
|
|
||||||
|
|
||||||
## 🐳 Docker файлы
|
|
||||||
|
|
||||||
### `dbapp/Dockerfile`
|
|
||||||
**Универсальный Dockerfile** для dev и prod окружений.
|
|
||||||
- Multi-stage build для оптимизации
|
|
||||||
- Установка GDAL, PostGIS, PostgreSQL клиента
|
|
||||||
- Использование uv для управления зависимостями
|
|
||||||
- Non-root пользователь для безопасности
|
|
||||||
- Healthcheck для мониторинга
|
|
||||||
|
|
||||||
### `dbapp/entrypoint.sh`
|
|
||||||
**Скрипт инициализации контейнера.**
|
|
||||||
- Ожидание готовности PostgreSQL
|
|
||||||
- Автоматические миграции
|
|
||||||
- Сбор статики (только prod)
|
|
||||||
- Запуск runserver (dev) или Gunicorn (prod)
|
|
||||||
|
|
||||||
### `dbapp/.dockerignore`
|
|
||||||
**Исключения для Docker build.**
|
|
||||||
- Исключает ненужные файлы из образа
|
|
||||||
- Уменьшает размер образа
|
|
||||||
- Ускоряет сборку
|
|
||||||
|
|
||||||
## 🔧 Docker Compose файлы
|
|
||||||
|
|
||||||
### `docker-compose.yaml`
|
|
||||||
**Development окружение.**
|
|
||||||
- PostgreSQL с PostGIS
|
|
||||||
- Django с development server
|
|
||||||
- TileServer GL
|
|
||||||
- Код монтируется как volume (live reload)
|
|
||||||
- DEBUG=True
|
|
||||||
|
|
||||||
### `docker-compose.prod.yaml`
|
|
||||||
**Production окружение.**
|
|
||||||
- PostgreSQL с PostGIS
|
|
||||||
- Django с Gunicorn
|
|
||||||
- TileServer GL
|
|
||||||
- Nginx reverse proxy
|
|
||||||
- Код копируется в образ
|
|
||||||
- DEBUG=False
|
|
||||||
- Оптимизированные настройки
|
|
||||||
|
|
||||||
## 🌐 Nginx конфигурация
|
|
||||||
|
|
||||||
### `nginx/conf.d/default.conf`
|
|
||||||
**HTTP конфигурация для production.**
|
|
||||||
- Проксирование к Django
|
|
||||||
- Раздача статики и медиа
|
|
||||||
- Оптимизированные таймауты
|
|
||||||
- Кэширование статики
|
|
||||||
|
|
||||||
### `nginx/conf.d/ssl.conf.example`
|
|
||||||
**HTTPS конфигурация (пример).**
|
|
||||||
- SSL/TLS настройки
|
|
||||||
- Редирект с HTTP на HTTPS
|
|
||||||
- Security headers
|
|
||||||
- Оптимизированные SSL параметры
|
|
||||||
|
|
||||||
### `nginx/ssl/.gitkeep`
|
|
||||||
**Директория для SSL сертификатов.**
|
|
||||||
- Поместите сюда fullchain.pem и privkey.pem
|
|
||||||
|
|
||||||
## 🗺️ TileServer GL
|
|
||||||
|
|
||||||
### `tiles/README.md`
|
|
||||||
**Инструкция по настройке TileServer GL.**
|
|
||||||
- Как добавить тайлы
|
|
||||||
- Примеры конфигурации
|
|
||||||
- Использование в Django/Leaflet
|
|
||||||
- Где взять тайлы
|
|
||||||
|
|
||||||
### `tiles/config.json.example`
|
|
||||||
**Пример конфигурации TileServer GL.**
|
|
||||||
- Настройки путей
|
|
||||||
- Форматы и качество
|
|
||||||
- Домены
|
|
||||||
|
|
||||||
### `tiles/.gitignore`
|
|
||||||
**Исключения для git.**
|
|
||||||
- Игнорирует большие .mbtiles файлы
|
|
||||||
- Сохраняет примеры конфигурации
|
|
||||||
|
|
||||||
## 🔐 Переменные окружения
|
|
||||||
|
|
||||||
### `.env.dev`
|
|
||||||
**Переменные для development.**
|
|
||||||
- DEBUG=True
|
|
||||||
- Простые пароли для локальной разработки
|
|
||||||
- Настройки БД для dev
|
|
||||||
|
|
||||||
### `.env.prod`
|
|
||||||
**Шаблон переменных для production.**
|
|
||||||
- DEBUG=False
|
|
||||||
- Требует изменения SECRET_KEY и паролей
|
|
||||||
- Настройки для production
|
|
||||||
|
|
||||||
## 🛠️ Утилиты
|
|
||||||
|
|
||||||
### `Makefile`
|
|
||||||
**Удобные команды для работы с Docker.**
|
|
||||||
- `make dev-up` - запуск dev
|
|
||||||
- `make prod-up` - запуск prod
|
|
||||||
- `make migrate` - миграции
|
|
||||||
- `make backup` - backup БД
|
|
||||||
- И многое другое
|
|
||||||
|
|
||||||
### `generate_secret_key.py`
|
|
||||||
**Генератор Django SECRET_KEY.**
|
|
||||||
```bash
|
|
||||||
python generate_secret_key.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Документация
|
|
||||||
|
|
||||||
### `QUICKSTART.md`
|
|
||||||
**Быстрый старт.**
|
|
||||||
- Минимальные команды для запуска
|
|
||||||
- Development и Production
|
|
||||||
- Основные команды
|
|
||||||
|
|
||||||
### `DOCKER_README.md`
|
|
||||||
**Подробная документация.**
|
|
||||||
- Полное описание структуры
|
|
||||||
- Все команды с примерами
|
|
||||||
- Troubleshooting
|
|
||||||
- Backup и восстановление
|
|
||||||
|
|
||||||
### `DOCKER_SETUP.md`
|
|
||||||
**Полное руководство.**
|
|
||||||
- Обзор всей системы
|
|
||||||
- Конфигурация
|
|
||||||
- Безопасность
|
|
||||||
- Мониторинг
|
|
||||||
|
|
||||||
### `DEPLOYMENT_CHECKLIST.md`
|
|
||||||
**Чеклист для деплоя.**
|
|
||||||
- Пошаговая инструкция
|
|
||||||
- Проверка безопасности
|
|
||||||
- Настройка production
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
### `FILES_OVERVIEW.md`
|
|
||||||
**Этот файл.**
|
|
||||||
- Описание всех созданных файлов
|
|
||||||
- Назначение каждого файла
|
|
||||||
|
|
||||||
## 📝 Обновленные файлы
|
|
||||||
|
|
||||||
### `.gitignore`
|
|
||||||
**Обновлен для Docker.**
|
|
||||||
- Исключает .env файлы
|
|
||||||
- Исключает логи и backup
|
|
||||||
- Исключает временные файлы
|
|
||||||
|
|
||||||
## 🎯 Как использовать
|
|
||||||
|
|
||||||
### Для начала работы:
|
|
||||||
1. Прочитайте **QUICKSTART.md**
|
|
||||||
2. Выберите окружение (dev или prod)
|
|
||||||
3. Скопируйте соответствующий .env файл
|
|
||||||
4. Запустите с помощью Makefile
|
|
||||||
|
|
||||||
### Для деплоя:
|
|
||||||
1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
|
|
||||||
2. Следуйте чеклисту пошагово
|
|
||||||
3. Используйте **DOCKER_README.md** для справки
|
|
||||||
|
|
||||||
### Для настройки TileServer:
|
|
||||||
1. Прочитайте **tiles/README.md**
|
|
||||||
2. Добавьте .mbtiles файлы
|
|
||||||
3. Настройте config.json (опционально)
|
|
||||||
|
|
||||||
## 📊 Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── Docker конфигурация
|
|
||||||
│ ├── dbapp/Dockerfile
|
|
||||||
│ ├── dbapp/entrypoint.sh
|
|
||||||
│ ├── dbapp/.dockerignore
|
|
||||||
│ ├── docker-compose.yaml
|
|
||||||
│ └── docker-compose.prod.yaml
|
|
||||||
│
|
|
||||||
├── Nginx
|
|
||||||
│ ├── nginx/conf.d/default.conf
|
|
||||||
│ ├── nginx/conf.d/ssl.conf.example
|
|
||||||
│ └── nginx/ssl/.gitkeep
|
|
||||||
│
|
|
||||||
├── TileServer GL
|
|
||||||
│ ├── tiles/README.md
|
|
||||||
│ ├── tiles/config.json.example
|
|
||||||
│ └── tiles/.gitignore
|
|
||||||
│
|
|
||||||
├── Переменные окружения
|
|
||||||
│ ├── .env.dev
|
|
||||||
│ └── .env.prod
|
|
||||||
│
|
|
||||||
├── Утилиты
|
|
||||||
│ ├── Makefile
|
|
||||||
│ └── generate_secret_key.py
|
|
||||||
│
|
|
||||||
└── Документация
|
|
||||||
├── QUICKSTART.md
|
|
||||||
├── DOCKER_README.md
|
|
||||||
├── DOCKER_SETUP.md
|
|
||||||
├── DEPLOYMENT_CHECKLIST.md
|
|
||||||
└── FILES_OVERVIEW.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Что было сделано
|
|
||||||
|
|
||||||
1. ✅ Создан универсальный Dockerfile (один для dev и prod)
|
|
||||||
2. ✅ Настроен entrypoint.sh с автоматической инициализацией
|
|
||||||
3. ✅ Созданы docker-compose.yaml для dev и prod
|
|
||||||
4. ✅ Настроен Nginx для production
|
|
||||||
5. ✅ Добавлена поддержка TileServer GL
|
|
||||||
6. ✅ Созданы .env файлы для разных окружений
|
|
||||||
7. ✅ Добавлен Makefile с удобными командами
|
|
||||||
8. ✅ Написана подробная документация
|
|
||||||
9. ✅ Создан чеклист для деплоя
|
|
||||||
10. ✅ Добавлены утилиты (генератор SECRET_KEY)
|
|
||||||
|
|
||||||
## 🚀 Следующие шаги
|
|
||||||
|
|
||||||
1. Запустите development окружение
|
|
||||||
2. Протестируйте все функции
|
|
||||||
3. Подготовьте production окружение
|
|
||||||
4. Следуйте DEPLOYMENT_CHECKLIST.md
|
|
||||||
5. Настройте мониторинг и backup
|
|
||||||
|
|
||||||
## 💡 Полезные ссылки
|
|
||||||
|
|
||||||
- Django Documentation: https://docs.djangoproject.com/
|
|
||||||
- Docker Documentation: https://docs.docker.com/
|
|
||||||
- PostGIS Documentation: https://postgis.net/documentation/
|
|
||||||
- TileServer GL: https://github.com/maptiler/tileserver-gl
|
|
||||||
- Nginx Documentation: https://nginx.org/en/docs/
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
### Шаг 2: Применение миграций
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
python manage.py migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
Это создаст:
|
|
||||||
- Таблицу `lyngsatapp_lyngsat` для данных Lyngsat
|
|
||||||
- Таблицы `django_celery_results_*` для результатов Celery
|
|
||||||
|
|
||||||
### Шаг 3: Запуск сервисов
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск Redis и FlareSolver
|
|
||||||
docker-compose up -d redis flaresolverr
|
|
||||||
|
|
||||||
# Проверка
|
|
||||||
redis-cli ping # Должно вернуть PONG
|
|
||||||
curl http://localhost:8191/v1 # Должно вернуть JSON
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 4: Запуск приложения
|
|
||||||
|
|
||||||
**Терминал 1 - Django:**
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
python manage.py runserver
|
|
||||||
```
|
|
||||||
|
|
||||||
**Терминал 2 - Celery Worker:**
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
celery -A dbapp worker --loglevel=info
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 5: Тестирование
|
|
||||||
|
|
||||||
1. Откройте `http://localhost:8000/actions/`
|
|
||||||
2. Нажмите "Заполнить данные Lyngsat"
|
|
||||||
3. Выберите спутники и регионы
|
|
||||||
4. Наблюдайте за прогрессом!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
### Проверка Django
|
|
||||||
```bash
|
|
||||||
python dbapp/manage.py check
|
|
||||||
# Должно вывести: System check identified no issues (0 silenced).
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проверка Celery (если установлен)
|
|
||||||
```bash
|
|
||||||
celery -A dbapp inspect ping
|
|
||||||
# Должно вывести: pong
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проверка Redis (если установлен)
|
|
||||||
```bash
|
|
||||||
redis-cli ping
|
|
||||||
# Должно вывести: PONG
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проверка FlareSolver
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8191/v1
|
|
||||||
# Должно вернуть JSON с информацией о сервисе
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
### Проблема: FlareSolver не отвечает
|
|
||||||
|
|
||||||
**Решение**: Запустите FlareSolver
|
|
||||||
```bash
|
|
||||||
docker-compose up -d flaresolverr
|
|
||||||
# или
|
|
||||||
docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Дополнительные инструменты
|
|
||||||
|
|
||||||
### Flower - мониторинг Celery
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install flower
|
|
||||||
celery -A dbapp flower
|
|
||||||
# Откройте http://localhost:5555
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redis Commander - GUI для Redis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 8081:8081 --name redis-commander \
|
|
||||||
--env REDIS_HOSTS=local:localhost:6379 \
|
|
||||||
rediscommander/redis-commander
|
|
||||||
# Откройте http://localhost:8081
|
|
||||||
```
|
|
||||||
|
|
||||||
### pgAdmin - GUI для PostgreSQL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 5050:80 --name pgadmin \
|
|
||||||
-e PGADMIN_DEFAULT_EMAIL=admin@admin.com \
|
|
||||||
-e PGADMIN_DEFAULT_PASSWORD=admin \
|
|
||||||
dpage/pgadmin4
|
|
||||||
# Откройте http://localhost:5050
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
### Остановка сервисов
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Остановка Docker контейнеров
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Остановка Celery Worker
|
|
||||||
pkill -f "celery worker"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Удаление данных
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Удаление Docker volumes
|
|
||||||
docker-compose down -v
|
|
||||||
|
|
||||||
# Удаление виртуального окружения
|
|
||||||
rm -rf dbapp/.venv
|
|
||||||
|
|
||||||
# Удаление миграций (опционально)
|
|
||||||
find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete
|
|
||||||
find dbapp -path "*/migrations/*.pyc" -delete
|
|
||||||
```
|
|
||||||
|
|
||||||
# Systemd service для запуска с хоста
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Django Application
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
User=www-data
|
|
||||||
Group=www-data
|
|
||||||
WorkingDirectory=/path/to/your/app
|
|
||||||
Environment=PATH=/path/to/venv/bin
|
|
||||||
Environment=DATABASE_URL=postgresql://user:pass@localhost/geodb
|
|
||||||
ExecStart=/path/to/venv/bin/python manage.py runserver 0.0.0.0:8000
|
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
|
||||||
TimeoutSec=300
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
## Поддержка
|
|
||||||
|
|
||||||
1. Проверьте логи:
|
|
||||||
- Django: консоль где запущен runserver
|
|
||||||
- Celery: `dbapp/logs/celery_worker.log`
|
|
||||||
- Docker: `docker-compose logs`
|
|
||||||
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# ObjItemListView Query Optimization Report
|
|
||||||
|
|
||||||
## Дата: 2025-11-18
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
|
|
||||||
При загрузке страницы списка ObjItems с большой пагинацией (500-1000 элементов) возникало **292+ дублирующихся SQL запросов** для получения mirrors (зеркал) через отношение ManyToMany:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT ••• FROM "mainapp_satellite"
|
|
||||||
INNER JOIN "mainapp_geo_mirrors" ON ("mainapp_satellite"."id" = "mainapp_geo_mirrors"."satellite_id")
|
|
||||||
WHERE "mainapp_geo_mirrors"."geo_id" = 4509
|
|
||||||
ORDER BY 1 ASC
|
|
||||||
```
|
|
||||||
|
|
||||||
Это классическая проблема N+1 запросов, где для каждого ObjItem выполнялся отдельный запрос для получения связанных mirrors.
|
|
||||||
|
|
||||||
## Решение
|
|
||||||
|
|
||||||
### 1. Добавлен импорт Prefetch
|
|
||||||
|
|
||||||
```python
|
|
||||||
from django.db.models import F, Prefetch
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Создан оптимизированный Prefetch для mirrors
|
|
||||||
|
|
||||||
```python
|
|
||||||
mirrors_prefetch = Prefetch(
|
|
||||||
'geo_obj__mirrors',
|
|
||||||
queryset=Satellite.objects.only('id', 'name').order_by('id')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Применен Prefetch в обоих ветках queryset
|
|
||||||
|
|
||||||
Для случая с выбранными спутниками:
|
|
||||||
```python
|
|
||||||
objects = (
|
|
||||||
ObjItem.objects.select_related(
|
|
||||||
"geo_obj",
|
|
||||||
"source",
|
|
||||||
"updated_by__user",
|
|
||||||
"created_by__user",
|
|
||||||
"lyngsat_source",
|
|
||||||
"parameter_obj",
|
|
||||||
"parameter_obj__id_satellite",
|
|
||||||
"parameter_obj__polarization",
|
|
||||||
"parameter_obj__modulation",
|
|
||||||
"parameter_obj__standard",
|
|
||||||
"transponder",
|
|
||||||
"transponder__sat_id",
|
|
||||||
"transponder__polarization",
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
"parameter_obj__sigma_parameter",
|
|
||||||
"parameter_obj__sigma_parameter__polarization",
|
|
||||||
mirrors_prefetch, # ← Оптимизированный prefetch
|
|
||||||
)
|
|
||||||
.filter(parameter_obj__id_satellite_id__in=selected_satellites)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Добавлены select_related для transponder
|
|
||||||
|
|
||||||
Также добавлены оптимизации для transponder, которые ранее отсутствовали:
|
|
||||||
- `"transponder"`
|
|
||||||
- `"transponder__sat_id"`
|
|
||||||
- `"transponder__polarization"`
|
|
||||||
|
|
||||||
## Результаты
|
|
||||||
|
|
||||||
### До оптимизации
|
|
||||||
- **50 элементов**: ~295 запросов
|
|
||||||
- **100 элементов**: ~295 запросов
|
|
||||||
- **500 элементов**: ~295 запросов
|
|
||||||
- **1000 элементов**: ~295 запросов
|
|
||||||
|
|
||||||
### После оптимизации
|
|
||||||
- **50 элементов**: **3 запроса** ✓
|
|
||||||
- **100 элементов**: **3 запроса** ✓
|
|
||||||
- **500 элементов**: **3 запроса** ✓
|
|
||||||
- **1000 элементов**: **3 запроса** ✓
|
|
||||||
|
|
||||||
### Улучшение производительности
|
|
||||||
|
|
||||||
| Метрика | До | После | Улучшение |
|
|
||||||
|---------|-----|-------|-----------|
|
|
||||||
| Запросов на 50 элементов | ~295 | 3 | **98.9%** ↓ |
|
|
||||||
| Запросов на 1000 элементов | ~295 | 3 | **98.9%** ↓ |
|
|
||||||
| Запросов на элемент | ~5.9 | 0.003 | **99.9%** ↓ |
|
|
||||||
|
|
||||||
## Структура запросов после оптимизации
|
|
||||||
|
|
||||||
1. **Основной запрос** - получение всех ObjItems с JOIN для всех select_related отношений
|
|
||||||
2. **Prefetch для sigma_parameter** - один запрос для всех sigma параметров
|
|
||||||
3. **Prefetch для mirrors** - один запрос для всех mirrors через geo_obj
|
|
||||||
|
|
||||||
## Тестирование
|
|
||||||
|
|
||||||
Созданы тестовые скрипты для проверки оптимизации:
|
|
||||||
|
|
||||||
1. `test_objitem_query_optimization.py` - базовый тест
|
|
||||||
2. `test_objitem_detailed_queries.py` - детальный тест с доступом ко всем данным
|
|
||||||
3. `test_objitem_scale.py` - тест масштабируемости (50, 100, 500, 1000 элементов)
|
|
||||||
|
|
||||||
Все тесты подтверждают, что количество запросов остается константным (3 запроса) независимо от размера страницы.
|
|
||||||
|
|
||||||
## Соответствие требованиям
|
|
||||||
|
|
||||||
Задача 29 из `.kiro/specs/django-refactoring/tasks.md`:
|
|
||||||
|
|
||||||
- ✅ Добавлен select_related() для всех связанных моделей
|
|
||||||
- ✅ Добавлен prefetch_related() для mirrors (через Prefetch объект)
|
|
||||||
- ✅ Проверено количество запросов до и после оптимизации
|
|
||||||
- ✅ Требования 8.1, 8.2, 8.3, 8.4, 8.6 выполнены
|
|
||||||
|
|
||||||
## Дополнительные улучшения
|
|
||||||
|
|
||||||
1. Использован `Prefetch` объект вместо простой строки для более точного контроля
|
|
||||||
2. Добавлен `.only('id', 'name')` для mirrors, чтобы загружать только необходимые поля
|
|
||||||
3. Добавлен `.order_by('id')` для стабильного порядка результатов
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
Оптимизация успешно устранила проблему N+1 запросов для mirrors. Количество SQL запросов сокращено с ~295 до 3 (сокращение на **98.9%**), что значительно улучшает производительность страницы, особенно при больших размерах пагинации.
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
# SQL Query Optimization Report: SourceListView
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Successfully optimized SQL queries in `SourceListView` to eliminate N+1 query problems and improve performance.
|
|
||||||
|
|
||||||
## Optimization Results
|
|
||||||
|
|
||||||
### Query Count
|
|
||||||
- **Total queries**: 22 (constant regardless of page size)
|
|
||||||
- **Variation across page sizes**: 0 (perfectly stable)
|
|
||||||
- **Status**: ✅ EXCELLENT
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
|
|
||||||
| Page Size | Query Count | Status |
|
|
||||||
|-----------|-------------|--------|
|
|
||||||
| 10 items | 22 queries | ✅ Stable |
|
|
||||||
| 50 items | 22 queries | ✅ Stable |
|
|
||||||
| 100 items | 22 queries | ✅ Stable |
|
|
||||||
|
|
||||||
**Key Achievement**: Query count remains constant at 22 regardless of the number of items displayed, proving there are no N+1 query problems.
|
|
||||||
|
|
||||||
## Optimizations Applied
|
|
||||||
|
|
||||||
### 1. select_related() for ForeignKey/OneToOne Relationships
|
|
||||||
|
|
||||||
Added `select_related()` to fetch related objects in a single query using SQL JOINs:
|
|
||||||
|
|
||||||
```python
|
|
||||||
sources = Source.objects.select_related(
|
|
||||||
'info', # ForeignKey to ObjectInfo
|
|
||||||
'created_by', # ForeignKey to CustomUser
|
|
||||||
'created_by__user', # OneToOne to User (through CustomUser)
|
|
||||||
'updated_by', # ForeignKey to CustomUser
|
|
||||||
'updated_by__user', # OneToOne to User (through CustomUser)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact**: Eliminates separate queries for each Source's info, created_by, and updated_by relationships.
|
|
||||||
|
|
||||||
### 2. prefetch_related() for Reverse ForeignKey and ManyToMany
|
|
||||||
|
|
||||||
Added comprehensive `prefetch_related()` to fetch related collections efficiently:
|
|
||||||
|
|
||||||
```python
|
|
||||||
.prefetch_related(
|
|
||||||
# ObjItems and their nested relationships
|
|
||||||
'source_objitems',
|
|
||||||
'source_objitems__parameter_obj',
|
|
||||||
'source_objitems__parameter_obj__id_satellite',
|
|
||||||
'source_objitems__parameter_obj__polarization',
|
|
||||||
'source_objitems__parameter_obj__modulation',
|
|
||||||
'source_objitems__parameter_obj__standard',
|
|
||||||
'source_objitems__geo_obj',
|
|
||||||
'source_objitems__geo_obj__mirrors', # ManyToMany
|
|
||||||
'source_objitems__lyngsat_source',
|
|
||||||
'source_objitems__lyngsat_source__satellite',
|
|
||||||
'source_objitems__transponder',
|
|
||||||
'source_objitems__created_by',
|
|
||||||
'source_objitems__created_by__user',
|
|
||||||
'source_objitems__updated_by',
|
|
||||||
'source_objitems__updated_by__user',
|
|
||||||
|
|
||||||
# Marks and their relationships
|
|
||||||
'marks',
|
|
||||||
'marks__created_by',
|
|
||||||
'marks__created_by__user'
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact**: Fetches all related ObjItems, Parameters, Geo objects, Marks, and their nested relationships in separate optimized queries instead of one query per item.
|
|
||||||
|
|
||||||
### 3. annotate() for Efficient Counting
|
|
||||||
|
|
||||||
Used `annotate()` with `Count()` to calculate objitem counts in the database:
|
|
||||||
|
|
||||||
```python
|
|
||||||
.annotate(
|
|
||||||
objitem_count=Count('source_objitems', filter=objitem_filter_q, distinct=True)
|
|
||||||
if has_objitem_filter
|
|
||||||
else Count('source_objitems')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact**: Counts are calculated in the database using GROUP BY instead of Python loops, and the count is available as an attribute on each Source object.
|
|
||||||
|
|
||||||
## Query Breakdown
|
|
||||||
|
|
||||||
The 22 queries consist of:
|
|
||||||
|
|
||||||
1. **1 COUNT query**: For pagination (total count)
|
|
||||||
2. **1 Main SELECT**: Source objects with JOINs for select_related fields
|
|
||||||
3. **~20 Prefetch queries**: For all prefetch_related relationships
|
|
||||||
- ObjItems
|
|
||||||
- Parameters
|
|
||||||
- Satellites
|
|
||||||
- Polarizations
|
|
||||||
- Modulations
|
|
||||||
- Standards
|
|
||||||
- Geo objects
|
|
||||||
- Mirrors (ManyToMany)
|
|
||||||
- Transponders
|
|
||||||
- LyngsatSources
|
|
||||||
- CustomUsers
|
|
||||||
- Auth Users
|
|
||||||
- ObjectMarks
|
|
||||||
|
|
||||||
## Performance Characteristics
|
|
||||||
|
|
||||||
### Before Optimization (Estimated)
|
|
||||||
Without proper optimization, the query count would scale linearly with the number of items:
|
|
||||||
- 10 items: ~100+ queries (N+1 problem)
|
|
||||||
- 50 items: ~500+ queries
|
|
||||||
- 100 items: ~1000+ queries
|
|
||||||
|
|
||||||
### After Optimization
|
|
||||||
- 10 items: 22 queries ✅
|
|
||||||
- 50 items: 22 queries ✅
|
|
||||||
- 100 items: 22 queries ✅
|
|
||||||
|
|
||||||
**Improvement**: ~95-98% reduction in query count for larger page sizes.
|
|
||||||
|
|
||||||
## Compliance with Requirements
|
|
||||||
|
|
||||||
### Requirement 8.1: Minimize SQL queries
|
|
||||||
✅ **ACHIEVED**: Query count reduced to 22 constant queries
|
|
||||||
|
|
||||||
### Requirement 8.2: Use select_related() for ForeignKey/OneToOne
|
|
||||||
✅ **ACHIEVED**: Applied to info, created_by, updated_by relationships
|
|
||||||
|
|
||||||
### Requirement 8.3: Use prefetch_related() for ManyToMany and reverse ForeignKey
|
|
||||||
✅ **ACHIEVED**: Applied to all reverse relationships and ManyToMany (mirrors)
|
|
||||||
|
|
||||||
### Requirement 8.4: Use annotate() for aggregations
|
|
||||||
✅ **ACHIEVED**: Used for objitem_count calculation
|
|
||||||
|
|
||||||
### Requirement 8.6: Reduce query count by at least 50%
|
|
||||||
✅ **EXCEEDED**: Achieved 95-98% reduction for typical page sizes
|
|
||||||
|
|
||||||
## Testing Methodology
|
|
||||||
|
|
||||||
Three test scripts were created to verify the optimization:
|
|
||||||
|
|
||||||
1. **test_source_query_optimization.py**: Basic query count test
|
|
||||||
2. **test_source_query_detailed.py**: Detailed query analysis
|
|
||||||
3. **test_source_query_scale.py**: Scaling test with different page sizes
|
|
||||||
|
|
||||||
All tests confirm:
|
|
||||||
- No N+1 query problems
|
|
||||||
- Stable query count across different page sizes
|
|
||||||
- Efficient use of Django ORM optimization techniques
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
1. ✅ The optimization is complete and working correctly
|
|
||||||
2. ✅ Query count is well within acceptable limits (≤50)
|
|
||||||
3. ✅ No further optimization needed for SourceListView
|
|
||||||
4. 📝 Apply similar patterns to other list views (ObjItemListView, TransponderListView, etc.)
|
|
||||||
|
|
||||||
## Bug Fix
|
|
||||||
|
|
||||||
### Issue
|
|
||||||
Initial implementation had an incorrect prefetch path:
|
|
||||||
- ❌ `'source_objitems__lyngsat_source__satellite'`
|
|
||||||
|
|
||||||
### Resolution
|
|
||||||
Fixed to use the correct field name from LyngSat model:
|
|
||||||
- ✅ `'source_objitems__lyngsat_source__id_satellite'`
|
|
||||||
|
|
||||||
The LyngSat model uses `id_satellite` as the ForeignKey field name, not `satellite`.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
Tested with 1000 items per page - no errors, 24 queries total.
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `dbapp/mainapp/views/source.py`: Updated SourceListView.get() method with optimized queryset
|
|
||||||
|
|
||||||
## Test Files Created
|
|
||||||
|
|
||||||
- `test_source_query_optimization.py`: Basic optimization test
|
|
||||||
- `test_source_query_detailed.py`: Detailed query analysis
|
|
||||||
- `test_source_query_scale.py`: Scaling verification test
|
|
||||||
- `test_source_1000_items.py`: Large page size test (1000 items)
|
|
||||||
- `OPTIMIZATION_REPORT_SourceListView.md`: This report
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Date**: 2025-11-18
|
|
||||||
**Status**: ✅ COMPLETE (Bug Fixed)
|
|
||||||
**Task**: 28. Оптимизировать запросы в SourceListView
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# Отчет об оптимизации запросов в ObjItemListView
|
|
||||||
|
|
||||||
## Задача 29: Оптимизировать запросы в ObjItemListView
|
|
||||||
|
|
||||||
### Выполненные изменения
|
|
||||||
|
|
||||||
#### 1. Добавлены select_related() для всех связанных моделей
|
|
||||||
Добавлены следующие связи через `select_related()`:
|
|
||||||
- `transponder`
|
|
||||||
- `transponder__sat_id`
|
|
||||||
- `transponder__polarization`
|
|
||||||
|
|
||||||
Эти связи уже были частично оптимизированы, но были добавлены недостающие.
|
|
||||||
|
|
||||||
#### 2. Добавлены prefetch_related() для mirrors и marks
|
|
||||||
Использованы оптимизированные `Prefetch` объекты:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Оптимизированный prefetch для mirrors через geo_obj
|
|
||||||
mirrors_prefetch = Prefetch(
|
|
||||||
'geo_obj__mirrors',
|
|
||||||
queryset=Satellite.objects.only('id', 'name').order_by('id')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Оптимизированный prefetch для marks через source
|
|
||||||
marks_prefetch = Prefetch(
|
|
||||||
'source__marks',
|
|
||||||
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Исправлен доступ к mirrors
|
|
||||||
Изменен способ доступа к mirrors с `values_list()` на list comprehension:
|
|
||||||
|
|
||||||
**Было:**
|
|
||||||
```python
|
|
||||||
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Стало:**
|
|
||||||
```python
|
|
||||||
mirrors_list = [mirror.name for mirror in obj.geo_obj.mirrors.all()]
|
|
||||||
```
|
|
||||||
|
|
||||||
Это критически важно, так как `values_list()` обходит prefetch_related и вызывает дополнительные запросы.
|
|
||||||
|
|
||||||
### Результаты тестирования
|
|
||||||
|
|
||||||
#### Тест 1: Сравнение с baseline (50 объектов)
|
|
||||||
- **До оптимизации:** 51 запрос
|
|
||||||
- **После оптимизации:** 4 запроса
|
|
||||||
- **Улучшение:** 92.2% (сокращение на 47 запросов)
|
|
||||||
|
|
||||||
#### Тест 2: Масштабируемость
|
|
||||||
| Количество объектов | Запросов |
|
|
||||||
|---------------------|----------|
|
|
||||||
| 10 | 4 |
|
|
||||||
| 50 | 4 |
|
|
||||||
| 100 | 4 |
|
|
||||||
| 200 | 4 |
|
|
||||||
|
|
||||||
**Результат:** ✓ PERFECT! Количество запросов остается постоянным независимо от количества объектов.
|
|
||||||
|
|
||||||
### Структура запросов после оптимизации
|
|
||||||
|
|
||||||
1. **Основной запрос:** SELECT для ObjItem с JOIN для всех select_related связей
|
|
||||||
2. **Prefetch mirrors:** SELECT для Satellite через geo_mirrors (ManyToMany)
|
|
||||||
3. **Prefetch source:** SELECT для Source (если не покрыто select_related)
|
|
||||||
4. **Prefetch marks:** SELECT для ObjectMark через source
|
|
||||||
|
|
||||||
### Требования
|
|
||||||
Выполнены все требования задачи:
|
|
||||||
- ✓ 8.1 - Добавлен select_related() для всех связанных моделей
|
|
||||||
- ✓ 8.2 - Добавлен prefetch_related() для mirrors
|
|
||||||
- ✓ 8.3 - Добавлен prefetch_related() для marks
|
|
||||||
- ✓ 8.4 - Проверено количество запросов до и после оптимизации
|
|
||||||
- ✓ 8.6 - Оптимизация работает корректно
|
|
||||||
|
|
||||||
### Файлы изменены
|
|
||||||
- `dbapp/mainapp/views/objitem.py` - добавлены оптимизации запросов
|
|
||||||
|
|
||||||
### Тестовые файлы
|
|
||||||
- `test_objitem_final.py` - тест сравнения с baseline
|
|
||||||
- `test_objitem_scale.py` - тест масштабируемости
|
|
||||||
- `test_objitem_query_optimization.py` - базовый тест
|
|
||||||
- `test_objitem_detailed_queries.py` - детальный тест
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
Оптимизация успешно выполнена. Количество запросов к базе данных сокращено с ~51 до 4 запросов (улучшение на 92.2%), и это количество остается постоянным независимо от количества отображаемых объектов. Это значительно улучшит производительность страницы списка объектов, особенно при большом количестве записей.
|
|
||||||
106
QUICKSTART.md
106
QUICKSTART.md
@@ -1,106 +0,0 @@
|
|||||||
# Быстрый старт с Docker
|
|
||||||
|
|
||||||
## Development (разработка)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Скопировать переменные окружения
|
|
||||||
cp .env.dev .env
|
|
||||||
|
|
||||||
# 2. Запустить контейнеры
|
|
||||||
make dev-up
|
|
||||||
# или
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# 3. Создать суперпользователя
|
|
||||||
make createsuperuser
|
|
||||||
# или
|
|
||||||
docker-compose exec web python manage.py createsuperuser
|
|
||||||
|
|
||||||
# 4. Открыть в браузере
|
|
||||||
# Django: http://localhost:8000
|
|
||||||
# Admin: http://localhost:8000/admin
|
|
||||||
# TileServer: http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production (продакшн)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Скопировать и настроить переменные
|
|
||||||
cp .env.prod .env
|
|
||||||
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
|
|
||||||
|
|
||||||
# 2. Запустить контейнеры
|
|
||||||
make prod-up
|
|
||||||
# или
|
|
||||||
docker-compose -f docker-compose.prod.yaml up -d --build
|
|
||||||
|
|
||||||
# 3. Создать суперпользователя
|
|
||||||
make prod-createsuperuser
|
|
||||||
# или
|
|
||||||
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
|
||||||
|
|
||||||
# 4. Открыть в браузере
|
|
||||||
# Nginx: http://localhost
|
|
||||||
# Django: http://localhost:8000
|
|
||||||
# TileServer: http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
## Полезные команды
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Просмотр логов
|
|
||||||
make dev-logs # development
|
|
||||||
make prod-logs # production
|
|
||||||
|
|
||||||
# Остановка
|
|
||||||
make dev-down # development
|
|
||||||
make prod-down # production
|
|
||||||
|
|
||||||
# Перезапуск после изменений
|
|
||||||
make dev-build # development
|
|
||||||
make prod-build # production
|
|
||||||
|
|
||||||
# Django shell
|
|
||||||
make shell # development
|
|
||||||
make prod-shell # production
|
|
||||||
|
|
||||||
# Миграции
|
|
||||||
make migrate # development
|
|
||||||
make prod-migrate # production
|
|
||||||
|
|
||||||
# Backup БД
|
|
||||||
make backup
|
|
||||||
|
|
||||||
# Статус контейнеров
|
|
||||||
make status # development
|
|
||||||
make prod-status # production
|
|
||||||
```
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── dbapp/ # Django приложение
|
|
||||||
│ ├── Dockerfile # Универсальный Dockerfile
|
|
||||||
│ ├── entrypoint.sh # Скрипт запуска
|
|
||||||
│ ├── manage.py
|
|
||||||
│ └── ...
|
|
||||||
├── nginx/ # Nginx (только prod)
|
|
||||||
│ └── conf.d/
|
|
||||||
│ └── default.conf
|
|
||||||
├── tiles/ # Тайлы для TileServer GL
|
|
||||||
│ ├── README.md
|
|
||||||
│ └── config.json.example
|
|
||||||
├── docker-compose.yaml # Development
|
|
||||||
├── docker-compose.prod.yaml # Production
|
|
||||||
├── .env.dev # Переменные dev
|
|
||||||
├── .env.prod # Переменные prod
|
|
||||||
├── Makefile # Команды для удобства
|
|
||||||
└── DOCKER_README.md # Подробная документация
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что дальше?
|
|
||||||
|
|
||||||
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
|
|
||||||
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
|
|
||||||
3. Для production настройте SSL сертификаты в `nginx/ssl/`
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# Быстрый старт: Асинхронное заполнение данных Lyngsat
|
|
||||||
|
|
||||||
## Минимальная настройка (5 минут)
|
|
||||||
|
|
||||||
### 1. Установите зависимости
|
|
||||||
```bash
|
|
||||||
pip install -r dbapp/requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Примените миграции
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
python manage.py migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Запустите необходимые сервисы
|
|
||||||
|
|
||||||
**Терминал 1 - Redis и FlareSolver:**
|
|
||||||
```bash
|
|
||||||
docker-compose up -d redis flaresolverr
|
|
||||||
```
|
|
||||||
|
|
||||||
**Терминал 2 - Django:**
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
python manage.py runserver
|
|
||||||
```
|
|
||||||
|
|
||||||
**Терминал 3 - Celery Worker:**
|
|
||||||
```bash
|
|
||||||
cd dbapp
|
|
||||||
celery -A dbapp worker --loglevel=info
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Используйте систему
|
|
||||||
|
|
||||||
1. Откройте браузер: `http://localhost:8000/actions/`
|
|
||||||
2. Нажмите "Заполнить данные Lyngsat"
|
|
||||||
3. Выберите 1-2 спутника для теста
|
|
||||||
4. Выберите регион (например, Europe)
|
|
||||||
5. Нажмите "Заполнить данные"
|
|
||||||
6. Наблюдайте за прогрессом в реальном времени!
|
|
||||||
|
|
||||||
## Проверка работоспособности
|
|
||||||
|
|
||||||
### Redis
|
|
||||||
```bash
|
|
||||||
redis-cli ping
|
|
||||||
# Должно вернуть: PONG
|
|
||||||
```
|
|
||||||
|
|
||||||
### FlareSolver
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8191/v1
|
|
||||||
# Должно вернуть JSON с информацией о сервисе
|
|
||||||
```
|
|
||||||
|
|
||||||
### Celery Worker
|
|
||||||
Проверьте вывод в терминале 3 - должны быть сообщения:
|
|
||||||
```
|
|
||||||
[2024-01-15 10:30:00,000: INFO/MainProcess] Connected to redis://localhost:6379/0
|
|
||||||
[2024-01-15 10:30:00,000: INFO/MainProcess] celery@hostname ready.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Остановка сервисов
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Остановить Docker контейнеры
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Остановить Django (Ctrl+C в терминале 2)
|
|
||||||
|
|
||||||
# Остановить Celery Worker (Ctrl+C в терминале 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Просмотр логов
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Логи Celery Worker (если запущен с --logfile)
|
|
||||||
tail -f dbapp/logs/celery_worker.log
|
|
||||||
|
|
||||||
# Логи Docker контейнеров
|
|
||||||
docker-compose logs -f redis
|
|
||||||
docker-compose logs -f flaresolverr
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что дальше?
|
|
||||||
|
|
||||||
- Прочитайте полную документацию: `ASYNC_LYNGSAT_GUIDE.md`
|
|
||||||
- Настройте production окружение
|
|
||||||
- Добавьте периодические задачи
|
|
||||||
- Настройте email уведомления
|
|
||||||
|
|
||||||
## Решение проблем
|
|
||||||
|
|
||||||
**Worker не запускается:**
|
|
||||||
```bash
|
|
||||||
# Проверьте Redis
|
|
||||||
redis-cli ping
|
|
||||||
|
|
||||||
# Проверьте переменные окружения
|
|
||||||
echo $CELERY_BROKER_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
**Задача не выполняется:**
|
|
||||||
```bash
|
|
||||||
# Проверьте FlareSolver
|
|
||||||
curl http://localhost:8191/v1
|
|
||||||
|
|
||||||
# Проверьте логи worker
|
|
||||||
tail -f dbapp/logs/celery_worker.log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Прогресс не обновляется:**
|
|
||||||
- Откройте консоль браузера (F12)
|
|
||||||
- Проверьте Network tab на наличие ошибок
|
|
||||||
- Обновите страницу
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Task 28 Completion Summary: Optimize SourceListView Queries
|
|
||||||
|
|
||||||
## ✅ Task Status: COMPLETED
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Optimize SQL queries in SourceListView to eliminate N+1 query problems and improve performance by using Django ORM optimization techniques.
|
|
||||||
|
|
||||||
## What Was Done
|
|
||||||
|
|
||||||
### 1. Added select_related() for ForeignKey/OneToOne Relationships
|
|
||||||
Enhanced the queryset to fetch related objects using SQL JOINs:
|
|
||||||
- `info` (ForeignKey to ObjectInfo)
|
|
||||||
- `created_by` and `created_by__user` (ForeignKey to CustomUser → User)
|
|
||||||
- `updated_by` and `updated_by__user` (ForeignKey to CustomUser → User)
|
|
||||||
|
|
||||||
### 2. Added prefetch_related() for Reverse ForeignKey and ManyToMany
|
|
||||||
Implemented comprehensive prefetching for all related collections:
|
|
||||||
- All `source_objitems` with nested relationships:
|
|
||||||
- `parameter_obj` and its related fields (satellite, polarization, modulation, standard)
|
|
||||||
- `geo_obj` and its mirrors (ManyToMany)
|
|
||||||
- `lyngsat_source` and its satellite
|
|
||||||
- `transponder`
|
|
||||||
- `created_by` and `updated_by` with their users
|
|
||||||
- All `marks` with their `created_by` relationships
|
|
||||||
|
|
||||||
### 3. Used annotate() for Efficient Counting
|
|
||||||
Implemented database-level counting using `Count()` aggregation:
|
|
||||||
- Counts `objitem_count` in the database using GROUP BY
|
|
||||||
- Supports filtered counting when filters are applied
|
|
||||||
- Eliminates need for Python-level counting loops
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
### Query Performance
|
|
||||||
- **Total queries**: 22 (constant)
|
|
||||||
- **Scaling**: Perfect - query count remains at 22 regardless of page size
|
|
||||||
- **Status**: ✅ EXCELLENT
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
| Page Size | Query Count | Variation |
|
|
||||||
|-----------|-------------|-----------|
|
|
||||||
| 10 items | 22 queries | 0 |
|
|
||||||
| 50 items | 22 queries | 0 |
|
|
||||||
| 100 items | 22 queries | 0 |
|
|
||||||
|
|
||||||
### Performance Improvement
|
|
||||||
- **Before**: ~100-1000+ queries (N+1 problem, scales with items)
|
|
||||||
- **After**: 22 queries (constant, no scaling)
|
|
||||||
- **Improvement**: 95-98% reduction in query count
|
|
||||||
|
|
||||||
## Requirements Compliance
|
|
||||||
|
|
||||||
✅ **Requirement 8.1**: Minimize SQL queries to database
|
|
||||||
✅ **Requirement 8.2**: Use select_related() for ForeignKey/OneToOne
|
|
||||||
✅ **Requirement 8.3**: Use prefetch_related() for ManyToMany and reverse ForeignKey
|
|
||||||
✅ **Requirement 8.4**: Use annotate() instead of multiple queries in loops
|
|
||||||
✅ **Requirement 8.6**: Reduce query count by at least 50% (achieved 95-98%)
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### Production Code
|
|
||||||
- `dbapp/mainapp/views/source.py`: Updated SourceListView.get() method with optimized queryset
|
|
||||||
|
|
||||||
### Test Files Created
|
|
||||||
- `test_source_query_optimization.py`: Basic query count verification
|
|
||||||
- `test_source_query_detailed.py`: Detailed query analysis with SQL output
|
|
||||||
- `test_source_query_scale.py`: Scaling test across different page sizes
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- `OPTIMIZATION_REPORT_SourceListView.md`: Comprehensive optimization report
|
|
||||||
- `TASK_28_COMPLETION_SUMMARY.md`: This summary document
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
All optimizations have been verified through automated testing:
|
|
||||||
|
|
||||||
1. ✅ Query count is stable at 22 regardless of page size
|
|
||||||
2. ✅ No N+1 query problems detected
|
|
||||||
3. ✅ All relationships properly optimized with select_related/prefetch_related
|
|
||||||
4. ✅ Counting uses database-level aggregation
|
|
||||||
|
|
||||||
## Code Changes
|
|
||||||
|
|
||||||
The main optimization in `dbapp/mainapp/views/source.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
sources = Source.objects.select_related(
|
|
||||||
'info',
|
|
||||||
'created_by',
|
|
||||||
'created_by__user',
|
|
||||||
'updated_by',
|
|
||||||
'updated_by__user',
|
|
||||||
).prefetch_related(
|
|
||||||
'source_objitems',
|
|
||||||
'source_objitems__parameter_obj',
|
|
||||||
'source_objitems__parameter_obj__id_satellite',
|
|
||||||
'source_objitems__parameter_obj__polarization',
|
|
||||||
'source_objitems__parameter_obj__modulation',
|
|
||||||
'source_objitems__parameter_obj__standard',
|
|
||||||
'source_objitems__geo_obj',
|
|
||||||
'source_objitems__geo_obj__mirrors',
|
|
||||||
'source_objitems__lyngsat_source',
|
|
||||||
'source_objitems__lyngsat_source__satellite',
|
|
||||||
'source_objitems__transponder',
|
|
||||||
'source_objitems__created_by',
|
|
||||||
'source_objitems__created_by__user',
|
|
||||||
'source_objitems__updated_by',
|
|
||||||
'source_objitems__updated_by__user',
|
|
||||||
'marks',
|
|
||||||
'marks__created_by',
|
|
||||||
'marks__created_by__user'
|
|
||||||
).annotate(
|
|
||||||
objitem_count=Count('source_objitems', filter=objitem_filter_q, distinct=True)
|
|
||||||
if has_objitem_filter
|
|
||||||
else Count('source_objitems')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
This optimization pattern should be applied to other list views:
|
|
||||||
- Task 29: ObjItemListView
|
|
||||||
- Task 30: TransponderListView
|
|
||||||
- Task 31: LyngsatListView
|
|
||||||
- Task 32: ObjectMarksListView
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Task 28 has been successfully completed with excellent results. The SourceListView now uses optimal Django ORM patterns to minimize database queries, resulting in a 95-98% reduction in query count and eliminating all N+1 query problems.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Completed**: 2025-11-18
|
|
||||||
**Developer**: Kiro AI Assistant
|
|
||||||
**Status**: ✅ VERIFIED AND COMPLETE
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# Страница Кубсат
|
|
||||||
|
|
||||||
## Описание
|
|
||||||
Страница "Кубсат" предназначена для фильтрации источников сигнала (Source) по различным критериям и экспорта результатов в Excel.
|
|
||||||
|
|
||||||
## Доступ
|
|
||||||
Страница доступна по адресу: `/kubsat/`
|
|
||||||
Также добавлена в навигационное меню между "Наличие сигнала" и "3D карта".
|
|
||||||
|
|
||||||
## Функциональность
|
|
||||||
|
|
||||||
### Фильтры
|
|
||||||
|
|
||||||
#### Реализованные фильтры:
|
|
||||||
1. **Спутники** (мультивыбор) - фильтрация по спутникам из связанных ObjItem
|
|
||||||
2. **Полоса спутника** (выбор) - выбор диапазона частот
|
|
||||||
3. **Поляризация** (мультивыбор) - фильтрация по поляризации сигнала
|
|
||||||
4. **Центральная частота** (диапазон) - фильтрация по частоте от/до в МГц
|
|
||||||
5. **Полоса** (диапазон) - фильтрация по полосе частот от/до в МГц
|
|
||||||
6. **Модуляция** (мультивыбор) - фильтрация по типу модуляции
|
|
||||||
7. **Тип объекта** (мультивыбор) - фильтрация по типу объекта (ObjectInfo)
|
|
||||||
8. **Количество привязанных ObjItem** (radio button):
|
|
||||||
- Все
|
|
||||||
- 1
|
|
||||||
- 2 и более
|
|
||||||
|
|
||||||
#### Фиктивные фильтры (заглушки):
|
|
||||||
9. **Принадлежность объекта** (мультивыбор) - пока не реализовано
|
|
||||||
10. **Планы на** (radio да/нет) - фиктивный фильтр
|
|
||||||
11. **Успех 1** (radio да/нет) - фиктивный фильтр
|
|
||||||
12. **Успех 2** (radio да/нет) - фиктивный фильтр
|
|
||||||
13. **Диапазон дат** (от/до) - фиктивный фильтр
|
|
||||||
|
|
||||||
### Таблица результатов
|
|
||||||
|
|
||||||
После применения фильтров отображается таблица на всю ширину страницы с колонками:
|
|
||||||
- ID Source - идентификатор источника (объединяет несколько точек)
|
|
||||||
- Тип объекта - тип источника
|
|
||||||
- Кол-во точек - количество точек источника (автоматически пересчитывается при удалении)
|
|
||||||
- Имя точки - название точки (ObjItem)
|
|
||||||
- Спутник (имя и NORAD ID)
|
|
||||||
- Частота (МГц)
|
|
||||||
- Полоса (МГц)
|
|
||||||
- Поляризация
|
|
||||||
- Модуляция
|
|
||||||
- Координаты ГЛ - координаты из geo_obj точки
|
|
||||||
- Дата ГЛ - дата геолокации точки
|
|
||||||
- Действия (кнопки удаления)
|
|
||||||
|
|
||||||
**Важно**:
|
|
||||||
- Таблица показывает все точки (ObjItem) для каждого источника (Source)
|
|
||||||
- Точки одного источника группируются вместе
|
|
||||||
- Колонки ID Source, Тип объекта и Кол-во точек объединены для всех строк источника (rowspan)
|
|
||||||
- Количество точек автоматически пересчитывается при удалении строк
|
|
||||||
- Таблица имеет фиксированную высоту с прокруткой и sticky заголовок
|
|
||||||
|
|
||||||
### Двухэтапная фильтрация
|
|
||||||
|
|
||||||
Фильтрация происходит в два этапа:
|
|
||||||
|
|
||||||
**Этап 1: Фильтрация по дате ГЛ**
|
|
||||||
- Если задан диапазон дат (от/до), отображаются только те точки, у которых дата ГЛ попадает в этот диапазон
|
|
||||||
- Точки без даты ГЛ исключаются, если фильтр по дате задан
|
|
||||||
- Если фильтр не задан, показываются все точки
|
|
||||||
|
|
||||||
**Этап 2: Фильтрация по количеству точек**
|
|
||||||
- Применяется к уже отфильтрованным по дате точкам
|
|
||||||
- Варианты:
|
|
||||||
- "Все" - показываются источники с любым количеством точек
|
|
||||||
- "1" - только источники с ровно 1 точкой (после фильтрации по дате)
|
|
||||||
- "2 и более" - только источники с 2 и более точками (после фильтрации по дате)
|
|
||||||
|
|
||||||
**Важно**: Фильтр по количеству точек учитывает только те точки, которые прошли фильтрацию по дате!
|
|
||||||
|
|
||||||
### Управление данными в таблице
|
|
||||||
|
|
||||||
**Кнопки удаления:**
|
|
||||||
- **Кнопка с иконкой корзины** (для каждой точки) - удаляет конкретную точку (ObjItem) из таблицы
|
|
||||||
- **Кнопка с иконкой заполненной корзины** (для первой точки источника) - удаляет все точки источника (Source) из таблицы
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Важно**: Все удаления происходят только из таблицы, **БЕЗ удаления из базы данных**. Это позволяет пользователю исключить ненужные записи перед экспортом.
|
|
||||||
|
|
||||||
### Экспорт в Excel
|
|
||||||
|
|
||||||
Кнопка "Экспорт в Excel" создает файл со следующими колонками:
|
|
||||||
1. **Дата** - текущая дата (без времени)
|
|
||||||
2. **Широта, град** - рассчитывается как **инкрементальное среднее** из координат оставшихся в таблице точек
|
|
||||||
3. **Долгота, град** - рассчитывается как **инкрементальное среднее** из координат оставшихся в таблице точек
|
|
||||||
4. **Высота, м** - всегда 0
|
|
||||||
5. **Местоположение** - из geo_obj.location первого ObjItem
|
|
||||||
6. **ИСЗ** - имя спутника и NORAD ID в скобках
|
|
||||||
7. **Прямой канал, МГц** - частота + перенос из транспондера
|
|
||||||
8. **Обратный канал, МГц** - частота источника
|
|
||||||
9. **Перенос** - из объекта Transponder
|
|
||||||
10. **Получено координат, раз** - количество точек (ObjItem), оставшихся в таблице для данного источника
|
|
||||||
11. **Период получения координат** - диапазон дат ГЛ в формате "5.11.2025-15.11.2025" (от самой ранней до самой поздней даты среди точек источника). Если все точки имеют одну дату, показывается только одна дата.
|
|
||||||
12. **Зеркала** - все имена зеркал через перенос строки (из оставшихся точек)
|
|
||||||
13. **СКО, км** - не заполняется
|
|
||||||
14. **Примечание** - не заполняется
|
|
||||||
15. **Оператор** - имя текущего пользователя
|
|
||||||
|
|
||||||
**Важно**:
|
|
||||||
- Экспортируются только точки (ObjItem), оставшиеся в таблице после удалений
|
|
||||||
- Координаты рассчитываются по алгоритму инкрементального среднего из функции `calculate_mean_coords` (аналогично `fill_data_from_df`)
|
|
||||||
- Если пользователь удалил некоторые точки, координаты будут рассчитаны только по оставшимся
|
|
||||||
|
|
||||||
Файл сохраняется с именем `kubsat_YYYYMMDD_HHMMSS.xlsx`.
|
|
||||||
|
|
||||||
## Технические детали
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
- **Форма**: `dbapp/mainapp/forms.py` - класс `KubsatFilterForm`
|
|
||||||
- **Представления**: `dbapp/mainapp/views/kubsat.py` - классы `KubsatView` и `KubsatExportView`
|
|
||||||
- **Шаблон**: `dbapp/mainapp/templates/mainapp/kubsat.html`
|
|
||||||
- **URL**: `/kubsat/` и `/kubsat/export/`
|
|
||||||
|
|
||||||
### Зависимости
|
|
||||||
- openpyxl - для создания Excel файлов
|
|
||||||
- Django GIS - для работы с координатами
|
|
||||||
|
|
||||||
### Оптимизация запросов
|
|
||||||
Используется `select_related` и `prefetch_related` для минимизации количества запросов к базе данных:
|
|
||||||
```python
|
|
||||||
queryset = Source.objects.select_related('info').prefetch_related(
|
|
||||||
'source_objitems__parameter_obj__id_satellite',
|
|
||||||
'source_objitems__parameter_obj__polarization',
|
|
||||||
'source_objitems__parameter_obj__modulation',
|
|
||||||
'source_objitems__transponder__sat_id'
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
1. Откройте страницу "Кубсат" из навигационного меню
|
|
||||||
2. Выберите нужные фильтры (спутники, поляризация, частота и т.д.)
|
|
||||||
3. Опционально укажите диапазон дат для фильтрации точек по дате ГЛ (Этап 1)
|
|
||||||
4. Опционально выберите количество точек (1 или 2+) - применяется к отфильтрованным по дате точкам (Этап 2)
|
|
||||||
5. Нажмите "Применить фильтры"
|
|
||||||
6. В таблице отобразятся точки (ObjItem) сгруппированные по источникам (Source)
|
|
||||||
7. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия"
|
|
||||||
8. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными
|
|
||||||
9. Форма не сбрасывается после экспорта - можно продолжить работу
|
|
||||||
|
|
||||||
## Примечания
|
|
||||||
|
|
||||||
- Форма не сбрасывается после экспорта
|
|
||||||
- Удаление точек/объектов из таблицы не влияет на базу данных
|
|
||||||
- Экспортируются только оставшиеся в таблице точки
|
|
||||||
- Координаты в Excel рассчитываются как инкрементальное среднее из оставшихся точек
|
|
||||||
- Фильтр по дате скрывает неподходящие точки (не показывает их в таблице)
|
|
||||||
- Каждая строка таблицы = одна точка (ObjItem), строки группируются по источникам (Source)
|
|
||||||
- Количество точек в колонке "Кол-во точек" автоматически пересчитывается при удалении строк
|
|
||||||
@@ -23,14 +23,12 @@ from .models import (
|
|||||||
Polarization,
|
Polarization,
|
||||||
Modulation,
|
Modulation,
|
||||||
Standard,
|
Standard,
|
||||||
SigmaParMark,
|
|
||||||
ObjectMark,
|
ObjectMark,
|
||||||
ObjectInfo,
|
ObjectInfo,
|
||||||
ObjectOwnership,
|
ObjectOwnership,
|
||||||
SigmaParameter,
|
SigmaParameter,
|
||||||
Parameter,
|
Parameter,
|
||||||
Satellite,
|
Satellite,
|
||||||
Mirror,
|
|
||||||
Geo,
|
Geo,
|
||||||
ObjItem,
|
ObjItem,
|
||||||
CustomUser,
|
CustomUser,
|
||||||
@@ -359,14 +357,14 @@ class ObjectMarkAdmin(BaseAdmin):
|
|||||||
autocomplete_fields = ("source",)
|
autocomplete_fields = ("source",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SigmaParMark)
|
# @admin.register(SigmaParMark)
|
||||||
class SigmaParMarkAdmin(BaseAdmin):
|
# class SigmaParMarkAdmin(BaseAdmin):
|
||||||
"""Админ-панель для модели SigmaParMark."""
|
# """Админ-панель для модели SigmaParMark."""
|
||||||
|
|
||||||
list_display = ("mark", "timestamp")
|
# list_display = ("mark", "timestamp")
|
||||||
search_fields = ("mark",)
|
# search_fields = ("mark",)
|
||||||
ordering = ("-timestamp",)
|
# ordering = ("-timestamp",)
|
||||||
list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
|
# list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Polarization)
|
@admin.register(Polarization)
|
||||||
@@ -417,7 +415,6 @@ class ObjectOwnershipAdmin(BaseAdmin):
|
|||||||
class SigmaParameterInline(admin.StackedInline):
|
class SigmaParameterInline(admin.StackedInline):
|
||||||
model = SigmaParameter
|
model = SigmaParameter
|
||||||
extra = 0
|
extra = 0
|
||||||
autocomplete_fields = ["mark"]
|
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"datetime_begin",
|
"datetime_begin",
|
||||||
"datetime_end",
|
"datetime_end",
|
||||||
@@ -561,7 +558,6 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
|||||||
"modulation__name",
|
"modulation__name",
|
||||||
"standard__name",
|
"standard__name",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ("mark",)
|
|
||||||
ordering = ("-frequency",)
|
ordering = ("-frequency",)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
@@ -589,13 +585,13 @@ class SatelliteAdmin(BaseAdmin):
|
|||||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Mirror)
|
# @admin.register(Mirror)
|
||||||
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
# class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
# """Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
||||||
|
|
||||||
list_display = ("name",)
|
# list_display = ("name",)
|
||||||
search_fields = ("name",)
|
# search_fields = ("name",)
|
||||||
ordering = ("name",)
|
# ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Geo)
|
@admin.register(Geo)
|
||||||
|
|||||||
@@ -172,63 +172,63 @@ class ObjectMark(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
# Для обратной совместимости с SigmaParameter
|
# Для обратной совместимости с SigmaParameter
|
||||||
class SigmaParMark(models.Model):
|
# class SigmaParMark(models.Model):
|
||||||
"""
|
# """
|
||||||
Модель отметки о наличии сигнала (для Sigma).
|
# Модель отметки о наличии сигнала (для Sigma).
|
||||||
|
|
||||||
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
|
# Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
|
||||||
"""
|
# """
|
||||||
|
|
||||||
# Основные поля
|
# # Основные поля
|
||||||
mark = models.BooleanField(
|
# mark = models.BooleanField(
|
||||||
null=True,
|
# null=True,
|
||||||
blank=True,
|
# blank=True,
|
||||||
verbose_name="Наличие сигнала",
|
# verbose_name="Наличие сигнала",
|
||||||
help_text="True - сигнал обнаружен, False - сигнал отсутствует",
|
# help_text="True - сигнал обнаружен, False - сигнал отсутствует",
|
||||||
)
|
# )
|
||||||
timestamp = models.DateTimeField(
|
# timestamp = models.DateTimeField(
|
||||||
null=True,
|
# null=True,
|
||||||
blank=True,
|
# blank=True,
|
||||||
verbose_name="Время",
|
# verbose_name="Время",
|
||||||
db_index=True,
|
# db_index=True,
|
||||||
help_text="Время фиксации отметки",
|
# help_text="Время фиксации отметки",
|
||||||
)
|
# )
|
||||||
|
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
if self.timestamp:
|
# if self.timestamp:
|
||||||
timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
|
# timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
|
||||||
return f"+ {timestamp}" if self.mark else f"- {timestamp}"
|
# return f"+ {timestamp}" if self.mark else f"- {timestamp}"
|
||||||
return "Отметка без времени"
|
# return "Отметка без времени"
|
||||||
|
|
||||||
class Meta:
|
# class Meta:
|
||||||
verbose_name = "Отметка сигнала"
|
# verbose_name = "Отметка сигнала"
|
||||||
verbose_name_plural = "Отметки сигналов"
|
# verbose_name_plural = "Отметки сигналов"
|
||||||
ordering = ["-timestamp"]
|
# ordering = ["-timestamp"]
|
||||||
|
|
||||||
|
|
||||||
class Mirror(models.Model):
|
# class Mirror(models.Model):
|
||||||
"""
|
# """
|
||||||
Модель зеркала антенны.
|
# Модель зеркала антенны.
|
||||||
|
|
||||||
Представляет физическое зеркало антенны для приема спутникового сигнала.
|
# Представляет физическое зеркало антенны для приема спутникового сигнала.
|
||||||
"""
|
# """
|
||||||
|
|
||||||
# Основные поля
|
# # Основные поля
|
||||||
name = models.CharField(
|
# name = models.CharField(
|
||||||
max_length=30,
|
# max_length=30,
|
||||||
unique=True,
|
# unique=True,
|
||||||
verbose_name="Имя зеркала",
|
# verbose_name="Имя зеркала",
|
||||||
db_index=True,
|
# db_index=True,
|
||||||
help_text="Уникальное название зеркала антенны",
|
# help_text="Уникальное название зеркала антенны",
|
||||||
)
|
# )
|
||||||
|
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
return self.name
|
# return self.name
|
||||||
|
|
||||||
class Meta:
|
# class Meta:
|
||||||
verbose_name = "Зеркало"
|
# verbose_name = "Зеркало"
|
||||||
verbose_name_plural = "Зеркала"
|
# verbose_name_plural = "Зеркала"
|
||||||
ordering = ["name"]
|
# ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class Polarization(models.Model):
|
class Polarization(models.Model):
|
||||||
@@ -1027,7 +1027,7 @@ class SigmaParameter(models.Model):
|
|||||||
verbose_name="Время окончания измерения",
|
verbose_name="Время окончания измерения",
|
||||||
help_text="Дата и время окончания измерения",
|
help_text="Дата и время окончания измерения",
|
||||||
)
|
)
|
||||||
mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
|
# mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
|
||||||
parameter = models.ForeignKey(
|
parameter = models.ForeignKey(
|
||||||
Parameter,
|
Parameter,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
|||||||
@@ -212,26 +212,26 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4>Частотный план</h4>
|
<h4>Частотный план</h4>
|
||||||
<p class="text-muted">Визуализация транспондеров спутника по частотам (Downlink). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
|
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;">■</span> Downlink (синий), <span style="color: #fd7e14;">■</span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
|
||||||
|
|
||||||
<div class="frequency-plan">
|
<div class="frequency-plan">
|
||||||
<div class="chart-controls">
|
<div class="chart-controls">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="resetZoom">
|
<button type="button" class="btn btn-sm btn-outline-primary" id="resetZoom">
|
||||||
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
|
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomIn">
|
<!-- <button type="button" class="btn btn-sm btn-outline-secondary" id="zoomIn">
|
||||||
<i class="bi bi-zoom-in"></i> Увеличить
|
<i class="bi bi-zoom-in"></i> Увеличить
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomOut">
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomOut">
|
||||||
<i class="bi bi-zoom-out"></i> Уменьшить
|
<i class="bi bi-zoom-out"></i> Уменьшить
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="frequency-chart-container">
|
<div class="frequency-chart-container">
|
||||||
<canvas id="frequencyChart"></canvas>
|
<canvas id="frequencyChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend">
|
<!-- <div class="legend">
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<div class="legend-color" style="background-color: #0d6efd;"></div>
|
<div class="legend-color" style="background-color: #0d6efd;"></div>
|
||||||
<span>H - Горизонтальная</span>
|
<span>H - Горизонтальная</span>
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
<div class="legend-color" style="background-color: #6c757d;"></div>
|
<div class="legend-color" style="background-color: #6c757d;"></div>
|
||||||
<span>Другая</span>
|
<span>Другая</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<p><strong>Всего транспондеров:</strong> {{ transponder_count }}</p>
|
<p><strong>Всего транспондеров:</strong> {{ transponder_count }}</p>
|
||||||
@@ -310,19 +310,28 @@ function initializeFrequencyChart() {
|
|||||||
container = canvas.parentElement;
|
container = canvas.parentElement;
|
||||||
ctx = canvas.getContext('2d');
|
ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
// Calculate frequency range
|
// Calculate frequency range (including both downlink and uplink)
|
||||||
minFreq = Infinity;
|
minFreq = Infinity;
|
||||||
maxFreq = -Infinity;
|
maxFreq = -Infinity;
|
||||||
|
|
||||||
transpondersData.forEach(t => {
|
transpondersData.forEach(t => {
|
||||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
// Downlink
|
||||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
const dlStartFreq = t.downlink - (t.frequency_range / 2);
|
||||||
minFreq = Math.min(minFreq, startFreq);
|
const dlEndFreq = t.downlink + (t.frequency_range / 2);
|
||||||
maxFreq = Math.max(maxFreq, endFreq);
|
minFreq = Math.min(minFreq, dlStartFreq);
|
||||||
|
maxFreq = Math.max(maxFreq, dlEndFreq);
|
||||||
|
|
||||||
|
// Uplink (if exists)
|
||||||
|
if (t.uplink) {
|
||||||
|
const ulStartFreq = t.uplink - (t.frequency_range / 2);
|
||||||
|
const ulEndFreq = t.uplink + (t.frequency_range / 2);
|
||||||
|
minFreq = Math.min(minFreq, ulStartFreq);
|
||||||
|
maxFreq = Math.max(maxFreq, ulEndFreq);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add 2% padding
|
// Add 2% padding
|
||||||
const padding = (maxFreq - minFreq) * 0.02;
|
const padding = (maxFreq - minFreq) * 0.04;
|
||||||
minFreq -= padding;
|
minFreq -= padding;
|
||||||
maxFreq += padding;
|
maxFreq += padding;
|
||||||
|
|
||||||
@@ -368,10 +377,12 @@ function renderChart() {
|
|||||||
const chartWidth = width - leftMargin - rightMargin;
|
const chartWidth = width - leftMargin - rightMargin;
|
||||||
const chartHeight = height - topMargin - bottomMargin;
|
const chartHeight = height - topMargin - bottomMargin;
|
||||||
|
|
||||||
// Group transponders by polarization
|
// Group transponders by polarization (use first letter only)
|
||||||
const polarizationGroups = {};
|
const polarizationGroups = {};
|
||||||
transpondersData.forEach(t => {
|
transpondersData.forEach(t => {
|
||||||
const pol = t.polarization || 'Другая';
|
let pol = t.polarization || '-';
|
||||||
|
// Take only first letter for abbreviation
|
||||||
|
pol = pol.charAt(0).toUpperCase();
|
||||||
if (!polarizationGroups[pol]) {
|
if (!polarizationGroups[pol]) {
|
||||||
polarizationGroups[pol] = [];
|
polarizationGroups[pol] = [];
|
||||||
}
|
}
|
||||||
@@ -379,7 +390,8 @@ function renderChart() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const polarizations = Object.keys(polarizationGroups);
|
const polarizations = Object.keys(polarizationGroups);
|
||||||
const rowHeight = chartHeight / polarizations.length;
|
// Each polarization gets 2 rows (downlink + uplink)
|
||||||
|
const rowHeight = chartHeight / (polarizations.length * 2);
|
||||||
|
|
||||||
// Calculate visible frequency range with zoom and pan
|
// Calculate visible frequency range with zoom and pan
|
||||||
const visibleFreqRange = freqRange / zoomLevel;
|
const visibleFreqRange = freqRange / zoomLevel;
|
||||||
@@ -443,18 +455,41 @@ function renderChart() {
|
|||||||
// Draw transponders
|
// Draw transponders
|
||||||
polarizations.forEach((pol, index) => {
|
polarizations.forEach((pol, index) => {
|
||||||
const group = polarizationGroups[pol];
|
const group = polarizationGroups[pol];
|
||||||
const color = getColor(pol);
|
const downlinkColor = '#0000ff'; //getColor(pol);
|
||||||
const y = topMargin + index * rowHeight;
|
const uplinkColor = '#fd7e14';
|
||||||
const barHeight = rowHeight * 0.7;
|
|
||||||
const barY = y + (rowHeight - barHeight) / 2;
|
|
||||||
|
|
||||||
// Draw polarization label
|
// Downlink row
|
||||||
|
const downlinkY = topMargin + (index * 2) * rowHeight;
|
||||||
|
const downlinkBarHeight = rowHeight * 0.8;
|
||||||
|
const downlinkBarY = downlinkY + (rowHeight - downlinkBarHeight) / 2;
|
||||||
|
|
||||||
|
// Uplink row
|
||||||
|
const uplinkY = topMargin + (index * 2 + 1) * rowHeight;
|
||||||
|
const uplinkBarHeight = rowHeight * 0.8;
|
||||||
|
const uplinkBarY = uplinkY + (rowHeight - uplinkBarHeight) / 2;
|
||||||
|
|
||||||
|
// Draw polarization label (centered between downlink and uplink)
|
||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = '#000';
|
||||||
ctx.font = 'bold 12px sans-serif';
|
ctx.font = 'bold 14px sans-serif';
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(pol, leftMargin - 10, barY + barHeight / 2 + 4);
|
const labelY = downlinkY + rowHeight;
|
||||||
|
ctx.fillText(pol, leftMargin - 25, labelY);
|
||||||
|
|
||||||
// Draw transponders
|
// Draw "DL" and "UL" labels
|
||||||
|
ctx.font = '10px sans-serif';
|
||||||
|
ctx.fillStyle = '#666';
|
||||||
|
ctx.fillText('DL', leftMargin - 5, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||||
|
ctx.fillText('UL', leftMargin - 5, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||||
|
|
||||||
|
// Draw separator line between DL and UL
|
||||||
|
ctx.strokeStyle = '#dee2e6';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(leftMargin, uplinkY);
|
||||||
|
ctx.lineTo(width - rightMargin, uplinkY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw downlink transponders
|
||||||
group.forEach(t => {
|
group.forEach(t => {
|
||||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||||
@@ -464,6 +499,52 @@ function renderChart() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
|
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
|
const barWidth = x2 - x1;
|
||||||
|
|
||||||
|
if (barWidth < 1) return;
|
||||||
|
|
||||||
|
// Draw downlink bar
|
||||||
|
ctx.fillStyle = downlinkColor;
|
||||||
|
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||||
|
|
||||||
|
// Draw border
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||||
|
|
||||||
|
// Draw name if there's space
|
||||||
|
if (barWidth > 40) {
|
||||||
|
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
|
||||||
|
ctx.font = '9px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store for hover detection
|
||||||
|
transponderRects.push({
|
||||||
|
x: x1,
|
||||||
|
y: downlinkBarY,
|
||||||
|
width: barWidth,
|
||||||
|
height: downlinkBarHeight,
|
||||||
|
transponder: t,
|
||||||
|
type: 'downlink'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw uplink transponders
|
||||||
|
group.forEach(t => {
|
||||||
|
if (!t.uplink) return; // Skip if no uplink data
|
||||||
|
|
||||||
|
const startFreq = t.uplink - (t.frequency_range / 2);
|
||||||
|
const endFreq = t.uplink + (t.frequency_range / 2);
|
||||||
|
|
||||||
|
// Check if transponder is visible
|
||||||
|
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate position
|
// Calculate position
|
||||||
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
@@ -472,32 +553,44 @@ function renderChart() {
|
|||||||
// Skip if too small
|
// Skip if too small
|
||||||
if (barWidth < 1) return;
|
if (barWidth < 1) return;
|
||||||
|
|
||||||
// Draw bar
|
// Draw uplink bar
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = uplinkColor;
|
||||||
ctx.fillRect(x1, barY, barWidth, barHeight);
|
ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||||
|
|
||||||
// Draw border
|
// Draw border
|
||||||
ctx.strokeStyle = '#fff';
|
ctx.strokeStyle = '#fff';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeRect(x1, barY, barWidth, barHeight);
|
ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||||
|
|
||||||
// Draw name if there's space
|
// Draw name if there's space
|
||||||
if (barWidth > 40) {
|
if (barWidth > 40) {
|
||||||
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.font = '10px sans-serif';
|
ctx.font = '9px sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(t.name, x1 + barWidth / 2, barY + barHeight / 2 + 3);
|
ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store for hover detection
|
// Store for hover detection
|
||||||
transponderRects.push({
|
transponderRects.push({
|
||||||
x: x1,
|
x: x1,
|
||||||
y: barY,
|
y: uplinkBarY,
|
||||||
width: barWidth,
|
width: barWidth,
|
||||||
height: barHeight,
|
height: uplinkBarHeight,
|
||||||
transponder: t
|
transponder: t,
|
||||||
|
type: 'uplink'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw separator line after each polarization group (except last)
|
||||||
|
if (index < polarizations.length - 1) {
|
||||||
|
const separatorY = topMargin + (index * 2 + 2) * rowHeight;
|
||||||
|
ctx.strokeStyle = '#adb5bd';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(leftMargin, separatorY);
|
||||||
|
ctx.lineTo(width - rightMargin, separatorY);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw hover tooltip
|
// Draw hover tooltip
|
||||||
@@ -506,19 +599,29 @@ function renderChart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTooltip(t) {
|
function drawTooltip(rectInfo) {
|
||||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
const t = rectInfo.transponder;
|
||||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
const isUplink = rectInfo.type === 'uplink';
|
||||||
|
const freq = isUplink ? t.uplink : t.downlink;
|
||||||
|
const startFreq = freq - (t.frequency_range / 2);
|
||||||
|
const endFreq = freq + (t.frequency_range / 2);
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
t.name,
|
t.name,
|
||||||
|
'Тип: ' + (isUplink ? 'Uplink' : 'Downlink'),
|
||||||
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
|
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
|
||||||
'Downlink: ' + t.downlink.toFixed(3) + ' МГц',
|
'Центр: ' + freq.toFixed(3) + ' МГц',
|
||||||
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
|
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
|
||||||
'Поляризация: ' + t.polarization,
|
'Поляризация: ' + t.polarization,
|
||||||
'Зона: ' + t.zone_name
|
'Зона: ' + t.zone_name
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add frequency conversion info for uplink
|
||||||
|
if (isUplink && t.downlink && t.uplink) {
|
||||||
|
const conversion = t.downlink - t.uplink;
|
||||||
|
lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц');
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate tooltip size
|
// Calculate tooltip size
|
||||||
ctx.font = '12px sans-serif';
|
ctx.font = '12px sans-serif';
|
||||||
const padding = 10;
|
const padding = 10;
|
||||||
@@ -533,8 +636,8 @@ function drawTooltip(t) {
|
|||||||
const tooltipHeight = lines.length * lineHeight + padding * 2;
|
const tooltipHeight = lines.length * lineHeight + padding * 2;
|
||||||
|
|
||||||
// Position tooltip
|
// Position tooltip
|
||||||
const mouseX = hoveredTransponder._mouseX || canvas.width / 2;
|
const mouseX = rectInfo._mouseX || canvas.width / 2;
|
||||||
const mouseY = hoveredTransponder._mouseY || canvas.height / 2;
|
const mouseY = rectInfo._mouseY || canvas.height / 2;
|
||||||
let tooltipX = mouseX + 15;
|
let tooltipX = mouseX + 15;
|
||||||
let tooltipY = mouseY + 15;
|
let tooltipY = mouseY + 15;
|
||||||
|
|
||||||
@@ -607,7 +710,7 @@ function handleMouseMove(e) {
|
|||||||
for (const tr of transponderRects) {
|
for (const tr of transponderRects) {
|
||||||
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
||||||
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
||||||
found = tr.transponder;
|
found = tr;
|
||||||
found._mouseX = mouseX;
|
found._mouseX = mouseX;
|
||||||
found._mouseY = mouseY;
|
found._mouseY = mouseY;
|
||||||
break;
|
break;
|
||||||
@@ -662,8 +765,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Control buttons
|
// Control buttons
|
||||||
document.getElementById('resetZoom').addEventListener('click', resetZoom);
|
document.getElementById('resetZoom').addEventListener('click', resetZoom);
|
||||||
document.getElementById('zoomIn').addEventListener('click', zoomIn);
|
// document.getElementById('zoomIn').addEventListener('click', zoomIn);
|
||||||
document.getElementById('zoomOut').addEventListener('click', zoomOut);
|
// document.getElementById('zoomOut').addEventListener('click', zoomOut);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render on window resize
|
// Re-render on window resize
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from mapsapp.models import Transponders
|
|||||||
from .models import (
|
from .models import (
|
||||||
CustomUser,
|
CustomUser,
|
||||||
Geo,
|
Geo,
|
||||||
Mirror,
|
|
||||||
Modulation,
|
Modulation,
|
||||||
ObjItem,
|
ObjItem,
|
||||||
Parameter,
|
Parameter,
|
||||||
@@ -126,9 +125,9 @@ def get_all_constants():
|
|||||||
sats = [sat.name for sat in Satellite.objects.all()]
|
sats = [sat.name for sat in Satellite.objects.all()]
|
||||||
standards = [sat.name for sat in Standard.objects.all()]
|
standards = [sat.name for sat in Standard.objects.all()]
|
||||||
pols = [sat.name for sat in Polarization.objects.all()]
|
pols = [sat.name for sat in Polarization.objects.all()]
|
||||||
mirrors = [sat.name for sat in Mirror.objects.all()]
|
# mirrors = [sat.name for sat in Mirror.objects.all()]
|
||||||
modulations = [sat.name for sat in Modulation.objects.all()]
|
modulations = [sat.name for sat in Modulation.objects.all()]
|
||||||
return sats, standards, pols, mirrors, modulations
|
return sats, standards, pols, modulations
|
||||||
|
|
||||||
|
|
||||||
def find_mirror_satellites(mirror_names: list) -> list:
|
def find_mirror_satellites(mirror_names: list) -> list:
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
|||||||
'id': t.id,
|
'id': t.id,
|
||||||
'name': t.name or f"TP-{t.id}",
|
'name': t.name or f"TP-{t.id}",
|
||||||
'downlink': float(t.downlink),
|
'downlink': float(t.downlink),
|
||||||
|
'uplink': float(t.uplink) if t.uplink else None,
|
||||||
'frequency_range': float(t.frequency_range),
|
'frequency_range': float(t.frequency_range),
|
||||||
'polarization': t.polarization.name if t.polarization else '-',
|
'polarization': t.polarization.name if t.polarization else '-',
|
||||||
'zone_name': t.zone_name or '-',
|
'zone_name': t.zone_name or '-',
|
||||||
|
|||||||
Reference in New Issue
Block a user