Рефакторинг и деплоинг
This commit is contained in:
23
.env.dev
Normal file
23
.env.dev
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Development Environment Variables
|
||||||
|
|
||||||
|
# Django Settings
|
||||||
|
DEBUG=True
|
||||||
|
ENVIRONMENT=development
|
||||||
|
DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||||
|
SECRET_KEY=django-insecure-dev-key-only-for-development
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||||
|
DB_NAME=geodb
|
||||||
|
DB_USER=geralt
|
||||||
|
DB_PASSWORD=123456
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Allowed Hosts
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||||
|
|
||||||
|
# PostgreSQL Configuration
|
||||||
|
POSTGRES_DB=geodb
|
||||||
|
POSTGRES_USER=geralt
|
||||||
|
POSTGRES_PASSWORD=123456
|
||||||
28
.env.prod
Normal file
28
.env.prod
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Production Environment Variables
|
||||||
|
# ВАЖНО: Измените все значения перед деплоем!
|
||||||
|
|
||||||
|
# Django Settings
|
||||||
|
DEBUG=False
|
||||||
|
ENVIRONMENT=production
|
||||||
|
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||||
|
SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||||
|
DB_NAME=geodb
|
||||||
|
DB_USER=geralt
|
||||||
|
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Allowed Hosts (comma-separated)
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||||
|
|
||||||
|
# PostgreSQL Configuration
|
||||||
|
POSTGRES_DB=geodb
|
||||||
|
POSTGRES_USER=geralt
|
||||||
|
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
||||||
|
|
||||||
|
# Gunicorn Configuration
|
||||||
|
GUNICORN_WORKERS=3
|
||||||
|
GUNICORN_TIMEOUT=120
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -11,10 +11,25 @@ wheels/
|
|||||||
.hintrc
|
.hintrc
|
||||||
.vscode
|
.vscode
|
||||||
data.json
|
data.json
|
||||||
|
|
||||||
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Django
|
||||||
|
*.log
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
staticfiles/
|
||||||
|
media/
|
||||||
|
|
||||||
django-leaflet
|
django-leaflet
|
||||||
admin-interface
|
admin-interface
|
||||||
|
Тестовые
|
||||||
|
tiles
|
||||||
|
.kiro
|
||||||
|
|
||||||
docker-*
|
# Docker
|
||||||
|
# docker-*
|
||||||
maplibre-gl-js-5.10.0.zip
|
maplibre-gl-js-5.10.0.zip
|
||||||
249
DEPLOYMENT_CHECKLIST.md
Normal file
249
DEPLOYMENT_CHECKLIST.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Чеклист для деплоя в Production
|
||||||
|
|
||||||
|
## Перед деплоем
|
||||||
|
|
||||||
|
### 1. Безопасность
|
||||||
|
|
||||||
|
- [ ] Сгенерирован новый `SECRET_KEY`
|
||||||
|
```bash
|
||||||
|
python generate_secret_key.py
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Изменены все пароли в `.env`:
|
||||||
|
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
|
||||||
|
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
|
||||||
|
|
||||||
|
- [ ] Настроен `ALLOWED_HOSTS` в `.env`:
|
||||||
|
```
|
||||||
|
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] `DEBUG=False` в `.env`
|
||||||
|
|
||||||
|
### 2. База данных
|
||||||
|
|
||||||
|
- [ ] Проверены все миграции:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Настроен backup БД (cron job):
|
||||||
|
```bash
|
||||||
|
0 2 * * * cd /path/to/project && make backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Статические файлы
|
||||||
|
|
||||||
|
- [ ] Проверена директория для статики:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. SSL/HTTPS (опционально, но рекомендуется)
|
||||||
|
|
||||||
|
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
|
||||||
|
- [ ] Сертификаты размещены в `nginx/ssl/`
|
||||||
|
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
|
||||||
|
- [ ] Обновлен `server_name` в `ssl.conf`
|
||||||
|
|
||||||
|
### 5. Nginx
|
||||||
|
|
||||||
|
- [ ] Проверена конфигурация Nginx:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
|
||||||
|
|
||||||
|
### 6. Docker
|
||||||
|
|
||||||
|
- [ ] Проверен `.dockerignore` - исключены ненужные файлы
|
||||||
|
- [ ] Проверен `.gitignore` - не коммитятся секреты
|
||||||
|
|
||||||
|
### 7. Переменные окружения
|
||||||
|
|
||||||
|
Проверьте `.env` файл:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Django
|
||||||
|
DEBUG=False
|
||||||
|
ENVIRONMENT=production
|
||||||
|
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||||
|
SECRET_KEY=<ваш-длинный-секретный-ключ>
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_NAME=geodb
|
||||||
|
DB_USER=geralt
|
||||||
|
DB_PASSWORD=<сильный-пароль>
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Allowed Hosts
|
||||||
|
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_DB=geodb
|
||||||
|
POSTGRES_USER=geralt
|
||||||
|
POSTGRES_PASSWORD=<тот-же-сильный-пароль>
|
||||||
|
|
||||||
|
# Gunicorn
|
||||||
|
GUNICORN_WORKERS=3
|
||||||
|
GUNICORN_TIMEOUT=120
|
||||||
|
```
|
||||||
|
|
||||||
|
## Деплой
|
||||||
|
|
||||||
|
### 1. Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd <project-directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.prod .env
|
||||||
|
nano .env # Отредактируйте все необходимые переменные
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запуск контейнеров
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка статуса
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml ps
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Создание суперпользователя
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Проверка работоспособности
|
||||||
|
|
||||||
|
- [ ] Открыть http://yourdomain.com
|
||||||
|
- [ ] Открыть http://yourdomain.com/admin
|
||||||
|
- [ ] Проверить статические файлы
|
||||||
|
- [ ] Проверить медиа файлы
|
||||||
|
- [ ] Проверить TileServer GL: http://yourdomain.com:8080
|
||||||
|
|
||||||
|
## После деплоя
|
||||||
|
|
||||||
|
### 1. Мониторинг
|
||||||
|
|
||||||
|
- [ ] Настроить мониторинг логов:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Проверить использование ресурсов:
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Backup
|
||||||
|
|
||||||
|
- [ ] Настроить автоматический backup БД
|
||||||
|
- [ ] Проверить восстановление из backup
|
||||||
|
- [ ] Настроить backup медиа файлов
|
||||||
|
|
||||||
|
### 3. Обновления
|
||||||
|
|
||||||
|
- [ ] Документировать процесс обновления
|
||||||
|
- [ ] Тестировать обновления на dev окружении
|
||||||
|
|
||||||
|
### 4. Безопасность
|
||||||
|
|
||||||
|
- [ ] Настроить firewall (UFW, iptables)
|
||||||
|
- [ ] Ограничить доступ к портам:
|
||||||
|
- Открыть: 80, 443
|
||||||
|
- Закрыть: 5432, 8000 (доступ только внутри Docker сети)
|
||||||
|
|
||||||
|
- [ ] Настроить fail2ban (опционально)
|
||||||
|
|
||||||
|
### 5. Производительность
|
||||||
|
|
||||||
|
- [ ] Настроить кэширование (Redis, Memcached)
|
||||||
|
- [ ] Оптимизировать количество Gunicorn workers
|
||||||
|
- [ ] Настроить CDN для статики (опционально)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Проблема: Контейнеры не запускаются
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить логи
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs
|
||||||
|
|
||||||
|
# Проверить конфигурацию
|
||||||
|
docker-compose -f docker-compose.prod.yaml config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: База данных недоступна
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить статус БД
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
|
||||||
|
|
||||||
|
# Проверить логи БД
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: Статические файлы не загружаются
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пересобрать статику
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Проверить права доступа
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: 502 Bad Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверить, что Django запущен
|
||||||
|
docker-compose -f docker-compose.prod.yaml ps web
|
||||||
|
|
||||||
|
# Проверить логи Gunicorn
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs web
|
||||||
|
|
||||||
|
# Проверить конфигурацию Nginx
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полезные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Перезапуск сервисов
|
||||||
|
docker-compose -f docker-compose.prod.yaml restart web
|
||||||
|
docker-compose -f docker-compose.prod.yaml restart nginx
|
||||||
|
|
||||||
|
# Обновление кода
|
||||||
|
git pull
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
|
||||||
|
# Backup БД
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||||
|
|
||||||
|
# Восстановление БД
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
|
||||||
|
|
||||||
|
# Очистка старых образов
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Контакты для поддержки
|
||||||
|
|
||||||
|
- Документация: [DOCKER_README.md](DOCKER_README.md)
|
||||||
|
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md)
|
||||||
262
DOCKER_README.md
Normal file
262
DOCKER_README.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# Docker Setup для Django + PostGIS + TileServer GL
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── dbapp/ # Django приложение
|
||||||
|
│ ├── Dockerfile # Универсальный Dockerfile
|
||||||
|
│ ├── entrypoint.sh # Скрипт запуска
|
||||||
|
│ └── ...
|
||||||
|
├── nginx/ # Конфигурация Nginx (только для prod)
|
||||||
|
│ └── conf.d/
|
||||||
|
│ └── default.conf
|
||||||
|
├── tiles/ # Тайлы для TileServer GL
|
||||||
|
├── docker-compose.yaml # Development окружение
|
||||||
|
├── docker-compose.prod.yaml # Production окружение
|
||||||
|
├── .env.dev # Переменные для development
|
||||||
|
└── .env.prod # Переменные для production
|
||||||
|
```
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
1. Скопируйте файл окружения:
|
||||||
|
```bash
|
||||||
|
cp .env.dev .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Запустите контейнеры:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Создайте суперпользователя:
|
||||||
|
```bash
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Приложение доступно:
|
||||||
|
- Django: http://localhost:8000
|
||||||
|
- TileServer GL: http://localhost:8080
|
||||||
|
- PostgreSQL: localhost:5432
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
1. Скопируйте и настройте файл окружения:
|
||||||
|
```bash
|
||||||
|
cp .env.prod .env
|
||||||
|
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Запустите контейнеры:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Создайте суперпользователя:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Приложение доступно:
|
||||||
|
- Nginx: http://localhost (порт 80)
|
||||||
|
- Django (напрямую): http://localhost:8000
|
||||||
|
- TileServer GL: http://localhost:8080
|
||||||
|
- PostgreSQL: localhost:5432
|
||||||
|
|
||||||
|
## Основные команды
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f web
|
||||||
|
|
||||||
|
# Выполнение команд Django
|
||||||
|
docker-compose exec web python manage.py migrate
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
docker-compose exec web python manage.py shell
|
||||||
|
|
||||||
|
# Пересборка после изменений в Dockerfile
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Полная очистка (включая volumes)
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose -f docker-compose.prod.yaml down
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs -f web
|
||||||
|
|
||||||
|
# Выполнение команд Django
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Пересборка
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Различия между Dev и Prod
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- Django development server (runserver)
|
||||||
|
- DEBUG=True
|
||||||
|
- Код монтируется как volume (изменения применяются сразу)
|
||||||
|
- Без Nginx
|
||||||
|
- Простые пароли (для локальной разработки)
|
||||||
|
|
||||||
|
### Production
|
||||||
|
- Gunicorn WSGI server
|
||||||
|
- DEBUG=False
|
||||||
|
- Код копируется в образ (не монтируется)
|
||||||
|
- Nginx как reverse proxy
|
||||||
|
- Сильные пароли и SECRET_KEY
|
||||||
|
- Сбор статики (collectstatic)
|
||||||
|
- Оптимизированные настройки безопасности
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
### Основные переменные (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Django
|
||||||
|
DEBUG=True/False
|
||||||
|
ENVIRONMENT=development/production
|
||||||
|
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
|
||||||
|
SECRET_KEY=your-secret-key
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_NAME=geodb
|
||||||
|
DB_USER=geralt
|
||||||
|
DB_PASSWORD=your-password
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Allowed Hosts
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
||||||
|
|
||||||
|
# Gunicorn (только для production)
|
||||||
|
GUNICORN_WORKERS=3
|
||||||
|
GUNICORN_TIMEOUT=120
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- `postgres_data_dev` - данные PostgreSQL
|
||||||
|
- `static_volume_dev` - статические файлы
|
||||||
|
- `media_volume_dev` - медиа файлы
|
||||||
|
- `logs_volume_dev` - логи
|
||||||
|
- `./dbapp:/app` - код приложения (live reload)
|
||||||
|
|
||||||
|
### Production
|
||||||
|
- `postgres_data_prod` - данные PostgreSQL
|
||||||
|
- `static_volume_prod` - статические файлы
|
||||||
|
- `media_volume_prod` - медиа файлы
|
||||||
|
- `logs_volume_prod` - логи
|
||||||
|
|
||||||
|
## TileServer GL
|
||||||
|
|
||||||
|
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
|
||||||
|
|
||||||
|
Пример структуры:
|
||||||
|
```
|
||||||
|
tiles/
|
||||||
|
├── config.json
|
||||||
|
└── your-tiles.mbtiles
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup и восстановление БД
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
docker-compose exec db pg_dump -U geralt geodb > backup.sql
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Проблемы с миграциями
|
||||||
|
```bash
|
||||||
|
docker-compose exec web python manage.py migrate --fake-initial
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с правами доступа
|
||||||
|
```bash
|
||||||
|
docker-compose exec -u root web chown -R app:app /app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Очистка всех данных
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка логов
|
||||||
|
```bash
|
||||||
|
# Все сервисы
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Конкретный сервис
|
||||||
|
docker-compose logs -f web
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность для Production
|
||||||
|
|
||||||
|
1. **Измените SECRET_KEY** - используйте длинный случайный ключ
|
||||||
|
2. **Измените пароли БД** - используйте сильные пароли
|
||||||
|
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
|
||||||
|
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
|
||||||
|
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
|
||||||
|
|
||||||
|
## Генерация SECRET_KEY
|
||||||
|
|
||||||
|
```python
|
||||||
|
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Мониторинг
|
||||||
|
|
||||||
|
### Проверка статуса контейнеров
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование ресурсов
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Healthcheck
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/admin/
|
||||||
|
```
|
||||||
307
DOCKER_SETUP.md
Normal file
307
DOCKER_SETUP.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# Docker Setup - Полное руководство
|
||||||
|
|
||||||
|
## 📋 Обзор
|
||||||
|
|
||||||
|
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
|
||||||
|
|
||||||
|
**Основные компоненты:**
|
||||||
|
- Django 5.2 с PostGIS
|
||||||
|
- PostgreSQL 17 с расширением PostGIS 3.4
|
||||||
|
- TileServer GL для работы с картографическими тайлами
|
||||||
|
- Nginx (только для production)
|
||||||
|
- Gunicorn WSGI сервер (production)
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
cp .env.dev .env
|
||||||
|
make dev-up
|
||||||
|
make createsuperuser
|
||||||
|
```
|
||||||
|
Откройте http://localhost:8000
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
cp .env.prod .env
|
||||||
|
# Отредактируйте .env (SECRET_KEY, пароли, домены)
|
||||||
|
make prod-up
|
||||||
|
make prod-createsuperuser
|
||||||
|
```
|
||||||
|
Откройте http://yourdomain.com
|
||||||
|
|
||||||
|
## 📁 Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── dbapp/ # Django приложение
|
||||||
|
│ ├── Dockerfile # Универсальный Dockerfile
|
||||||
|
│ ├── entrypoint.sh # Скрипт инициализации
|
||||||
|
│ ├── .dockerignore # Исключения для Docker
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── nginx/ # Nginx конфигурация (prod)
|
||||||
|
│ ├── conf.d/
|
||||||
|
│ │ ├── default.conf # HTTP конфигурация
|
||||||
|
│ │ └── ssl.conf.example # HTTPS конфигурация (пример)
|
||||||
|
│ └── ssl/ # SSL сертификаты
|
||||||
|
│
|
||||||
|
├── tiles/ # Тайлы для TileServer GL
|
||||||
|
│ ├── README.md # Инструкция по настройке
|
||||||
|
│ ├── config.json.example # Пример конфигурации
|
||||||
|
│ └── .gitignore
|
||||||
|
│
|
||||||
|
├── docker-compose.yaml # Development окружение
|
||||||
|
├── docker-compose.prod.yaml # Production окружение
|
||||||
|
│
|
||||||
|
├── .env.dev # Переменные для dev
|
||||||
|
├── .env.prod # Переменные для prod (шаблон)
|
||||||
|
│
|
||||||
|
├── Makefile # Удобные команды
|
||||||
|
├── generate_secret_key.py # Генератор SECRET_KEY
|
||||||
|
│
|
||||||
|
└── Документация:
|
||||||
|
├── QUICKSTART.md # Быстрый старт
|
||||||
|
├── DOCKER_README.md # Подробная документация
|
||||||
|
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
|
||||||
|
└── DOCKER_SETUP.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
**Один универсальный Dockerfile** для dev и prod:
|
||||||
|
- Multi-stage build для оптимизации размера
|
||||||
|
- Установка GDAL, PostGIS зависимостей
|
||||||
|
- Использование uv для управления зависимостями
|
||||||
|
- Non-root пользователь для безопасности
|
||||||
|
- Healthcheck для мониторинга
|
||||||
|
|
||||||
|
### entrypoint.sh
|
||||||
|
|
||||||
|
Скрипт автоматически:
|
||||||
|
- Ждет готовности PostgreSQL
|
||||||
|
- Выполняет миграции
|
||||||
|
- Собирает статику (только prod)
|
||||||
|
- Запускает runserver (dev) или Gunicorn (prod)
|
||||||
|
|
||||||
|
Поведение определяется переменной `ENVIRONMENT`:
|
||||||
|
- `development` → Django development server
|
||||||
|
- `production` → Gunicorn WSGI server
|
||||||
|
|
||||||
|
### docker-compose.yaml (Development)
|
||||||
|
|
||||||
|
**Сервисы:**
|
||||||
|
- `db` - PostgreSQL с PostGIS
|
||||||
|
- `web` - Django приложение
|
||||||
|
- `tileserver` - TileServer GL
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Код монтируется как volume (live reload)
|
||||||
|
- DEBUG=True
|
||||||
|
- Django development server
|
||||||
|
- Простые пароли для локальной разработки
|
||||||
|
|
||||||
|
### docker-compose.prod.yaml (Production)
|
||||||
|
|
||||||
|
**Сервисы:**
|
||||||
|
- `db` - PostgreSQL с PostGIS
|
||||||
|
- `web` - Django с Gunicorn
|
||||||
|
- `tileserver` - TileServer GL
|
||||||
|
- `nginx` - Reverse proxy
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Код копируется в образ (не монтируется)
|
||||||
|
- DEBUG=False
|
||||||
|
- Gunicorn WSGI server
|
||||||
|
- Nginx для статики и проксирования
|
||||||
|
- Сильные пароли из .env
|
||||||
|
- Сбор статики (collectstatic)
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### Для Production обязательно:
|
||||||
|
|
||||||
|
1. **Сгенерируйте SECRET_KEY:**
|
||||||
|
```bash
|
||||||
|
python generate_secret_key.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Измените пароли БД** в `.env`
|
||||||
|
|
||||||
|
3. **Настройте ALLOWED_HOSTS:**
|
||||||
|
```
|
||||||
|
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Настройте SSL/HTTPS** (рекомендуется):
|
||||||
|
- Получите сертификаты (Let's Encrypt)
|
||||||
|
- Поместите в `nginx/ssl/`
|
||||||
|
- Используйте `nginx/conf.d/ssl.conf.example`
|
||||||
|
|
||||||
|
5. **Ограничьте доступ к портам:**
|
||||||
|
- Открыть: 80, 443
|
||||||
|
- Закрыть: 5432, 8000
|
||||||
|
|
||||||
|
## 📊 Мониторинг
|
||||||
|
|
||||||
|
### Логи
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
make dev-logs
|
||||||
|
|
||||||
|
# Production
|
||||||
|
make prod-logs
|
||||||
|
|
||||||
|
# Конкретный сервис
|
||||||
|
docker-compose logs -f web
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статус
|
||||||
|
```bash
|
||||||
|
make status # Development
|
||||||
|
make prod-status # Production
|
||||||
|
docker stats # Использование ресурсов
|
||||||
|
```
|
||||||
|
|
||||||
|
### Healthcheck
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💾 Backup и восстановление
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
```bash
|
||||||
|
make backup
|
||||||
|
# или
|
||||||
|
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T db psql -U geralt geodb < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Автоматический backup (cron)
|
||||||
|
```bash
|
||||||
|
# Добавьте в crontab
|
||||||
|
0 2 * * * cd /path/to/project && make backup
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Обновление
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
make dev-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
make prod-build
|
||||||
|
make prod-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗺️ TileServer GL
|
||||||
|
|
||||||
|
Поместите `.mbtiles` файлы в директорию `tiles/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tiles/
|
||||||
|
├── world.mbtiles
|
||||||
|
└── satellite.mbtiles
|
||||||
|
```
|
||||||
|
|
||||||
|
Доступ: http://localhost:8080
|
||||||
|
|
||||||
|
Подробнее: [tiles/README.md](tiles/README.md)
|
||||||
|
|
||||||
|
## 🛠️ Makefile команды
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
make dev-up # Запустить
|
||||||
|
make dev-down # Остановить
|
||||||
|
make dev-build # Пересобрать
|
||||||
|
make dev-logs # Логи
|
||||||
|
make dev-restart # Перезапустить web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
make prod-up # Запустить
|
||||||
|
make prod-down # Остановить
|
||||||
|
make prod-build # Пересобрать
|
||||||
|
make prod-logs # Логи
|
||||||
|
make prod-restart # Перезапустить web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Django
|
||||||
|
```bash
|
||||||
|
make shell # Django shell
|
||||||
|
make migrate # Миграции
|
||||||
|
make makemigrations # Создать миграции
|
||||||
|
make createsuperuser # Создать суперпользователя
|
||||||
|
make collectstatic # Собрать статику
|
||||||
|
```
|
||||||
|
|
||||||
|
### Утилиты
|
||||||
|
```bash
|
||||||
|
make backup # Backup БД
|
||||||
|
make status # Статус контейнеров
|
||||||
|
make clean # Очистка (с volumes)
|
||||||
|
make clean-all # Полная очистка
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Дополнительная документация
|
||||||
|
|
||||||
|
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
|
||||||
|
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
|
||||||
|
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
|
||||||
|
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
|
||||||
|
|
||||||
|
## ❓ Troubleshooting
|
||||||
|
|
||||||
|
### Контейнеры не запускаются
|
||||||
|
```bash
|
||||||
|
docker-compose logs
|
||||||
|
docker-compose config
|
||||||
|
```
|
||||||
|
|
||||||
|
### База данных недоступна
|
||||||
|
```bash
|
||||||
|
docker-compose exec db pg_isready -U geralt
|
||||||
|
docker-compose logs db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статические файлы не загружаются
|
||||||
|
```bash
|
||||||
|
docker-compose exec web python manage.py collectstatic --noinput
|
||||||
|
docker-compose exec web ls -la /app/staticfiles
|
||||||
|
```
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
```bash
|
||||||
|
docker-compose ps web
|
||||||
|
docker-compose logs web
|
||||||
|
docker-compose exec nginx nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
|
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
|
||||||
|
2. ✅ Запустите development окружение
|
||||||
|
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
|
||||||
|
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
|
||||||
|
5. ✅ Настройте SSL для production
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем:
|
||||||
|
1. Проверьте логи: `make dev-logs` или `make prod-logs`
|
||||||
|
2. Изучите документацию в этой директории
|
||||||
|
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей
|
||||||
240
FILES_OVERVIEW.md
Normal file
240
FILES_OVERVIEW.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Обзор созданных файлов Docker Setup
|
||||||
|
|
||||||
|
## 🐳 Docker файлы
|
||||||
|
|
||||||
|
### `dbapp/Dockerfile`
|
||||||
|
**Универсальный Dockerfile** для dev и prod окружений.
|
||||||
|
- Multi-stage build для оптимизации
|
||||||
|
- Установка GDAL, PostGIS, PostgreSQL клиента
|
||||||
|
- Использование uv для управления зависимостями
|
||||||
|
- Non-root пользователь для безопасности
|
||||||
|
- Healthcheck для мониторинга
|
||||||
|
|
||||||
|
### `dbapp/entrypoint.sh`
|
||||||
|
**Скрипт инициализации контейнера.**
|
||||||
|
- Ожидание готовности PostgreSQL
|
||||||
|
- Автоматические миграции
|
||||||
|
- Сбор статики (только prod)
|
||||||
|
- Запуск runserver (dev) или Gunicorn (prod)
|
||||||
|
|
||||||
|
### `dbapp/.dockerignore`
|
||||||
|
**Исключения для Docker build.**
|
||||||
|
- Исключает ненужные файлы из образа
|
||||||
|
- Уменьшает размер образа
|
||||||
|
- Ускоряет сборку
|
||||||
|
|
||||||
|
## 🔧 Docker Compose файлы
|
||||||
|
|
||||||
|
### `docker-compose.yaml`
|
||||||
|
**Development окружение.**
|
||||||
|
- PostgreSQL с PostGIS
|
||||||
|
- Django с development server
|
||||||
|
- TileServer GL
|
||||||
|
- Код монтируется как volume (live reload)
|
||||||
|
- DEBUG=True
|
||||||
|
|
||||||
|
### `docker-compose.prod.yaml`
|
||||||
|
**Production окружение.**
|
||||||
|
- PostgreSQL с PostGIS
|
||||||
|
- Django с Gunicorn
|
||||||
|
- TileServer GL
|
||||||
|
- Nginx reverse proxy
|
||||||
|
- Код копируется в образ
|
||||||
|
- DEBUG=False
|
||||||
|
- Оптимизированные настройки
|
||||||
|
|
||||||
|
## 🌐 Nginx конфигурация
|
||||||
|
|
||||||
|
### `nginx/conf.d/default.conf`
|
||||||
|
**HTTP конфигурация для production.**
|
||||||
|
- Проксирование к Django
|
||||||
|
- Раздача статики и медиа
|
||||||
|
- Оптимизированные таймауты
|
||||||
|
- Кэширование статики
|
||||||
|
|
||||||
|
### `nginx/conf.d/ssl.conf.example`
|
||||||
|
**HTTPS конфигурация (пример).**
|
||||||
|
- SSL/TLS настройки
|
||||||
|
- Редирект с HTTP на HTTPS
|
||||||
|
- Security headers
|
||||||
|
- Оптимизированные SSL параметры
|
||||||
|
|
||||||
|
### `nginx/ssl/.gitkeep`
|
||||||
|
**Директория для SSL сертификатов.**
|
||||||
|
- Поместите сюда fullchain.pem и privkey.pem
|
||||||
|
|
||||||
|
## 🗺️ TileServer GL
|
||||||
|
|
||||||
|
### `tiles/README.md`
|
||||||
|
**Инструкция по настройке TileServer GL.**
|
||||||
|
- Как добавить тайлы
|
||||||
|
- Примеры конфигурации
|
||||||
|
- Использование в Django/Leaflet
|
||||||
|
- Где взять тайлы
|
||||||
|
|
||||||
|
### `tiles/config.json.example`
|
||||||
|
**Пример конфигурации TileServer GL.**
|
||||||
|
- Настройки путей
|
||||||
|
- Форматы и качество
|
||||||
|
- Домены
|
||||||
|
|
||||||
|
### `tiles/.gitignore`
|
||||||
|
**Исключения для git.**
|
||||||
|
- Игнорирует большие .mbtiles файлы
|
||||||
|
- Сохраняет примеры конфигурации
|
||||||
|
|
||||||
|
## 🔐 Переменные окружения
|
||||||
|
|
||||||
|
### `.env.dev`
|
||||||
|
**Переменные для development.**
|
||||||
|
- DEBUG=True
|
||||||
|
- Простые пароли для локальной разработки
|
||||||
|
- Настройки БД для dev
|
||||||
|
|
||||||
|
### `.env.prod`
|
||||||
|
**Шаблон переменных для production.**
|
||||||
|
- DEBUG=False
|
||||||
|
- Требует изменения SECRET_KEY и паролей
|
||||||
|
- Настройки для production
|
||||||
|
|
||||||
|
## 🛠️ Утилиты
|
||||||
|
|
||||||
|
### `Makefile`
|
||||||
|
**Удобные команды для работы с Docker.**
|
||||||
|
- `make dev-up` - запуск dev
|
||||||
|
- `make prod-up` - запуск prod
|
||||||
|
- `make migrate` - миграции
|
||||||
|
- `make backup` - backup БД
|
||||||
|
- И многое другое
|
||||||
|
|
||||||
|
### `generate_secret_key.py`
|
||||||
|
**Генератор Django SECRET_KEY.**
|
||||||
|
```bash
|
||||||
|
python generate_secret_key.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
### `QUICKSTART.md`
|
||||||
|
**Быстрый старт.**
|
||||||
|
- Минимальные команды для запуска
|
||||||
|
- Development и Production
|
||||||
|
- Основные команды
|
||||||
|
|
||||||
|
### `DOCKER_README.md`
|
||||||
|
**Подробная документация.**
|
||||||
|
- Полное описание структуры
|
||||||
|
- Все команды с примерами
|
||||||
|
- Troubleshooting
|
||||||
|
- Backup и восстановление
|
||||||
|
|
||||||
|
### `DOCKER_SETUP.md`
|
||||||
|
**Полное руководство.**
|
||||||
|
- Обзор всей системы
|
||||||
|
- Конфигурация
|
||||||
|
- Безопасность
|
||||||
|
- Мониторинг
|
||||||
|
|
||||||
|
### `DEPLOYMENT_CHECKLIST.md`
|
||||||
|
**Чеклист для деплоя.**
|
||||||
|
- Пошаговая инструкция
|
||||||
|
- Проверка безопасности
|
||||||
|
- Настройка production
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
### `FILES_OVERVIEW.md`
|
||||||
|
**Этот файл.**
|
||||||
|
- Описание всех созданных файлов
|
||||||
|
- Назначение каждого файла
|
||||||
|
|
||||||
|
## 📝 Обновленные файлы
|
||||||
|
|
||||||
|
### `.gitignore`
|
||||||
|
**Обновлен для Docker.**
|
||||||
|
- Исключает .env файлы
|
||||||
|
- Исключает логи и backup
|
||||||
|
- Исключает временные файлы
|
||||||
|
|
||||||
|
## 🎯 Как использовать
|
||||||
|
|
||||||
|
### Для начала работы:
|
||||||
|
1. Прочитайте **QUICKSTART.md**
|
||||||
|
2. Выберите окружение (dev или prod)
|
||||||
|
3. Скопируйте соответствующий .env файл
|
||||||
|
4. Запустите с помощью Makefile
|
||||||
|
|
||||||
|
### Для деплоя:
|
||||||
|
1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
|
||||||
|
2. Следуйте чеклисту пошагово
|
||||||
|
3. Используйте **DOCKER_README.md** для справки
|
||||||
|
|
||||||
|
### Для настройки TileServer:
|
||||||
|
1. Прочитайте **tiles/README.md**
|
||||||
|
2. Добавьте .mbtiles файлы
|
||||||
|
3. Настройте config.json (опционально)
|
||||||
|
|
||||||
|
## 📊 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── Docker конфигурация
|
||||||
|
│ ├── dbapp/Dockerfile
|
||||||
|
│ ├── dbapp/entrypoint.sh
|
||||||
|
│ ├── dbapp/.dockerignore
|
||||||
|
│ ├── docker-compose.yaml
|
||||||
|
│ └── docker-compose.prod.yaml
|
||||||
|
│
|
||||||
|
├── Nginx
|
||||||
|
│ ├── nginx/conf.d/default.conf
|
||||||
|
│ ├── nginx/conf.d/ssl.conf.example
|
||||||
|
│ └── nginx/ssl/.gitkeep
|
||||||
|
│
|
||||||
|
├── TileServer GL
|
||||||
|
│ ├── tiles/README.md
|
||||||
|
│ ├── tiles/config.json.example
|
||||||
|
│ └── tiles/.gitignore
|
||||||
|
│
|
||||||
|
├── Переменные окружения
|
||||||
|
│ ├── .env.dev
|
||||||
|
│ └── .env.prod
|
||||||
|
│
|
||||||
|
├── Утилиты
|
||||||
|
│ ├── Makefile
|
||||||
|
│ └── generate_secret_key.py
|
||||||
|
│
|
||||||
|
└── Документация
|
||||||
|
├── QUICKSTART.md
|
||||||
|
├── DOCKER_README.md
|
||||||
|
├── DOCKER_SETUP.md
|
||||||
|
├── DEPLOYMENT_CHECKLIST.md
|
||||||
|
└── FILES_OVERVIEW.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Что было сделано
|
||||||
|
|
||||||
|
1. ✅ Создан универсальный Dockerfile (один для dev и prod)
|
||||||
|
2. ✅ Настроен entrypoint.sh с автоматической инициализацией
|
||||||
|
3. ✅ Созданы docker-compose.yaml для dev и prod
|
||||||
|
4. ✅ Настроен Nginx для production
|
||||||
|
5. ✅ Добавлена поддержка TileServer GL
|
||||||
|
6. ✅ Созданы .env файлы для разных окружений
|
||||||
|
7. ✅ Добавлен Makefile с удобными командами
|
||||||
|
8. ✅ Написана подробная документация
|
||||||
|
9. ✅ Создан чеклист для деплоя
|
||||||
|
10. ✅ Добавлены утилиты (генератор SECRET_KEY)
|
||||||
|
|
||||||
|
## 🚀 Следующие шаги
|
||||||
|
|
||||||
|
1. Запустите development окружение
|
||||||
|
2. Протестируйте все функции
|
||||||
|
3. Подготовьте production окружение
|
||||||
|
4. Следуйте DEPLOYMENT_CHECKLIST.md
|
||||||
|
5. Настройте мониторинг и backup
|
||||||
|
|
||||||
|
## 💡 Полезные ссылки
|
||||||
|
|
||||||
|
- Django Documentation: https://docs.djangoproject.com/
|
||||||
|
- Docker Documentation: https://docs.docker.com/
|
||||||
|
- PostGIS Documentation: https://postgis.net/documentation/
|
||||||
|
- TileServer GL: https://github.com/maptiler/tileserver-gl
|
||||||
|
- Nginx Documentation: https://nginx.org/en/docs/
|
||||||
99
Makefile
Normal file
99
Makefile
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
.PHONY: help dev-up dev-down dev-build dev-logs prod-up prod-down prod-build prod-logs shell migrate createsuperuser clean
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Доступные команды:"
|
||||||
|
@echo " make dev-up - Запустить development окружение"
|
||||||
|
@echo " make dev-down - Остановить development окружение"
|
||||||
|
@echo " make dev-build - Пересобрать development контейнеры"
|
||||||
|
@echo " make dev-logs - Показать логи development"
|
||||||
|
@echo " make prod-up - Запустить production окружение"
|
||||||
|
@echo " make prod-down - Остановить production окружение"
|
||||||
|
@echo " make prod-build - Пересобрать production контейнеры"
|
||||||
|
@echo " make prod-logs - Показать логи production"
|
||||||
|
@echo " make shell - Открыть Django shell"
|
||||||
|
@echo " make migrate - Выполнить миграции"
|
||||||
|
@echo " make createsuperuser - Создать суперпользователя"
|
||||||
|
@echo " make clean - Удалить все контейнеры и volumes"
|
||||||
|
|
||||||
|
# Development команды
|
||||||
|
dev-up:
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
dev-down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
dev-build:
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
dev-logs:
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
dev-restart:
|
||||||
|
docker-compose restart web
|
||||||
|
|
||||||
|
# Production команды
|
||||||
|
prod-up:
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d
|
||||||
|
|
||||||
|
prod-down:
|
||||||
|
docker-compose -f docker-compose.prod.yaml down
|
||||||
|
|
||||||
|
prod-build:
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
|
||||||
|
prod-logs:
|
||||||
|
docker-compose -f docker-compose.prod.yaml logs -f
|
||||||
|
|
||||||
|
prod-restart:
|
||||||
|
docker-compose -f docker-compose.prod.yaml restart web
|
||||||
|
|
||||||
|
# Django команды (для development по умолчанию)
|
||||||
|
shell:
|
||||||
|
docker-compose exec web python manage.py shell
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
docker-compose exec web python manage.py migrate
|
||||||
|
|
||||||
|
makemigrations:
|
||||||
|
docker-compose exec web python manage.py makemigrations
|
||||||
|
|
||||||
|
createsuperuser:
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
collectstatic:
|
||||||
|
docker-compose exec web python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Для production
|
||||||
|
prod-shell:
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py shell
|
||||||
|
|
||||||
|
prod-migrate:
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
|
||||||
|
|
||||||
|
prod-createsuperuser:
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Backup и восстановление
|
||||||
|
backup:
|
||||||
|
docker-compose exec db pg_dump -U geralt geodb > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
restore:
|
||||||
|
@read -p "Введите имя файла backup: " file; \
|
||||||
|
docker-compose exec -T db psql -U geralt geodb < $$file
|
||||||
|
|
||||||
|
# Очистка
|
||||||
|
clean:
|
||||||
|
docker-compose down -v
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
clean-all:
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose -f docker-compose.prod.yaml down -v
|
||||||
|
docker system prune -af --volumes
|
||||||
|
|
||||||
|
# Проверка статуса
|
||||||
|
status:
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
prod-status:
|
||||||
|
docker-compose -f docker-compose.prod.yaml ps
|
||||||
106
QUICKSTART.md
Normal file
106
QUICKSTART.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Быстрый старт с Docker
|
||||||
|
|
||||||
|
## Development (разработка)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Скопировать переменные окружения
|
||||||
|
cp .env.dev .env
|
||||||
|
|
||||||
|
# 2. Запустить контейнеры
|
||||||
|
make dev-up
|
||||||
|
# или
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# 3. Создать суперпользователя
|
||||||
|
make createsuperuser
|
||||||
|
# или
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# 4. Открыть в браузере
|
||||||
|
# Django: http://localhost:8000
|
||||||
|
# Admin: http://localhost:8000/admin
|
||||||
|
# TileServer: http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production (продакшн)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Скопировать и настроить переменные
|
||||||
|
cp .env.prod .env
|
||||||
|
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
|
||||||
|
|
||||||
|
# 2. Запустить контейнеры
|
||||||
|
make prod-up
|
||||||
|
# или
|
||||||
|
docker-compose -f docker-compose.prod.yaml up -d --build
|
||||||
|
|
||||||
|
# 3. Создать суперпользователя
|
||||||
|
make prod-createsuperuser
|
||||||
|
# или
|
||||||
|
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# 4. Открыть в браузере
|
||||||
|
# Nginx: http://localhost
|
||||||
|
# Django: http://localhost:8000
|
||||||
|
# TileServer: http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полезные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Просмотр логов
|
||||||
|
make dev-logs # development
|
||||||
|
make prod-logs # production
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
make dev-down # development
|
||||||
|
make prod-down # production
|
||||||
|
|
||||||
|
# Перезапуск после изменений
|
||||||
|
make dev-build # development
|
||||||
|
make prod-build # production
|
||||||
|
|
||||||
|
# Django shell
|
||||||
|
make shell # development
|
||||||
|
make prod-shell # production
|
||||||
|
|
||||||
|
# Миграции
|
||||||
|
make migrate # development
|
||||||
|
make prod-migrate # production
|
||||||
|
|
||||||
|
# Backup БД
|
||||||
|
make backup
|
||||||
|
|
||||||
|
# Статус контейнеров
|
||||||
|
make status # development
|
||||||
|
make prod-status # production
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── dbapp/ # Django приложение
|
||||||
|
│ ├── Dockerfile # Универсальный Dockerfile
|
||||||
|
│ ├── entrypoint.sh # Скрипт запуска
|
||||||
|
│ ├── manage.py
|
||||||
|
│ └── ...
|
||||||
|
├── nginx/ # Nginx (только prod)
|
||||||
|
│ └── conf.d/
|
||||||
|
│ └── default.conf
|
||||||
|
├── tiles/ # Тайлы для TileServer GL
|
||||||
|
│ ├── README.md
|
||||||
|
│ └── config.json.example
|
||||||
|
├── docker-compose.yaml # Development
|
||||||
|
├── docker-compose.prod.yaml # Production
|
||||||
|
├── .env.dev # Переменные dev
|
||||||
|
├── .env.prod # Переменные prod
|
||||||
|
├── Makefile # Команды для удобства
|
||||||
|
└── DOCKER_README.md # Подробная документация
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что дальше?
|
||||||
|
|
||||||
|
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
|
||||||
|
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
|
||||||
|
3. Для production настройте SSL сертификаты в `nginx/ssl/`
|
||||||
@@ -1,29 +1,60 @@
|
|||||||
__pycache__
|
# Git
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
.pytest_cache
|
|
||||||
.coverage
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
README.md
|
.gitattributes
|
||||||
.env
|
|
||||||
.DS_Store
|
# Python
|
||||||
.settings
|
__pycache__
|
||||||
.vscode
|
*.py[cod]
|
||||||
.idea
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
__pycache__/
|
|
||||||
*.so
|
# Django
|
||||||
.Python
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
/staticfiles/
|
||||||
|
/media/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
.coverage
|
.coverage
|
||||||
.pytest_cache
|
htmlcov/
|
||||||
.venv
|
.tox/
|
||||||
venv/
|
|
||||||
env/
|
# Documentation
|
||||||
.pyre/
|
*.md
|
||||||
node_modules/
|
docs/
|
||||||
|
|
||||||
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yaml
|
||||||
|
.dockerignore
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ ENVIRONMENT=production
|
|||||||
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
|
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
|
||||||
DB_NAME=geodb
|
DB_NAME=geodb
|
||||||
DB_USER=geralt
|
DB_USER=geralt
|
||||||
DB_PASSWORD=your_secure_db_password
|
DB_PASSWORD=123456
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
ALLOWED_HOSTS=localhost,yourdomain.com
|
ALLOWED_HOSTS=localhost,yourdomain.com
|
||||||
@@ -1,43 +1,57 @@
|
|||||||
# Use Python 3.13 slim image as base
|
FROM python:3.13-slim
|
||||||
FROM python:3.13.9-slim
|
|
||||||
|
|
||||||
# Set environment variables
|
# Install system dependencies
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONPATH=/app \
|
|
||||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
|
||||||
|
|
||||||
# Install system dependencies including GDAL and PostGIS dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gdal-bin \
|
gdal-bin \
|
||||||
libgdal-dev \
|
libgdal-dev \
|
||||||
proj-bin \
|
proj-bin \
|
||||||
proj-data \
|
proj-data \
|
||||||
libproj-dev \
|
libproj-dev \
|
||||||
|
libproj25 \
|
||||||
libgeos-dev \
|
libgeos-dev \
|
||||||
postgresql-client \
|
libgeos-c1v5 \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
postgresql-client \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
|
libpq5 \
|
||||||
|
netcat-openbsd \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Set work directory
|
# Set work directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Upgrade pip
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
|
# Copy requirements file
|
||||||
|
COPY requirements.txt ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy project files
|
# Copy project files
|
||||||
COPY pyproject.toml uv.lock ./
|
|
||||||
|
|
||||||
# Install uv and dependencies
|
|
||||||
RUN pip install --no-cache-dir uv && \
|
|
||||||
uv sync --frozen --no-dev
|
|
||||||
|
|
||||||
# Copy project code (после установки зависимостей для лучшего кэширования)
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Collect static files
|
# Create directories
|
||||||
RUN uv run manage.py collectstatic --noinput
|
RUN mkdir -p /app/staticfiles /app/logs /app/media
|
||||||
|
|
||||||
|
# Set permissions for entrypoint
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd --create-home --shell /bin/bash app && \
|
||||||
|
chown -R app:app /app
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Run gunicorn server
|
# Run entrypoint script
|
||||||
CMD [".venv/bin/gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# Multi-stage build for production
|
|
||||||
FROM python:3.13-slim as requirements-stage
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gdal-bin \
|
|
||||||
libgdal-dev \
|
|
||||||
proj-bin \
|
|
||||||
proj-data \
|
|
||||||
libproj-dev \
|
|
||||||
libgeos-dev \
|
|
||||||
build-essential \
|
|
||||||
libpq-dev \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies for GDAL
|
|
||||||
RUN pip install --upgrade pip && \
|
|
||||||
pip install --no-cache-dir GDAL==$(gdal-config --version)
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy project requirements
|
|
||||||
COPY pyproject.toml uv.lock ./
|
|
||||||
|
|
||||||
# Install uv package manager
|
|
||||||
RUN pip install --upgrade pip && pip install uv
|
|
||||||
|
|
||||||
# Install dependencies using uv
|
|
||||||
RUN uv pip install --system --only-binary=gdal,shapely,pyproj --no-cache-dir -r uv.lock
|
|
||||||
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
# Install runtime system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gdal-bin \
|
|
||||||
libgdal30 \
|
|
||||||
libproj25 \
|
|
||||||
libgeos-c1v5 \
|
|
||||||
postgresql-client \
|
|
||||||
libpq5 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
|
||||||
|
|
||||||
# Set work directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy Python dependencies from previous stage
|
|
||||||
COPY --from=requirements-stage /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
|
||||||
COPY --from=requirements-stage /usr/local/bin /usr/local/bin
|
|
||||||
|
|
||||||
# Copy project
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Create non-root user for security
|
|
||||||
RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app
|
|
||||||
USER app
|
|
||||||
|
|
||||||
# Collect static files
|
|
||||||
RUN python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Run gunicorn server
|
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120", "dbapp.wsgi:application"]
|
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Settings module initialization.
|
||||||
|
|
||||||
|
Automatically determines the environment and loads appropriate settings.
|
||||||
|
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
|
||||||
|
Defaults to 'development' if not set.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Determine the environment and import the appropriate settings
|
# Determine the environment from DJANGO_ENVIRONMENT variable
|
||||||
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
|
# Defaults to 'development' for safety
|
||||||
|
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
|
||||||
|
|
||||||
if ENVIRONMENT == 'production':
|
if ENVIRONMENT == 'production':
|
||||||
from .production import *
|
from .production import *
|
||||||
|
print("Loading production settings...")
|
||||||
else:
|
else:
|
||||||
from .development import *
|
from .development import *
|
||||||
|
print("Loading development settings...")
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
"""
|
"""
|
||||||
Django settings for dbapp project.
|
Django settings for dbapp project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
Base settings shared across all environments.
|
||||||
|
Environment-specific settings are in development.py and production.py
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables from .env file
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PATH CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# GDAL/GEOS configuration for Windows
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
OSGEO4W = r"C:\Program Files\OSGeo4W"
|
OSGEO4W = r"C:\Program Files\OSGeo4W"
|
||||||
assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
|
assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
|
||||||
@@ -24,58 +28,71 @@ if os.name == 'nt':
|
|||||||
os.environ['PROJ_LIB'] = os.path.join(OSGEO4W, r"share\proj")
|
os.environ['PROJ_LIB'] = os.path.join(OSGEO4W, r"share\proj")
|
||||||
os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH']
|
os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH']
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# ============================================================================
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
# SECURITY SETTINGS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# GDAL_LIBRARY_PATH = r'C:/Program Files/OSGeo4W/bin/gdall311.dll'
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq')
|
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
# This should be overridden in environment-specific settings
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
# Allowed hosts - should be overridden in environment-specific settings
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# ============================================================================
|
||||||
|
# APPLICATION DEFINITION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
# Django Autocomplete Light (must be before admin)
|
||||||
'dal',
|
'dal',
|
||||||
'dal_select2',
|
'dal_select2',
|
||||||
"admin_interface",
|
|
||||||
"colorfield",
|
# Admin interface customization
|
||||||
|
'admin_interface',
|
||||||
|
'colorfield',
|
||||||
|
|
||||||
|
# Django GIS
|
||||||
'django.contrib.gis',
|
'django.contrib.gis',
|
||||||
'leaflet',
|
|
||||||
'dynamic_raw_id',
|
# Django core apps
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.humanize',
|
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'mainapp',
|
'django.contrib.humanize',
|
||||||
'mapsapp',
|
|
||||||
|
# Third-party apps
|
||||||
|
'leaflet',
|
||||||
|
'dynamic_raw_id',
|
||||||
'rangefilter',
|
'rangefilter',
|
||||||
'django_admin_multiple_choice_list_filter',
|
'django_admin_multiple_choice_list_filter',
|
||||||
'more_admin_filters',
|
'more_admin_filters',
|
||||||
'import_export',
|
'import_export',
|
||||||
'debug_toolbar'
|
|
||||||
|
# Project apps
|
||||||
|
'mainapp',
|
||||||
|
'mapsapp',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Note: Custom user model is implemented via OneToOneField relationship
|
# Note: Custom user model is implemented via OneToOneField relationship
|
||||||
|
# If you need a custom user model, uncomment and configure:
|
||||||
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MIDDLEWARE CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил
|
|
||||||
'django.middleware.locale.LocaleMiddleware', #Добавил
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware', #Добавил
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
@@ -85,15 +102,20 @@ MIDDLEWARE = [
|
|||||||
|
|
||||||
ROOT_URLCONF = 'dbapp.urls'
|
ROOT_URLCONF = 'dbapp.urls'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEMPLATES CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [
|
'DIRS': [
|
||||||
BASE_DIR / 'templates', # Main project templates directory
|
BASE_DIR / 'templates',
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
@@ -104,9 +126,9 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'dbapp.wsgi.application'
|
WSGI_APPLICATION = 'dbapp.wsgi.application'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# Database
|
# DATABASE CONFIGURATION
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
# ============================================================================
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
@@ -120,27 +142,28 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# ============================================================================
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
# PASSWORD VALIDATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
# {
|
{
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
# },
|
},
|
||||||
# {
|
{
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
# },
|
},
|
||||||
# {
|
{
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
# },
|
},
|
||||||
# {
|
{
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
# },
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# Internationalization
|
# INTERNATIONALIZATION
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# ============================================================================
|
||||||
|
|
||||||
LANGUAGE_CODE = 'ru'
|
LANGUAGE_CODE = 'ru'
|
||||||
|
|
||||||
@@ -150,51 +173,54 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Authentication settings
|
# ============================================================================
|
||||||
|
# AUTHENTICATION CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = 'login'
|
||||||
LOGIN_REDIRECT_URL = 'home'
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
LOGOUT_REDIRECT_URL = 'home'
|
LOGOUT_REDIRECT_URL = 'home'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# Static files (CSS, JavaScript, Images)
|
# STATIC FILES CONFIGURATION
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# ============================================================================
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR.parent / 'static', # Reference to the static directory at project root
|
BASE_DIR.parent / 'static',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Default primary key field type
|
# STATIC_ROOT will be set in production.py
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEFAULT SETTINGS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# THIRD-PARTY APP CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
# Admin Interface
|
||||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||||
SILENCED_SYSTEM_CHECKS = ["security.W019"]
|
SILENCED_SYSTEM_CHECKS = ["security.W019"]
|
||||||
|
|
||||||
|
# Leaflet Configuration
|
||||||
LEAFLET_CONFIG = {
|
LEAFLET_CONFIG = {
|
||||||
'ATTRIBUTION_PREFIX': '',
|
'ATTRIBUTION_PREFIX': '',
|
||||||
'TILES': [('Satellite', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {'attribution': '© Esri', 'maxZoom': 16}),
|
'TILES': [
|
||||||
('Streets', 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {'attribution': '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'})
|
(
|
||||||
|
'Satellite',
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
|
{'attribution': '© Esri', 'maxZoom': 16}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Streets',
|
||||||
|
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
{'attribution': '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'}
|
||||||
|
)
|
||||||
],
|
],
|
||||||
# 'RESET_VIEW': False,
|
|
||||||
# 'NO_GLOBALS': False,
|
|
||||||
# 'PLUGINS': {
|
|
||||||
# 'leaflet-measure': {
|
|
||||||
# 'css': ['https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.css'],
|
|
||||||
# 'js': 'https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.js',
|
|
||||||
# 'auto-include': True,
|
|
||||||
# },
|
|
||||||
# 'leaflet-featuregroup': {
|
|
||||||
# # 'css': ['https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.css'],
|
|
||||||
# 'js': 'https://cdn.jsdelivr.net/npm/leaflet.featuregroup.subgroup@1.0.2/dist/leaflet.featuregroup.subgroup.min.js',
|
|
||||||
# 'auto-include': True,
|
|
||||||
# },
|
|
||||||
# }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL_IPS = [
|
|
||||||
'127.0.0.1',
|
|
||||||
]
|
|
||||||
@@ -1,9 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Development-specific settings.
|
||||||
|
"""
|
||||||
|
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
# Development-specific settings
|
# ============================================================================
|
||||||
|
# DEBUG CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ALLOWED HOSTS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# Allow all hosts in development
|
# Allow all hosts in development
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
# Additional development settings can go here
|
# ============================================================================
|
||||||
|
# INSTALLED APPS - Development additions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
INSTALLED_APPS += [
|
||||||
|
'debug_toolbar',
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MIDDLEWARE - Development additions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Add debug toolbar middleware at the beginning
|
||||||
|
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEBUG TOOLBAR CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
INTERNAL_IPS = [
|
||||||
|
'127.0.0.1',
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# EMAIL CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Use console backend for development
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
@@ -1,48 +1,135 @@
|
|||||||
from .base import *
|
"""
|
||||||
|
Production-specific settings.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Production-specific settings
|
from .base import *
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEBUG CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TEMPLATE_DEBUG = DEBUG
|
|
||||||
|
|
||||||
# In production, specify allowed hosts explicitly
|
# ============================================================================
|
||||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS_PROD', 'localhost,127.0.0.1').split(',')
|
# ALLOWED HOSTS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# Security settings for production
|
# In production, specify allowed hosts explicitly from environment variable
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
# ============================================================================
|
||||||
SECURE_HSTS_SECONDS = 31536000
|
# SECURITY SETTINGS
|
||||||
SECURE_REDIRECT_EXEMPT = []
|
# ============================================================================
|
||||||
|
|
||||||
|
# SSL/HTTPS settings
|
||||||
SECURE_SSL_REDIRECT = True
|
SECURE_SSL_REDIRECT = True
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
# Template caching for production
|
# Security headers
|
||||||
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
|
||||||
|
# HSTS settings
|
||||||
|
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
|
||||||
|
# Additional security settings
|
||||||
|
SECURE_REDIRECT_EXEMPT = []
|
||||||
|
X_FRAME_OPTIONS = "DENY"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEMPLATE CACHING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [
|
"DIRS": [
|
||||||
BASE_DIR / 'templates', # Main project templates directory
|
BASE_DIR / "templates",
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.debug",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
'loaders': [
|
"loaders": [
|
||||||
('django.template.loaders.cached.Loader', [
|
(
|
||||||
'django.template.loaders.filesystem.Loader',
|
"django.template.loaders.cached.Loader",
|
||||||
'django.template.loaders.app_directories.Loader',
|
[
|
||||||
]),
|
"django.template.loaders.filesystem.Loader",
|
||||||
|
"django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Static files settings for production
|
# ============================================================================
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
# STATIC FILES CONFIGURATION
|
||||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
# ============================================================================
|
||||||
|
|
||||||
|
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
||||||
|
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOGGING CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"verbose": {
|
||||||
|
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"simple": {
|
||||||
|
"format": "{levelname} {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"require_debug_false": {
|
||||||
|
"()": "django.utils.log.RequireDebugFalse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"level": "INFO",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "simple",
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"level": "ERROR",
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
"mail_admins": {
|
||||||
|
"level": "ERROR",
|
||||||
|
"class": "django.utils.log.AdminEmailHandler",
|
||||||
|
"filters": ["require_debug_false"],
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console", "file"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"django.request": {
|
||||||
|
"handlers": ["mail_admins", "file"],
|
||||||
|
"level": "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ from django.contrib.auth import views as auth_views
|
|||||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('admin/dynamic_raw_id/', include('dynamic_raw_id.urls')),
|
|
||||||
path('admin/', admin.site.urls, name='admin'),
|
path('admin/', admin.site.urls, name='admin'),
|
||||||
# path('admin/map/', views.show_map_view, name='admin_show_map'),
|
path('', include('mainapp.urls', namespace='mainapp')),
|
||||||
path('', include('mainapp.urls')),
|
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||||
path('', include('mapsapp.urls')),
|
|
||||||
# Authentication URLs
|
# Authentication URLs
|
||||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||||
path('logout/', views.custom_logout, name='logout'),
|
path('logout/', views.custom_logout, name='logout'),
|
||||||
|
|||||||
37
dbapp/entrypoint.sh
Executable file
37
dbapp/entrypoint.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Определяем окружение (по умолчанию production)
|
||||||
|
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||||
|
|
||||||
|
echo "Starting in $ENVIRONMENT mode..."
|
||||||
|
|
||||||
|
# Ждем PostgreSQL
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
while ! nc -z $DB_HOST $DB_PORT; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "PostgreSQL started"
|
||||||
|
|
||||||
|
# Выполняем миграции
|
||||||
|
echo "Running migrations..."
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
|
# Собираем статику (только для production)
|
||||||
|
if [ "$ENVIRONMENT" = "production" ]; then
|
||||||
|
echo "Collecting static files..."
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Запускаем сервер в зависимости от окружения
|
||||||
|
if [ "$ENVIRONMENT" = "development" ]; then
|
||||||
|
echo "Starting Django development server..."
|
||||||
|
exec python manage.py runserver 0.0.0.0:8000
|
||||||
|
else
|
||||||
|
echo "Starting Gunicorn..."
|
||||||
|
exec gunicorn --bind 0.0.0.0:8000 \
|
||||||
|
--workers ${GUNICORN_WORKERS:-3} \
|
||||||
|
--timeout ${GUNICORN_TIMEOUT:-120} \
|
||||||
|
--reload \
|
||||||
|
dbapp.wsgi:application
|
||||||
|
fi
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
# admin.py
|
# Django imports
|
||||||
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from import_export.admin import ImportExportActionModelAdmin
|
||||||
|
from leaflet.admin import LeafletGeoAdmin
|
||||||
|
from more_admin_filters import (
|
||||||
|
MultiSelectDropdownFilter,
|
||||||
|
MultiSelectRelatedDropdownFilter,
|
||||||
|
)
|
||||||
|
from rangefilter.filters import (
|
||||||
|
DateRangeQuickSelectListFilterBuilder,
|
||||||
|
NumericRangeFilterBuilder,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Polarization,
|
Polarization,
|
||||||
Modulation,
|
Modulation,
|
||||||
@@ -14,37 +33,61 @@ from .models import (
|
|||||||
ObjItem,
|
ObjItem,
|
||||||
CustomUser
|
CustomUser
|
||||||
)
|
)
|
||||||
from leaflet.admin import LeafletGeoAdmin
|
from .filters import (
|
||||||
from django import forms
|
GeoKupDistanceFilter,
|
||||||
from django.contrib.auth.models import Group
|
GeoValidDistanceFilter,
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
UniqueToggleFilter,
|
||||||
from django.contrib.auth.models import User
|
HasSigmaParameterFilter
|
||||||
from django.contrib.gis.db import models as gis
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from leaflet.forms.widgets import LeafletWidget
|
|
||||||
|
|
||||||
from rangefilter.filters import (
|
|
||||||
DateRangeFilterBuilder,
|
|
||||||
DateTimeRangeFilterBuilder,
|
|
||||||
NumericRangeFilterBuilder,
|
|
||||||
DateRangeQuickSelectListFilterBuilder,
|
|
||||||
)
|
)
|
||||||
from dynamic_raw_id.admin import DynamicRawIDMixin
|
|
||||||
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
|
|
||||||
from import_export.admin import ImportExportActionModelAdmin
|
|
||||||
from .filters import GeoKupDistanceFilter, GeoValidDistanceFilter, UniqueToggleFilter, HasSigmaParameterFilter
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.site_title = "Геолокация"
|
admin.site.site_title = "Геолокация"
|
||||||
admin.site.site_header = "Geolocation"
|
admin.site.site_header = "Geolocation"
|
||||||
admin.site.index_title = "Geo"
|
admin.site.index_title = "Geo"
|
||||||
|
|
||||||
# Unregister default User and Group since we're customizing them
|
# Unregister default User and Group since we're customizing them
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
admin.site.unregister(Group)
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Base Admin Classes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class BaseAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Базовый класс для всех admin моделей.
|
||||||
|
|
||||||
|
Предоставляет общую функциональность:
|
||||||
|
- Кнопки сохранения сверху и снизу
|
||||||
|
- Настройка количества элементов на странице
|
||||||
|
- Автоматическое заполнение полей created_by и updated_by
|
||||||
|
"""
|
||||||
|
save_on_top = True
|
||||||
|
list_per_page = 50
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
"""
|
||||||
|
Автоматически заполняет поля created_by и updated_by при сохранении.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: HTTP запрос
|
||||||
|
obj: Сохраняемый объект модели
|
||||||
|
form: Форма с данными
|
||||||
|
change: True если это редактирование, False если создание
|
||||||
|
"""
|
||||||
|
if not change:
|
||||||
|
# При создании нового объекта устанавливаем created_by
|
||||||
|
if hasattr(obj, 'created_by') and not obj.created_by_id:
|
||||||
|
obj.created_by = getattr(request.user, 'customuser', None)
|
||||||
|
|
||||||
|
# При любом сохранении обновляем updated_by
|
||||||
|
if hasattr(obj, 'updated_by'):
|
||||||
|
obj.updated_by = getattr(request.user, 'customuser', None)
|
||||||
|
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
class CustomUserInline(admin.StackedInline):
|
class CustomUserInline(admin.StackedInline):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
can_delete = False
|
can_delete = False
|
||||||
@@ -130,41 +173,167 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
|
||||||
|
|
||||||
# @admin.register(CustomUser)
|
# ============================================================================
|
||||||
# class CustomUserAdmin(admin.ModelAdmin):
|
# Custom Admin Actions
|
||||||
# list_display = ('user', 'role')
|
# ============================================================================
|
||||||
# list_filter = ('role',)
|
|
||||||
# raw_id_fields = ('user',) # For better performance with large number of users
|
@admin.action(description="Показать выбранные на карте")
|
||||||
|
def show_on_map(modeladmin, request, queryset):
|
||||||
|
"""
|
||||||
|
Action для отображения выбранных Geo объектов на карте.
|
||||||
|
|
||||||
|
Оптимизирован для работы с большим количеством объектов:
|
||||||
|
использует values_list для получения только ID.
|
||||||
|
"""
|
||||||
|
selected_ids = queryset.values_list('id', flat=True)
|
||||||
|
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||||
|
return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Показать выбранные объекты на карте")
|
||||||
|
def show_selected_on_map(modeladmin, request, queryset):
|
||||||
|
"""
|
||||||
|
Action для отображения выбранных ObjItem объектов на карте.
|
||||||
|
|
||||||
|
Оптимизирован для работы с большим количеством объектов:
|
||||||
|
использует values_list для получения только ID.
|
||||||
|
"""
|
||||||
|
selected_ids = queryset.values_list('id', flat=True)
|
||||||
|
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||||
|
return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Экспортировать выбранные объекты в CSV")
|
||||||
|
def export_objects_to_csv(modeladmin, request, queryset):
|
||||||
|
"""
|
||||||
|
Action для экспорта выбранных ObjItem объектов в CSV формат.
|
||||||
|
|
||||||
|
Оптимизирован с использованием select_related и prefetch_related
|
||||||
|
для минимизации количества запросов к БД.
|
||||||
|
"""
|
||||||
|
import csv
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
# Оптимизируем queryset
|
||||||
|
queryset = queryset.select_related(
|
||||||
|
'geo_obj',
|
||||||
|
'created_by__user',
|
||||||
|
'updated_by__user'
|
||||||
|
).prefetch_related(
|
||||||
|
'parameters_obj__id_satellite',
|
||||||
|
'parameters_obj__polarization',
|
||||||
|
'parameters_obj__modulation'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"'
|
||||||
|
response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
writer.writerow([
|
||||||
|
'Название',
|
||||||
|
'Спутник',
|
||||||
|
'Частота (МГц)',
|
||||||
|
'Полоса (МГц)',
|
||||||
|
'Поляризация',
|
||||||
|
'Модуляция',
|
||||||
|
'ОСШ',
|
||||||
|
'Координаты геолокации',
|
||||||
|
'Координаты Кубсата',
|
||||||
|
'Координаты оперативного отдела',
|
||||||
|
'Расстояние Гео-Куб (км)',
|
||||||
|
'Расстояние Гео-Опер (км)',
|
||||||
|
'Дата создания',
|
||||||
|
'Дата обновления'
|
||||||
|
])
|
||||||
|
|
||||||
|
for obj in queryset:
|
||||||
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
|
geo = obj.geo_obj
|
||||||
|
|
||||||
|
# Форматирование координат
|
||||||
|
def format_coords(coords):
|
||||||
|
if not coords:
|
||||||
|
return "-"
|
||||||
|
lon, lat = coords.coords[0], coords.coords[1]
|
||||||
|
lon_str = f"{lon}E" if lon > 0 else f"{abs(lon)}W"
|
||||||
|
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||||
|
return f"{lat_str} {lon_str}"
|
||||||
|
|
||||||
|
writer.writerow([
|
||||||
|
obj.name,
|
||||||
|
param.id_satellite.name if param and param.id_satellite else "-",
|
||||||
|
param.frequency if param else "-",
|
||||||
|
param.freq_range if param else "-",
|
||||||
|
param.polarization.name if param and param.polarization else "-",
|
||||||
|
param.modulation.name if param and param.modulation else "-",
|
||||||
|
param.snr if param else "-",
|
||||||
|
format_coords(geo) if geo and geo.coords else "-",
|
||||||
|
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
||||||
|
format_coords(geo) if geo and geo.coords_valid else "-",
|
||||||
|
round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-",
|
||||||
|
round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-",
|
||||||
|
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
||||||
|
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-"
|
||||||
|
])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Inline Admin Classes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class ParameterObjItemInline(admin.StackedInline):
|
||||||
|
model = ObjItem.parameters_obj.through
|
||||||
|
extra = 0
|
||||||
|
max_num = 1
|
||||||
|
verbose_name = "ВЧ загрузка"
|
||||||
|
verbose_name_plural = "ВЧ загрузки"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Admin Classes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@admin.register(SigmaParMark)
|
@admin.register(SigmaParMark)
|
||||||
class SigmaParMarkAdmin(admin.ModelAdmin):
|
class SigmaParMarkAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели SigmaParMark."""
|
||||||
list_display = ("mark", "timestamp")
|
list_display = ("mark", "timestamp")
|
||||||
search_fields = ("mark",)
|
search_fields = ("mark",)
|
||||||
ordering = ("timestamp",)
|
ordering = ("-timestamp",)
|
||||||
|
list_filter = (
|
||||||
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Polarization)
|
@admin.register(Polarization)
|
||||||
class PolarizationAdmin(admin.ModelAdmin):
|
class PolarizationAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели Polarization."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Modulation)
|
@admin.register(Modulation)
|
||||||
class ModulationAdmin(admin.ModelAdmin):
|
class ModulationAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели Modulation."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SourceType)
|
@admin.register(SourceType)
|
||||||
class SourceTypeAdmin(admin.ModelAdmin):
|
class SourceTypeAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели SourceType."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Standard)
|
@admin.register(Standard)
|
||||||
class StandardAdmin(admin.ModelAdmin):
|
class StandardAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели Standard."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@@ -183,7 +352,15 @@ class SigmaParameterInline(admin.StackedInline):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(Parameter)
|
@admin.register(Parameter)
|
||||||
class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
|
"""
|
||||||
|
Админ-панель для модели Parameter.
|
||||||
|
|
||||||
|
Оптимизирована для работы с большим количеством параметров:
|
||||||
|
- Использует select_related для оптимизации запросов
|
||||||
|
- Предоставляет фильтры по основным характеристикам
|
||||||
|
- Поддерживает импорт/экспорт данных
|
||||||
|
"""
|
||||||
list_display = (
|
list_display = (
|
||||||
"id_satellite",
|
"id_satellite",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -195,7 +372,9 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
"standard",
|
"standard",
|
||||||
"sigma_parameter"
|
"sigma_parameter"
|
||||||
)
|
)
|
||||||
list_display_links = ("frequency", "id_satellite", )
|
list_display_links = ("frequency", "id_satellite")
|
||||||
|
list_select_related = ("polarization", "modulation", "standard", "id_satellite")
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
HasSigmaParameterFilter,
|
HasSigmaParameterFilter,
|
||||||
("id_satellite", MultiSelectRelatedDropdownFilter),
|
("id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
@@ -206,8 +385,9 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
("freq_range", NumericRangeFilterBuilder()),
|
("freq_range", NumericRangeFilterBuilder()),
|
||||||
("snr", NumericRangeFilterBuilder()),
|
("snr", NumericRangeFilterBuilder()),
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"id_satellite",
|
"id_satellite__name",
|
||||||
"frequency",
|
"frequency",
|
||||||
"freq_range",
|
"freq_range",
|
||||||
"bod_velocity",
|
"bod_velocity",
|
||||||
@@ -216,46 +396,52 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
"polarization__name",
|
"polarization__name",
|
||||||
"standard__name",
|
"standard__name",
|
||||||
)
|
)
|
||||||
ordering = ("frequency",)
|
|
||||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite",)
|
ordering = ("-frequency",)
|
||||||
autocomplete_fields = ('objitems',)
|
autocomplete_fields = ("objitems",)
|
||||||
# raw_id_fields = ("id_sigma_parameter", )
|
|
||||||
inlines = [SigmaParameterInline]
|
inlines = [SigmaParameterInline]
|
||||||
# autocomplete_fields = ("id_sigma_parameter", )
|
|
||||||
|
|
||||||
def sigma_parameter(self, obj):
|
def sigma_parameter(self, obj):
|
||||||
|
"""Отображает связанный параметр Sigma."""
|
||||||
sigma_obj = obj.sigma_parameter.all()
|
sigma_obj = obj.sigma_parameter.all()
|
||||||
if sigma_obj:
|
if sigma_obj:
|
||||||
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
||||||
return '-'
|
return "-"
|
||||||
sigma_parameter.short_description = "ВЧ sigma"
|
sigma_parameter.short_description = "ВЧ sigma"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SigmaParameter)
|
@admin.register(SigmaParameter)
|
||||||
class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
|
"""
|
||||||
|
Админ-панель для модели SigmaParameter.
|
||||||
|
|
||||||
|
Оптимизирована для работы с параметрами Sigma:
|
||||||
|
- Использует select_related и prefetch_related для оптимизации
|
||||||
|
- Предоставляет фильтры по основным характеристикам
|
||||||
|
- Поддерживает импорт/экспорт данных
|
||||||
|
"""
|
||||||
list_display = (
|
list_display = (
|
||||||
"id_satellite",
|
"id_satellite",
|
||||||
# "status",
|
|
||||||
"frequency",
|
"frequency",
|
||||||
"transfer_frequency",
|
"transfer_frequency",
|
||||||
"freq_range",
|
"freq_range",
|
||||||
# "power",
|
|
||||||
"polarization",
|
"polarization",
|
||||||
"modulation",
|
"modulation",
|
||||||
"bod_velocity",
|
"bod_velocity",
|
||||||
"snr",
|
"snr",
|
||||||
# "standard",
|
|
||||||
"parameter",
|
"parameter",
|
||||||
# "packets",
|
|
||||||
"datetime_begin",
|
"datetime_begin",
|
||||||
"datetime_end",
|
"datetime_end",
|
||||||
)
|
)
|
||||||
|
list_display_links = ("id_satellite",)
|
||||||
|
list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization")
|
||||||
|
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"datetime_begin",
|
"datetime_begin",
|
||||||
"datetime_end",
|
"datetime_end",
|
||||||
"transfer_frequency"
|
"transfer_frequency"
|
||||||
)
|
)
|
||||||
list_display_links = ("id_satellite",)
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("id_satellite__name", MultiSelectDropdownFilter),
|
("id_satellite__name", MultiSelectDropdownFilter),
|
||||||
("modulation__name", MultiSelectDropdownFilter),
|
("modulation__name", MultiSelectDropdownFilter),
|
||||||
@@ -263,7 +449,10 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
("frequency", NumericRangeFilterBuilder()),
|
("frequency", NumericRangeFilterBuilder()),
|
||||||
("freq_range", NumericRangeFilterBuilder()),
|
("freq_range", NumericRangeFilterBuilder()),
|
||||||
("snr", NumericRangeFilterBuilder()),
|
("snr", NumericRangeFilterBuilder()),
|
||||||
|
("datetime_begin", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
("datetime_end", DateRangeQuickSelectListFilterBuilder()),
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"id_satellite__name",
|
"id_satellite__name",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -273,45 +462,63 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
"modulation__name",
|
"modulation__name",
|
||||||
"standard__name",
|
"standard__name",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ('mark',)
|
|
||||||
ordering = ("frequency",)
|
autocomplete_fields = ("mark",)
|
||||||
list_select_related = ("modulation", "standard", "id_satellite", "parameter")
|
ordering = ("-frequency",)
|
||||||
prefetch_related = ("mark",)
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Оптимизированный queryset с prefetch_related для mark."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
return qs.prefetch_related("mark")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Satellite)
|
@admin.register(Satellite)
|
||||||
class SatelliteAdmin(admin.ModelAdmin):
|
class SatelliteAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели Satellite."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Mirror)
|
@admin.register(Mirror)
|
||||||
class MirrorAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
|
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Geo)
|
@admin.register(Geo)
|
||||||
class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||||
|
"""
|
||||||
|
Админ-панель для модели Geo с поддержкой карты Leaflet.
|
||||||
|
|
||||||
|
Оптимизирована для работы с геоданными:
|
||||||
|
- Использует prefetch_related для оптимизации запросов к mirrors
|
||||||
|
- Предоставляет фильтры по зеркалам, локации и дате
|
||||||
|
- Поддерживает импорт/экспорт данных
|
||||||
|
- Интегрирована с Leaflet для отображения на карте
|
||||||
|
"""
|
||||||
form = LocationForm
|
form = LocationForm
|
||||||
|
|
||||||
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Основная информация", {
|
("Основная информация", {
|
||||||
"fields": ("mirrors", "location", "distance_coords_kup",
|
"fields": ("mirrors", "location", "distance_coords_kup",
|
||||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
|
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment")
|
||||||
}),
|
}),
|
||||||
("Координаты: геолокация", {
|
("Координаты: геолокация", {
|
||||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
"fields": ("longitude_geo", "latitude_geo", "coords")
|
||||||
}),
|
}),
|
||||||
("Координаты: Кубсат", {
|
("Координаты: Кубсат", {
|
||||||
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
||||||
}),
|
}),
|
||||||
("Координаты: Оперативный отдел", {
|
("Координаты: Оперативный отдел", {
|
||||||
"fields": ("longitude_valid", "latitude_valid", "coords_valid"),
|
"fields": ("longitude_valid", "latitude_valid", "coords_valid")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"formatted_timestamp",
|
"formatted_timestamp",
|
||||||
"location",
|
"location",
|
||||||
@@ -321,35 +528,41 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
|||||||
"valid_coords",
|
"valid_coords",
|
||||||
"is_average",
|
"is_average",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ('mirrors',)
|
|
||||||
list_display_links = ("formatted_timestamp",)
|
list_display_links = ("formatted_timestamp",)
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("mirrors", MultiSelectRelatedDropdownFilter),
|
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||||
"is_average",
|
"is_average",
|
||||||
("location", MultiSelectDropdownFilter),
|
("location", MultiSelectDropdownFilter),
|
||||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"mirrors__name",
|
"mirrors__name",
|
||||||
"location",
|
"location",
|
||||||
"coords",
|
|
||||||
"coords_kupsat",
|
|
||||||
"coords_valid"
|
|
||||||
)
|
)
|
||||||
prefetch_related = ("mirrors", )
|
|
||||||
|
|
||||||
|
autocomplete_fields = ("mirrors",)
|
||||||
|
ordering = ("-timestamp",)
|
||||||
|
actions = [show_on_map]
|
||||||
|
|
||||||
settings_overrides = {
|
settings_overrides = {
|
||||||
'DEFAULT_CENTER': (55.7558, 37.6173),
|
'DEFAULT_CENTER': (55.7558, 37.6173),
|
||||||
'DEFAULT_ZOOM': 12,
|
'DEFAULT_ZOOM': 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
return qs.prefetch_related("mirrors")
|
||||||
|
|
||||||
def mirrors_names(self, obj):
|
def mirrors_names(self, obj):
|
||||||
|
"""Отображает список зеркал через запятую."""
|
||||||
return ", ".join(m.name for m in obj.mirrors.all())
|
return ", ".join(m.name for m in obj.mirrors.all())
|
||||||
mirrors_names.short_description = "Зеркала"
|
mirrors_names.short_description = "Зеркала"
|
||||||
|
|
||||||
def formatted_timestamp(self, obj):
|
def formatted_timestamp(self, obj):
|
||||||
|
"""Форматирует timestamp в локальное время."""
|
||||||
if not obj.timestamp:
|
if not obj.timestamp:
|
||||||
return ""
|
return ""
|
||||||
local_time = timezone.localtime(obj.timestamp)
|
local_time = timezone.localtime(obj.timestamp)
|
||||||
@@ -358,6 +571,9 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
|||||||
formatted_timestamp.admin_order_field = "timestamp"
|
formatted_timestamp.admin_order_field = "timestamp"
|
||||||
|
|
||||||
def geo_coords(self, obj):
|
def geo_coords(self, obj):
|
||||||
|
"""Отображает координаты геолокации в формате широта/долгота."""
|
||||||
|
if not obj.coords:
|
||||||
|
return "-"
|
||||||
longitude = obj.coords.coords[0]
|
longitude = obj.coords.coords[0]
|
||||||
latitude = obj.coords.coords[1]
|
latitude = obj.coords.coords[1]
|
||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
@@ -366,6 +582,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
|||||||
geo_coords.short_description = "Координаты геолокации"
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
|
|
||||||
def kupsat_coords(self, obj):
|
def kupsat_coords(self, obj):
|
||||||
|
"""Отображает координаты Кубсата в формате широта/долгота."""
|
||||||
if obj.coords_kupsat is None:
|
if obj.coords_kupsat is None:
|
||||||
return "-"
|
return "-"
|
||||||
longitude = obj.coords_kupsat.coords[0]
|
longitude = obj.coords_kupsat.coords[0]
|
||||||
@@ -376,6 +593,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
|||||||
kupsat_coords.short_description = "Координаты Кубсата"
|
kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
def valid_coords(self, obj):
|
def valid_coords(self, obj):
|
||||||
|
"""Отображает координаты оперативного отдела в формате широта/долгота."""
|
||||||
if obj.coords_valid is None:
|
if obj.coords_valid is None:
|
||||||
return "-"
|
return "-"
|
||||||
longitude = obj.coords_valid.coords[0]
|
longitude = obj.coords_valid.coords[0]
|
||||||
@@ -385,38 +603,20 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
|||||||
return f"{lat} {lon}"
|
return f"{lat} {lon}"
|
||||||
valid_coords.short_description = "Координаты оперативного отдела"
|
valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
def show_on_map(modeladmin, request, queryset):
|
|
||||||
# Получаем список ID выбранных объектов
|
|
||||||
selected_ids = queryset.values_list('id', flat=True)
|
|
||||||
# Формируем строку вида "1,2,3"
|
|
||||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
|
||||||
# Перенаправляем на ваш кастомный view с картой
|
|
||||||
return redirect(reverse('admin_show_map') + f'?ids={ids_str}')
|
|
||||||
|
|
||||||
show_on_map.short_description = "Показать выбранные на карте"
|
|
||||||
|
|
||||||
|
|
||||||
def show_selected_on_map(modeladmin, request, queryset):
|
|
||||||
# Получаем список ID выбранных объектов
|
|
||||||
selected_ids = queryset.values_list('id', flat=True)
|
|
||||||
# Формируем строку вида "1,2,3"
|
|
||||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
|
||||||
# Перенаправляем на view, который будет отображать карту с выбранными объектами
|
|
||||||
return redirect(reverse('show_selected_objects_map') + f'?ids={ids_str}')
|
|
||||||
|
|
||||||
show_selected_on_map.short_description = "Показать выбранные объекты на карте"
|
|
||||||
show_selected_on_map.icon = 'map'
|
|
||||||
|
|
||||||
class ParameterObjItemInline(admin.StackedInline):
|
|
||||||
model = ObjItem.parameters_obj.through
|
|
||||||
extra = 0
|
|
||||||
max_num = 1
|
|
||||||
verbose_name = "ВЧ загрузка"
|
|
||||||
verbose_name_plural = "ВЧ загрузки"
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ObjItem)
|
@admin.register(ObjItem)
|
||||||
class ObjectAdmin(admin.ModelAdmin):
|
class ObjItemAdmin(BaseAdmin):
|
||||||
|
"""
|
||||||
|
Админ-панель для модели ObjItem.
|
||||||
|
|
||||||
|
Оптимизирована для работы с большим количеством объектов:
|
||||||
|
- Использует select_related и prefetch_related для оптимизации запросов
|
||||||
|
- Предоставляет фильтры по основным параметрам
|
||||||
|
- Поддерживает поиск по имени, координатам и частоте
|
||||||
|
- Включает кастомные actions для отображения на карте
|
||||||
|
"""
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
"sat_name",
|
"sat_name",
|
||||||
@@ -436,6 +636,8 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
list_display_links = ("name",)
|
list_display_links = ("name",)
|
||||||
|
list_select_related = ("geo_obj", "created_by__user", "updated_by__user")
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
UniqueToggleFilter,
|
UniqueToggleFilter,
|
||||||
("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
@@ -445,39 +647,53 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
("parameters_obj__modulation", MultiSelectRelatedDropdownFilter),
|
("parameters_obj__modulation", MultiSelectRelatedDropdownFilter),
|
||||||
("parameters_obj__polarization", MultiSelectRelatedDropdownFilter),
|
("parameters_obj__polarization", MultiSelectRelatedDropdownFilter),
|
||||||
GeoKupDistanceFilter,
|
GeoKupDistanceFilter,
|
||||||
GeoValidDistanceFilter
|
GeoValidDistanceFilter,
|
||||||
|
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
("updated_at", DateRangeQuickSelectListFilterBuilder()),
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"name",
|
"name",
|
||||||
"geo_obj__coords",
|
"geo_obj__location",
|
||||||
"parameters_obj__frequency",
|
"parameters_obj__frequency",
|
||||||
|
"parameters_obj__id_satellite__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
ordering = ("name",)
|
ordering = ("-updated_at",)
|
||||||
inlines = [ParameterObjItemInline, GeoInline]
|
inlines = [ParameterObjItemInline, GeoInline]
|
||||||
actions = [show_on_map, show_selected_on_map]
|
actions = [show_selected_on_map, export_objects_to_csv]
|
||||||
readonly_fields = ('created_at', 'created_by', 'updated_at', 'updated_by')
|
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
("Основная информация", {
|
||||||
|
"fields": ("name",)
|
||||||
|
}),
|
||||||
|
("Метаданные", {
|
||||||
|
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||||
|
"classes": ("collapse",)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
"""
|
||||||
|
Оптимизированный queryset с использованием select_related и prefetch_related.
|
||||||
|
|
||||||
|
Загружает связанные объекты одним запросом для улучшения производительности.
|
||||||
|
"""
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.select_related('geo_obj', 'created_by', 'updated_by').prefetch_related(
|
return qs.select_related(
|
||||||
'parameters_obj__id_satellite',
|
"geo_obj",
|
||||||
'parameters_obj__polarization',
|
"created_by__user",
|
||||||
'parameters_obj__modulation',
|
"updated_by__user"
|
||||||
'parameters_obj__standard'
|
).prefetch_related(
|
||||||
|
"parameters_obj__id_satellite",
|
||||||
|
"parameters_obj__polarization",
|
||||||
|
"parameters_obj__modulation",
|
||||||
|
"parameters_obj__standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
return self.readonly_fields
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
|
||||||
if not change:
|
|
||||||
if not obj.created_by_id:
|
|
||||||
obj.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
|
||||||
obj.updated_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
|
||||||
super().save_model(request, obj, form, change)
|
|
||||||
|
|
||||||
def sat_name(self, obj):
|
def sat_name(self, obj):
|
||||||
|
"""Отображает название спутника из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param and param.id_satellite:
|
if param and param.id_satellite:
|
||||||
return param.id_satellite.name
|
return param.id_satellite.name
|
||||||
@@ -486,7 +702,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
sat_name.admin_order_field = "parameters_obj__id_satellite__name"
|
sat_name.admin_order_field = "parameters_obj__id_satellite__name"
|
||||||
|
|
||||||
def freq(self, obj):
|
def freq(self, obj):
|
||||||
# param = obj.parameters_obj.first()
|
"""Отображает частоту из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param:
|
if param:
|
||||||
return param.frequency
|
return param.frequency
|
||||||
@@ -495,6 +711,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
freq.admin_order_field = "parameters_obj__frequency"
|
freq.admin_order_field = "parameters_obj__frequency"
|
||||||
|
|
||||||
def distance_geo_kup(self, obj):
|
def distance_geo_kup(self, obj):
|
||||||
|
"""Отображает расстояние между геолокацией и Кубсатом."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or geo.distance_coords_kup is None:
|
if not geo or geo.distance_coords_kup is None:
|
||||||
return "-"
|
return "-"
|
||||||
@@ -502,6 +719,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
distance_geo_kup.short_description = "Гео-куб, км"
|
distance_geo_kup.short_description = "Гео-куб, км"
|
||||||
|
|
||||||
def distance_geo_valid(self, obj):
|
def distance_geo_valid(self, obj):
|
||||||
|
"""Отображает расстояние между геолокацией и оперативным отделом."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or geo.distance_coords_valid is None:
|
if not geo or geo.distance_coords_valid is None:
|
||||||
return "-"
|
return "-"
|
||||||
@@ -509,6 +727,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
distance_geo_valid.short_description = "Гео-опер, км"
|
distance_geo_valid.short_description = "Гео-опер, км"
|
||||||
|
|
||||||
def distance_kup_valid(self, obj):
|
def distance_kup_valid(self, obj):
|
||||||
|
"""Отображает расстояние между Кубсатом и оперативным отделом."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or geo.distance_kup_valid is None:
|
if not geo or geo.distance_kup_valid is None:
|
||||||
return "-"
|
return "-"
|
||||||
@@ -516,7 +735,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
distance_kup_valid.short_description = "Куб-опер, км"
|
distance_kup_valid.short_description = "Куб-опер, км"
|
||||||
|
|
||||||
def pol(self, obj):
|
def pol(self, obj):
|
||||||
# Get the first parameter associated with this objitem to display polarization
|
"""Отображает поляризацию из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param and param.polarization:
|
if param and param.polarization:
|
||||||
return param.polarization.name
|
return param.polarization.name
|
||||||
@@ -524,7 +743,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
pol.short_description = "Поляризация"
|
pol.short_description = "Поляризация"
|
||||||
|
|
||||||
def freq_range(self, obj):
|
def freq_range(self, obj):
|
||||||
# Get the first parameter associated with this objitem to display freq_range
|
"""Отображает полосу частот из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param:
|
if param:
|
||||||
return param.freq_range
|
return param.freq_range
|
||||||
@@ -533,7 +752,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
freq_range.admin_order_field = "parameters_obj__freq_range"
|
freq_range.admin_order_field = "parameters_obj__freq_range"
|
||||||
|
|
||||||
def bod_velocity(self, obj):
|
def bod_velocity(self, obj):
|
||||||
# Get the first parameter associated with this objitem to display bod_velocity
|
"""Отображает символьную скорость из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param:
|
if param:
|
||||||
return param.bod_velocity
|
return param.bod_velocity
|
||||||
@@ -541,7 +760,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
bod_velocity.short_description = "Сим. v, БОД"
|
bod_velocity.short_description = "Сим. v, БОД"
|
||||||
|
|
||||||
def modulation(self, obj):
|
def modulation(self, obj):
|
||||||
# Get the first parameter associated with this objitem to display modulation
|
"""Отображает модуляцию из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param and param.modulation:
|
if param and param.modulation:
|
||||||
return param.modulation.name
|
return param.modulation.name
|
||||||
@@ -549,7 +768,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
modulation.short_description = "Модуляция"
|
modulation.short_description = "Модуляция"
|
||||||
|
|
||||||
def snr(self, obj):
|
def snr(self, obj):
|
||||||
# Get the first parameter associated with this objitem to display snr
|
"""Отображает отношение сигнал/шум из связанного параметра."""
|
||||||
param = next(iter(obj.parameters_obj.all()), None)
|
param = next(iter(obj.parameters_obj.all()), None)
|
||||||
if param:
|
if param:
|
||||||
return param.snr
|
return param.snr
|
||||||
@@ -557,6 +776,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
snr.short_description = "ОСШ"
|
snr.short_description = "ОСШ"
|
||||||
|
|
||||||
def geo_coords(self, obj):
|
def geo_coords(self, obj):
|
||||||
|
"""Отображает координаты геолокации в формате широта/долгота."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or not geo.coords:
|
if not geo or not geo.coords:
|
||||||
return "-"
|
return "-"
|
||||||
@@ -569,6 +789,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
geo_coords.admin_order_field = "geo_obj__coords"
|
geo_coords.admin_order_field = "geo_obj__coords"
|
||||||
|
|
||||||
def kupsat_coords(self, obj):
|
def kupsat_coords(self, obj):
|
||||||
|
"""Отображает координаты Кубсата в формате широта/долгота."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or not geo.coords_kupsat:
|
if not geo or not geo.coords_kupsat:
|
||||||
return "-"
|
return "-"
|
||||||
@@ -580,6 +801,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
|||||||
kupsat_coords.short_description = "Координаты Кубсата"
|
kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
def valid_coords(self, obj):
|
def valid_coords(self, obj):
|
||||||
|
"""Отображает координаты оперативного отдела в формате широта/долгота."""
|
||||||
geo = obj.geo_obj
|
geo = obj.geo_obj
|
||||||
if not geo or not geo.coords_valid:
|
if not geo or not geo.coords_valid:
|
||||||
return "-"
|
return "-"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from .models import ObjItem
|
# Third-party imports
|
||||||
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from .models import ObjItem
|
||||||
|
|
||||||
def get_clusters(coords: list[tuple[float, float]]):
|
def get_clusters(coords: list[tuple[float, float]]):
|
||||||
coords = np.radians(coords)
|
coords = np.radians(coords)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
# Django imports
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
|
|
||||||
|
# Local imports
|
||||||
from .models import ObjItem
|
from .models import ObjItem
|
||||||
|
|
||||||
class GeoKupDistanceFilter(SimpleListFilter):
|
class GeoKupDistanceFilter(SimpleListFilter):
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
# Django imports
|
||||||
from django import forms
|
from django import forms
|
||||||
from .models import Satellite, Polarization, ObjItem, Parameter, Geo, Modulation, Standard
|
|
||||||
|
# Local imports
|
||||||
|
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
||||||
|
|
||||||
class UploadFileForm(forms.Form):
|
class UploadFileForm(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
|
|||||||
19
dbapp/mainapp/migrations/0005_alter_geo_objitem.py
Normal file
19
dbapp/mainapp/migrations/0005_alter_geo_objitem.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-07 19:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='objitem',
|
||||||
|
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.contrib.gis.db.models.functions
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.db.models.expressions
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0005_alter_geo_objitem'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='customuser',
|
||||||
|
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='geo',
|
||||||
|
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='mirror',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='modulation',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='objitem',
|
||||||
|
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='polarization',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='satellite',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='sigmaparmark',
|
||||||
|
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='sourcetype',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='standard',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='role',
|
||||||
|
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='user',
|
||||||
|
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='coords',
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='coords_kupsat',
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='coords_valid',
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_coords_kup',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_kup_valid',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='is_average',
|
||||||
|
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='location',
|
||||||
|
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='mirrors',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='objitem',
|
||||||
|
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mirror',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulation',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='bod_velocity',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='freq_range',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='frequency',
|
||||||
|
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='snr',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='polarization',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='satellite',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='satellite',
|
||||||
|
name='norad',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='bod_velocity',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='datetime_begin',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='datetime_end',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='freq_range',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='frequency',
|
||||||
|
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='packets',
|
||||||
|
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='power',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='snr',
|
||||||
|
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='transfer',
|
||||||
|
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparmark',
|
||||||
|
name='mark',
|
||||||
|
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparmark',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sourcetype',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sourcetype',
|
||||||
|
name='objitem',
|
||||||
|
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='standard',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='geo',
|
||||||
|
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='geo',
|
||||||
|
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='objitem',
|
||||||
|
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='objitem',
|
||||||
|
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='objitem',
|
||||||
|
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
229
dbapp/mainapp/mixins.py
Normal file
229
dbapp/mainapp/mixins.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
"""
|
||||||
|
Переиспользуемые миксины для представлений mainapp.
|
||||||
|
|
||||||
|
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
|
||||||
|
включая проверку прав доступа, обработку координат и сообщений.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRequiredMixin(UserPassesTestMixin):
|
||||||
|
"""
|
||||||
|
Mixin для проверки роли пользователя.
|
||||||
|
|
||||||
|
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
required_roles (list): Список допустимых ролей для доступа.
|
||||||
|
По умолчанию ['admin', 'moderator'].
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class MyView(RoleRequiredMixin, View):
|
||||||
|
required_roles = ['admin', 'moderator']
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Только пользователи с ролью admin или moderator могут получить доступ
|
||||||
|
return render(request, 'template.html')
|
||||||
|
"""
|
||||||
|
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def test_func(self) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, имеет ли пользователь требуемую роль.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
|
||||||
|
"""
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not hasattr(self.request.user, "customuser"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.request.user.customuser.role in self.required_roles
|
||||||
|
|
||||||
|
|
||||||
|
class CoordinateProcessingMixin:
|
||||||
|
"""
|
||||||
|
Mixin для обработки координат из POST данных форм.
|
||||||
|
|
||||||
|
Предоставляет методы для извлечения и обработки координат различных типов
|
||||||
|
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class MyFormView(CoordinateProcessingMixin, FormView):
|
||||||
|
def form_valid(self, form):
|
||||||
|
geo_instance = Geo()
|
||||||
|
self.process_coordinates(geo_instance)
|
||||||
|
geo_instance.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||||
|
"""
|
||||||
|
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||||
|
|
||||||
|
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
|
||||||
|
и устанавливает соответствующие поля объекта Geo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geo_instance: Экземпляр модели Geo для обновления координат.
|
||||||
|
prefix (str): Префикс для полей формы (по умолчанию 'geo').
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Метод ожидает следующие поля в request.POST:
|
||||||
|
- geo_longitude, geo_latitude: координаты геолокации
|
||||||
|
- kupsat_longitude, kupsat_latitude: координаты кубсата
|
||||||
|
- valid_longitude, valid_latitude: координаты оперативников
|
||||||
|
"""
|
||||||
|
# Обрабатываем координаты геолокации
|
||||||
|
geo_coords = self._extract_coordinates("geo")
|
||||||
|
if geo_coords:
|
||||||
|
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
||||||
|
|
||||||
|
# Обрабатываем координаты Кубсата
|
||||||
|
kupsat_coords = self._extract_coordinates("kupsat")
|
||||||
|
if kupsat_coords:
|
||||||
|
geo_instance.coords_kupsat = Point(
|
||||||
|
kupsat_coords[0], kupsat_coords[1], srid=4326
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обрабатываем координаты оперативников
|
||||||
|
valid_coords = self._extract_coordinates("valid")
|
||||||
|
if valid_coords:
|
||||||
|
geo_instance.coords_valid = Point(
|
||||||
|
valid_coords[0], valid_coords[1], srid=4326
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||||
|
"""
|
||||||
|
Извлекает координаты указанного типа из POST данных.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
|
||||||
|
если координаты не найдены или невалидны.
|
||||||
|
"""
|
||||||
|
longitude_key = f"{coord_type}_longitude"
|
||||||
|
latitude_key = f"{coord_type}_latitude"
|
||||||
|
|
||||||
|
longitude = self.request.POST.get(longitude_key)
|
||||||
|
latitude = self.request.POST.get(latitude_key)
|
||||||
|
|
||||||
|
if longitude and latitude:
|
||||||
|
try:
|
||||||
|
return (float(longitude), float(latitude))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_timestamp(self, geo_instance) -> None:
|
||||||
|
"""
|
||||||
|
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geo_instance: Экземпляр модели Geo для обновления timestamp.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Метод ожидает следующие поля в request.POST:
|
||||||
|
- timestamp_date: дата в формате YYYY-MM-DD
|
||||||
|
- timestamp_time: время в формате HH:MM
|
||||||
|
"""
|
||||||
|
timestamp_date = self.request.POST.get("timestamp_date")
|
||||||
|
timestamp_time = self.request.POST.get("timestamp_time")
|
||||||
|
|
||||||
|
if timestamp_date and timestamp_time:
|
||||||
|
try:
|
||||||
|
naive_datetime = datetime.strptime(
|
||||||
|
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
geo_instance.timestamp = naive_datetime
|
||||||
|
except ValueError:
|
||||||
|
# Если формат даты/времени неверный, пропускаем
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FormMessageMixin:
|
||||||
|
"""
|
||||||
|
Mixin для стандартизации сообщений об успехе и ошибках в формах.
|
||||||
|
|
||||||
|
Автоматически добавляет сообщения пользователю при успешной или неуспешной
|
||||||
|
обработке формы.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
success_message (str): Сообщение при успешной обработке формы.
|
||||||
|
error_message (str): Сообщение при ошибке обработки формы.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class MyFormView(FormMessageMixin, FormView):
|
||||||
|
success_message = "Данные успешно сохранены!"
|
||||||
|
error_message = "Ошибка при сохранении данных"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# Автоматически добавит success_message
|
||||||
|
return super().form_valid(form)
|
||||||
|
"""
|
||||||
|
|
||||||
|
success_message = "Операция выполнена успешно"
|
||||||
|
error_message = "Произошла ошибка при обработке формы"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
Обрабатывает валидную форму и добавляет сообщение об успехе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form: Валидная форма Django.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: Результат обработки родительского метода form_valid.
|
||||||
|
"""
|
||||||
|
if self.success_message:
|
||||||
|
messages.success(self.request, self.success_message)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
"""
|
||||||
|
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form: Невалидная форма Django.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: Результат обработки родительского метода form_invalid.
|
||||||
|
"""
|
||||||
|
if self.error_message:
|
||||||
|
messages.error(self.request, self.error_message)
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_success_message(self) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает сообщение об успехе.
|
||||||
|
|
||||||
|
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сообщение об успехе.
|
||||||
|
"""
|
||||||
|
return self.success_message
|
||||||
|
|
||||||
|
def get_error_message(self) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает сообщение об ошибке.
|
||||||
|
|
||||||
|
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сообщение об ошибке.
|
||||||
|
"""
|
||||||
|
return self.error_message
|
||||||
@@ -1,61 +1,122 @@
|
|||||||
from django.db import models
|
# Django imports
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.gis.db import models as gis
|
from django.contrib.gis.db import models as gis
|
||||||
from django.contrib.gis.db.models import functions
|
from django.contrib.gis.db.models import functions
|
||||||
from django.db.models import F, ExpressionWrapper
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import ExpressionWrapper, F
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
def get_default_polarization():
|
def get_default_polarization():
|
||||||
obj, created = Polarization.objects.get_or_create(
|
obj, created = Polarization.objects.get_or_create(name="-")
|
||||||
name="-"
|
|
||||||
)
|
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
def get_default_modulation():
|
def get_default_modulation():
|
||||||
obj, created = Modulation.objects.get_or_create(
|
obj, created = Modulation.objects.get_or_create(name="-")
|
||||||
name="-"
|
|
||||||
)
|
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
def get_default_standard():
|
def get_default_standard():
|
||||||
obj, created = Standard.objects.get_or_create(
|
obj, created = Standard.objects.get_or_create(name="-")
|
||||||
name="-"
|
|
||||||
)
|
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
class CustomUser(models.Model):
|
class CustomUser(models.Model):
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
"""
|
||||||
|
Расширенная модель пользователя с ролями.
|
||||||
|
|
||||||
|
Добавляет систему ролей к стандартной модели User Django.
|
||||||
|
"""
|
||||||
|
|
||||||
ROLE_CHOICES = [
|
ROLE_CHOICES = [
|
||||||
('admin', 'Администратор'),
|
("admin", "Администратор"),
|
||||||
('moderator', 'Модератор'),
|
("moderator", "Модератор"),
|
||||||
('user', 'Пользователь'),
|
("user", "Пользователь"),
|
||||||
]
|
]
|
||||||
|
|
||||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user', verbose_name='Роль пользователя')
|
# Связи
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Пользователь",
|
||||||
|
help_text="Связанный пользователь Django",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
role = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ROLE_CHOICES,
|
||||||
|
default="user",
|
||||||
|
verbose_name="Роль пользователя",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Роль пользователя в системе",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.first_name} {self.user.last_name}" if self.user.first_name and self.user.last_name else self.user.username
|
return (
|
||||||
|
f"{self.user.first_name} {self.user.last_name}"
|
||||||
|
if self.user.first_name and self.user.last_name
|
||||||
|
else self.user.username
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Пользователь"
|
verbose_name = "Пользователь"
|
||||||
verbose_name_plural = "Пользователи"
|
verbose_name_plural = "Пользователи"
|
||||||
|
ordering = ["user__username"]
|
||||||
|
|
||||||
|
|
||||||
class SigmaParMark(models.Model):
|
class SigmaParMark(models.Model):
|
||||||
mark = models.BooleanField(null=True, blank=True, verbose_name="Наличие сигнала")
|
"""
|
||||||
timestamp = models.DateTimeField(null=True, blank=True, verbose_name="Время")
|
Модель отметки о наличии сигнала.
|
||||||
|
|
||||||
|
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
mark = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Наличие сигнала",
|
||||||
|
help_text="True - сигнал обнаружен, False - сигнал отсутствует",
|
||||||
|
)
|
||||||
|
timestamp = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Время",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Время фиксации отметки",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
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 "Отметка без времени"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Отметка"
|
verbose_name = "Отметка"
|
||||||
verbose_name_plural = "Отметки"
|
verbose_name_plural = "Отметки"
|
||||||
|
ordering = ["-timestamp"]
|
||||||
|
|
||||||
|
|
||||||
class Mirror(models.Model):
|
class Mirror(models.Model):
|
||||||
name = models.CharField(max_length=30, unique=True, verbose_name="Имя зеркала")
|
"""
|
||||||
|
Модель зеркала антенны.
|
||||||
|
|
||||||
|
Представляет физическое зеркало антенны для приема спутникового сигнала.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Имя зеркала",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Уникальное название зеркала антенны",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -63,9 +124,24 @@ class Mirror(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Зеркало"
|
verbose_name = "Зеркало"
|
||||||
verbose_name_plural = "Зеркала"
|
verbose_name_plural = "Зеркала"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class Polarization(models.Model):
|
class Polarization(models.Model):
|
||||||
name = models.CharField(max_length=20, unique=True, verbose_name="Поляризация")
|
"""
|
||||||
|
Модель поляризации сигнала.
|
||||||
|
|
||||||
|
Определяет тип поляризации спутникового сигнала (H, V, L, R и т.д.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Поляризация",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -73,10 +149,24 @@ class Polarization(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Поляризация"
|
verbose_name = "Поляризация"
|
||||||
verbose_name_plural = "Поляризация"
|
verbose_name_plural = "Поляризация"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class Modulation(models.Model):
|
class Modulation(models.Model):
|
||||||
name = models.CharField(max_length=20, unique=True, verbose_name="Модуляция", db_index=True)
|
"""
|
||||||
|
Модель типа модуляции сигнала.
|
||||||
|
|
||||||
|
Определяет схему модуляции (QPSK, 8PSK, 16APSK и т.д.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Модуляция",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -84,10 +174,24 @@ class Modulation(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Модуляция"
|
verbose_name = "Модуляция"
|
||||||
verbose_name_plural = "Модуляции"
|
verbose_name_plural = "Модуляции"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class Standard(models.Model):
|
class Standard(models.Model):
|
||||||
name = models.CharField(max_length=20, unique=True, verbose_name="Стандарт")
|
"""
|
||||||
|
Модель стандарта передачи данных.
|
||||||
|
|
||||||
|
Определяет стандарт передачи (DVB-S, DVB-S2, DVB-S2X и т.д.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Стандарт",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -95,11 +199,30 @@ class Standard(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Стандарт"
|
verbose_name = "Стандарт"
|
||||||
verbose_name_plural = "Стандарты"
|
verbose_name_plural = "Стандарты"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class Satellite(models.Model):
|
class Satellite(models.Model):
|
||||||
name = models.CharField(max_length=100, unique=True, verbose_name="Имя спутника", db_index=True)
|
"""
|
||||||
norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID")
|
Модель спутника.
|
||||||
|
|
||||||
|
Представляет спутник связи с его основными характеристиками.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Имя спутника",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Название спутника",
|
||||||
|
)
|
||||||
|
norad = models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="NORAD ID",
|
||||||
|
help_text="Идентификатор NORAD для отслеживания спутника",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -107,69 +230,250 @@ class Satellite(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Спутник"
|
verbose_name = "Спутник"
|
||||||
verbose_name_plural = "Спутники"
|
verbose_name_plural = "Спутники"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemQuerySet(models.QuerySet):
|
||||||
|
"""Custom QuerySet для модели ObjItem с оптимизированными запросами"""
|
||||||
|
|
||||||
|
def with_related(self):
|
||||||
|
"""Оптимизирует запросы, загружая связанные объекты"""
|
||||||
|
return self.select_related(
|
||||||
|
"geo_obj",
|
||||||
|
"updated_by__user",
|
||||||
|
"created_by__user",
|
||||||
|
"source_type_obj",
|
||||||
|
).prefetch_related(
|
||||||
|
"parameters_obj__id_satellite",
|
||||||
|
"parameters_obj__polarization",
|
||||||
|
"parameters_obj__modulation",
|
||||||
|
"parameters_obj__standard",
|
||||||
|
)
|
||||||
|
|
||||||
|
def recent(self, days=30):
|
||||||
|
"""Возвращает объекты, созданные за последние N дней"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
cutoff_date = timezone.now() - timedelta(days=days)
|
||||||
|
return self.filter(created_at__gte=cutoff_date)
|
||||||
|
|
||||||
|
def by_user(self, user):
|
||||||
|
"""Возвращает объекты, созданные указанным пользователем"""
|
||||||
|
return self.filter(created_by=user)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemManager(models.Manager):
|
||||||
|
"""Custom Manager для модели ObjItem"""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return ObjItemQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
|
def with_related(self):
|
||||||
|
"""Возвращает queryset с предзагруженными связанными объектами"""
|
||||||
|
return self.get_queryset().with_related()
|
||||||
|
|
||||||
|
def recent(self, days=30):
|
||||||
|
"""Возвращает недавно созданные объекты"""
|
||||||
|
return self.get_queryset().recent(days)
|
||||||
|
|
||||||
|
def by_user(self, user):
|
||||||
|
"""Возвращает объекты пользователя"""
|
||||||
|
return self.get_queryset().by_user(user)
|
||||||
|
|
||||||
|
|
||||||
class ObjItem(models.Model):
|
class ObjItem(models.Model):
|
||||||
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
|
"""
|
||||||
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
|
Модель объекта (источника сигнала).
|
||||||
# id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
|
|
||||||
# id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
|
|
||||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
|
|
||||||
# id_source_type = models.ForeignKey(SourceType, on_delete=models.SET_NULL, related_name="objitems", verbose_name='Тип источника', null=True, blank=True)
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации и типе источника.
|
||||||
created_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_created",
|
"""
|
||||||
null=True, blank=True, verbose_name="Создан пользователем")
|
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата последнего изменения")
|
|
||||||
updated_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_updated",
|
|
||||||
null=True, blank=True, verbose_name="Изменен пользователем")
|
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
max_length=100,
|
||||||
|
verbose_name="Имя объекта",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Название объекта/источника сигнала",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Метаданные
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Дата создания",
|
||||||
|
help_text="Дата и время создания записи",
|
||||||
|
)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
CustomUser,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="objitems_created",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Создан пользователем",
|
||||||
|
help_text="Пользователь, создавший запись",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Дата последнего изменения",
|
||||||
|
help_text="Дата и время последнего изменения",
|
||||||
|
)
|
||||||
|
updated_by = models.ForeignKey(
|
||||||
|
CustomUser,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="objitems_updated",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Изменен пользователем",
|
||||||
|
help_text="Пользователь, последним изменивший запись",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom manager
|
||||||
|
objects = ObjItemManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Объект {self.name}"
|
return f"Объект {self.name}" if self.name else f"Объект #{self.pk}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Объект"
|
verbose_name = "Объект"
|
||||||
verbose_name_plural = "Объекты"
|
verbose_name_plural = "Объекты"
|
||||||
# constraints = [
|
ordering = ["-updated_at"]
|
||||||
# models.UniqueConstraint(
|
indexes = [
|
||||||
# fields=['id_vch_load', 'id_geo'],
|
models.Index(fields=["name"]),
|
||||||
# name='unique_objitem_combination'
|
models.Index(fields=["-updated_at"]),
|
||||||
# )
|
models.Index(fields=["-created_at"]),
|
||||||
# ]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SourceType(models.Model):
|
class SourceType(models.Model):
|
||||||
name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника")
|
"""
|
||||||
objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="source_type_obj", null=True)
|
Модель типа источника сигнала.
|
||||||
|
|
||||||
|
Классифицирует источники по типам (наземный, морской, воздушный и т.д.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Тип источника",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Тип источника сигнала",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
objitem = models.OneToOneField(
|
||||||
|
ObjItem,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name="Объект",
|
||||||
|
related_name="source_type_obj",
|
||||||
|
null=True,
|
||||||
|
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"]
|
||||||
|
|
||||||
|
|
||||||
class Parameter(models.Model):
|
class Parameter(models.Model):
|
||||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True)
|
id_satellite = models.ForeignKey(
|
||||||
|
Satellite,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="parameters",
|
||||||
|
verbose_name="Спутник",
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
polarization = models.ForeignKey(
|
polarization = models.ForeignKey(
|
||||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations", null=True, blank=True, verbose_name="Поляризация"
|
Polarization,
|
||||||
|
default=get_default_polarization,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="polarizations",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Поляризация",
|
||||||
|
)
|
||||||
|
frequency = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Частота, МГц",
|
||||||
|
db_index=True,
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||||
|
help_text="Частота в диапазоне от 0 до 50000 МГц",
|
||||||
|
)
|
||||||
|
freq_range = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Полоса частот, МГц",
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||||
|
help_text="Полоса частот в диапазоне от 0 до 1000 МГц",
|
||||||
|
)
|
||||||
|
bod_velocity = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Символьная скорость, БОД",
|
||||||
|
validators=[MinValueValidator(0)],
|
||||||
|
help_text="Символьная скорость должна быть положительной",
|
||||||
)
|
)
|
||||||
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
|
|
||||||
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
|
|
||||||
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
|
||||||
modulation = models.ForeignKey(
|
modulation = models.ForeignKey(
|
||||||
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations", null=True, blank=True, verbose_name="Модуляция"
|
Modulation,
|
||||||
|
default=get_default_modulation,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="modulations",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Модуляция",
|
||||||
|
)
|
||||||
|
snr = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="ОСШ",
|
||||||
|
validators=[MinValueValidator(-50), MaxValueValidator(100)],
|
||||||
|
help_text="Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||||||
)
|
)
|
||||||
snr = models.FloatField(default=0, null=True, blank=True, verbose_name="ОСШ")
|
|
||||||
standard = models.ForeignKey(
|
standard = models.ForeignKey(
|
||||||
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт"
|
Standard,
|
||||||
|
default=get_default_standard,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="standards",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Стандарт",
|
||||||
)
|
)
|
||||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||||||
objitems = models.ManyToManyField(ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True)
|
objitems = models.ManyToManyField(
|
||||||
|
ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True
|
||||||
|
)
|
||||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Валидация на уровне модели"""
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Проверка что частота больше полосы частот
|
||||||
|
if self.frequency and self.freq_range:
|
||||||
|
if self.freq_range > self.frequency:
|
||||||
|
raise ValidationError(
|
||||||
|
{"freq_range": "Полоса частот не может быть больше частоты"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверка что символьная скорость соответствует полосе частот
|
||||||
|
if self.bod_velocity and self.freq_range:
|
||||||
|
if self.bod_velocity > self.freq_range * 1000000: # Конвертация МГц в Гц
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"bod_velocity": "Символьная скорость не может превышать полосу частот"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
polarization_name = self.polarization.name if self.polarization else "-"
|
polarization_name = self.polarization.name if self.polarization else "-"
|
||||||
@@ -180,8 +484,8 @@ class Parameter(models.Model):
|
|||||||
verbose_name = "ВЧ загрузка"
|
verbose_name = "ВЧ загрузка"
|
||||||
verbose_name_plural = "ВЧ загрузки"
|
verbose_name_plural = "ВЧ загрузки"
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['id_satellite', 'frequency']),
|
models.Index(fields=["id_satellite", "frequency"]),
|
||||||
models.Index(fields=['frequency', 'polarization']),
|
models.Index(fields=["frequency", "polarization"]),
|
||||||
]
|
]
|
||||||
# constraints = [
|
# constraints = [
|
||||||
# models.UniqueConstraint(
|
# models.UniqueConstraint(
|
||||||
@@ -195,53 +499,149 @@ class Parameter(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SigmaParameter(models.Model):
|
class SigmaParameter(models.Model):
|
||||||
TRANSFERS = [
|
TRANSFERS = [(-1.0, "-"), (9750.0, "9750 МГц"), (10750.0, "10750 МГц")]
|
||||||
(-1.0, "-"),
|
|
||||||
(9750.0, "9750 МГц"),
|
|
||||||
(10750.0, "10750 МГц")
|
|
||||||
]
|
|
||||||
|
|
||||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник")
|
id_satellite = models.ForeignKey(
|
||||||
|
Satellite,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="sigmapar_sat",
|
||||||
|
verbose_name="Спутник",
|
||||||
|
)
|
||||||
transfer = models.FloatField(
|
transfer = models.FloatField(
|
||||||
choices=TRANSFERS,
|
choices=TRANSFERS,
|
||||||
default=-1.0,
|
default=-1.0,
|
||||||
verbose_name="Перенос по частоте"
|
verbose_name="Перенос по частоте",
|
||||||
|
help_text="Выберите перенос по частоте",
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Статус",
|
||||||
|
help_text="Статус измерения",
|
||||||
|
)
|
||||||
|
frequency = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Частота, МГц",
|
||||||
|
db_index=True,
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||||
|
help_text="Частота в диапазоне от 0 до 50000 МГц",
|
||||||
)
|
)
|
||||||
status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус")
|
|
||||||
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
|
|
||||||
transfer_frequency = models.GeneratedField(
|
transfer_frequency = models.GeneratedField(
|
||||||
expression=ExpressionWrapper(
|
expression=ExpressionWrapper(
|
||||||
F('frequency') + F('transfer'),
|
F("frequency") + F("transfer"), output_field=models.FloatField()
|
||||||
output_field=models.FloatField()
|
|
||||||
),
|
),
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True, blank=True, verbose_name="Частота в Ku, МГц"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Частота в Ku, МГц",
|
||||||
|
)
|
||||||
|
freq_range = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Полоса частот, МГц",
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||||
|
help_text="Полоса частот в диапазоне от 0 до 1000 МГц",
|
||||||
|
)
|
||||||
|
power = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Мощность, дБм",
|
||||||
|
validators=[MinValueValidator(-100), MaxValueValidator(100)],
|
||||||
|
help_text="Мощность сигнала в диапазоне от -100 до 100 дБм",
|
||||||
|
)
|
||||||
|
bod_velocity = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Символьная скорость, БОД",
|
||||||
|
validators=[MinValueValidator(0)],
|
||||||
|
help_text="Символьная скорость должна быть положительной",
|
||||||
)
|
)
|
||||||
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
|
|
||||||
power = models.FloatField(default=0, null=True, blank=True, verbose_name="Мощность, дБм")
|
|
||||||
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
|
||||||
polarization = models.ForeignKey(
|
polarization = models.ForeignKey(
|
||||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations_sigma", null=True, blank=True, verbose_name="Поляризация"
|
Polarization,
|
||||||
|
default=get_default_polarization,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="polarizations_sigma",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Поляризация",
|
||||||
)
|
)
|
||||||
modulation = models.ForeignKey(
|
modulation = models.ForeignKey(
|
||||||
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция"
|
Modulation,
|
||||||
|
default=get_default_modulation,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="modulations_sigma",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Модуляция",
|
||||||
|
)
|
||||||
|
snr = models.FloatField(
|
||||||
|
default=0,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="ОСШ, Дб",
|
||||||
|
validators=[MinValueValidator(-50), MaxValueValidator(100)],
|
||||||
|
help_text="Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||||||
)
|
)
|
||||||
snr = models.FloatField(default=0, null=True, blank=True, verbose_name="ОСШ, Дб")
|
|
||||||
standard = models.ForeignKey(
|
standard = models.ForeignKey(
|
||||||
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards_sigma", null=True, blank=True, verbose_name="Стандарт"
|
Standard,
|
||||||
|
default=get_default_standard,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="standards_sigma",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Стандарт",
|
||||||
|
)
|
||||||
|
packets = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Пакетность",
|
||||||
|
help_text="Наличие пакетной передачи",
|
||||||
|
)
|
||||||
|
datetime_begin = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Время начала измерения",
|
||||||
|
help_text="Дата и время начала измерения",
|
||||||
|
)
|
||||||
|
datetime_end = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Время окончания измерения",
|
||||||
|
help_text="Дата и время окончания измерения",
|
||||||
)
|
)
|
||||||
packets = models.BooleanField(null=True, blank=True, verbose_name="Пакетность")
|
|
||||||
datetime_begin = models.DateTimeField(null=True, blank=True, verbose_name="Время начала измерения")
|
|
||||||
datetime_end = models.DateTimeField(null=True, blank=True, verbose_name="Время окончания измерения")
|
|
||||||
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,
|
||||||
related_name='sigma_parameter',
|
related_name="sigma_parameter",
|
||||||
verbose_name="ВЧ",
|
verbose_name="ВЧ",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Валидация на уровне модели"""
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Проверка что время окончания больше времени начала
|
||||||
|
if self.datetime_begin and self.datetime_end:
|
||||||
|
if self.datetime_end < self.datetime_begin:
|
||||||
|
raise ValidationError(
|
||||||
|
{"datetime_end": "Время окончания должно быть позже времени начала"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверка что частота больше полосы частот
|
||||||
|
if self.frequency and self.freq_range:
|
||||||
|
if self.freq_range > self.frequency:
|
||||||
|
raise ValidationError(
|
||||||
|
{"freq_range": "Полоса частот не может быть больше частоты"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -252,53 +652,129 @@ class SigmaParameter(models.Model):
|
|||||||
verbose_name = "ВЧ sigma"
|
verbose_name = "ВЧ sigma"
|
||||||
verbose_name_plural = "ВЧ sigma"
|
verbose_name_plural = "ВЧ sigma"
|
||||||
|
|
||||||
|
|
||||||
class Geo(models.Model):
|
class Geo(models.Model):
|
||||||
mirrors = models.ManyToManyField(Mirror, related_name="geo_mirrors", verbose_name="Зеркала",)
|
"""
|
||||||
timestamp = models.DateTimeField(null=True, blank=True, verbose_name="Время", db_index=True)
|
Модель геолокационных данных.
|
||||||
coords = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координата геолокации")
|
|
||||||
location = models.CharField(max_length=255, null=True, blank=True, verbose_name="Метоположение")
|
Хранит информацию о местоположении источника сигнала, включая координаты,
|
||||||
comment = models.CharField(max_length=255, blank=True, verbose_name="Комментарий")
|
данные от различных источников (геолокация, кубсат, оперативники) и расстояния между ними.
|
||||||
coords_kupsat = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты Кубсата")
|
"""
|
||||||
coords_valid = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты оперативников")
|
|
||||||
is_average = models.BooleanField(null=True, blank=True, verbose_name="Усреднённое")
|
# Основные поля
|
||||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="geos_added", verbose_name="Пользователь", null=True, blank=True)
|
timestamp = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Время",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Время фиксации геолокации",
|
||||||
|
)
|
||||||
|
location = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Местоположение",
|
||||||
|
help_text="Текстовое описание местоположения",
|
||||||
|
)
|
||||||
|
comment = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Комментарий",
|
||||||
|
help_text="Дополнительные комментарии",
|
||||||
|
)
|
||||||
|
is_average = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Усреднённое",
|
||||||
|
help_text="Является ли координата усредненной",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Координаты
|
||||||
|
coords = gis.PointField(
|
||||||
|
srid=4326,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Координата геолокации",
|
||||||
|
help_text="Основные координаты геолокации (WGS84)",
|
||||||
|
)
|
||||||
|
coords_kupsat = gis.PointField(
|
||||||
|
srid=4326,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Координаты Кубсата",
|
||||||
|
help_text="Координаты, полученные от кубсата (WGS84)",
|
||||||
|
)
|
||||||
|
coords_valid = gis.PointField(
|
||||||
|
srid=4326,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Координаты оперативников",
|
||||||
|
help_text="Координаты, предоставленные оперативным отделом (WGS84)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Вычисляемые поля - расстояния
|
||||||
distance_coords_kup = models.GeneratedField(
|
distance_coords_kup = models.GeneratedField(
|
||||||
expression=functions.Distance("coords", "coords_kupsat") / 1000,
|
expression=functions.Distance("coords", "coords_kupsat") / 1000,
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True, blank=True, verbose_name="Расстояние между купсатом и гео, км"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Расстояние между кубсатом и гео, км",
|
||||||
)
|
)
|
||||||
distance_coords_valid = models.GeneratedField(
|
distance_coords_valid = models.GeneratedField(
|
||||||
expression=functions.Distance("coords", "coords_valid") / 1000,
|
expression=functions.Distance("coords", "coords_valid") / 1000,
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True, blank=True, verbose_name="Расстояние между гео и оперативным отделом, км"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Расстояние между гео и оперативным отделом, км",
|
||||||
)
|
)
|
||||||
distance_kup_valid = models.GeneratedField(
|
distance_kup_valid = models.GeneratedField(
|
||||||
expression=functions.Distance("coords_valid", "coords_kupsat") / 1000,
|
expression=functions.Distance("coords_valid", "coords_kupsat") / 1000,
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Расстояние между кубсатом и оперативным отделом, км",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
mirrors = models.ManyToManyField(
|
||||||
|
Mirror,
|
||||||
|
related_name="geo_mirrors",
|
||||||
|
verbose_name="Зеркала",
|
||||||
|
blank=True,
|
||||||
|
help_text="Зеркала антенн, использованные для приема",
|
||||||
|
)
|
||||||
|
objitem = models.OneToOneField(
|
||||||
|
ObjItem,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Объект",
|
||||||
|
related_name="geo_obj",
|
||||||
|
null=True,
|
||||||
|
help_text="Связанный объект",
|
||||||
)
|
)
|
||||||
objitem = models.OneToOneField(ObjItem, on_delete=models.CASCADE, verbose_name="Гео", related_name="geo_obj", null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.coords:
|
||||||
longitude = self.coords.coords[0]
|
longitude = self.coords.coords[0]
|
||||||
latitude = self.coords.coords[1]
|
latitude = self.coords.coords[1]
|
||||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
return f"{lat} {lon}, {self.location}"
|
location_str = f", {self.location}" if self.location else ""
|
||||||
|
return f"{lat} {lon}{location_str}"
|
||||||
|
return f"Гео #{self.pk}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Гео"
|
verbose_name = "Гео"
|
||||||
verbose_name_plural = "Гео"
|
verbose_name_plural = "Гео"
|
||||||
|
ordering = ["-timestamp"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["-timestamp"]),
|
||||||
|
models.Index(fields=["location"]),
|
||||||
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=[
|
fields=["timestamp", "coords"], name="unique_geo_combination"
|
||||||
'timestamp', 'coords'
|
|
||||||
],
|
|
||||||
name='unique_geo_combination'
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Django imports
|
||||||
from django.contrib.admin.filters import ChoicesFieldListFilter
|
from django.contrib.admin.filters import ChoicesFieldListFilter
|
||||||
from django.forms import Media
|
from django.forms import Media
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
# Local imports
|
||||||
from .models import CustomUser
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alert messages -->
|
<!-- Alert messages -->
|
||||||
{% if messages %}
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main feature cards -->
|
<!-- Main feature cards -->
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
@@ -35,7 +28,7 @@
|
|||||||
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
|
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
|
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
|
||||||
<a href="{% url 'load_excel_data' %}" class="btn btn-primary">
|
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
|
||||||
Перейти к загрузке данных
|
Перейти к загрузке данных
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +49,7 @@
|
|||||||
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
|
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
|
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
|
||||||
<a href="{% url 'load_csv_data' %}" class="btn btn-success">
|
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
|
||||||
Перейти к загрузке данных
|
Перейти к загрузке данных
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +75,7 @@
|
|||||||
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
||||||
<a href="{% url 'add_sats' %}" class="btn btn-info">
|
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
|
||||||
Добавить список спутников
|
Добавить список спутников
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +96,7 @@
|
|||||||
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
|
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
|
||||||
<a href="{% url 'add_trans' %}" class="btn btn-warning">
|
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
|
||||||
Добавить транспондеры
|
Добавить транспондеры
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
|
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
|
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
|
||||||
<a href="{% url 'vch_load' %}" class="btn btn-danger">
|
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
|
||||||
Добавить данные ВЧ загрузки
|
Добавить данные ВЧ загрузки
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,8 +138,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<a href="{% url '2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
<a href="{% url 'mapsapp:2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
||||||
<a href="{% url '3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
<a href="{% url 'mapsapp:3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +158,7 @@
|
|||||||
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
|
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
|
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
|
||||||
<a href="{% url 'link_vch_sigma' %}" class="btn btn-info">
|
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
|
||||||
Открыть форму
|
Открыть форму
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,7 +178,7 @@
|
|||||||
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
|
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
|
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
|
||||||
<a href="{% url 'kubsat_excel' %}" class="btn btn-success">
|
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
|
||||||
Добавить событие
|
Добавить событие
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,14 +11,7 @@
|
|||||||
<h2 class="mb-0">Загрузка данных из CSV</h2>
|
<h2 class="mb-0">Загрузка данных из CSV</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if messages %}
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
|
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
|
||||||
|
|
||||||
@@ -26,17 +19,10 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Form fields with Bootstrap styling -->
|
<!-- Form fields with Bootstrap styling -->
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите CSV файл:</label>
|
|
||||||
{{ form.file }}
|
|
||||||
{% if form.file.errors %}
|
|
||||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">Загрузите CSV-файл с данными для обработки</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -11,14 +11,7 @@
|
|||||||
<h2 class="mb-0">Загрузка данных из Excel</h2>
|
<h2 class="mb-0">Загрузка данных из Excel</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if messages %}
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
|
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
|
||||||
|
|
||||||
@@ -26,34 +19,12 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Form fields with Bootstrap styling -->
|
<!-- Form fields with Bootstrap styling -->
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||||
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите Excel файл:</label>
|
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
|
||||||
{{ form.file }}
|
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
|
||||||
{% if form.file.errors %}
|
|
||||||
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">Загрузите Excel-файл (.xlsx или .xls) с данными для обработки</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
|
||||||
{{ form.sat_choice }}
|
|
||||||
{% if form.sat_choice.errors %}
|
|
||||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.number_input.id_for_label }}" class="form-label">Количество строк для обработки:</label>
|
|
||||||
{{ form.number_input }}
|
|
||||||
{% if form.number_input.errors %}
|
|
||||||
<div class="text-danger mt-1">{{ form.number_input.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">Оставьте пустым или введите 0 для обработки всех строк</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,79 +1,42 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
<title>{% block title %}Геолокация{% endblock %}</title>
|
<title>{% block title %}Геолокация{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Дополнительные стили (если нужно) -->
|
<!-- Дополнительные стили -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
<!-- Навигационная панель -->
|
<!-- Навигационная панель -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
{% include 'mainapp/components/_navbar.html' %}
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{% url 'home' %}">Геолокация</a>
|
<!-- Сообщения -->
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<div class="container mt-3">
|
||||||
<span class="navbar-toggler-icon"></span>
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<ul class="navbar-nav me-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'home' %}">Объекты</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'actions' %}">Действия</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url '3dmap' %}">3D карта</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url '2dmap' %}">2D карта</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
|
||||||
{% if user.first_name and user.last_name %}
|
|
||||||
{{ user.first_name }} {{ user.last_name }}
|
|
||||||
{% elif user.get_full_name %}
|
|
||||||
{{ user.get_full_name }}
|
|
||||||
{% else %}
|
|
||||||
{{ user.username }}
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
|
||||||
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<ul class="navbar-nav ms-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'login' %}">Войти</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Основной контент -->
|
<!-- Основной контент -->
|
||||||
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
|
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}"></script>
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
|
||||||
|
|
||||||
|
<!-- Дополнительные скрипты -->
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
33
dbapp/mainapp/templates/mainapp/components/_form_field.html
Normal file
33
dbapp/mainapp/templates/mainapp/components/_form_field.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент для отображения полей формы
|
||||||
|
Использование:
|
||||||
|
{% include 'mainapp/components/_form_field.html' with field=form.field_name %}
|
||||||
|
{% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% if field.field.widget.input_type == 'checkbox' %}
|
||||||
|
<div class="form-check">
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
25
dbapp/mainapp/templates/mainapp/components/_messages.html
Normal file
25
dbapp/mainapp/templates/mainapp/components/_messages.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент для отображения сообщений Django
|
||||||
|
Использование:
|
||||||
|
{% include 'mainapp/components/_messages.html' %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages-container">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{% if message.tags == 'error' %}
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
|
{% elif message.tags == 'success' %}
|
||||||
|
<i class="bi bi-check-circle-fill me-2"></i>
|
||||||
|
{% elif message.tags == 'warning' %}
|
||||||
|
<i class="bi bi-exclamation-circle-fill me-2"></i>
|
||||||
|
{% elif message.tags == 'info' %}
|
||||||
|
<i class="bi bi-info-circle-fill me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
57
dbapp/mainapp/templates/mainapp/components/_navbar.html
Normal file
57
dbapp/mainapp/templates/mainapp/components/_navbar.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент навигационной панели
|
||||||
|
Использование:
|
||||||
|
{% include 'mainapp/components/_navbar.html' %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
{% if user.first_name and user.last_name %}
|
||||||
|
{{ user.first_name }} {{ user.last_name }}
|
||||||
|
{% elif user.get_full_name %}
|
||||||
|
{{ user.get_full_name }}
|
||||||
|
{% else %}
|
||||||
|
{{ user.username }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'login' %}">Войти</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
56
dbapp/mainapp/templates/mainapp/components/_pagination.html
Normal file
56
dbapp/mainapp/templates/mainapp/components/_pagination.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент пагинации
|
||||||
|
Использование:
|
||||||
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj %}
|
||||||
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<nav aria-label="Page navigation" class="d-flex align-items-center">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1" title="Первая">
|
||||||
|
<i class="bi bi-chevron-bar-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" title="Предыдущая">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ num }}</span>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" title="Следующая">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" title="Последняя">
|
||||||
|
<i class="bi bi-chevron-bar-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if show_info %}
|
||||||
|
<div class="ms-3 text-muted small">
|
||||||
|
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент для заголовков таблиц с сортировкой
|
||||||
|
Использование:
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<th scope="col">
|
||||||
|
{% if sortable != False %}
|
||||||
|
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == field %}-{{ field }}{% elif sort == '-'|add:field %}{{ field }}{% else %}{{ field }}{% endif %}"
|
||||||
|
class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||||
|
{{ label }}
|
||||||
|
{% if sort == field %}
|
||||||
|
<i class="bi bi-sort-up ms-1"></i>
|
||||||
|
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||||
|
class="text-white ms-1" title="Сбросить сортировку">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</a>
|
||||||
|
{% elif sort == '-'|add:field %}
|
||||||
|
<i class="bi bi-sort-down ms-1"></i>
|
||||||
|
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||||
|
class="text-white ms-1" title="Сбросить сортировку">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-arrow-down-up ms-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ label }}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
@@ -55,8 +55,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
{% comment %} <a href="{% url 'home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
|
{% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
|
||||||
<button type="submit" class="btn btn-info">Выполнить привязку</button>
|
<button type="submit" class="btn btn-info">Выполнить привязку</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
|
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="submit" class="btn btn-danger">Удалить</button>
|
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary ms-2">Отмена</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load static leaflet_tags %}
|
{% load static leaflet_tags %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary btn-action">Назад</a>
|
<a href="{% url 'mainapp:objitem_list' %}{% if return_params %}?{{ return_params }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,10 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=form.name %}
|
||||||
<label for="{{ form.name.id_for_label }}" class="form-label">Имя объекта:</label>
|
|
||||||
{{ form.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -139,54 +137,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.id_satellite %}
|
||||||
<label for="{{ param_form.id_satellite.id_for_label }}" class="form-label">Спутник:</label>
|
|
||||||
{{ param_form.id_satellite }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.frequency %}
|
||||||
<label for="{{ param_form.frequency.id_for_label }}" class="form-label">Частота, МГц:</label>
|
|
||||||
{{ param_form.frequency }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.freq_range %}
|
||||||
<label for="{{ param_form.freq_range.id_for_label }}" class="form-label">Полоса, МГц:</label>
|
|
||||||
{{ param_form.freq_range }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.polarization %}
|
||||||
<label for="{{ param_form.polarization.id_for_label }}" class="form-label">Поляризация:</label>
|
|
||||||
{{ param_form.polarization }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.bod_velocity %}
|
||||||
<label for="{{ param_form.bod_velocity.id_for_label }}" class="form-label">Симв. скорость, БОД:</label>
|
|
||||||
{{ param_form.bod_velocity }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.modulation %}
|
||||||
<label for="{{ param_form.modulation.id_for_label }}" class="form-label">Модуляция:</label>
|
|
||||||
{{ param_form.modulation }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.snr %}
|
||||||
<label for="{{ param_form.snr.id_for_label }}" class="form-label">ОСШ:</label>
|
|
||||||
{{ param_form.snr }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=param_form.standard %}
|
||||||
<label for="{{ param_form.standard.id_for_label }}" class="form-label">Стандарт:</label>
|
|
||||||
{{ param_form.standard }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% comment %} </div> {% endcomment %}
|
{% comment %} </div> {% endcomment %}
|
||||||
@@ -220,7 +194,7 @@
|
|||||||
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_geo_latitude" name="geo_latitude"
|
id="id_geo_latitude" name="geo_latitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -228,7 +202,7 @@
|
|||||||
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_geo_longitude" name="geo_longitude"
|
id="id_geo_longitude" name="geo_longitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +217,7 @@
|
|||||||
<label for="id_kupsat_longitude" class="form-label">Долгота:</label>
|
<label for="id_kupsat_longitude" class="form-label">Долгота:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_kupsat_longitude" name="kupsat_longitude"
|
id="id_kupsat_longitude" name="kupsat_longitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -251,7 +225,7 @@
|
|||||||
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
|
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_kupsat_latitude" name="kupsat_latitude"
|
id="id_kupsat_latitude" name="kupsat_latitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,7 +240,7 @@
|
|||||||
<label for="id_valid_longitude" class="form-label">Долгота:</label>
|
<label for="id_valid_longitude" class="form-label">Долгота:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_valid_longitude" name="valid_longitude"
|
id="id_valid_longitude" name="valid_longitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -274,7 +248,7 @@
|
|||||||
<label for="id_valid_latitude" class="form-label">Широта:</label>
|
<label for="id_valid_latitude" class="form-label">Широта:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control"
|
||||||
id="id_valid_latitude" name="valid_latitude"
|
id="id_valid_latitude" name="valid_latitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,16 +256,10 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=geo_form.location %}
|
||||||
<label for="{{ geo_form.location.id_for_label }}" class="form-label">Местоположение:</label>
|
|
||||||
{{ geo_form.location }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=geo_form.comment %}
|
||||||
<label for="{{ geo_form.comment.id_for_label }}" class="form-label">Комментарий:</label>
|
|
||||||
{{ geo_form.comment }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -316,10 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
{% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %}
|
||||||
<label for="{{ geo_form.is_average.id_for_label }}" class="form-check-label">Усреднённое:</label>
|
|
||||||
{{ geo_form.is_average }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -368,7 +333,7 @@
|
|||||||
<div class="d-flex justify-content-end mt-4">
|
<div class="d-flex justify-content-end mt-4">
|
||||||
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
{% if object %}
|
{% if object %}
|
||||||
<a href="{% url 'objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
|
<a href="{% url 'mainapp:objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -29,9 +29,12 @@
|
|||||||
<!-- Search bar made more compact -->
|
<!-- Search bar made more compact -->
|
||||||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..." value="{{ search_query|default:'' }}">
|
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..."
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
value="{{ search_query|default:'' }}">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
<button type="button" class="btn btn-outline-primary"
|
||||||
|
onclick="performSearch()">Найти</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="clearSearch()">Очистить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,11 +47,13 @@
|
|||||||
<i class="bi bi-pencil"></i> Изменить
|
<i class="bi bi-pencil"></i> Изменить
|
||||||
</button> {% endcomment %}
|
</button> {% endcomment %}
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить" onclick="deleteSelectedObjects()">
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||||
|
onclick="deleteSelectedObjects()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" onclick="showSelectedOnMap()">
|
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||||
|
onclick="showSelectedOnMap()">
|
||||||
<i class="bi bi-map"></i> Карта
|
<i class="bi bi-map"></i> Карта
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +61,9 @@
|
|||||||
<!-- Items per page select moved here -->
|
<!-- Items per page select moved here -->
|
||||||
<div>
|
<div>
|
||||||
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||||||
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm d-inline-block" style="width: auto;" onchange="updateItemsPerPage()">
|
<select name="items_per_page" id="items-per-page"
|
||||||
|
class="form-select form-select-sm d-inline-block" style="width: auto;"
|
||||||
|
onchange="updateItemsPerPage()">
|
||||||
{% for option in available_items_per_page %}
|
{% for option in available_items_per_page %}
|
||||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||||
{{ option }}
|
{{ option }}
|
||||||
@@ -67,168 +74,152 @@
|
|||||||
|
|
||||||
<!-- Column visibility toggle button -->
|
<!-- Column visibility toggle button -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||||
|
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-gear"></i> Колонки
|
<i class="bi bi-gear"></i> Колонки
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050;">
|
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050;">
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" id="select-all-columns" checked onchange="toggleAllColumns(this)"> Выбрать всё
|
<input type="checkbox" id="select-all-columns" checked
|
||||||
|
onchange="toggleAllColumns(this)"> Выбрать всё
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать
|
<input type="checkbox" class="column-toggle" data-column="0" checked
|
||||||
|
onchange="toggleColumn(this)"> Выбрать
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> Имя
|
<input type="checkbox" class="column-toggle" data-column="1" checked
|
||||||
|
onchange="toggleColumn(this)"> Имя
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Спутник
|
<input type="checkbox" class="column-toggle" data-column="2" checked
|
||||||
|
onchange="toggleColumn(this)"> Спутник
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Част, МГц
|
<input type="checkbox" class="column-toggle" data-column="3" checked
|
||||||
|
onchange="toggleColumn(this)"> Част, МГц
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Полоса, МГц
|
<input type="checkbox" class="column-toggle" data-column="4" checked
|
||||||
|
onchange="toggleColumn(this)"> Полоса, МГц
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Поляризация
|
<input type="checkbox" class="column-toggle" data-column="5" checked
|
||||||
|
onchange="toggleColumn(this)"> Поляризация
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Сим. V
|
<input type="checkbox" class="column-toggle" data-column="6" checked
|
||||||
|
onchange="toggleColumn(this)"> Сим. V
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Модул
|
<input type="checkbox" class="column-toggle" data-column="7" checked
|
||||||
|
onchange="toggleColumn(this)"> Модул
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> ОСШ
|
<input type="checkbox" class="column-toggle" data-column="8" checked
|
||||||
|
onchange="toggleColumn(this)"> ОСШ
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Время ГЛ
|
<input type="checkbox" class="column-toggle" data-column="9" checked
|
||||||
|
onchange="toggleColumn(this)"> Время ГЛ
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Местоположение
|
<input type="checkbox" class="column-toggle" data-column="10" checked
|
||||||
|
onchange="toggleColumn(this)"> Местоположение
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Геолокация
|
<input type="checkbox" class="column-toggle" data-column="11" checked
|
||||||
|
onchange="toggleColumn(this)"> Геолокация
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="12" checked onchange="toggleColumn(this)"> Кубсат
|
<input type="checkbox" class="column-toggle" data-column="12" checked
|
||||||
|
onchange="toggleColumn(this)"> Кубсат
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Опер. отд
|
<input type="checkbox" class="column-toggle" data-column="13" checked
|
||||||
|
onchange="toggleColumn(this)"> Опер. отд
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Гео-куб, км
|
<input type="checkbox" class="column-toggle" data-column="14" checked
|
||||||
|
onchange="toggleColumn(this)"> Гео-куб, км
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Гео-опер, км
|
<input type="checkbox" class="column-toggle" data-column="15" checked
|
||||||
|
onchange="toggleColumn(this)"> Гео-опер, км
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Куб-опер, км
|
<input type="checkbox" class="column-toggle" data-column="16" checked
|
||||||
|
onchange="toggleColumn(this)"> Куб-опер, км
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="17" checked onchange="toggleColumn(this)"> Обновлено
|
<input type="checkbox" class="column-toggle" data-column="17" checked
|
||||||
|
onchange="toggleColumn(this)"> Обновлено
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="18" checked onchange="toggleColumn(this)"> Кем (обновление)
|
<input type="checkbox" class="column-toggle" data-column="18" checked
|
||||||
|
onchange="toggleColumn(this)"> Кем (обновление)
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="19" checked onchange="toggleColumn(this)"> Создано
|
<input type="checkbox" class="column-toggle" data-column="19" checked
|
||||||
|
onchange="toggleColumn(this)"> Создано
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" class="column-toggle" data-column="20" checked onchange="toggleColumn(this)"> Кем (создание)
|
<input type="checkbox" class="column-toggle" data-column="20" checked
|
||||||
|
onchange="toggleColumn(this)"> Кем (создание)
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination moved here -->
|
<!-- Pagination -->
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
{% if page_obj.paginator.num_pages > 1 %}
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||||
<nav aria-label="Page navigation" class="d-flex align-items-center">
|
|
||||||
<ul class="pagination mb-0">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1" title="Первая"><<</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" title="Предыдущая"><</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
|
||||||
{% if page_obj.number == num %}
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">{{ num }}</span>
|
|
||||||
</li>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" title="Следующая">></a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" title="Последняя">>></a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Pagination Info -->
|
|
||||||
{% if page_obj %}
|
|
||||||
<div class="ms-3 text-muted small">
|
|
||||||
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,13 +238,14 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Спутник:</label>
|
<label class="form-label">Спутник:</label>
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
|
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||||
{% for satellite in satellites %}
|
{% for satellite in satellites %}
|
||||||
<option value="{{ satellite.id }}"
|
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||||
{% if satellite.id in selected_satellites %}selected{% endif %}>
|
|
||||||
{{ satellite.name }}
|
{{ satellite.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -263,42 +255,51 @@
|
|||||||
<!-- Frequency Filter -->
|
<!-- Frequency Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Частота, МГц:</label>
|
<label class="form-label">Частота, МГц:</label>
|
||||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
|
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
|
||||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
|
placeholder="От" value="{{ freq_min|default:'' }}">
|
||||||
|
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ freq_max|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Range Filter -->
|
<!-- Range Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Полоса, МГц:</label>
|
<label class="form-label">Полоса, МГц:</label>
|
||||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
|
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1"
|
||||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
|
placeholder="От" value="{{ range_min|default:'' }}">
|
||||||
|
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ range_max|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SNR Filter -->
|
<!-- SNR Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">ОСШ:</label>
|
<label class="form-label">ОСШ:</label>
|
||||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
|
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1"
|
||||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
|
placeholder="От" value="{{ snr_min|default:'' }}">
|
||||||
|
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ snr_max|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Symbol Rate Filter -->
|
<!-- Symbol Rate Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Сим. v, БОД:</label>
|
<label class="form-label">Сим. v, БОД:</label>
|
||||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
|
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1"
|
||||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
|
placeholder="От" value="{{ bod_min|default:'' }}">
|
||||||
|
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ bod_max|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modulation Filter -->
|
<!-- Modulation Filter -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Модуляция:</label>
|
<label class="form-label">Модуляция:</label>
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
|
onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
|
onclick="selectAllOptions('modulation', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
|
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
|
||||||
{% for mod in modulations %}
|
{% for mod in modulations %}
|
||||||
<option value="{{ mod.id }}"
|
<option value="{{ mod.id }}" {% if mod.id in selected_modulations %}selected{% endif %}>
|
||||||
{% if mod.id in selected_modulations %}selected{% endif %}>
|
|
||||||
{{ mod.name }}
|
{{ mod.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -309,13 +310,14 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Поляризация:</label>
|
<label class="form-label">Поляризация:</label>
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
|
onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
|
onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||||
{% for pol in polarizations %}
|
{% for pol in polarizations %}
|
||||||
<option value="{{ pol.id }}"
|
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||||
{% if pol.id in selected_polarizations %}selected{% endif %}>
|
|
||||||
{{ pol.name }}
|
{{ pol.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -327,13 +329,13 @@
|
|||||||
<label class="form-label">Координаты Кубсата:</label>
|
<label class="form-label">Координаты Кубсата:</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
|
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1"
|
||||||
{% if has_kupsat == '1' %}checked{% endif %}>
|
value="1" {% if has_kupsat == '1' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
|
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0"
|
||||||
{% if has_kupsat == '0' %}checked{% endif %}>
|
value="0" {% if has_kupsat == '0' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -344,18 +346,41 @@
|
|||||||
<label class="form-label">Координаты опер. отдела:</label>
|
<label class="form-label">Координаты опер. отдела:</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
|
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1"
|
||||||
{% if has_valid == '1' %}checked{% endif %}>
|
value="1" {% if has_valid == '1' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
<label class="form-check-label" for="has_valid_1">Есть</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
|
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0"
|
||||||
{% if has_valid == '0' %}checked{% endif %}>
|
value="0" {% if has_valid == '0' %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
<label class="form-check-label" for="has_valid_0">Нет</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Дата ГЛ:</label>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="setDateRange('today')">Сегодня</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="setDateRange('week')">Неделя</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm w-100 mb-1" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="setDateRange('month')">Месяц</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="setDateRange('year')">Год</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||||
|
placeholder="От" value="{{ date_from|default:'' }}">
|
||||||
|
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ date_to|default:'' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Apply Filters and Reset Buttons -->
|
<!-- Apply Filters and Reset Buttons -->
|
||||||
<div class="d-grid gap-2 mt-2">
|
<div class="d-grid gap-2 mt-2">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||||
@@ -377,127 +402,36 @@
|
|||||||
<th scope="col" class="text-center" style="width: 3%;">
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
<input type="checkbox" id="select-all" class="form-check-input">
|
<input type="checkbox" id="select-all" class="form-check-input">
|
||||||
</th>
|
</th>
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||||
<!-- Столбец "Имя" -->
|
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
||||||
<th scope="col">
|
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'name' %}-name{% elif sort == '-name' %}name{% else %}name{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
||||||
Имя
|
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
||||||
{% if sort == 'name' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-name' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
{% include 'mainapp/components/_table_header.html' with label="Сим. V" field="bod_velocity" sort=sort %}
|
||||||
</a>
|
{% include 'mainapp/components/_table_header.html' with label="Модул" field="modulation" sort=sort %}
|
||||||
</th>
|
{% include 'mainapp/components/_table_header.html' with label="ОСШ" field="snr" sort=sort %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
|
||||||
<!-- Столбец "Спутник" -->
|
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %}
|
||||||
<th scope="col">
|
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %}
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'satellite' %}-satellite{% elif sort == '-satellite' %}satellite{% else %}satellite{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
{% include 'mainapp/components/_table_header.html' with label="Кубсат" field="" sortable=False %}
|
||||||
Спутник
|
{% include 'mainapp/components/_table_header.html' with label="Опер. отд" field="" sortable=False %}
|
||||||
{% if sort == 'satellite' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-satellite' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
{% include 'mainapp/components/_table_header.html' with label="Гео-куб, км" field="" sortable=False %}
|
||||||
</a>
|
{% include 'mainapp/components/_table_header.html' with label="Гео-опер, км" field="" sortable=False %}
|
||||||
</th>
|
{% include 'mainapp/components/_table_header.html' with label="Куб-опер, км" field="" sortable=False %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %}
|
||||||
<!-- Столбец "Част, МГц" -->
|
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
|
||||||
<th scope="col">
|
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'frequency' %}-frequency{% elif sort == '-frequency' %}frequency{% else %}frequency{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
|
||||||
Част, МГц
|
|
||||||
{% if sort == 'frequency' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-frequency' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Полоса, МГц" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'freq_range' %}-freq_range{% elif sort == '-freq_range' %}freq_range{% else %}freq_range{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Полоса, МГц
|
|
||||||
{% if sort == 'freq_range' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-freq_range' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Поляризация" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'polarization' %}-polarization{% elif sort == '-polarization' %}polarization{% else %}polarization{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Поляризация
|
|
||||||
{% if sort == 'polarization' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-polarization' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Сим. V" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'bod_velocity' %}-bod_velocity{% elif sort == '-bod_velocity' %}bod_velocity{% else %}bod_velocity{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Сим. V
|
|
||||||
{% if sort == 'bod_velocity' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-bod_velocity' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Модул" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'modulation' %}-modulation{% elif sort == '-modulation' %}modulation{% else %}modulation{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Модул
|
|
||||||
{% if sort == 'modulation' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-modulation' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "ОСШ" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'snr' %}-snr{% elif sort == '-snr' %}snr{% else %}snr{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
ОСШ
|
|
||||||
{% if sort == 'snr' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-snr' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Время ГЛ" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'geo_timestamp' %}-geo_timestamp{% elif sort == '-geo_timestamp' %}geo_timestamp{% else %}geo_timestamp{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Время ГЛ
|
|
||||||
{% if sort == 'geo_timestamp' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-geo_timestamp' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th scope="col">Местоположение</th>
|
|
||||||
<!-- Столбец "Геолокация" - без сортировки -->
|
|
||||||
<th scope="col">Геолокация</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Кубсат" - без сортировки -->
|
|
||||||
<th scope="col">Кубсат</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Опер. отд" - без сортировки -->
|
|
||||||
<th scope="col">Опер. отд</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Гео-куб, км" - без сортировки -->
|
|
||||||
<th scope="col">Гео-куб, км</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Гео-опер, км" - без сортировки -->
|
|
||||||
<th scope="col">Гео-опер, км</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Куб-опер, км" - без сортировки -->
|
|
||||||
<th scope="col">Куб-опер, км</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Обновлено" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'updated_at' %}-updated_at{% elif sort == '-updated_at' %}updated_at{% else %}updated_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Обновлено
|
|
||||||
{% if sort == 'updated_at' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-updated_at' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Кем (обновление)" - без сортировки -->
|
|
||||||
<th scope="col">Кем(обн)</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Создано" -->
|
|
||||||
<th scope="col">
|
|
||||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'created_at' %}-created_at{% elif sort == '-created_at' %}created_at{% else %}created_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
|
||||||
Создано
|
|
||||||
{% if sort == 'created_at' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-created_at' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<!-- Столбец "Кем (создание)" - без сортировки -->
|
|
||||||
<th scope="col">Кем(созд)</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in processed_objects %}
|
{% for item in processed_objects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
<input type="checkbox" class="form-check-input item-checkbox"
|
||||||
|
value="{{ item.id }}">
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{% if item.obj.id %}{% url 'objitem_update' item.obj.id %}{% endif %}">{{ item.name }}</a></td>
|
<td><a href="{% if item.obj.id %}{% url 'mainapp:objitem_update' item.obj.id %}?return_params={{ request.GET.urlencode }}{% endif %}">{{ item.name }}</a></td>
|
||||||
<td>{{ item.satellite_name }}</td>
|
<td>{{ item.satellite_name }}</td>
|
||||||
<td>{{ item.frequency }}</td>
|
<td>{{ item.frequency }}</td>
|
||||||
<td>{{ item.freq_range }}</td>
|
<td>{{ item.freq_range }}</td>
|
||||||
@@ -585,7 +519,7 @@ function showSelectedOnMap() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to the map view with selected IDs as query parameter
|
// Redirect to the map view with selected IDs as query parameter
|
||||||
const url = '{% url "show_selected_objects_map" %}' + '?ids=' + selectedIds.join(',');
|
const url = '{% url "mainapp:show_selected_objects_map" %}' + '?ids=' + selectedIds.join(',');
|
||||||
window.open(url, '_blank'); // Open in a new tab
|
window.open(url, '_blank'); // Open in a new tab
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +590,7 @@ function deleteSelectedObjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send AJAX request to delete selected objects
|
// Send AJAX request to delete selected objects
|
||||||
fetch('{% url "delete_selected_objects" %}', {
|
fetch('{% url "mainapp:delete_selected_objects" %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: 'ids=' + selectedIds.join(',')
|
body: 'ids=' + selectedIds.join(',')
|
||||||
@@ -750,6 +684,35 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
setupRadioLikeCheckboxes('has_kupsat');
|
setupRadioLikeCheckboxes('has_kupsat');
|
||||||
setupRadioLikeCheckboxes('has_valid');
|
setupRadioLikeCheckboxes('has_valid');
|
||||||
|
|
||||||
|
// Date range quick selection functions
|
||||||
|
window.setDateRange = function (period) {
|
||||||
|
const dateFrom = document.getElementById('date_from');
|
||||||
|
const dateTo = document.getElementById('date_to');
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
// Set end date to today
|
||||||
|
dateTo.valueAsDate = today;
|
||||||
|
|
||||||
|
// Calculate start date based on period
|
||||||
|
const startDate = new Date();
|
||||||
|
switch (period) {
|
||||||
|
case 'today':
|
||||||
|
startDate.setDate(today.getDate());
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
startDate.setDate(today.getDate() - 7);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
startDate.setMonth(today.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
startDate.setFullYear(today.getFullYear() - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFrom.valueAsDate = startDate;
|
||||||
|
};
|
||||||
|
|
||||||
// Function to select/deselect all options in a select element
|
// Function to select/deselect all options in a select element
|
||||||
window.selectAllOptions = function (selectName, selectAll) {
|
window.selectAllOptions = function (selectName, selectAll) {
|
||||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
|
||||||
<button type="submit" class="btn btn-success">Выполнить</button>
|
<button type="submit" class="btn btn-success">Выполнить</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div> {% endcomment %}
|
</div> {% endcomment %}
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
<button type="submit" class="btn btn-warning">Добавить в базу</button>
|
<button type="submit" class="btn btn-warning">Добавить в базу</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
<button type="submit" class="btn btn-danger">Обработать файл</button>
|
<button type="submit" class="btn btn-danger">Обработать файл</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
3
dbapp/mainapp/templatetags/__init__.py
Normal file
3
dbapp/mainapp/templatetags/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Template tags для mainapp.
|
||||||
|
"""
|
||||||
133
dbapp/mainapp/templatetags/coordinate_filters.py
Normal file
133
dbapp/mainapp/templatetags/coordinate_filters.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
Пользовательские фильтры шаблонов для форматирования координат.
|
||||||
|
|
||||||
|
Этот модуль содержит фильтры Django для форматирования географических координат
|
||||||
|
в читаемый вид в шаблонах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django import template
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='format_coords')
|
||||||
|
def format_coords(point: Optional[Point]) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует объект Point в читаемую строку координат.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
point (Point): Объект Point из GeoDjango или None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка координат в формате "XXN/S YYE/W"
|
||||||
|
или "-" если point равен None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
В шаблоне:
|
||||||
|
{{ geo_obj.coords|format_coords }}
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
"55.75N 37.62E"
|
||||||
|
"""
|
||||||
|
if not point:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
longitude = point.coords[0]
|
||||||
|
latitude = point.coords[1]
|
||||||
|
|
||||||
|
lon_direction = "E" if longitude > 0 else "W"
|
||||||
|
lat_direction = "N" if latitude > 0 else "S"
|
||||||
|
|
||||||
|
lon_value = abs(longitude)
|
||||||
|
lat_value = abs(latitude)
|
||||||
|
|
||||||
|
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
|
||||||
|
except (AttributeError, IndexError, TypeError):
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='format_coords_decimal')
|
||||||
|
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует объект Point в десятичные координаты с заданной точностью.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
point (Point): Объект Point из GeoDjango или None.
|
||||||
|
precision (int): Количество знаков после запятой (по умолчанию 6).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка координат в формате "lat, lon"
|
||||||
|
или "-" если point равен None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
В шаблоне:
|
||||||
|
{{ geo_obj.coords|format_coords_decimal:4 }}
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
"55.7500, 37.6200"
|
||||||
|
"""
|
||||||
|
if not point:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
longitude = point.coords[0]
|
||||||
|
latitude = point.coords[1]
|
||||||
|
|
||||||
|
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
|
||||||
|
return format_str.format(latitude, longitude)
|
||||||
|
except (AttributeError, IndexError, TypeError, ValueError):
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='coords_to_lat')
|
||||||
|
def coords_to_lat(point: Optional[Point]) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Извлекает широту из объекта Point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
point (Point): Объект Point из GeoDjango или None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Значение широты или None если point равен None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
В шаблоне:
|
||||||
|
{{ geo_obj.coords|coords_to_lat }}
|
||||||
|
"""
|
||||||
|
if not point:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return point.coords[1]
|
||||||
|
except (AttributeError, IndexError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='coords_to_lon')
|
||||||
|
def coords_to_lon(point: Optional[Point]) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Извлекает долготу из объекта Point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
point (Point): Объект Point из GeoDjango или None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Значение долготы или None если point равен None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
В шаблоне:
|
||||||
|
{{ geo_obj.coords|coords_to_lon }}
|
||||||
|
"""
|
||||||
|
if not point:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return point.coords[0]
|
||||||
|
except (AttributeError, IndexError, TypeError):
|
||||||
|
return None
|
||||||
@@ -1,3 +1,179 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase, RequestFactory
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from .models import CustomUser, Geo, ObjItem
|
||||||
|
from .utils import format_coordinates, parse_pagination_params
|
||||||
|
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
class FormatCoordinatesTestCase(TestCase):
|
||||||
|
"""Тесты для функции format_coordinates"""
|
||||||
|
|
||||||
|
def test_format_positive_coordinates(self):
|
||||||
|
"""Тест форматирования положительных координат"""
|
||||||
|
result = format_coordinates(37.62, 55.75)
|
||||||
|
self.assertEqual(result, "55.75N 37.62E")
|
||||||
|
|
||||||
|
def test_format_negative_longitude(self):
|
||||||
|
"""Тест форматирования с отрицательной долготой"""
|
||||||
|
result = format_coordinates(-122.42, 37.77)
|
||||||
|
self.assertEqual(result, "37.77N 122.42W")
|
||||||
|
|
||||||
|
def test_format_negative_latitude(self):
|
||||||
|
"""Тест форматирования с отрицательной широтой"""
|
||||||
|
result = format_coordinates(151.21, -33.87)
|
||||||
|
self.assertEqual(result, "33.87S 151.21E")
|
||||||
|
|
||||||
|
def test_format_both_negative(self):
|
||||||
|
"""Тест форматирования с обеими отрицательными координатами"""
|
||||||
|
result = format_coordinates(-58.38, -34.60)
|
||||||
|
self.assertEqual(result, "34.6S 58.38W")
|
||||||
|
|
||||||
|
|
||||||
|
class ParsePaginationParamsTestCase(TestCase):
|
||||||
|
"""Тесты для функции parse_pagination_params"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Тест значений по умолчанию"""
|
||||||
|
request = self.factory.get("/")
|
||||||
|
page, per_page = parse_pagination_params(request)
|
||||||
|
self.assertEqual(page, 1)
|
||||||
|
self.assertEqual(per_page, 50)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
"""Тест пользовательских значений"""
|
||||||
|
request = self.factory.get("/?page=3&items_per_page=100")
|
||||||
|
page, per_page = parse_pagination_params(request)
|
||||||
|
self.assertEqual(page, 3)
|
||||||
|
self.assertEqual(per_page, 100)
|
||||||
|
|
||||||
|
def test_invalid_page_number(self):
|
||||||
|
"""Тест невалидного номера страницы"""
|
||||||
|
request = self.factory.get("/?page=invalid")
|
||||||
|
page, per_page = parse_pagination_params(request)
|
||||||
|
self.assertEqual(page, 1)
|
||||||
|
|
||||||
|
def test_negative_page_number(self):
|
||||||
|
"""Тест отрицательного номера страницы"""
|
||||||
|
request = self.factory.get("/?page=-5")
|
||||||
|
page, per_page = parse_pagination_params(request)
|
||||||
|
self.assertEqual(page, 1)
|
||||||
|
|
||||||
|
def test_max_items_per_page_limit(self):
|
||||||
|
"""Тест ограничения максимального количества элементов"""
|
||||||
|
request = self.factory.get("/?items_per_page=20000")
|
||||||
|
page, per_page = parse_pagination_params(request)
|
||||||
|
self.assertEqual(per_page, 10000)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRequiredMixinTestCase(TestCase):
|
||||||
|
"""Тесты для RoleRequiredMixin"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_admin_has_access(self):
|
||||||
|
"""Тест что администратор имеет доступ"""
|
||||||
|
user = User.objects.create_user(username="testuser", password="12345")
|
||||||
|
# Get the automatically created CustomUser and set role to 'admin'
|
||||||
|
custom_user = CustomUser.objects.get(user=user)
|
||||||
|
custom_user.role = "admin"
|
||||||
|
custom_user.save()
|
||||||
|
|
||||||
|
# Refresh user to get updated customuser
|
||||||
|
user.refresh_from_db()
|
||||||
|
|
||||||
|
class TestView(RoleRequiredMixin, View):
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
view = TestView()
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = user
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
self.assertTrue(view.test_func())
|
||||||
|
|
||||||
|
def test_user_without_role_denied(self):
|
||||||
|
"""Тест что пользователь без роли не имеет доступа"""
|
||||||
|
user_no_role = User.objects.create_user(username="norole", password="12345")
|
||||||
|
# Get the automatically created CustomUser - default role is 'user'
|
||||||
|
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
|
||||||
|
self.assertEqual(custom_user_no_role.role, "user")
|
||||||
|
|
||||||
|
class TestView(RoleRequiredMixin, View):
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
view = TestView()
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = user_no_role
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
self.assertFalse(view.test_func())
|
||||||
|
|
||||||
|
|
||||||
|
class CoordinateProcessingMixinTestCase(TestCase):
|
||||||
|
"""Тесты для CoordinateProcessingMixin"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_extract_geo_coordinates(self):
|
||||||
|
"""Тест извлечения координат геолокации"""
|
||||||
|
|
||||||
|
class TestView(CoordinateProcessingMixin, View):
|
||||||
|
pass
|
||||||
|
|
||||||
|
view = TestView()
|
||||||
|
request = self.factory.post(
|
||||||
|
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
coords = view._extract_coordinates("geo")
|
||||||
|
self.assertIsNotNone(coords)
|
||||||
|
self.assertEqual(coords, (37.62, 55.75))
|
||||||
|
|
||||||
|
def test_extract_invalid_coordinates(self):
|
||||||
|
"""Тест извлечения невалидных координат"""
|
||||||
|
|
||||||
|
class TestView(CoordinateProcessingMixin, View):
|
||||||
|
pass
|
||||||
|
|
||||||
|
view = TestView()
|
||||||
|
request = self.factory.post(
|
||||||
|
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
coords = view._extract_coordinates("geo")
|
||||||
|
self.assertIsNone(coords)
|
||||||
|
|
||||||
|
def test_process_coordinates(self):
|
||||||
|
"""Тест обработки координат и применения к объекту Geo"""
|
||||||
|
|
||||||
|
class TestView(CoordinateProcessingMixin, View):
|
||||||
|
pass
|
||||||
|
|
||||||
|
view = TestView()
|
||||||
|
request = self.factory.post(
|
||||||
|
"/",
|
||||||
|
{
|
||||||
|
"geo_longitude": "37.62",
|
||||||
|
"geo_latitude": "55.75",
|
||||||
|
"kupsat_longitude": "37.63",
|
||||||
|
"kupsat_latitude": "55.76",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
geo_instance = Geo()
|
||||||
|
view.process_coordinates(geo_instance)
|
||||||
|
|
||||||
|
self.assertIsNotNone(geo_instance.coords)
|
||||||
|
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
|
||||||
|
self.assertIsNotNone(geo_instance.coords_kupsat)
|
||||||
|
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.conf.urls.static import static
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'mainapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
|
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
|
||||||
@@ -23,6 +24,4 @@ urlpatterns = [
|
|||||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||||
# path('upload/', views.upload_file, name='upload_file'),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -1,27 +1,44 @@
|
|||||||
from .models import (
|
# Standard library imports
|
||||||
Satellite,
|
import io
|
||||||
Standard,
|
|
||||||
Polarization,
|
|
||||||
Mirror,
|
|
||||||
Modulation,
|
|
||||||
Geo,
|
|
||||||
Parameter,
|
|
||||||
SigmaParameter,
|
|
||||||
ObjItem,
|
|
||||||
CustomUser
|
|
||||||
)
|
|
||||||
from mapsapp.models import Transponders
|
|
||||||
from datetime import datetime, time
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
from django.contrib.gis.geos import Point
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import io
|
from datetime import datetime, time
|
||||||
from django.db.models import F, Count, Exists, OuterRef, Min, Max
|
|
||||||
from geopy.geocoders import Nominatim
|
# Django imports
|
||||||
import reverse_geocoder as rg
|
from django.contrib.gis.geos import Point
|
||||||
from time import sleep
|
from django.db.models import F
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from mapsapp.models import Transponders
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
CustomUser,
|
||||||
|
Geo,
|
||||||
|
Mirror,
|
||||||
|
Modulation,
|
||||||
|
ObjItem,
|
||||||
|
Parameter,
|
||||||
|
Polarization,
|
||||||
|
Satellite,
|
||||||
|
SigmaParameter,
|
||||||
|
Standard,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Константы
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Значения по умолчанию для пагинации
|
||||||
|
DEFAULT_ITEMS_PER_PAGE = 50
|
||||||
|
MAX_ITEMS_PER_PAGE = 10000
|
||||||
|
|
||||||
|
# Значения по умолчанию для данных
|
||||||
|
DEFAULT_NUMERIC_VALUE = -1.0
|
||||||
|
MINIMUM_BANDWIDTH_MHZ = 0.08
|
||||||
|
|
||||||
|
|
||||||
def get_all_constants():
|
def get_all_constants():
|
||||||
sats = [sat.name for sat in Satellite.objects.all()]
|
sats = [sat.name for sat in Satellite.objects.all()]
|
||||||
@@ -31,9 +48,10 @@ def get_all_constants():
|
|||||||
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, mirrors, modulations
|
||||||
|
|
||||||
|
|
||||||
def coords_transform(coords: str):
|
def coords_transform(coords: str):
|
||||||
lat_part, lon_part = coords.strip().split()
|
lat_part, lon_part = coords.strip().split()
|
||||||
sign_map = {'N': 1, 'E': 1, 'S': -1, 'W': -1}
|
sign_map = {"N": 1, "E": 1, "S": -1, "W": -1}
|
||||||
|
|
||||||
lat_sign_char = lat_part[-1]
|
lat_sign_char = lat_part[-1]
|
||||||
lat_value = float(lat_part[:-1].replace(",", "."))
|
lat_value = float(lat_part[:-1].replace(",", "."))
|
||||||
@@ -45,55 +63,87 @@ def coords_transform(coords: str):
|
|||||||
|
|
||||||
return (longitude, latitude)
|
return (longitude, latitude)
|
||||||
|
|
||||||
|
|
||||||
def remove_str(s: str):
|
def remove_str(s: str):
|
||||||
if isinstance(s, str):
|
if isinstance(s, str):
|
||||||
if s.strip() == "-" or s.strip() == "" or s.strip() == " " or "неизв" in s.strip():
|
if (
|
||||||
|
s.strip() == "-"
|
||||||
|
or s.strip() == ""
|
||||||
|
or s.strip() == " "
|
||||||
|
or "неизв" in s.strip()
|
||||||
|
):
|
||||||
return -1
|
return -1
|
||||||
return float(s.strip().replace(",", "."))
|
return float(s.strip().replace(",", "."))
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||||
try:
|
try:
|
||||||
df.rename(columns={'Модуляция ': 'Модуляция'}, inplace=True)
|
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
consts = get_all_constants()
|
consts = get_all_constants()
|
||||||
df.fillna(-1, inplace=True)
|
df.fillna(-1, inplace=True)
|
||||||
for stroka in df.iterrows():
|
for stroka in df.iterrows():
|
||||||
geo_point = Point(coords_transform(stroka[1]['Координаты']), srid=4326)
|
geo_point = Point(coords_transform(stroka[1]["Координаты"]), srid=4326)
|
||||||
valid_point = None
|
valid_point = None
|
||||||
kupsat_point = None
|
kupsat_point = None
|
||||||
try:
|
try:
|
||||||
if stroka[1]['Координаты объекта'] != -1 and stroka[1]['Координаты Кубсата'] != '+':
|
if (
|
||||||
if 'ИРИ' not in stroka[1]['Координаты объекта'] and 'БЛА' not in stroka[1]['Координаты объекта']:
|
stroka[1]["Координаты объекта"] != -1
|
||||||
valid_point = list(map(float, stroka[1]['Координаты объекта'].replace(',', '.').split('. ')))
|
and stroka[1]["Координаты Кубсата"] != "+"
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
"ИРИ" not in stroka[1]["Координаты объекта"]
|
||||||
|
and "БЛА" not in stroka[1]["Координаты объекта"]
|
||||||
|
):
|
||||||
|
valid_point = list(
|
||||||
|
map(
|
||||||
|
float,
|
||||||
|
stroka[1]["Координаты объекта"]
|
||||||
|
.replace(",", ".")
|
||||||
|
.split(". "),
|
||||||
|
)
|
||||||
|
)
|
||||||
valid_point = Point(valid_point[1], valid_point[0], srid=4326)
|
valid_point = Point(valid_point[1], valid_point[0], srid=4326)
|
||||||
if stroka[1]['Координаты Кубсата'] != -1 and stroka[1]['Координаты Кубсата'] != '+':
|
if (
|
||||||
kupsat_point = list(map(float, stroka[1]['Координаты Кубсата'].replace(',', '.').split('. ')))
|
stroka[1]["Координаты Кубсата"] != -1
|
||||||
|
and stroka[1]["Координаты Кубсата"] != "+"
|
||||||
|
):
|
||||||
|
kupsat_point = list(
|
||||||
|
map(
|
||||||
|
float,
|
||||||
|
stroka[1]["Координаты Кубсата"].replace(",", ".").split(". "),
|
||||||
|
)
|
||||||
|
)
|
||||||
kupsat_point = Point(kupsat_point[1], kupsat_point[0], srid=4326)
|
kupsat_point = Point(kupsat_point[1], kupsat_point[0], srid=4326)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("В таблице нет столбцов с координатами кубсата")
|
print("В таблице нет столбцов с координатами кубсата")
|
||||||
try:
|
try:
|
||||||
polarization_obj, _ = Polarization.objects.get_or_create(name=stroka[1]['Поляризация'].strip())
|
polarization_obj, _ = Polarization.objects.get_or_create(
|
||||||
|
name=stroka[1]["Поляризация"].strip()
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
|
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
|
||||||
freq = remove_str(stroka[1]['Частота, МГц'])
|
freq = remove_str(stroka[1]["Частота, МГц"])
|
||||||
freq_line = remove_str(stroka[1]['Полоса, МГц'])
|
freq_line = remove_str(stroka[1]["Полоса, МГц"])
|
||||||
v = remove_str(stroka[1]['Символьная скорость, БОД'])
|
v = remove_str(stroka[1]["Символьная скорость, БОД"])
|
||||||
try:
|
try:
|
||||||
mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip())
|
mod_obj, _ = Modulation.objects.get_or_create(
|
||||||
|
name=stroka[1]["Модуляция"].strip()
|
||||||
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
mod_obj, _ = Modulation.objects.get_or_create(name='-')
|
mod_obj, _ = Modulation.objects.get_or_create(name="-")
|
||||||
snr = remove_str(stroka[1]['ОСШ'])
|
snr = remove_str(stroka[1]["ОСШ"])
|
||||||
date = stroka[1]['Дата'].date()
|
date = stroka[1]["Дата"].date()
|
||||||
time_ = stroka[1]['Время']
|
time_ = stroka[1]["Время"]
|
||||||
if isinstance(time_, str):
|
if isinstance(time_, str):
|
||||||
time_ = time_.strip()
|
time_ = time_.strip()
|
||||||
time_ = time(0, 0, 0)
|
time_ = time(0, 0, 0)
|
||||||
timestamp = datetime.combine(date, time_)
|
timestamp = datetime.combine(date, time_)
|
||||||
current_mirrors = []
|
current_mirrors = []
|
||||||
mirror_1 = stroka[1]['Зеркало 1'].strip().split("\n")
|
mirror_1 = stroka[1]["Зеркало 1"].strip().split("\n")
|
||||||
mirror_2 = stroka[1]['Зеркало 2'].strip().split("\n")
|
mirror_2 = stroka[1]["Зеркало 2"].strip().split("\n")
|
||||||
if len(mirror_1) > 1:
|
if len(mirror_1) > 1:
|
||||||
for mir in mirror_1:
|
for mir in mirror_1:
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
||||||
@@ -108,9 +158,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
|||||||
elif mirror_2[0] not in consts[3]:
|
elif mirror_2[0] not in consts[3]:
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
||||||
current_mirrors.append(mirror_2[0].strip())
|
current_mirrors.append(mirror_2[0].strip())
|
||||||
location = stroka[1]['Местоопределение'].strip()
|
location = stroka[1]["Местоопределение"].strip()
|
||||||
comment = stroka[1]['Комментарий']
|
comment = stroka[1]["Комментарий"]
|
||||||
source = stroka[1]['Объект наблюдения']
|
source = stroka[1]["Объект наблюдения"]
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
|
|
||||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||||
@@ -127,46 +177,57 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
|||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
coords=geo_point,
|
coords=geo_point,
|
||||||
defaults={
|
defaults={
|
||||||
'coords_kupsat': kupsat_point,
|
"coords_kupsat": kupsat_point,
|
||||||
'coords_valid': valid_point,
|
"coords_valid": valid_point,
|
||||||
'location': location,
|
"location": location,
|
||||||
'comment': comment,
|
"comment": comment,
|
||||||
'is_average': (comment != -1.0),
|
"is_average": (comment != -1.0),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
geo.save()
|
geo.save()
|
||||||
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||||
|
|
||||||
existing_obj_items = ObjItem.objects.filter(
|
existing_obj_items = ObjItem.objects.filter(
|
||||||
parameters_obj=vch_load_obj,
|
parameters_obj=vch_load_obj, geo_obj=geo
|
||||||
geo_obj=geo
|
|
||||||
)
|
)
|
||||||
if not existing_obj_items.exists():
|
if not existing_obj_items.exists():
|
||||||
obj_item = ObjItem.objects.create(
|
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
|
||||||
name=source,
|
|
||||||
created_by=user_to_use
|
|
||||||
)
|
|
||||||
obj_item.parameters_obj.set([vch_load_obj])
|
obj_item.parameters_obj.set([vch_load_obj])
|
||||||
geo.objitem = obj_item
|
geo.objitem = obj_item
|
||||||
geo.save()
|
geo.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_satellite_list():
|
def add_satellite_list():
|
||||||
sats = ['AZERSPACE 2', 'Amos 4', 'Astra 4A', 'ComsatBW-1', 'Eutelsat 16A',
|
sats = [
|
||||||
'Eutelsat 21B', 'Eutelsat 7B', 'ExpressAM6', 'Hellas Sat 3',
|
"AZERSPACE 2",
|
||||||
'Intelsat 39', 'Intelsat 17',
|
"Amos 4",
|
||||||
'NSS 12', 'Sicral 2', 'SkyNet 5B', 'SkyNet 5D', 'Syracuse 4A',
|
"Astra 4A",
|
||||||
'Turksat 3A', 'Turksat 4A', 'WGS 10', 'Yamal 402']
|
"ComsatBW-1",
|
||||||
|
"Eutelsat 16A",
|
||||||
|
"Eutelsat 21B",
|
||||||
|
"Eutelsat 7B",
|
||||||
|
"ExpressAM6",
|
||||||
|
"Hellas Sat 3",
|
||||||
|
"Intelsat 39",
|
||||||
|
"Intelsat 17",
|
||||||
|
"NSS 12",
|
||||||
|
"Sicral 2",
|
||||||
|
"SkyNet 5B",
|
||||||
|
"SkyNet 5D",
|
||||||
|
"Syracuse 4A",
|
||||||
|
"Turksat 3A",
|
||||||
|
"Turksat 4A",
|
||||||
|
"WGS 10",
|
||||||
|
"Yamal 402",
|
||||||
|
]
|
||||||
|
|
||||||
for sat in sats:
|
for sat in sats:
|
||||||
sat_obj, _ = Satellite.objects.get_or_create(
|
sat_obj, _ = Satellite.objects.get_or_create(name=sat)
|
||||||
name=sat
|
|
||||||
)
|
|
||||||
sat_obj.save()
|
sat_obj.save()
|
||||||
|
|
||||||
|
|
||||||
def parse_string(s: str):
|
def parse_string(s: str):
|
||||||
pattern = r'^(.+?) (-?\d+\,\d+) \[(-?\d+\,\d+)\] ([^\s]+) ([A-Za-z]) - (\d{1,2}\.\d{1,2}\.\d{1,4} \d{1,2}:\d{1,2}:\d{1,2})$'
|
pattern = r"^(.+?) (-?\d+\,\d+) \[(-?\d+\,\d+)\] ([^\s]+) ([A-Za-z]) - (\d{1,2}\.\d{1,2}\.\d{1,4} \d{1,2}:\d{1,2}:\d{1,2})$"
|
||||||
match = re.match(pattern, s)
|
match = re.match(pattern, s)
|
||||||
if match:
|
if match:
|
||||||
return list(match.groups())
|
return list(match.groups())
|
||||||
@@ -175,21 +236,21 @@ def parse_string(s: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_point_from_json(filepath: str):
|
def get_point_from_json(filepath: str):
|
||||||
with open(filepath, encoding='utf-8-sig') as jf:
|
with open(filepath, encoding="utf-8-sig") as jf:
|
||||||
data = json.load(jf)
|
data = json.load(jf)
|
||||||
|
|
||||||
for obj in data:
|
for obj in data:
|
||||||
if not obj.get('bearingBehavior', {}):
|
if not obj.get("bearingBehavior", {}):
|
||||||
if obj['tacticObjectType'] == "source":
|
if obj["tacticObjectType"] == "source":
|
||||||
# if not obj['bearingBehavior']:
|
# if not obj['bearingBehavior']:
|
||||||
source_id = obj['id']
|
source_id = obj["id"]
|
||||||
name = obj['name']
|
name = obj["name"]
|
||||||
elements = parse_string(name)
|
elements = parse_string(name)
|
||||||
sat_name = elements[0]
|
sat_name = elements[0]
|
||||||
freq = elements[1]
|
freq = elements[1]
|
||||||
freq_range = elements[2]
|
freq_range = elements[2]
|
||||||
pol = elements[4]
|
pol = elements[4]
|
||||||
timestamp = datetime.strptime(elements[-1], '%d.%m.%y %H:%M:%S')
|
timestamp = datetime.strptime(elements[-1], "%d.%m.%y %H:%M:%S")
|
||||||
lat = None
|
lat = None
|
||||||
lon = None
|
lon = None
|
||||||
for pos in data:
|
for pos in data:
|
||||||
@@ -197,157 +258,170 @@ def get_point_from_json(filepath: str):
|
|||||||
lat = pos["latitude"]
|
lat = pos["latitude"]
|
||||||
lon = pos["longitude"]
|
lon = pos["longitude"]
|
||||||
break
|
break
|
||||||
print(f"Name - {sat_name}, f - {freq}, f range - {freq_range}, pol - {pol} "
|
print(
|
||||||
f"time - {timestamp}, pos - ({lat}, {lon})")
|
f"Name - {sat_name}, f - {freq}, f range - {freq_range}, pol - {pol} "
|
||||||
|
f"time - {timestamp}, pos - ({lat}, {lon})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_points_from_csv(file_content, current_user=None):
|
def get_points_from_csv(file_content, current_user=None):
|
||||||
df = pd.read_csv(io.StringIO(file_content), sep=";",
|
df = pd.read_csv(
|
||||||
names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
io.StringIO(file_content),
|
||||||
df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
sep=";",
|
||||||
df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
names=[
|
||||||
|
"id",
|
||||||
|
"obj",
|
||||||
|
"lat",
|
||||||
|
"lon",
|
||||||
|
"h",
|
||||||
|
"time",
|
||||||
|
"sat",
|
||||||
|
"norad_id",
|
||||||
|
"freq",
|
||||||
|
"f_range",
|
||||||
|
"et",
|
||||||
|
"qaul",
|
||||||
|
"mir_1",
|
||||||
|
"mir_2",
|
||||||
|
"mir_3",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
df[["lat", "lon", "freq", "f_range"]] = (
|
||||||
|
df[["lat", "lon", "freq", "f_range"]]
|
||||||
|
.replace(",", ".", regex=True)
|
||||||
|
.astype(float)
|
||||||
|
)
|
||||||
|
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
|
||||||
for row in df.iterrows():
|
for row in df.iterrows():
|
||||||
row = row[1]
|
row = row[1]
|
||||||
match row['obj'].split(' ')[-1]:
|
match row["obj"].split(" ")[-1]:
|
||||||
case 'V':
|
case "V":
|
||||||
pol = 'Вертикальная'
|
pol = "Вертикальная"
|
||||||
case 'H':
|
case "H":
|
||||||
pol = 'Горизонтальная'
|
pol = "Горизонтальная"
|
||||||
case 'R':
|
case "R":
|
||||||
pol = 'Правая'
|
pol = "Правая"
|
||||||
case 'L':
|
case "L":
|
||||||
pol = 'Левая'
|
pol = "Левая"
|
||||||
case _:
|
case _:
|
||||||
pol = '-'
|
pol = "-"
|
||||||
pol_obj, _ = Polarization.objects.get_or_create(
|
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||||
name=pol
|
|
||||||
)
|
|
||||||
sat_obj, _ = Satellite.objects.get_or_create(
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
name=row['sat'],
|
name=row["sat"], defaults={"norad": row["norad_id"]}
|
||||||
defaults={'norad': row['norad_id']}
|
|
||||||
)
|
|
||||||
mir_1_obj, _ = Mirror.objects.get_or_create(
|
|
||||||
name=row['mir_1']
|
|
||||||
)
|
|
||||||
mir_2_obj, _ = Mirror.objects.get_or_create(
|
|
||||||
name=row['mir_2']
|
|
||||||
)
|
|
||||||
mir_lst = [row['mir_1'], row['mir_2']]
|
|
||||||
if not pd.isna(row['mir_3']):
|
|
||||||
mir_3_obj, _ = Mirror.objects.get_or_create(
|
|
||||||
|
|
||||||
name=row['mir_3']
|
|
||||||
)
|
)
|
||||||
|
mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
|
||||||
|
mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
|
||||||
|
mir_lst = [row["mir_1"], row["mir_2"]]
|
||||||
|
if not pd.isna(row["mir_3"]):
|
||||||
|
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
|
|
||||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||||
id_satellite=sat_obj,
|
id_satellite=sat_obj,
|
||||||
polarization=pol_obj,
|
polarization=pol_obj,
|
||||||
frequency=row['freq'],
|
frequency=row["freq"],
|
||||||
freq_range=row['f_range'],
|
freq_range=row["f_range"],
|
||||||
# defaults={'id_user_add': user_to_use}
|
# defaults={'id_user_add': user_to_use}
|
||||||
)
|
)
|
||||||
|
|
||||||
geo_obj, _ = Geo.objects.get_or_create(
|
geo_obj, _ = Geo.objects.get_or_create(
|
||||||
timestamp=row['time'],
|
timestamp=row["time"],
|
||||||
coords=Point(row['lon'], row['lat'], srid=4326),
|
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||||
defaults={
|
defaults={
|
||||||
'is_average': False,
|
"is_average": False,
|
||||||
# 'id_user_add': user_to_use,
|
# 'id_user_add': user_to_use,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||||
|
|
||||||
existing_obj_items = ObjItem.objects.filter(
|
existing_obj_items = ObjItem.objects.filter(
|
||||||
parameters_obj=vch_load_obj,
|
parameters_obj=vch_load_obj, geo_obj=geo_obj
|
||||||
geo_obj=geo_obj
|
|
||||||
)
|
)
|
||||||
if not existing_obj_items.exists():
|
if not existing_obj_items.exists():
|
||||||
obj_item = ObjItem.objects.create(
|
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
|
||||||
name=row['obj'],
|
|
||||||
created_by=user_to_use
|
|
||||||
)
|
|
||||||
obj_item.parameters_obj.set([vch_load_obj])
|
obj_item.parameters_obj.set([vch_load_obj])
|
||||||
geo_obj.objitem = obj_item
|
geo_obj.objitem = obj_item
|
||||||
geo_obj.save()
|
geo_obj.save()
|
||||||
|
|
||||||
|
|
||||||
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||||
filename = file.name.split('_')
|
filename = file.name.split("_")
|
||||||
transfer = filename[3]
|
transfer = filename[3]
|
||||||
match filename[2]:
|
match filename[2]:
|
||||||
case 'H':
|
case "H":
|
||||||
pol = 'Горизонтальная'
|
pol = "Горизонтальная"
|
||||||
case 'V':
|
case "V":
|
||||||
pol = 'Вертикальная'
|
pol = "Вертикальная"
|
||||||
case 'R':
|
case "R":
|
||||||
pol = 'Правая'
|
pol = "Правая"
|
||||||
case 'L':
|
case "L":
|
||||||
pol = 'Левая'
|
pol = "Левая"
|
||||||
case _:
|
case _:
|
||||||
pol = '-'
|
pol = "-"
|
||||||
|
|
||||||
tables = pd.read_html(file, encoding='windows-1251')
|
tables = pd.read_html(file, encoding="windows-1251")
|
||||||
df = tables[0]
|
df = tables[0]
|
||||||
df = df.drop(0).reset_index(drop=True)
|
df = df.drop(0).reset_index(drop=True)
|
||||||
df.columns = df.iloc[0]
|
df.columns = df.iloc[0]
|
||||||
df = df.drop(0).reset_index(drop=True)
|
df = df.drop(0).reset_index(drop=True)
|
||||||
df.replace('Неизвестно', '-', inplace=True)
|
df.replace("Неизвестно", "-", inplace=True)
|
||||||
df[['Частота, МГц', 'Полоса, МГц', 'Мощность, дБм']] = df[['Частота, МГц', 'Полоса, МГц', 'Мощность, дБм']].apply(pd.to_numeric)
|
df[["Частота, МГц", "Полоса, МГц", "Мощность, дБм"]] = df[
|
||||||
df['Время начала измерения'] = df['Время начала измерения'].apply(lambda x: datetime.strptime(x, '%d.%m.%Y %H:%M:%S'))
|
["Частота, МГц", "Полоса, МГц", "Мощность, дБм"]
|
||||||
df['Время окончания измерения'] = df['Время окончания измерения'].apply(lambda x: datetime.strptime(x, '%d.%m.%Y %H:%M:%S'))
|
].apply(pd.to_numeric)
|
||||||
|
df["Время начала измерения"] = df["Время начала измерения"].apply(
|
||||||
|
lambda x: datetime.strptime(x, "%d.%m.%Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
df["Время окончания измерения"] = df["Время окончания измерения"].apply(
|
||||||
|
lambda x: datetime.strptime(x, "%d.%m.%Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
for stroka in df.iterrows():
|
for stroka in df.iterrows():
|
||||||
value = stroka[1]
|
value = stroka[1]
|
||||||
if value['Полоса, МГц'] < 0.08:
|
if value["Полоса, МГц"] < 0.08:
|
||||||
continue
|
continue
|
||||||
if '-' in value['Символьная скорость']:
|
if "-" in value["Символьная скорость"]:
|
||||||
bod_velocity = -1.0
|
bod_velocity = -1.0
|
||||||
else:
|
else:
|
||||||
bod_velocity = value['Символьная скорость']
|
bod_velocity = value["Символьная скорость"]
|
||||||
if '-' in value['Сигнал/шум, дБ']:
|
if "-" in value["Сигнал/шум, дБ"]:
|
||||||
snr = -1.0
|
snr = -1.0
|
||||||
else:
|
else:
|
||||||
snr = value['Сигнал/шум, дБ']
|
snr = value["Сигнал/шум, дБ"]
|
||||||
if value['Пакетность'] == 'да':
|
if value["Пакетность"] == "да":
|
||||||
pack = True
|
pack = True
|
||||||
elif value['Пакетность'] == 'нет':
|
elif value["Пакетность"] == "нет":
|
||||||
pack = False
|
pack = False
|
||||||
else:
|
else:
|
||||||
pack = None
|
pack = None
|
||||||
|
|
||||||
polarization, _ = Polarization.objects.get_or_create(
|
polarization, _ = Polarization.objects.get_or_create(name=pol)
|
||||||
name=pol
|
|
||||||
)
|
|
||||||
|
|
||||||
mod, _ = Modulation.objects.get_or_create(
|
mod, _ = Modulation.objects.get_or_create(name=value["Модуляция"])
|
||||||
name=value['Модуляция']
|
standard, _ = Standard.objects.get_or_create(name=value["Стандарт"])
|
||||||
)
|
|
||||||
standard, _ = Standard.objects.get_or_create(
|
|
||||||
name=value['Стандарт']
|
|
||||||
)
|
|
||||||
sigma_load, _ = SigmaParameter.objects.get_or_create(
|
sigma_load, _ = SigmaParameter.objects.get_or_create(
|
||||||
id_satellite=sat,
|
id_satellite=sat,
|
||||||
frequency=value['Частота, МГц'],
|
frequency=value["Частота, МГц"],
|
||||||
freq_range=value['Полоса, МГц'],
|
freq_range=value["Полоса, МГц"],
|
||||||
polarization=polarization,
|
polarization=polarization,
|
||||||
defaults={
|
defaults={
|
||||||
"transfer": float(transfer),
|
"transfer": float(transfer),
|
||||||
# "polarization": polarization,
|
# "polarization": polarization,
|
||||||
"status": value['Статус'],
|
"status": value["Статус"],
|
||||||
"power": value['Мощность, дБм'],
|
"power": value["Мощность, дБм"],
|
||||||
"bod_velocity": bod_velocity,
|
"bod_velocity": bod_velocity,
|
||||||
"modulation": mod,
|
"modulation": mod,
|
||||||
"snr": snr,
|
"snr": snr,
|
||||||
"packets": pack,
|
"packets": pack,
|
||||||
"datetime_begin": value['Время начала измерения'],
|
"datetime_begin": value["Время начала измерения"],
|
||||||
"datetime_end": value['Время окончания измерения'],
|
"datetime_end": value["Время окончания измерения"],
|
||||||
}
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
sigma_load.save()
|
sigma_load.save()
|
||||||
|
|
||||||
def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float):
|
|
||||||
|
def compare_and_link_vch_load(
|
||||||
|
sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float
|
||||||
|
):
|
||||||
item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
|
item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
|
||||||
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
|
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
|
||||||
link_count = 0
|
link_count = 0
|
||||||
@@ -358,43 +432,57 @@ def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: fl
|
|||||||
continue
|
continue
|
||||||
for sigma in vch_sigma:
|
for sigma in vch_sigma:
|
||||||
if (
|
if (
|
||||||
abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq and
|
abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq
|
||||||
abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100 and
|
and abs(sigma.freq_range - vch_load.freq_range)
|
||||||
sigma.polarization == vch_load.polarization
|
<= vch_load.freq_range * eps_frange / 100
|
||||||
|
and sigma.polarization == vch_load.polarization
|
||||||
):
|
):
|
||||||
sigma.parameter = vch_load
|
sigma.parameter = vch_load
|
||||||
sigma.save()
|
sigma.save()
|
||||||
link_count += 1
|
link_count += 1
|
||||||
return obj_count, link_count
|
return obj_count, link_count
|
||||||
|
|
||||||
|
|
||||||
def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
||||||
df_in = pd.read_excel(data_in)
|
df_in = pd.read_excel(data_in)
|
||||||
df = pd.DataFrame(columns=['Дата', 'Широта', 'Долгота',
|
df = pd.DataFrame(
|
||||||
'Высота', 'Населённый пункт', 'ИСЗ',
|
columns=[
|
||||||
'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос, МГц', 'Полоса, МГц', 'Зеркала'])
|
"Дата",
|
||||||
|
"Широта",
|
||||||
|
"Долгота",
|
||||||
|
"Высота",
|
||||||
|
"Населённый пункт",
|
||||||
|
"ИСЗ",
|
||||||
|
"Прямой канал, МГц",
|
||||||
|
"Обратный канал, МГц",
|
||||||
|
"Перенос, МГц",
|
||||||
|
"Полоса, МГц",
|
||||||
|
"Зеркала",
|
||||||
|
]
|
||||||
|
)
|
||||||
for row in df_in.iterrows():
|
for row in df_in.iterrows():
|
||||||
value = row[1]
|
value = row[1]
|
||||||
date = datetime.date(datetime.now())
|
date = datetime.date(datetime.now())
|
||||||
isz = value['ИСЗ']
|
isz = value["ИСЗ"]
|
||||||
try:
|
try:
|
||||||
lat = float(value['Широта, град'].strip().replace(',', '.'))
|
lat = float(value["Широта, град"].strip().replace(",", "."))
|
||||||
lon = float(value['Долгота, град'].strip().replace(',', '.'))
|
lon = float(value["Долгота, град"].strip().replace(",", "."))
|
||||||
downlink = float(value['Обратный канал, МГц'].strip().replace(',', '.'))
|
downlink = float(value["Обратный канал, МГц"].strip().replace(",", "."))
|
||||||
freq_range = float(value['Полоса, МГц'].strip().replace(',', '.'))
|
freq_range = float(value["Полоса, МГц"].strip().replace(",", "."))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
lat = value['Широта, град']
|
lat = value["Широта, град"]
|
||||||
lon = value['Долгота, град']
|
lon = value["Долгота, град"]
|
||||||
downlink = value['Обратный канал, МГц']
|
downlink = value["Обратный канал, МГц"]
|
||||||
freq_range = value['Полоса, МГц']
|
freq_range = value["Полоса, МГц"]
|
||||||
print(e)
|
print(e)
|
||||||
norad = int(re.findall(r'\((\d+)\)', isz)[0])
|
norad = int(re.findall(r"\((\d+)\)", isz)[0])
|
||||||
sat_obj = Satellite.objects.get(norad=norad)
|
sat_obj = Satellite.objects.get(norad=norad)
|
||||||
pol_obj = Polarization.objects.get(name=value['Поляризация'].strip())
|
pol_obj = Polarization.objects.get(name=value["Поляризация"].strip())
|
||||||
transponder = Transponders.objects.filter(
|
transponder = Transponders.objects.filter(
|
||||||
sat_id=sat_obj,
|
sat_id=sat_obj,
|
||||||
polarization=pol_obj,
|
polarization=pol_obj,
|
||||||
downlink__gte=downlink - F('frequency_range')/2,
|
downlink__gte=downlink - F("frequency_range") / 2,
|
||||||
downlink__lte=downlink + F('frequency_range')/2,
|
downlink__lte=downlink + F("frequency_range") / 2,
|
||||||
).first()
|
).first()
|
||||||
# try:
|
# try:
|
||||||
# location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address']
|
# location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address']
|
||||||
@@ -402,24 +490,137 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
|||||||
# except AttributeError:
|
# except AttributeError:
|
||||||
# loc_name = ''
|
# loc_name = ''
|
||||||
# sleep(1)
|
# sleep(1)
|
||||||
loc_name = ''
|
loc_name = ""
|
||||||
if transponder: # and not (len(transponder) > 1):
|
if transponder: # and not (len(transponder) > 1):
|
||||||
transfer = transponder.transfer
|
transfer = transponder.transfer
|
||||||
uplink = transfer + downlink
|
uplink = transfer + downlink
|
||||||
new_row = pd.DataFrame([{'Дата': date,
|
new_row = pd.DataFrame(
|
||||||
'Широта': lat,
|
[
|
||||||
'Долгота': lon,
|
{
|
||||||
'Высота': 0.0,
|
"Дата": date,
|
||||||
'Населённый пункт': loc_name,
|
"Широта": lat,
|
||||||
'ИСЗ': isz,
|
"Долгота": lon,
|
||||||
'Прямой канал, МГц': uplink,
|
"Высота": 0.0,
|
||||||
'Обратный канал, МГц': downlink,
|
"Населённый пункт": loc_name,
|
||||||
'Перенос, МГц': transfer,
|
"ИСЗ": isz,
|
||||||
'Полоса, МГц': freq_range,
|
"Прямой канал, МГц": uplink,
|
||||||
'Зеркала': ''
|
"Обратный канал, МГц": downlink,
|
||||||
}])
|
"Перенос, МГц": transfer,
|
||||||
|
"Полоса, МГц": freq_range,
|
||||||
|
"Зеркала": "",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
df = pd.concat([df, new_row], ignore_index=True)
|
df = pd.concat([df, new_row], ignore_index=True)
|
||||||
else:
|
else:
|
||||||
print("Ничего не найдено в транспондерах")
|
print("Ничего не найдено в транспондерах")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Утилиты для форматирования
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def format_coordinates(longitude: float, latitude: float) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует координаты в читаемый вид.
|
||||||
|
|
||||||
|
Преобразует числовые координаты в формат с указанием направления
|
||||||
|
(N/S для широты, E/W для долготы).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
longitude (float): Долгота в десятичных градусах.
|
||||||
|
latitude (float): Широта в десятичных градусах.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка координат в формате "XXN/S YYE/W".
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> format_coordinates(37.62, 55.75)
|
||||||
|
'55.75N 37.62E'
|
||||||
|
>>> format_coordinates(-122.42, 37.77)
|
||||||
|
'37.77N 122.42W'
|
||||||
|
"""
|
||||||
|
lon_direction = "E" if longitude > 0 else "W"
|
||||||
|
lat_direction = "N" if latitude > 0 else "S"
|
||||||
|
|
||||||
|
lon_value = abs(longitude)
|
||||||
|
lat_value = abs(latitude)
|
||||||
|
|
||||||
|
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pagination_params(
|
||||||
|
request, default_per_page: int = DEFAULT_ITEMS_PER_PAGE
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Извлекает и валидирует параметры пагинации из запроса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: HTTP запрос Django.
|
||||||
|
default_per_page (int): Количество элементов на странице по умолчанию.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: Кортеж (page_number, items_per_page), где:
|
||||||
|
- page_number (int): Номер текущей страницы (по умолчанию 1).
|
||||||
|
- items_per_page (int): Количество элементов на странице.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> page, per_page = parse_pagination_params(request, default_per_page=100)
|
||||||
|
>>> paginator = Paginator(objects, per_page)
|
||||||
|
>>> page_obj = paginator.get_page(page)
|
||||||
|
"""
|
||||||
|
page_number = request.GET.get("page", 1)
|
||||||
|
items_per_page = request.GET.get("items_per_page", str(default_per_page))
|
||||||
|
|
||||||
|
# Валидация page_number
|
||||||
|
try:
|
||||||
|
page_number = int(page_number)
|
||||||
|
if page_number < 1:
|
||||||
|
page_number = 1
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page_number = 1
|
||||||
|
|
||||||
|
# Валидация items_per_page
|
||||||
|
try:
|
||||||
|
items_per_page = int(items_per_page)
|
||||||
|
if items_per_page < 1:
|
||||||
|
items_per_page = default_per_page
|
||||||
|
# Ограничиваем максимальное значение для предотвращения перегрузки
|
||||||
|
if items_per_page > MAX_ITEMS_PER_PAGE:
|
||||||
|
items_per_page = MAX_ITEMS_PER_PAGE
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
items_per_page = default_per_page
|
||||||
|
|
||||||
|
return page_number, items_per_page
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_param_subquery(field_name: str):
|
||||||
|
"""
|
||||||
|
Создает подзапрос для получения первого параметра объекта.
|
||||||
|
|
||||||
|
Используется для аннотации queryset с полями из связанной модели Parameter.
|
||||||
|
Возвращает значение указанного поля из первого параметра объекта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name (str): Имя поля модели Parameter для извлечения.
|
||||||
|
Может включать связанные поля через __ (например, 'id_satellite__name').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Subquery: Django Subquery объект для использования в annotate().
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from django.db.models import Subquery, OuterRef
|
||||||
|
>>> freq_subq = get_first_param_subquery('frequency')
|
||||||
|
>>> objects = ObjItem.objects.annotate(first_freq=Subquery(freq_subq))
|
||||||
|
>>> for obj in objects:
|
||||||
|
... print(obj.first_freq)
|
||||||
|
"""
|
||||||
|
from django.db.models import OuterRef
|
||||||
|
|
||||||
|
return (
|
||||||
|
Parameter.objects.filter(objitems=OuterRef("pk"))
|
||||||
|
.order_by("id")
|
||||||
|
.values(field_name)[:1]
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,45 @@
|
|||||||
|
# Django imports
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Transponders
|
|
||||||
from rangefilter.filters import NumericRangeFilterBuilder
|
# Third-party imports
|
||||||
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
|
|
||||||
from import_export.admin import ImportExportActionModelAdmin
|
from import_export.admin import ImportExportActionModelAdmin
|
||||||
|
from more_admin_filters import MultiSelectRelatedDropdownFilter
|
||||||
|
from rangefilter.filters import NumericRangeFilterBuilder
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from .models import Transponders
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Base Admin Classes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class BaseAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Базовый класс для всех admin моделей mapsapp.
|
||||||
|
|
||||||
|
Предоставляет общую функциональность:
|
||||||
|
- Кнопки сохранения сверху и снизу
|
||||||
|
- Настройка количества элементов на странице
|
||||||
|
"""
|
||||||
|
save_on_top = True
|
||||||
|
list_per_page = 50
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Admin Classes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@admin.register(Transponders)
|
@admin.register(Transponders)
|
||||||
class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||||
|
"""
|
||||||
|
Админ-панель для модели Transponders.
|
||||||
|
|
||||||
|
Оптимизирована для работы с транспондерами:
|
||||||
|
- Использует select_related для оптимизации запросов
|
||||||
|
- Предоставляет фильтры по спутникам, поляризации и зоне
|
||||||
|
- Поддерживает импорт/экспорт данных
|
||||||
|
"""
|
||||||
list_display = (
|
list_display = (
|
||||||
"sat_id",
|
"sat_id",
|
||||||
"name",
|
"name",
|
||||||
@@ -16,13 +50,18 @@ class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
|||||||
"transfer",
|
"transfer",
|
||||||
"polarization",
|
"polarization",
|
||||||
)
|
)
|
||||||
|
list_display_links = ("name",)
|
||||||
|
list_select_related = ("polarization", "sat_id")
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
("polarization", MultiSelectRelatedDropdownFilter),
|
("polarization", MultiSelectRelatedDropdownFilter),
|
||||||
("sat_id", MultiSelectRelatedDropdownFilter),
|
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||||
# ("frequency", NumericRangeFilterBuilder()),
|
("downlink", NumericRangeFilterBuilder()),
|
||||||
"zone_name"
|
("uplink", NumericRangeFilterBuilder()),
|
||||||
|
("frequency_range", NumericRangeFilterBuilder()),
|
||||||
|
"zone_name",
|
||||||
)
|
)
|
||||||
search_fields = ("name", "sat_id__name")
|
|
||||||
|
search_fields = ("name", "sat_id__name", "zone_name")
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
# def sat_name(self, obj):
|
autocomplete_fields = ("sat_id", "polarization")
|
||||||
# return
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-07 20:58
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mainapp.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
|
||||||
|
('mapsapp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='transponders',
|
||||||
|
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='downlink',
|
||||||
|
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='frequency_range',
|
||||||
|
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='polarization',
|
||||||
|
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='sat_id',
|
||||||
|
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='uplink',
|
||||||
|
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transponders',
|
||||||
|
name='zone_name',
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='transponders',
|
||||||
|
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='transponders',
|
||||||
|
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,18 +1,81 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from mainapp.models import Satellite, Polarization, get_default_polarization
|
from django.db.models import ExpressionWrapper, F
|
||||||
from django.db.models import F, ExpressionWrapper
|
|
||||||
from django.db.models.functions import Abs
|
from django.db.models.functions import Abs
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from mainapp.models import Polarization, Satellite, get_default_polarization
|
||||||
|
|
||||||
|
|
||||||
class Transponders(models.Model):
|
class Transponders(models.Model):
|
||||||
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
|
"""
|
||||||
downlink = models.FloatField(blank=True, null=True, verbose_name="Downlink")
|
Модель транспондера спутника.
|
||||||
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса")
|
|
||||||
uplink = models.FloatField(blank=True, null=True, verbose_name="Uplink")
|
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
|
||||||
zone_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Название зоны")
|
"""
|
||||||
polarization = models.ForeignKey(
|
|
||||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
|
# Основные поля
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Название транспондера",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Название транспондера"
|
||||||
)
|
)
|
||||||
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
|
downlink = models.FloatField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Downlink",
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||||
|
help_text="Частота downlink в МГц (0-50000)"
|
||||||
|
)
|
||||||
|
frequency_range = models.FloatField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Полоса",
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||||
|
help_text="Полоса частот в МГц (0-1000)"
|
||||||
|
)
|
||||||
|
uplink = models.FloatField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Uplink",
|
||||||
|
validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||||||
|
help_text="Частота uplink в МГц (0-50000)"
|
||||||
|
)
|
||||||
|
zone_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Название зоны",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Название зоны покрытия транспондера"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
polarization = models.ForeignKey(
|
||||||
|
Polarization,
|
||||||
|
default=get_default_polarization,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
related_name="tran_polarizations",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Поляризация",
|
||||||
|
help_text="Поляризация сигнала"
|
||||||
|
)
|
||||||
|
sat_id = models.ForeignKey(
|
||||||
|
Satellite,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="tran_satellite",
|
||||||
|
verbose_name="Спутник",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Спутник, которому принадлежит транспондер"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Вычисляемые поля
|
||||||
transfer = models.GeneratedField(
|
transfer = models.GeneratedField(
|
||||||
expression=ExpressionWrapper(
|
expression=ExpressionWrapper(
|
||||||
Abs(F('downlink') - F('uplink')),
|
Abs(F('downlink') - F('uplink')),
|
||||||
@@ -20,14 +83,35 @@ class Transponders(models.Model):
|
|||||||
),
|
),
|
||||||
output_field=models.FloatField(),
|
output_field=models.FloatField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
null=True, blank=True, verbose_name="Перенос"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Перенос"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Валидация на уровне модели"""
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Проверка что downlink и uplink заданы
|
||||||
|
if self.downlink and self.uplink:
|
||||||
|
# Обычно uplink выше downlink для спутниковой связи
|
||||||
|
if self.uplink < self.downlink:
|
||||||
|
raise ValidationError({
|
||||||
|
'uplink': 'Частота uplink обычно выше частоты downlink'
|
||||||
|
})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.name:
|
||||||
return self.name
|
return self.name
|
||||||
|
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Транспондер"
|
verbose_name = "Транспондер"
|
||||||
verbose_name_plural = "Транспондеры"
|
verbose_name_plural = "Транспондеры"
|
||||||
|
ordering = ['sat_id', 'downlink']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['sat_id', 'downlink']),
|
||||||
|
models.Index(fields=['sat_id', 'zone_name']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
<title>{% block title %}Карта{% endblock %}</title>
|
<title>{% block title %}Карта{% endblock %}</title>
|
||||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||||
|
<!-- Leaflet CSS -->
|
||||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Extra CSS -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -34,10 +37,10 @@
|
|||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
<!-- Leaflet JavaScript -->
|
||||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||||
{% comment %} <script src="{% static 'leaflet-tree/LayersTree.js' %}"></script> {% endcomment %}
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let map = L.map('map').setView([0, 0], 2);
|
let map = L.map('map').setView([0, 0], 2);
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
<title>Cesium Map Editor</title>
|
<title>Cesium Map Editor</title>
|
||||||
<script src="{% static 'cesium/Cesium.js' %}"></script>
|
<!-- Cesium Library -->
|
||||||
|
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
|
||||||
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Styles -->
|
||||||
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.conf.urls.static import static
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'mapsapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
|
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
|
||||||
@@ -10,8 +11,4 @@ urlpatterns = [
|
|||||||
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
|
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
|
||||||
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
|
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
|
||||||
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
|
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
|
||||||
# path('', views.home_page, name='home'),
|
|
||||||
# path('excel-data', views.load_excel_data, name='load_excel_data'),
|
|
||||||
# path('satellites', views.add_satellites, name='add_sats'),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import requests
|
# Standard library imports
|
||||||
import re
|
|
||||||
import json
|
import json
|
||||||
from .models import Transponders
|
import re
|
||||||
from mainapp.models import Polarization, Satellite
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from mainapp.models import Polarization, Satellite
|
||||||
|
|
||||||
|
from .models import Transponders
|
||||||
|
|
||||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||||
for pos, value in data.get('page', {}).get('positions').items():
|
for pos, value in data.get('page', {}).get('positions').items():
|
||||||
for name in value['satellites']:
|
for name in value['satellites']:
|
||||||
@@ -92,6 +98,7 @@ def parse_transponders_from_json(filepath: str):
|
|||||||
tran_obj.save()
|
tran_obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
# Third-party imports (additional)
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
def parse_transponders_from_xml(data_in: BytesIO):
|
def parse_transponders_from_xml(data_in: BytesIO):
|
||||||
|
|||||||
@@ -1,84 +1,148 @@
|
|||||||
from django.shortcuts import render
|
# Standard library imports
|
||||||
from django.http import JsonResponse
|
from typing import Any, Dict
|
||||||
import requests
|
|
||||||
from django.core import serializers
|
# Django imports
|
||||||
from django.http import HttpResponse, HttpResponseNotFound
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.decorators.cache import cache_page
|
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
|
||||||
from django.views.decorators.http import require_GET
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Local imports
|
||||||
from mainapp.models import Satellite
|
from mainapp.models import Satellite
|
||||||
from .models import Transponders
|
from .models import Transponders
|
||||||
from .utils import get_band_names
|
from .utils import get_band_names
|
||||||
|
|
||||||
class CesiumMapView(TemplateView):
|
|
||||||
|
class CesiumMapView(LoginRequiredMixin, TemplateView):
|
||||||
|
"""
|
||||||
|
Представление для отображения 3D карты с использованием Cesium.
|
||||||
|
|
||||||
|
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
|
||||||
|
"""
|
||||||
template_name = 'mapsapp/map3d.html'
|
template_name = 'mapsapp/map3d.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['sats'] = Satellite.objects.all()
|
# Оптимизированный запрос - загружаем только необходимые поля
|
||||||
|
context['sats'] = Satellite.objects.filter(
|
||||||
|
parameters__objitems__isnull=False
|
||||||
|
).distinct().only('id', 'name').order_by('name')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
class GetFootprintsView(View):
|
class GetFootprintsView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API для получения зон покрытия (footprints) спутника.
|
||||||
|
|
||||||
|
Возвращает список названий зон покрытия для указанного спутника.
|
||||||
|
"""
|
||||||
def get(self, request, sat_id):
|
def get(self, request, sat_id):
|
||||||
try:
|
try:
|
||||||
sat_name = Satellite.objects.get(id=sat_id).name
|
# Оптимизированный запрос - загружаем только поле name
|
||||||
|
sat_name = Satellite.objects.only('name').get(id=sat_id).name
|
||||||
footprint_names = get_band_names(sat_name)
|
footprint_names = get_band_names(sat_name)
|
||||||
|
|
||||||
return JsonResponse(footprint_names, safe=False)
|
return JsonResponse(footprint_names, safe=False)
|
||||||
|
except Satellite.DoesNotExist:
|
||||||
|
return JsonResponse({"error": "Спутник не найден"}, status=404)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"error": str(e)}, status=400)
|
return JsonResponse({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class TileProxyView(View):
|
class TileProxyView(View):
|
||||||
|
"""
|
||||||
|
Прокси для загрузки тайлов карты покрытия спутников.
|
||||||
|
|
||||||
|
Кэширует тайлы на 7 дней для улучшения производительности.
|
||||||
|
"""
|
||||||
|
# Константы
|
||||||
|
TILE_BASE_URL = "https://static.satbeams.com/tiles"
|
||||||
|
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
|
||||||
|
REQUEST_TIMEOUT = 10 # секунд
|
||||||
|
|
||||||
@method_decorator(require_GET)
|
@method_decorator(require_GET)
|
||||||
@method_decorator(cache_page(60 * 60 * 24)) # Cache for 24 hours
|
@method_decorator(cache_page(CACHE_DURATION))
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, footprint_name, z, x, y):
|
def get(self, request, footprint_name, z, x, y):
|
||||||
|
# Валидация имени footprint
|
||||||
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
||||||
return HttpResponse("Invalid footprint name", status=400)
|
return HttpResponse("Invalid footprint name", status=400)
|
||||||
|
|
||||||
url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png"
|
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(url, timeout=10)
|
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
response = HttpResponse(resp.content, content_type='image/png')
|
response = HttpResponse(resp.content, content_type='image/png')
|
||||||
response["Access-Control-Allow-Origin"] = "*"
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotFound("Tile not found")
|
return HttpResponseNotFound("Tile not found")
|
||||||
except Exception as e:
|
except requests.Timeout:
|
||||||
|
return HttpResponse("Request timeout", status=504)
|
||||||
|
except requests.RequestException as e:
|
||||||
return HttpResponse(f"Proxy error: {e}", status=500)
|
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||||
|
|
||||||
class LeafletMapView(TemplateView):
|
class LeafletMapView(LoginRequiredMixin, TemplateView):
|
||||||
|
"""
|
||||||
|
Представление для отображения 2D карты с использованием Leaflet.
|
||||||
|
|
||||||
|
Отображает спутники и транспондеры на интерактивной 2D карте.
|
||||||
|
"""
|
||||||
template_name = 'mapsapp/map2d.html'
|
template_name = 'mapsapp/map2d.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['sats'] = Satellite.objects.all()
|
# Оптимизированные запросы - загружаем только необходимые поля
|
||||||
context['trans'] = Transponders.objects.all()
|
context['sats'] = Satellite.objects.filter(
|
||||||
|
parameters__objitems__isnull=False
|
||||||
|
).distinct().only('id', 'name').order_by('name')
|
||||||
|
|
||||||
|
context['trans'] = Transponders.objects.select_related(
|
||||||
|
'sat_id', 'polarization'
|
||||||
|
).only(
|
||||||
|
'id', 'name', 'sat_id__name', 'polarization__name',
|
||||||
|
'downlink', 'frequency_range', 'zone_name'
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class GetTransponderOnSatIdView(View):
|
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API для получения транспондеров спутника.
|
||||||
|
|
||||||
|
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
|
||||||
|
"""
|
||||||
def get(self, request, sat_id):
|
def get(self, request, sat_id):
|
||||||
trans = Transponders.objects.filter(sat_id=sat_id)
|
# Оптимизированный запрос с select_related и only
|
||||||
output = []
|
trans = Transponders.objects.filter(
|
||||||
for tran in trans:
|
sat_id=sat_id
|
||||||
output.append(
|
).select_related('polarization').only(
|
||||||
|
'name', 'downlink', 'frequency_range',
|
||||||
|
'zone_name', 'polarization__name'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not trans.exists():
|
||||||
|
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
|
||||||
|
|
||||||
|
# Используем list comprehension для лучшей производительности
|
||||||
|
output = [
|
||||||
{
|
{
|
||||||
"name": tran.name,
|
"name": tran.name,
|
||||||
"frequency": tran.downlink,
|
"frequency": tran.downlink,
|
||||||
"frequency_range": tran.frequency_range,
|
"frequency_range": tran.frequency_range,
|
||||||
"zone_name": tran.zone_name,
|
"zone_name": tran.zone_name,
|
||||||
"polarization": tran.polarization.name
|
"polarization": tran.polarization.name if tran.polarization else "-"
|
||||||
}
|
}
|
||||||
)
|
for tran in trans
|
||||||
if not trans:
|
]
|
||||||
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
|
|
||||||
|
|
||||||
return JsonResponse(output, safe=False)
|
return JsonResponse(output, safe=False)
|
||||||
@@ -29,6 +29,7 @@ dependencies = [
|
|||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
"psycopg>=3.2.10",
|
"psycopg>=3.2.10",
|
||||||
|
"psycopg2-binary>=2.9.11",
|
||||||
"redis>=6.4.0",
|
"redis>=6.4.0",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"reverse-geocoder>=1.5.1",
|
"reverse-geocoder>=1.5.1",
|
||||||
|
|||||||
31
dbapp/requirements.txt
Normal file
31
dbapp/requirements.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
aiosqlite>=0.21.0
|
||||||
|
bcrypt>=5.0.0
|
||||||
|
beautifulsoup4>=4.14.2
|
||||||
|
django>=5.2.7
|
||||||
|
django-admin-interface>=0.30.1
|
||||||
|
django-admin-multiple-choice-list-filter>=0.1.1
|
||||||
|
django-admin-rangefilter>=0.13.3
|
||||||
|
django-autocomplete-light>=3.12.1
|
||||||
|
django-daisy>=1.1.2
|
||||||
|
django-debug-toolbar>=6.0.0
|
||||||
|
django-dynamic-raw-id>=4.4
|
||||||
|
django-import-export>=4.3.10
|
||||||
|
django-leaflet>=0.32.0
|
||||||
|
django-map-widgets>=0.5.1
|
||||||
|
django-more-admin-filters>=1.13
|
||||||
|
dotenv>=0.9.9
|
||||||
|
geopy>=2.4.1
|
||||||
|
gunicorn>=23.0.0
|
||||||
|
lxml>=6.0.2
|
||||||
|
matplotlib>=3.10.7
|
||||||
|
numpy>=2.3.3
|
||||||
|
openpyxl>=3.1.5
|
||||||
|
pandas>=2.3.3
|
||||||
|
psycopg>=3.2.10
|
||||||
|
psycopg2-binary>=2.9.11
|
||||||
|
redis>=6.4.0
|
||||||
|
requests>=2.32.5
|
||||||
|
reverse-geocoder>=1.5.1
|
||||||
|
scikit-learn>=1.7.2
|
||||||
|
selenium>=4.38.0
|
||||||
|
setuptools>=80.9.0
|
||||||
32
dbapp/uv.lock
generated
32
dbapp/uv.lock
generated
@@ -264,6 +264,7 @@ dependencies = [
|
|||||||
{ name = "openpyxl" },
|
{ name = "openpyxl" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "psycopg" },
|
{ name = "psycopg" },
|
||||||
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "reverse-geocoder" },
|
{ name = "reverse-geocoder" },
|
||||||
@@ -298,6 +299,7 @@ requires-dist = [
|
|||||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
{ name = "psycopg", specifier = ">=3.2.10" },
|
{ name = "psycopg", specifier = ">=3.2.10" },
|
||||||
|
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||||
{ name = "redis", specifier = ">=6.4.0" },
|
{ name = "redis", specifier = ">=6.4.0" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "reverse-geocoder", specifier = ">=1.5.1" },
|
{ name = "reverse-geocoder", specifier = ">=1.5.1" },
|
||||||
@@ -938,6 +940,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" },
|
{ url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.23"
|
version = "2.23"
|
||||||
|
|||||||
95
docker-compose.prod.yaml
Normal file
95
docker-compose.prod.yaml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:17-3.4
|
||||||
|
container_name: postgres-postgis-prod
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-geodb}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-geralt}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data_prod:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-geralt} -d ${POSTGRES_DB:-geodb}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ./dbapp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: django-app-prod
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- DEBUG=False
|
||||||
|
- ENVIRONMENT=production
|
||||||
|
- DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||||
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
|
- DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||||
|
- DB_NAME=${DB_NAME:-geodb}
|
||||||
|
- DB_USER=${DB_USER:-geralt}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD:-123456}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
|
||||||
|
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-3}
|
||||||
|
- GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120}
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- static_volume_prod:/app/staticfiles
|
||||||
|
- media_volume_prod:/app/media
|
||||||
|
- logs_volume_prod:/app/logs
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
tileserver:
|
||||||
|
image: maptiler/tileserver-gl:latest
|
||||||
|
container_name: tileserver-gl-prod
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./tiles:/data
|
||||||
|
- tileserver_config_prod:/config
|
||||||
|
environment:
|
||||||
|
- VERBOSE=false
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: nginx-prod
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
- static_volume_prod:/app/staticfiles:ro
|
||||||
|
- media_volume_prod:/app/media:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_prod:
|
||||||
|
static_volume_prod:
|
||||||
|
media_volume_prod:
|
||||||
|
logs_volume_prod:
|
||||||
|
tileserver_config_prod:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
82
docker-compose.yaml
Normal file
82
docker-compose.yaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:17-3.4
|
||||||
|
container_name: postgres-postgis-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: geodb
|
||||||
|
POSTGRES_USER: geralt
|
||||||
|
POSTGRES_PASSWORD: 123456
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data_dev:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U geralt -d geodb"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ./dbapp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: django-app-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
- ENVIRONMENT=development
|
||||||
|
- DJANGO_SETTINGS_MODULE=dbapp.settings.development
|
||||||
|
- SECRET_KEY=django-insecure-dev-key-change-in-production
|
||||||
|
- DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||||
|
- DB_NAME=geodb
|
||||||
|
- DB_USER=geralt
|
||||||
|
- DB_PASSWORD=123456
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
# Монтируем только код приложения, не весь проект
|
||||||
|
- ./dbapp/dbapp:/app/dbapp
|
||||||
|
- ./dbapp/mainapp:/app/mainapp
|
||||||
|
- ./dbapp/mapsapp:/app/mapsapp
|
||||||
|
- ./dbapp/lyngsatapp:/app/lyngsatapp
|
||||||
|
- ./dbapp/static:/app/static
|
||||||
|
- ./dbapp/manage.py:/app/manage.py
|
||||||
|
- static_volume_dev:/app/staticfiles
|
||||||
|
- media_volume_dev:/app/media
|
||||||
|
- logs_volume_dev:/app/logs
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
# tileserver:
|
||||||
|
# image: maptiler/tileserver-gl:latest
|
||||||
|
# container_name: tileserver-gl-dev
|
||||||
|
# restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
# volumes:
|
||||||
|
# - ./tiles:/data
|
||||||
|
# - tileserver_config_dev:/config
|
||||||
|
# environment:
|
||||||
|
# - VERBOSE=true
|
||||||
|
# networks:
|
||||||
|
# - app-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_dev:
|
||||||
|
static_volume_dev:
|
||||||
|
media_volume_dev:
|
||||||
|
logs_volume_dev:
|
||||||
|
# tileserver_config_dev:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
17
generate_secret_key.py
Executable file
17
generate_secret_key.py
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для генерации Django SECRET_KEY
|
||||||
|
Использование: python generate_secret_key.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.utils import get_random_secret_key
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
secret_key = get_random_secret_key()
|
||||||
|
print("\nВаш новый SECRET_KEY:")
|
||||||
|
print("=" * 80)
|
||||||
|
print(secret_key)
|
||||||
|
print("=" * 80)
|
||||||
|
print("\nСкопируйте его в файл .env:")
|
||||||
|
print(f"SECRET_KEY={secret_key}")
|
||||||
|
print()
|
||||||
Reference in New Issue
Block a user