Compare commits
2 Commits
0d239ef1de
...
ed9a79f94a
| Author | SHA1 | Date | |
|---|---|---|---|
| ed9a79f94a | |||
| 9a9900cfa6 |
21
.env.prod
21
.env.prod
@@ -1,25 +1,28 @@
|
|||||||
# Django Settings
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
|
DJANGO_ENVIRONMENT=production
|
||||||
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
||||||
SECRET_KEY=change-this-to-a-very-long-random-secret-key-in-production
|
SECRET_KEY=django-insecure-dev-key-only-for-production
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
DB_ENGINE=django.contrib.gis.db.backends.postgis
|
||||||
DB_NAME=geodb
|
DB_NAME=geodb
|
||||||
DB_USER=geralt
|
DB_USER=geralt
|
||||||
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
DB_PASSWORD=123456
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
|
||||||
# Allowed Hosts (comma-separated)
|
# Allowed Hosts
|
||||||
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
|
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||||
|
|
||||||
|
# CSRF Trusted Origins (include port if using non-standard port)
|
||||||
|
CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,http://localhost:8080,http://127.0.0.1:8080
|
||||||
|
|
||||||
# PostgreSQL Configuration
|
# PostgreSQL Configuration
|
||||||
POSTGRES_DB=geodb
|
POSTGRES_DB=geodb
|
||||||
POSTGRES_USER=geralt
|
POSTGRES_USER=geralt
|
||||||
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
POSTGRES_PASSWORD=123456
|
||||||
|
|
||||||
# Gunicorn Configuration
|
# Redis Configuration
|
||||||
GUNICORN_WORKERS=3
|
REDIS_URL=redis://redis:6379/1
|
||||||
GUNICORN_TIMEOUT=120
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Ensure shell scripts always use LF line endings
|
||||||
|
*.sh text eol=lf
|
||||||
|
entrypoint.sh text eol=lf
|
||||||
|
|
||||||
|
# Python files
|
||||||
|
*.py text eol=lf
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile text eol=lf
|
||||||
|
docker-compose*.yaml text eol=lf
|
||||||
|
.dockerignore text eol=lf
|
||||||
110
dbapp/Dockerfile
110
dbapp/Dockerfile
@@ -1,57 +1,53 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.13.7-slim AS builder
|
||||||
|
|
||||||
# Install system dependencies
|
# Устанавливаем системные библиотеки для GIS, Postgres, сборки пакетов
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gdal-bin \
|
build-essential \
|
||||||
libgdal-dev \
|
gdal-bin libgdal-dev \
|
||||||
proj-bin \
|
libproj-dev proj-bin \
|
||||||
proj-data \
|
libpq-dev \
|
||||||
libproj-dev \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
libproj25 \
|
|
||||||
libgeos-dev \
|
WORKDIR /app
|
||||||
libgeos-c1v5 \
|
|
||||||
build-essential \
|
# Устанавливаем uv пакетно-менеджер глобально
|
||||||
postgresql-client \
|
RUN pip install --no-cache-dir uv
|
||||||
libpq-dev \
|
|
||||||
libpq5 \
|
# Копируем зависимости
|
||||||
netcat-openbsd \
|
COPY pyproject.toml uv.lock ./
|
||||||
gcc \
|
|
||||||
g++ \
|
# Синхронизируем зависимости (включая prod + dev), чтобы билдить
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
RUN uv sync --locked
|
||||||
|
|
||||||
# Set environment variables
|
# Копируем весь код приложения
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
COPY . .
|
||||||
PYTHONUNBUFFERED=1
|
|
||||||
|
# --- рантайм-стадия — минимальный образ для продакшена ---
|
||||||
# Set work directory
|
FROM python:3.13.7-slim
|
||||||
WORKDIR /app
|
|
||||||
|
WORKDIR /app
|
||||||
# Upgrade pip
|
|
||||||
RUN pip install --upgrade pip
|
# Устанавливаем только runtime-системные библиотеки
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
# Copy requirements file
|
gdal-bin \
|
||||||
COPY requirements.txt ./
|
libproj-dev proj-bin \
|
||||||
|
libpq5 \
|
||||||
# Install dependencies
|
postgresql-client \
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy project files
|
# Копируем всё из билдера
|
||||||
COPY . .
|
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
# Create directories
|
COPY --from=builder /app /app
|
||||||
RUN mkdir -p /app/staticfiles /app/logs /app/media
|
|
||||||
|
# Загружаем переменные окружения из .env (см. docker-compose)
|
||||||
# Set permissions for entrypoint
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
RUN chmod +x /app/entrypoint.sh
|
PATH="/usr/local/bin:$PATH"
|
||||||
|
|
||||||
# Create non-root user
|
# Делаем entrypoint.sh исполняемым
|
||||||
RUN useradd --create-home --shell /bin/bash app && \
|
RUN chmod +x /app/entrypoint.sh
|
||||||
chown -R app:app /app
|
|
||||||
|
EXPOSE 8000
|
||||||
USER app
|
|
||||||
|
# Используем entrypoint для инициализации (миграции, статика)
|
||||||
# Expose port
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Run entrypoint script
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
||||||
@@ -1,135 +1,141 @@
|
|||||||
"""
|
"""
|
||||||
Production-specific settings.
|
Production-specific settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DEBUG CONFIGURATION
|
# DEBUG CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ALLOWED HOSTS
|
# ALLOWED HOSTS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# In production, specify allowed hosts explicitly from environment variable
|
# In production, specify allowed hosts explicitly from environment variable
|
||||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||||
|
|
||||||
# ============================================================================
|
# CSRF trusted origins (required for forms to work behind proxy)
|
||||||
# SECURITY SETTINGS
|
CSRF_TRUSTED_ORIGINS = os.getenv(
|
||||||
# ============================================================================
|
"CSRF_TRUSTED_ORIGINS",
|
||||||
|
"http://localhost,http://127.0.0.1,http://localhost:8080,http://127.0.0.1:8080"
|
||||||
# SSL/HTTPS settings
|
).split(",")
|
||||||
SECURE_SSL_REDIRECT = True
|
|
||||||
SESSION_COOKIE_SECURE = True
|
# ============================================================================
|
||||||
CSRF_COOKIE_SECURE = True
|
# SECURITY SETTINGS
|
||||||
|
# ============================================================================
|
||||||
# Security headers
|
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
# SSL/HTTPS settings (disable for local testing without SSL)
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
SECURE_SSL_REDIRECT = os.getenv("SECURE_SSL_REDIRECT", "False") == "True"
|
||||||
|
SESSION_COOKIE_SECURE = os.getenv("SESSION_COOKIE_SECURE", "False") == "True"
|
||||||
# HSTS settings
|
CSRF_COOKIE_SECURE = os.getenv("CSRF_COOKIE_SECURE", "False") == "True"
|
||||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
# Security headers
|
||||||
SECURE_HSTS_PRELOAD = True
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
# Additional security settings
|
|
||||||
SECURE_REDIRECT_EXEMPT = []
|
# HSTS settings (disable for local testing)
|
||||||
X_FRAME_OPTIONS = "DENY"
|
SECURE_HSTS_SECONDS = int(os.getenv("SECURE_HSTS_SECONDS", "0"))
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = os.getenv("SECURE_HSTS_INCLUDE_SUBDOMAINS", "False") == "True"
|
||||||
# ============================================================================
|
SECURE_HSTS_PRELOAD = os.getenv("SECURE_HSTS_PRELOAD", "False") == "True"
|
||||||
# TEMPLATE CACHING
|
|
||||||
# ============================================================================
|
# Additional security settings
|
||||||
|
SECURE_REDIRECT_EXEMPT = []
|
||||||
TEMPLATES = [
|
X_FRAME_OPTIONS = "DENY"
|
||||||
{
|
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
# ============================================================================
|
||||||
"DIRS": [
|
# TEMPLATE CACHING
|
||||||
BASE_DIR / "templates",
|
# ============================================================================
|
||||||
],
|
|
||||||
"APP_DIRS": True,
|
TEMPLATES = [
|
||||||
"OPTIONS": {
|
{
|
||||||
"context_processors": [
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"django.template.context_processors.debug",
|
"DIRS": [
|
||||||
"django.template.context_processors.request",
|
BASE_DIR / "templates",
|
||||||
"django.contrib.auth.context_processors.auth",
|
],
|
||||||
"django.contrib.messages.context_processors.messages",
|
"APP_DIRS": False, # Must be False when using custom loaders
|
||||||
],
|
"OPTIONS": {
|
||||||
"loaders": [
|
"context_processors": [
|
||||||
(
|
"django.template.context_processors.debug",
|
||||||
"django.template.loaders.cached.Loader",
|
"django.template.context_processors.request",
|
||||||
[
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.template.loaders.filesystem.Loader",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"django.template.loaders.app_directories.Loader",
|
],
|
||||||
],
|
"loaders": [
|
||||||
),
|
(
|
||||||
],
|
"django.template.loaders.cached.Loader",
|
||||||
},
|
[
|
||||||
},
|
"django.template.loaders.filesystem.Loader",
|
||||||
]
|
"django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
# ============================================================================
|
),
|
||||||
# STATIC FILES CONFIGURATION
|
],
|
||||||
# ============================================================================
|
},
|
||||||
|
},
|
||||||
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
]
|
||||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
|
||||||
|
# ============================================================================
|
||||||
# ============================================================================
|
# STATIC FILES CONFIGURATION
|
||||||
# LOGGING CONFIGURATION
|
# ============================================================================
|
||||||
# ============================================================================
|
|
||||||
|
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
||||||
LOGGING = {
|
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
# ============================================================================
|
||||||
"formatters": {
|
# LOGGING CONFIGURATION
|
||||||
"verbose": {
|
# ============================================================================
|
||||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
|
||||||
"style": "{",
|
LOGGING = {
|
||||||
},
|
"version": 1,
|
||||||
"simple": {
|
"disable_existing_loggers": False,
|
||||||
"format": "{levelname} {message}",
|
"formatters": {
|
||||||
"style": "{",
|
"verbose": {
|
||||||
},
|
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||||
},
|
"style": "{",
|
||||||
"filters": {
|
},
|
||||||
"require_debug_false": {
|
"simple": {
|
||||||
"()": "django.utils.log.RequireDebugFalse",
|
"format": "{levelname} {message}",
|
||||||
},
|
"style": "{",
|
||||||
},
|
},
|
||||||
"handlers": {
|
},
|
||||||
"console": {
|
"filters": {
|
||||||
"level": "INFO",
|
"require_debug_false": {
|
||||||
"class": "logging.StreamHandler",
|
"()": "django.utils.log.RequireDebugFalse",
|
||||||
"formatter": "simple",
|
},
|
||||||
},
|
},
|
||||||
"file": {
|
"handlers": {
|
||||||
"level": "ERROR",
|
"console": {
|
||||||
"class": "logging.FileHandler",
|
"level": "INFO",
|
||||||
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "verbose",
|
"formatter": "simple",
|
||||||
},
|
},
|
||||||
"mail_admins": {
|
"file": {
|
||||||
"level": "ERROR",
|
"level": "ERROR",
|
||||||
"class": "django.utils.log.AdminEmailHandler",
|
"class": "logging.FileHandler",
|
||||||
"filters": ["require_debug_false"],
|
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
|
||||||
"formatter": "verbose",
|
"formatter": "verbose",
|
||||||
},
|
},
|
||||||
},
|
"mail_admins": {
|
||||||
"loggers": {
|
"level": "ERROR",
|
||||||
"django": {
|
"class": "django.utils.log.AdminEmailHandler",
|
||||||
"handlers": ["console", "file"],
|
"filters": ["require_debug_false"],
|
||||||
"level": "INFO",
|
"formatter": "verbose",
|
||||||
"propagate": True,
|
},
|
||||||
},
|
},
|
||||||
"django.request": {
|
"loggers": {
|
||||||
"handlers": ["mail_admins", "file"],
|
"django": {
|
||||||
"level": "ERROR",
|
"handlers": ["console", "file"],
|
||||||
"propagate": False,
|
"level": "INFO",
|
||||||
},
|
"propagate": True,
|
||||||
},
|
},
|
||||||
}
|
"django.request": {
|
||||||
|
"handlers": ["mail_admins", "file"],
|
||||||
|
"level": "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
"""
|
"""
|
||||||
URL configuration for dbapp project.
|
URL configuration for dbapp project.
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||||
Examples:
|
Examples:
|
||||||
Function views
|
Function views
|
||||||
1. Add an import: from my_app import views
|
1. Add an import: from my_app import views
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
Class-based views
|
Class-based views
|
||||||
1. Add an import: from other_app.views import Home
|
1. Add an import: from other_app.views import Home
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
Including another URLconf
|
Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.conf import settings
|
||||||
from django.urls import path, include
|
from django.contrib import admin
|
||||||
from mainapp.views import custom_logout
|
from django.urls import path, include
|
||||||
from django.contrib.auth import views as auth_views
|
from mainapp.views import custom_logout
|
||||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls, name='admin'),
|
path('admin/', admin.site.urls, name='admin'),
|
||||||
path('', include('mainapp.urls', namespace='mainapp')),
|
path('', include('mainapp.urls', namespace='mainapp')),
|
||||||
path('', include('mapsapp.urls', namespace='mapsapp')),
|
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||||
path('lyngsat/', include('lyngsatapp.urls', namespace='lyngsatapp')),
|
path('lyngsat/', include('lyngsatapp.urls', namespace='lyngsatapp')),
|
||||||
# Authentication URLs
|
# Authentication URLs
|
||||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||||
path('logout/', custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
] + debug_toolbar_urls()
|
]
|
||||||
|
|
||||||
|
# Only include debug toolbar in development
|
||||||
|
if settings.DEBUG:
|
||||||
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
urlpatterns += debug_toolbar_urls()
|
||||||
|
|||||||
74
dbapp/entrypoint.sh
Executable file → Normal file
74
dbapp/entrypoint.sh
Executable file → Normal file
@@ -1,37 +1,37 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Определяем окружение (по умолчанию production)
|
# Определяем окружение (по умолчанию production)
|
||||||
ENVIRONMENT=${ENVIRONMENT:-production}
|
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||||
|
|
||||||
echo "Starting in $ENVIRONMENT mode..."
|
echo "Starting in $ENVIRONMENT mode..."
|
||||||
|
|
||||||
# Ждем PostgreSQL
|
# Ждем PostgreSQL
|
||||||
echo "Waiting for PostgreSQL..."
|
echo "Waiting for PostgreSQL..."
|
||||||
while ! nc -z $DB_HOST $DB_PORT; do
|
until PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -c '\q' 2>/dev/null; do
|
||||||
sleep 0.1
|
echo "PostgreSQL is unavailable - sleeping"
|
||||||
done
|
sleep 1
|
||||||
echo "PostgreSQL started"
|
done
|
||||||
|
echo "PostgreSQL started"
|
||||||
# Выполняем миграции
|
|
||||||
echo "Running migrations..."
|
# Выполняем миграции
|
||||||
python manage.py migrate --noinput
|
echo "Running migrations..."
|
||||||
|
uv run python manage.py migrate --noinput
|
||||||
# Собираем статику (только для production)
|
|
||||||
if [ "$ENVIRONMENT" = "production" ]; then
|
# Собираем статику (только для production)
|
||||||
echo "Collecting static files..."
|
if [ "$ENVIRONMENT" = "production" ]; then
|
||||||
python manage.py collectstatic --noinput
|
echo "Collecting static files..."
|
||||||
fi
|
uv run python manage.py collectstatic --noinput
|
||||||
|
fi
|
||||||
# Запускаем сервер в зависимости от окружения
|
|
||||||
if [ "$ENVIRONMENT" = "development" ]; then
|
# Запускаем сервер в зависимости от окружения
|
||||||
echo "Starting Django development server..."
|
if [ "$ENVIRONMENT" = "development" ]; then
|
||||||
exec python manage.py runserver 0.0.0.0:8000
|
echo "Starting Django development server..."
|
||||||
else
|
exec uv run python manage.py runserver 0.0.0.0:8000
|
||||||
echo "Starting Gunicorn..."
|
else
|
||||||
exec gunicorn --bind 0.0.0.0:8000 \
|
echo "Starting Gunicorn..."
|
||||||
--workers ${GUNICORN_WORKERS:-3} \
|
exec uv run gunicorn --bind 0.0.0.0:8000 \
|
||||||
--timeout ${GUNICORN_TIMEOUT:-120} \
|
--workers ${GUNICORN_WORKERS:-3} \
|
||||||
--reload \
|
--timeout ${GUNICORN_TIMEOUT:-120} \
|
||||||
dbapp.wsgi:application
|
dbapp.wsgi:application
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,285 +1,285 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from .models import LyngSat
|
from .models import LyngSat
|
||||||
from mainapp.models import Satellite, Polarization, Modulation, Standard
|
from mainapp.models import Satellite, Polarization, Modulation, Standard
|
||||||
from mainapp.utils import parse_pagination_params
|
from mainapp.utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
class LyngSatListView(LoginRequiredMixin, ListView):
|
class LyngSatListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Представление для отображения списка источников LyngSat с фильтрацией и пагинацией.
|
Представление для отображения списка источников LyngSat с фильтрацией и пагинацией.
|
||||||
"""
|
"""
|
||||||
model = LyngSat
|
model = LyngSat
|
||||||
template_name = 'lyngsatapp/lyngsat_list.html'
|
template_name = 'lyngsatapp/lyngsat_list.html'
|
||||||
context_object_name = 'lyngsat_items'
|
context_object_name = 'lyngsat_items'
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Возвращает отфильтрованный и отсортированный queryset.
|
Возвращает отфильтрованный и отсортированный queryset.
|
||||||
"""
|
"""
|
||||||
queryset = LyngSat.objects.select_related(
|
queryset = LyngSat.objects.select_related(
|
||||||
'id_satellite',
|
'id_satellite',
|
||||||
'polarization',
|
'polarization',
|
||||||
'modulation',
|
'modulation',
|
||||||
'standard'
|
'standard'
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Поиск по ID
|
# Поиск по ID
|
||||||
search_query = self.request.GET.get('search', '').strip()
|
search_query = self.request.GET.get('search', '').strip()
|
||||||
if search_query:
|
if search_query:
|
||||||
try:
|
try:
|
||||||
search_id = int(search_query)
|
search_id = int(search_query)
|
||||||
queryset = queryset.filter(id=search_id)
|
queryset = queryset.filter(id=search_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
queryset = queryset.none()
|
queryset = queryset.none()
|
||||||
|
|
||||||
# Фильтр по спутнику
|
# Фильтр по спутнику
|
||||||
satellite_ids = self.request.GET.getlist('satellite_id')
|
satellite_ids = self.request.GET.getlist('satellite_id')
|
||||||
if satellite_ids:
|
if satellite_ids:
|
||||||
queryset = queryset.filter(id_satellite_id__in=satellite_ids)
|
queryset = queryset.filter(id_satellite_id__in=satellite_ids)
|
||||||
|
|
||||||
# Фильтр по поляризации
|
# Фильтр по поляризации
|
||||||
polarization_ids = self.request.GET.getlist('polarization_id')
|
polarization_ids = self.request.GET.getlist('polarization_id')
|
||||||
if polarization_ids:
|
if polarization_ids:
|
||||||
queryset = queryset.filter(polarization_id__in=polarization_ids)
|
queryset = queryset.filter(polarization_id__in=polarization_ids)
|
||||||
|
|
||||||
# Фильтр по модуляции
|
# Фильтр по модуляции
|
||||||
modulation_ids = self.request.GET.getlist('modulation_id')
|
modulation_ids = self.request.GET.getlist('modulation_id')
|
||||||
if modulation_ids:
|
if modulation_ids:
|
||||||
queryset = queryset.filter(modulation_id__in=modulation_ids)
|
queryset = queryset.filter(modulation_id__in=modulation_ids)
|
||||||
|
|
||||||
# Фильтр по стандарту
|
# Фильтр по стандарту
|
||||||
standard_ids = self.request.GET.getlist('standard_id')
|
standard_ids = self.request.GET.getlist('standard_id')
|
||||||
if standard_ids:
|
if standard_ids:
|
||||||
queryset = queryset.filter(standard_id__in=standard_ids)
|
queryset = queryset.filter(standard_id__in=standard_ids)
|
||||||
|
|
||||||
# Фильтр по частоте
|
# Фильтр по частоте
|
||||||
freq_min = self.request.GET.get('freq_min', '').strip()
|
freq_min = self.request.GET.get('freq_min', '').strip()
|
||||||
freq_max = self.request.GET.get('freq_max', '').strip()
|
freq_max = self.request.GET.get('freq_max', '').strip()
|
||||||
if freq_min:
|
if freq_min:
|
||||||
try:
|
try:
|
||||||
queryset = queryset.filter(frequency__gte=float(freq_min))
|
queryset = queryset.filter(frequency__gte=float(freq_min))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if freq_max:
|
if freq_max:
|
||||||
try:
|
try:
|
||||||
queryset = queryset.filter(frequency__lte=float(freq_max))
|
queryset = queryset.filter(frequency__lte=float(freq_max))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Фильтр по символьной скорости
|
# Фильтр по символьной скорости
|
||||||
sym_min = self.request.GET.get('sym_min', '').strip()
|
sym_min = self.request.GET.get('sym_min', '').strip()
|
||||||
sym_max = self.request.GET.get('sym_max', '').strip()
|
sym_max = self.request.GET.get('sym_max', '').strip()
|
||||||
if sym_min:
|
if sym_min:
|
||||||
try:
|
try:
|
||||||
queryset = queryset.filter(sym_velocity__gte=float(sym_min))
|
queryset = queryset.filter(sym_velocity__gte=float(sym_min))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if sym_max:
|
if sym_max:
|
||||||
try:
|
try:
|
||||||
queryset = queryset.filter(sym_velocity__lte=float(sym_max))
|
queryset = queryset.filter(sym_velocity__lte=float(sym_max))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Фильтр по дате обновления
|
# Фильтр по дате обновления
|
||||||
date_from = self.request.GET.get('date_from', '').strip()
|
date_from = self.request.GET.get('date_from', '').strip()
|
||||||
date_to = self.request.GET.get('date_to', '').strip()
|
date_to = self.request.GET.get('date_to', '').strip()
|
||||||
if date_from:
|
if date_from:
|
||||||
queryset = queryset.filter(last_update__gte=date_from)
|
queryset = queryset.filter(last_update__gte=date_from)
|
||||||
if date_to:
|
if date_to:
|
||||||
queryset = queryset.filter(last_update__lte=date_to)
|
queryset = queryset.filter(last_update__lte=date_to)
|
||||||
|
|
||||||
# Сортировка
|
# Сортировка
|
||||||
sort = self.request.GET.get('sort', '-id')
|
sort = self.request.GET.get('sort', '-id')
|
||||||
valid_sort_fields = ['id', '-id', 'frequency', '-frequency', 'sym_velocity', '-sym_velocity', 'last_update', '-last_update']
|
valid_sort_fields = ['id', '-id', 'frequency', '-frequency', 'sym_velocity', '-sym_velocity', 'last_update', '-last_update']
|
||||||
if sort in valid_sort_fields:
|
if sort in valid_sort_fields:
|
||||||
queryset = queryset.order_by(sort)
|
queryset = queryset.order_by(sort)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.order_by('-id')
|
queryset = queryset.order_by('-id')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Добавляет дополнительный контекст для шаблона.
|
Добавляет дополнительный контекст для шаблона.
|
||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
# Параметры пагинации
|
# Параметры пагинации
|
||||||
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
|
page_number, items_per_page = parse_pagination_params(self.request, default_per_page=50)
|
||||||
context['items_per_page'] = items_per_page
|
context['items_per_page'] = items_per_page
|
||||||
context['available_items_per_page'] = [25, 50, 100, 200, 500]
|
context['available_items_per_page'] = [25, 50, 100, 200, 500]
|
||||||
|
|
||||||
# Пагинация
|
# Пагинация
|
||||||
paginator = Paginator(self.get_queryset(), items_per_page)
|
paginator = Paginator(self.get_queryset(), items_per_page)
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
context['page_obj'] = page_obj
|
context['page_obj'] = page_obj
|
||||||
context['lyngsat_items'] = page_obj.object_list
|
context['lyngsat_items'] = page_obj.object_list
|
||||||
|
|
||||||
# Параметры поиска и фильтрации
|
# Параметры поиска и фильтрации
|
||||||
context['search_query'] = self.request.GET.get('search', '')
|
context['search_query'] = self.request.GET.get('search', '')
|
||||||
context['sort'] = self.request.GET.get('sort', '-id')
|
context['sort'] = self.request.GET.get('sort', '-id')
|
||||||
|
|
||||||
# Данные для фильтров - только спутники с существующими записями LyngSat
|
# Данные для фильтров - только спутники с существующими записями LyngSat
|
||||||
satellites = Satellite.objects.filter(
|
satellites = Satellite.objects.filter(
|
||||||
lyngsat__isnull=False
|
lyngsat__isnull=False
|
||||||
).distinct().order_by('name')
|
).distinct().order_by('name')
|
||||||
polarizations = Polarization.objects.all().order_by('name')
|
polarizations = Polarization.objects.all().order_by('name')
|
||||||
modulations = Modulation.objects.all().order_by('name')
|
modulations = Modulation.objects.all().order_by('name')
|
||||||
standards = Standard.objects.all().order_by('name')
|
standards = Standard.objects.all().order_by('name')
|
||||||
|
|
||||||
# Выбранные фильтры
|
# Выбранные фильтры
|
||||||
selected_satellites = [int(x) for x in self.request.GET.getlist('satellite_id') if x.isdigit()]
|
selected_satellites = [int(x) for x in self.request.GET.getlist('satellite_id') if x.isdigit()]
|
||||||
selected_polarizations = [int(x) for x in self.request.GET.getlist('polarization_id') if x.isdigit()]
|
selected_polarizations = [int(x) for x in self.request.GET.getlist('polarization_id') if x.isdigit()]
|
||||||
selected_modulations = [int(x) for x in self.request.GET.getlist('modulation_id') if x.isdigit()]
|
selected_modulations = [int(x) for x in self.request.GET.getlist('modulation_id') if x.isdigit()]
|
||||||
selected_standards = [int(x) for x in self.request.GET.getlist('standard_id') if x.isdigit()]
|
selected_standards = [int(x) for x in self.request.GET.getlist('standard_id') if x.isdigit()]
|
||||||
|
|
||||||
# Параметры фильтров
|
# Параметры фильтров
|
||||||
freq_min = self.request.GET.get('freq_min', '')
|
freq_min = self.request.GET.get('freq_min', '')
|
||||||
freq_max = self.request.GET.get('freq_max', '')
|
freq_max = self.request.GET.get('freq_max', '')
|
||||||
sym_min = self.request.GET.get('sym_min', '')
|
sym_min = self.request.GET.get('sym_min', '')
|
||||||
sym_max = self.request.GET.get('sym_max', '')
|
sym_max = self.request.GET.get('sym_max', '')
|
||||||
date_from = self.request.GET.get('date_from', '')
|
date_from = self.request.GET.get('date_from', '')
|
||||||
date_to = self.request.GET.get('date_to', '')
|
date_to = self.request.GET.get('date_to', '')
|
||||||
|
|
||||||
# Action buttons HTML for toolbar component
|
# Action buttons HTML for toolbar component
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
action_buttons_html = f'''
|
action_buttons_html = f'''
|
||||||
<a href="{reverse('mainapp:fill_lyngsat_data')}" class="btn btn-secondary btn-sm" title="Заполнить данные Lyngsat">
|
<a href="{reverse('mainapp:fill_lyngsat_data')}" class="btn btn-secondary btn-sm" title="Заполнить данные Lyngsat">
|
||||||
<i class="bi bi-cloud-download"></i> Добавить данные
|
<i class="bi bi-cloud-download"></i> Добавить данные
|
||||||
</a>
|
</a>
|
||||||
<a href="{reverse('mainapp:link_lyngsat')}" class="btn btn-primary btn-sm" title="Привязать источники LyngSat">
|
<a href="{reverse('mainapp:link_lyngsat')}" class="btn btn-primary btn-sm" title="Привязать источники LyngSat">
|
||||||
<i class="bi bi-link-45deg"></i> Привязать
|
<i class="bi bi-link-45deg"></i> Привязать
|
||||||
</a>
|
</a>
|
||||||
<a href="{reverse('mainapp:unlink_all_lyngsat')}" class="btn btn-warning btn-sm" title="Отвязать все источники LyngSat">
|
<a href="{reverse('mainapp:unlink_all_lyngsat')}" class="btn btn-warning btn-sm" title="Отвязать все источники LyngSat">
|
||||||
<i class="bi bi-x-circle"></i> Отвязать
|
<i class="bi bi-x-circle"></i> Отвязать
|
||||||
</a>
|
</a>
|
||||||
'''
|
'''
|
||||||
context['action_buttons_html'] = action_buttons_html
|
context['action_buttons_html'] = action_buttons_html
|
||||||
|
|
||||||
# Build filter HTML list for filter_panel component
|
# Build filter HTML list for filter_panel component
|
||||||
filter_html_list = []
|
filter_html_list = []
|
||||||
|
|
||||||
# Satellite filter
|
# Satellite filter
|
||||||
satellite_options = ''.join([
|
satellite_options = ''.join([
|
||||||
f'<option value="{sat.id}" {"selected" if sat.id in selected_satellites else ""}>{sat.name}</option>'
|
f'<option value="{sat.id}" {"selected" if sat.id in selected_satellites else ""}>{sat.name}</option>'
|
||||||
for sat in satellites
|
for sat in satellites
|
||||||
])
|
])
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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"
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
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', 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">
|
||||||
{satellite_options}
|
{satellite_options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Polarization filter
|
# Polarization filter
|
||||||
polarization_options = ''.join([
|
polarization_options = ''.join([
|
||||||
f'<option value="{pol.id}" {"selected" if pol.id in selected_polarizations else ""}>{pol.name}</option>'
|
f'<option value="{pol.id}" {"selected" if pol.id in selected_polarizations else ""}>{pol.name}</option>'
|
||||||
for pol in polarizations
|
for pol in polarizations
|
||||||
])
|
])
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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"
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
onclick="selectAllOptions('polarization_id', true)">Выбрать</button>
|
onclick="selectAllOptions('polarization_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('polarization_id', false)">Снять</button>
|
onclick="selectAllOptions('polarization_id', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
|
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||||||
{polarization_options}
|
{polarization_options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Modulation filter
|
# Modulation filter
|
||||||
modulation_options = ''.join([
|
modulation_options = ''.join([
|
||||||
f'<option value="{mod.id}" {"selected" if mod.id in selected_modulations else ""}>{mod.name}</option>'
|
f'<option value="{mod.id}" {"selected" if mod.id in selected_modulations else ""}>{mod.name}</option>'
|
||||||
for mod in modulations
|
for mod in modulations
|
||||||
])
|
])
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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"
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
onclick="selectAllOptions('modulation_id', true)">Выбрать</button>
|
onclick="selectAllOptions('modulation_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('modulation_id', false)">Снять</button>
|
onclick="selectAllOptions('modulation_id', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
|
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||||||
{modulation_options}
|
{modulation_options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Standard filter
|
# Standard filter
|
||||||
standard_options = ''.join([
|
standard_options = ''.join([
|
||||||
f'<option value="{std.id}" {"selected" if std.id in selected_standards else ""}>{std.name}</option>'
|
f'<option value="{std.id}" {"selected" if std.id in selected_standards else ""}>{std.name}</option>'
|
||||||
for std in standards
|
for std in standards
|
||||||
])
|
])
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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"
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
onclick="selectAllOptions('standard_id', true)">Выбрать</button>
|
onclick="selectAllOptions('standard_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('standard_id', false)">Снять</button>
|
onclick="selectAllOptions('standard_id', false)">Снять</button>
|
||||||
</div>
|
</div>
|
||||||
<select name="standard_id" class="form-select form-select-sm mb-2" multiple size="4">
|
<select name="standard_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||||||
{standard_options}
|
{standard_options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Frequency filter
|
# Frequency filter
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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"
|
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
|
||||||
placeholder="От" value="{freq_min}">
|
placeholder="От" value="{freq_min}">
|
||||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
||||||
placeholder="До" value="{freq_max}">
|
placeholder="До" value="{freq_max}">
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Symbol rate filter
|
# Symbol rate filter
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<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="sym_min" class="form-control form-control-sm mb-1"
|
<input type="number" step="0.001" name="sym_min" class="form-control form-control-sm mb-1"
|
||||||
placeholder="От" value="{sym_min}">
|
placeholder="От" value="{sym_min}">
|
||||||
<input type="number" step="0.001" name="sym_max" class="form-control form-control-sm"
|
<input type="number" step="0.001" name="sym_max" class="form-control form-control-sm"
|
||||||
placeholder="До" value="{sym_max}">
|
placeholder="До" value="{sym_max}">
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Date filter
|
# Date filter
|
||||||
filter_html_list.append(f'''
|
filter_html_list.append(f'''
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Дата обновления:</label>
|
<label class="form-label">Дата обновления:</label>
|
||||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||||
placeholder="От" value="{date_from}">
|
placeholder="От" value="{date_from}">
|
||||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||||
placeholder="До" value="{date_to}">
|
placeholder="До" value="{date_to}">
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
context['filter_html_list'] = filter_html_list
|
context['filter_html_list'] = filter_html_list
|
||||||
|
|
||||||
# Enable full width layout
|
# Enable full width layout
|
||||||
context['full_width_page'] = True
|
context['full_width_page'] = True
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,7 @@
|
|||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-x: auto;
|
height: 400px;
|
||||||
}
|
|
||||||
|
|
||||||
#frequencyCanvas {
|
|
||||||
display: block;
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
@@ -44,18 +39,16 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transponder-tooltip {
|
.chart-controls {
|
||||||
position: absolute;
|
display: flex;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
gap: 10px;
|
||||||
color: white;
|
margin-bottom: 15px;
|
||||||
padding: 10px;
|
flex-wrap: wrap;
|
||||||
border-radius: 4px;
|
}
|
||||||
font-size: 0.85rem;
|
|
||||||
pointer-events: none;
|
.chart-controls button {
|
||||||
z-index: 1000;
|
padding: 5px 15px;
|
||||||
display: none;
|
font-size: 0.9rem;
|
||||||
max-width: 300px;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -214,16 +207,28 @@
|
|||||||
|
|
||||||
{% if action == 'update' and transponders %}
|
{% if action == 'update' and transponders %}
|
||||||
<!-- Frequency Plan Visualization -->
|
<!-- Frequency Plan Visualization -->
|
||||||
<!-- <div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4>Частотный план</h4>
|
<h4>Частотный план</h4>
|
||||||
<p class="text-muted">Визуализация транспондеров спутника по частотам (Downlink). Наведите курсор на транспондер для подробной информации.</p>
|
<p class="text-muted">Визуализация транспондеров спутника по частотам (Downlink). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
|
||||||
|
|
||||||
<div class="frequency-plan">
|
<div class="frequency-plan">
|
||||||
|
<div class="chart-controls">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="resetZoom">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomIn">
|
||||||
|
<i class="bi bi-zoom-in"></i> Увеличить
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomOut">
|
||||||
|
<i class="bi bi-zoom-out"></i> Уменьшить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="frequency-chart-container">
|
<div class="frequency-chart-container">
|
||||||
<canvas id="frequencyCanvas"></canvas>
|
<canvas id="frequencyChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
@@ -256,9 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div class="transponder-tooltip" id="transponderTooltip"></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -267,7 +270,7 @@
|
|||||||
{% if action == 'update' and transponders %}
|
{% if action == 'update' and transponders %}
|
||||||
<script>
|
<script>
|
||||||
// Transponder data from Django
|
// Transponder data from Django
|
||||||
const transponders = {{ transponders|safe }};
|
const transpondersData = {{ transponders|safe }};
|
||||||
|
|
||||||
// Color mapping for polarizations
|
// Color mapping for polarizations
|
||||||
const polarizationColors = {
|
const polarizationColors = {
|
||||||
@@ -278,88 +281,129 @@ const polarizationColors = {
|
|||||||
'default': '#6c757d'
|
'default': '#6c757d'
|
||||||
};
|
};
|
||||||
|
|
||||||
let canvas, ctx, tooltip;
|
|
||||||
let hoveredTransponder = null;
|
|
||||||
|
|
||||||
function getColor(polarization) {
|
function getColor(polarization) {
|
||||||
return polarizationColors[polarization] || polarizationColors['default'];
|
return polarizationColors[polarization] || polarizationColors['default'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFrequencyPlan() {
|
// Chart state
|
||||||
if (!transponders || transponders.length === 0) {
|
let canvas, ctx, container;
|
||||||
|
let zoomLevel = 1;
|
||||||
|
let panOffset = 0;
|
||||||
|
let isDragging = false;
|
||||||
|
let dragStartX = 0;
|
||||||
|
let dragStartOffset = 0;
|
||||||
|
let hoveredTransponder = null;
|
||||||
|
let transponderRects = [];
|
||||||
|
|
||||||
|
// Frequency range
|
||||||
|
let minFreq, maxFreq, freqRange;
|
||||||
|
let originalMinFreq, originalMaxFreq, originalFreqRange;
|
||||||
|
|
||||||
|
function initializeFrequencyChart() {
|
||||||
|
if (!transpondersData || transpondersData.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas = document.getElementById('frequencyCanvas');
|
canvas = document.getElementById('frequencyChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
container = canvas.parentElement;
|
||||||
ctx = canvas.getContext('2d');
|
ctx = canvas.getContext('2d');
|
||||||
tooltip = document.getElementById('transponderTooltip');
|
|
||||||
|
|
||||||
// Find min and max frequencies
|
// Calculate frequency range
|
||||||
let minFreq = Infinity;
|
minFreq = Infinity;
|
||||||
let maxFreq = -Infinity;
|
maxFreq = -Infinity;
|
||||||
|
|
||||||
transponders.forEach(t => {
|
transpondersData.forEach(t => {
|
||||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||||
minFreq = Math.min(minFreq, startFreq);
|
minFreq = Math.min(minFreq, startFreq);
|
||||||
maxFreq = Math.max(maxFreq, endFreq);
|
maxFreq = Math.max(maxFreq, endFreq);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add padding (5%)
|
// Add 2% padding
|
||||||
const padding = (maxFreq - minFreq) * 0.05;
|
const padding = (maxFreq - minFreq) * 0.02;
|
||||||
minFreq -= padding;
|
minFreq -= padding;
|
||||||
maxFreq += padding;
|
maxFreq += padding;
|
||||||
|
|
||||||
const freqRange = maxFreq - minFreq;
|
// Store original values
|
||||||
|
originalMinFreq = minFreq;
|
||||||
|
originalMaxFreq = maxFreq;
|
||||||
|
originalFreqRange = maxFreq - minFreq;
|
||||||
|
freqRange = originalFreqRange;
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
||||||
|
canvas.addEventListener('mousedown', handleMouseDown);
|
||||||
|
canvas.addEventListener('mousemove', handleMouseMove);
|
||||||
|
canvas.addEventListener('mouseup', handleMouseUp);
|
||||||
|
canvas.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart() {
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
// Set canvas size
|
// Set canvas size
|
||||||
const container = canvas.parentElement;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
const canvasWidth = Math.max(container.clientWidth - 40, 800);
|
const rect = container.getBoundingClientRect();
|
||||||
const rowHeight = 50;
|
const width = rect.width;
|
||||||
const topMargin = 40;
|
const height = rect.height;
|
||||||
const bottomMargin = 60;
|
|
||||||
|
|
||||||
// Group transponders by polarization to stack them
|
canvas.width = width * dpr;
|
||||||
|
canvas.height = height * dpr;
|
||||||
|
canvas.style.width = width + 'px';
|
||||||
|
canvas.style.height = height + 'px';
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
const leftMargin = 60;
|
||||||
|
const rightMargin = 20;
|
||||||
|
const topMargin = 40;
|
||||||
|
const bottomMargin = 40;
|
||||||
|
const chartWidth = width - leftMargin - rightMargin;
|
||||||
|
const chartHeight = height - topMargin - bottomMargin;
|
||||||
|
|
||||||
|
// Group transponders by polarization
|
||||||
const polarizationGroups = {};
|
const polarizationGroups = {};
|
||||||
transponders.forEach(t => {
|
transpondersData.forEach(t => {
|
||||||
const pol = t.polarization || 'default';
|
const pol = t.polarization || 'Другая';
|
||||||
if (!polarizationGroups[pol]) {
|
if (!polarizationGroups[pol]) {
|
||||||
polarizationGroups[pol] = [];
|
polarizationGroups[pol] = [];
|
||||||
}
|
}
|
||||||
polarizationGroups[pol].push(t);
|
polarizationGroups[pol].push(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
const numRows = Object.keys(polarizationGroups).length;
|
const polarizations = Object.keys(polarizationGroups);
|
||||||
const canvasHeight = topMargin + (numRows * rowHeight) + bottomMargin;
|
const rowHeight = chartHeight / polarizations.length;
|
||||||
|
|
||||||
// Set canvas dimensions (use device pixel ratio for sharp rendering)
|
// Calculate visible frequency range with zoom and pan
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const visibleFreqRange = freqRange / zoomLevel;
|
||||||
canvas.width = canvasWidth * dpr;
|
const centerFreq = (minFreq + maxFreq) / 2;
|
||||||
canvas.height = canvasHeight * dpr;
|
const visibleMinFreq = centerFreq - visibleFreqRange / 2 + panOffset;
|
||||||
canvas.style.width = canvasWidth + 'px';
|
const visibleMaxFreq = centerFreq + visibleFreqRange / 2 + panOffset;
|
||||||
canvas.style.height = canvasHeight + 'px';
|
|
||||||
ctx.scale(dpr, dpr);
|
|
||||||
|
|
||||||
// Clear canvas
|
|
||||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
||||||
|
|
||||||
// Draw frequency axis
|
// Draw frequency axis
|
||||||
ctx.strokeStyle = '#dee2e6';
|
ctx.strokeStyle = '#dee2e6';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, topMargin);
|
ctx.moveTo(leftMargin, topMargin);
|
||||||
ctx.lineTo(canvasWidth, topMargin);
|
ctx.lineTo(width - rightMargin, topMargin);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Draw frequency labels
|
// Draw frequency labels and grid
|
||||||
ctx.fillStyle = '#6c757d';
|
ctx.fillStyle = '#6c757d';
|
||||||
ctx.font = '12px sans-serif';
|
ctx.font = '11px sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
const numLabels = 10;
|
const numTicks = 10;
|
||||||
for (let i = 0; i <= numLabels; i++) {
|
for (let i = 0; i <= numTicks; i++) {
|
||||||
const freq = minFreq + (freqRange * i / numLabels);
|
const freq = visibleMinFreq + (visibleMaxFreq - visibleMinFreq) * i / numTicks;
|
||||||
const x = (canvasWidth * i / numLabels);
|
const x = leftMargin + chartWidth * i / numTicks;
|
||||||
|
|
||||||
// Draw tick
|
// Draw tick
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -367,114 +411,266 @@ function renderFrequencyPlan() {
|
|||||||
ctx.lineTo(x, topMargin - 5);
|
ctx.lineTo(x, topMargin - 5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw grid line
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, topMargin);
|
||||||
|
ctx.lineTo(x, height - bottomMargin);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#dee2e6';
|
||||||
|
|
||||||
// Draw label
|
// Draw label
|
||||||
ctx.fillText(freq.toFixed(1), x, topMargin - 10);
|
ctx.fillText(freq.toFixed(1), x, topMargin - 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "МГц" label
|
// Draw axis title
|
||||||
ctx.textAlign = 'right';
|
ctx.fillStyle = '#000';
|
||||||
ctx.fillText('МГц', canvasWidth, topMargin - 25);
|
ctx.font = 'bold 12px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Частота (МГц)', width / 2, topMargin - 25);
|
||||||
|
|
||||||
// Store transponder positions for hover detection
|
// Draw polarization label
|
||||||
const transponderRects = [];
|
ctx.save();
|
||||||
|
ctx.translate(15, height / 2);
|
||||||
|
ctx.rotate(-Math.PI / 2);
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Поляризация', 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Clear transponder rects for hover detection
|
||||||
|
transponderRects = [];
|
||||||
|
|
||||||
// Draw transponders
|
// Draw transponders
|
||||||
let yOffset = topMargin + 10;
|
polarizations.forEach((pol, index) => {
|
||||||
|
|
||||||
Object.keys(polarizationGroups).forEach((pol, groupIndex) => {
|
|
||||||
const group = polarizationGroups[pol];
|
const group = polarizationGroups[pol];
|
||||||
const color = getColor(pol);
|
const color = getColor(pol);
|
||||||
|
const y = topMargin + index * rowHeight;
|
||||||
|
const barHeight = rowHeight * 0.7;
|
||||||
|
const barY = y + (rowHeight - barHeight) / 2;
|
||||||
|
|
||||||
// Draw polarization label
|
// Draw polarization label
|
||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = '#000';
|
||||||
ctx.font = 'bold 14px sans-serif';
|
ctx.font = 'bold 12px sans-serif';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(`${pol}:`, 5, yOffset + 20);
|
ctx.fillText(pol, leftMargin - 10, barY + barHeight / 2 + 4);
|
||||||
|
|
||||||
|
// Draw transponders
|
||||||
group.forEach(t => {
|
group.forEach(t => {
|
||||||
const startFreq = t.downlink - (t.frequency_range / 2);
|
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||||
const endFreq = t.downlink + (t.frequency_range / 2);
|
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||||
|
|
||||||
const x = ((startFreq - minFreq) / freqRange) * canvasWidth;
|
// Check if transponder is visible
|
||||||
const width = ((endFreq - startFreq) / freqRange) * canvasWidth;
|
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
|
||||||
const y = yOffset;
|
return;
|
||||||
const height = 30;
|
}
|
||||||
|
|
||||||
// Draw transponder bar
|
// Calculate position
|
||||||
|
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
|
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
|
||||||
|
const barWidth = x2 - x1;
|
||||||
|
|
||||||
|
// Skip if too small
|
||||||
|
if (barWidth < 1) return;
|
||||||
|
|
||||||
|
// Draw bar
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(x, y, width, height);
|
ctx.fillRect(x1, barY, barWidth, barHeight);
|
||||||
|
|
||||||
// Draw border
|
// Draw border
|
||||||
ctx.strokeStyle = '#fff';
|
ctx.strokeStyle = '#fff';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeRect(x, y, width, height);
|
ctx.strokeRect(x1, barY, barWidth, barHeight);
|
||||||
|
|
||||||
// Draw transponder name if there's enough space
|
// Draw name if there's space
|
||||||
if (width > 50) {
|
if (barWidth > 40) {
|
||||||
ctx.fillStyle = pol === 'R' ? '#000' : '#fff';
|
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
|
||||||
ctx.font = '11px sans-serif';
|
ctx.font = '10px sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(t.name, x + width / 2, y + height / 2 + 4);
|
ctx.fillText(t.name, x1 + barWidth / 2, barY + barHeight / 2 + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store for hover detection
|
// Store for hover detection
|
||||||
transponderRects.push({
|
transponderRects.push({
|
||||||
x, y, width, height,
|
x: x1,
|
||||||
data: t
|
y: barY,
|
||||||
|
width: barWidth,
|
||||||
|
height: barHeight,
|
||||||
|
transponder: t
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
yOffset += rowHeight;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add mouse move event for tooltip
|
// Draw hover tooltip
|
||||||
canvas.addEventListener('mousemove', (e) => {
|
if (hoveredTransponder) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
drawTooltip(hoveredTransponder);
|
||||||
const mouseX = e.clientX - rect.left;
|
}
|
||||||
const mouseY = e.clientY - rect.top;
|
}
|
||||||
|
|
||||||
|
function drawTooltip(t) {
|
||||||
|
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||||
|
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
t.name,
|
||||||
|
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
|
||||||
|
'Downlink: ' + t.downlink.toFixed(3) + ' МГц',
|
||||||
|
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
|
||||||
|
'Поляризация: ' + t.polarization,
|
||||||
|
'Зона: ' + t.zone_name
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate tooltip size
|
||||||
|
ctx.font = '12px sans-serif';
|
||||||
|
const padding = 10;
|
||||||
|
const lineHeight = 16;
|
||||||
|
let maxWidth = 0;
|
||||||
|
lines.forEach(line => {
|
||||||
|
const width = ctx.measureText(line).width;
|
||||||
|
maxWidth = Math.max(maxWidth, width);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipWidth = maxWidth + padding * 2;
|
||||||
|
const tooltipHeight = lines.length * lineHeight + padding * 2;
|
||||||
|
|
||||||
|
// Position tooltip
|
||||||
|
const mouseX = hoveredTransponder._mouseX || canvas.width / 2;
|
||||||
|
const mouseY = hoveredTransponder._mouseY || canvas.height / 2;
|
||||||
|
let tooltipX = mouseX + 15;
|
||||||
|
let tooltipY = mouseY + 15;
|
||||||
|
|
||||||
|
// Keep tooltip in bounds
|
||||||
|
if (tooltipX + tooltipWidth > canvas.width) {
|
||||||
|
tooltipX = mouseX - tooltipWidth - 15;
|
||||||
|
}
|
||||||
|
if (tooltipY + tooltipHeight > canvas.height) {
|
||||||
|
tooltipY = mouseY - tooltipHeight - 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw tooltip background
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
|
||||||
|
ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
|
||||||
|
|
||||||
|
// Draw tooltip text
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.font = 'bold 12px sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.fillText(lines[0], tooltipX + padding, tooltipY + padding + 12);
|
||||||
|
|
||||||
|
ctx.font = '11px sans-serif';
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
ctx.fillText(lines[i], tooltipX + padding, tooltipY + padding + 12 + i * lineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWheel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
const newZoom = Math.max(1, Math.min(20, zoomLevel * delta));
|
||||||
|
|
||||||
|
if (newZoom !== zoomLevel) {
|
||||||
|
zoomLevel = newZoom;
|
||||||
|
|
||||||
hoveredTransponder = null;
|
// Adjust pan to keep center
|
||||||
|
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel);
|
||||||
|
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset));
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(e) {
|
||||||
|
isDragging = true;
|
||||||
|
dragStartX = e.clientX;
|
||||||
|
dragStartOffset = panOffset;
|
||||||
|
canvas.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mouseX = e.clientX - rect.left;
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
const dx = e.clientX - dragStartX;
|
||||||
|
const freqPerPixel = (freqRange / zoomLevel) / (rect.width - 80);
|
||||||
|
panOffset = dragStartOffset - dx * freqPerPixel;
|
||||||
|
|
||||||
|
// Limit pan
|
||||||
|
const maxPan = (originalFreqRange * (zoomLevel - 1)) / (2 * zoomLevel);
|
||||||
|
panOffset = Math.max(-maxPan, Math.min(maxPan, panOffset));
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
} else {
|
||||||
|
// Check hover
|
||||||
|
let found = null;
|
||||||
for (const tr of transponderRects) {
|
for (const tr of transponderRects) {
|
||||||
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
||||||
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
||||||
hoveredTransponder = tr.data;
|
found = tr.transponder;
|
||||||
|
found._mouseX = mouseX;
|
||||||
|
found._mouseY = mouseY;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoveredTransponder) {
|
if (found !== hoveredTransponder) {
|
||||||
const startFreq = hoveredTransponder.downlink - (hoveredTransponder.frequency_range / 2);
|
hoveredTransponder = found;
|
||||||
const endFreq = hoveredTransponder.downlink + (hoveredTransponder.frequency_range / 2);
|
canvas.style.cursor = found ? 'pointer' : 'default';
|
||||||
|
renderChart();
|
||||||
tooltip.innerHTML = `<strong>${hoveredTransponder.name}</strong>
|
} else if (found) {
|
||||||
Downlink: ${hoveredTransponder.downlink.toFixed(3)} МГц
|
found._mouseX = mouseX;
|
||||||
Полоса: ${hoveredTransponder.frequency_range.toFixed(3)} МГц
|
found._mouseY = mouseY;
|
||||||
Диапазон: ${startFreq.toFixed(3)} - ${endFreq.toFixed(3)} МГц
|
|
||||||
Поляризация: ${hoveredTransponder.polarization}
|
|
||||||
Зона: ${hoveredTransponder.zone_name}`;
|
|
||||||
tooltip.style.display = 'block';
|
|
||||||
tooltip.style.left = (e.pageX + 15) + 'px';
|
|
||||||
tooltip.style.top = (e.pageY + 15) + 'px';
|
|
||||||
} else {
|
|
||||||
tooltip.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
canvas.addEventListener('mouseleave', () => {
|
|
||||||
tooltip.style.display = 'none';
|
|
||||||
hoveredTransponder = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render on page load
|
function handleMouseUp() {
|
||||||
document.addEventListener('DOMContentLoaded', renderFrequencyPlan);
|
isDragging = false;
|
||||||
|
canvas.style.cursor = hoveredTransponder ? 'pointer' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
isDragging = false;
|
||||||
|
hoveredTransponder = null;
|
||||||
|
canvas.style.cursor = 'default';
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
zoomLevel = 1;
|
||||||
|
panOffset = 0;
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
zoomLevel = Math.min(20, zoomLevel * 1.2);
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
zoomLevel = Math.max(1, zoomLevel / 1.2);
|
||||||
|
if (zoomLevel === 1) {
|
||||||
|
panOffset = 0;
|
||||||
|
}
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeFrequencyChart();
|
||||||
|
|
||||||
|
// Control buttons
|
||||||
|
document.getElementById('resetZoom').addEventListener('click', resetZoom);
|
||||||
|
document.getElementById('zoomIn').addEventListener('click', zoomIn);
|
||||||
|
document.getElementById('zoomOut').addEventListener('click', zoomOut);
|
||||||
|
});
|
||||||
|
|
||||||
// Re-render on window resize
|
// Re-render on window resize
|
||||||
let resizeTimeout;
|
let resizeTimeout;
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
clearTimeout(resizeTimeout);
|
clearTimeout(resizeTimeout);
|
||||||
resizeTimeout = setTimeout(renderFrequencyPlan, 250);
|
resizeTimeout = setTimeout(renderChart, 250);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,118 +1,118 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from .views import (
|
from .views import (
|
||||||
ActionsPageView,
|
ActionsPageView,
|
||||||
AddSatellitesView,
|
AddSatellitesView,
|
||||||
AddTranspondersView,
|
AddTranspondersView,
|
||||||
ClusterTestView,
|
ClusterTestView,
|
||||||
ClearLyngsatCacheView,
|
ClearLyngsatCacheView,
|
||||||
DeleteSelectedObjectsView,
|
DeleteSelectedObjectsView,
|
||||||
DeleteSelectedSourcesView,
|
DeleteSelectedSourcesView,
|
||||||
DeleteSelectedTranspondersView,
|
DeleteSelectedTranspondersView,
|
||||||
DeleteSelectedSatellitesView,
|
DeleteSelectedSatellitesView,
|
||||||
FillLyngsatDataView,
|
FillLyngsatDataView,
|
||||||
GeoPointsAPIView,
|
GeoPointsAPIView,
|
||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
HomeView,
|
HomeView,
|
||||||
KubsatView,
|
KubsatView,
|
||||||
KubsatExportView,
|
KubsatExportView,
|
||||||
LinkLyngsatSourcesView,
|
LinkLyngsatSourcesView,
|
||||||
LinkVchSigmaView,
|
LinkVchSigmaView,
|
||||||
LoadCsvDataView,
|
LoadCsvDataView,
|
||||||
LoadExcelDataView,
|
LoadExcelDataView,
|
||||||
LyngsatDataAPIView,
|
LyngsatDataAPIView,
|
||||||
LyngsatTaskStatusAPIView,
|
LyngsatTaskStatusAPIView,
|
||||||
LyngsatTaskStatusView,
|
LyngsatTaskStatusView,
|
||||||
ObjItemCreateView,
|
ObjItemCreateView,
|
||||||
ObjItemDeleteView,
|
ObjItemDeleteView,
|
||||||
ObjItemDetailView,
|
ObjItemDetailView,
|
||||||
ObjItemListView,
|
ObjItemListView,
|
||||||
ObjItemUpdateView,
|
ObjItemUpdateView,
|
||||||
ProcessKubsatView,
|
ProcessKubsatView,
|
||||||
SatelliteDataAPIView,
|
SatelliteDataAPIView,
|
||||||
SatelliteListView,
|
SatelliteListView,
|
||||||
SatelliteCreateView,
|
SatelliteCreateView,
|
||||||
SatelliteUpdateView,
|
SatelliteUpdateView,
|
||||||
ShowMapView,
|
ShowMapView,
|
||||||
ShowSelectedObjectsMapView,
|
ShowSelectedObjectsMapView,
|
||||||
ShowSourcesMapView,
|
ShowSourcesMapView,
|
||||||
ShowSourceWithPointsMapView,
|
ShowSourceWithPointsMapView,
|
||||||
ShowSourceAveragingStepsMapView,
|
ShowSourceAveragingStepsMapView,
|
||||||
SourceListView,
|
SourceListView,
|
||||||
SourceUpdateView,
|
SourceUpdateView,
|
||||||
SourceDeleteView,
|
SourceDeleteView,
|
||||||
SourceObjItemsAPIView,
|
SourceObjItemsAPIView,
|
||||||
SigmaParameterDataAPIView,
|
SigmaParameterDataAPIView,
|
||||||
TransponderDataAPIView,
|
TransponderDataAPIView,
|
||||||
TransponderListView,
|
TransponderListView,
|
||||||
TransponderCreateView,
|
TransponderCreateView,
|
||||||
TransponderUpdateView,
|
TransponderUpdateView,
|
||||||
UnlinkAllLyngsatSourcesView,
|
UnlinkAllLyngsatSourcesView,
|
||||||
UploadVchLoadView,
|
UploadVchLoadView,
|
||||||
custom_logout,
|
custom_logout,
|
||||||
)
|
)
|
||||||
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Root URL now points to SourceListView (Requirement 1.1)
|
# Root URL now points to SourceListView (Requirement 1.1)
|
||||||
path('', SourceListView.as_view(), name='home'),
|
path('', SourceListView.as_view(), name='home'),
|
||||||
# Redirect old /home/ URL to source_list for backward compatibility (Requirement 1.2)
|
# Redirect old /home/ URL to source_list for backward compatibility (Requirement 1.2)
|
||||||
path('home/', RedirectView.as_view(pattern_name='mainapp:source_list', permanent=True), name='home_redirect'),
|
path('home/', RedirectView.as_view(pattern_name='mainapp:source_list', permanent=True), name='home_redirect'),
|
||||||
# Keep /sources/ as an alias (Requirement 1.2)
|
# Keep /sources/ as an alias (Requirement 1.2)
|
||||||
path('sources/', SourceListView.as_view(), name='source_list'),
|
path('sources/', SourceListView.as_view(), name='source_list'),
|
||||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||||
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
||||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||||
path('transponders/', TransponderListView.as_view(), name='transponder_list'),
|
path('transponders/', TransponderListView.as_view(), name='transponder_list'),
|
||||||
path('transponder/create/', TransponderCreateView.as_view(), name='transponder_create'),
|
path('transponder/create/', TransponderCreateView.as_view(), name='transponder_create'),
|
||||||
path('transponder/<int:pk>/edit/', TransponderUpdateView.as_view(), name='transponder_update'),
|
path('transponder/<int:pk>/edit/', TransponderUpdateView.as_view(), name='transponder_update'),
|
||||||
path('delete-selected-transponders/', DeleteSelectedTranspondersView.as_view(), name='delete_selected_transponders'),
|
path('delete-selected-transponders/', DeleteSelectedTranspondersView.as_view(), name='delete_selected_transponders'),
|
||||||
path('satellites/', SatelliteListView.as_view(), name='satellite_list'),
|
path('satellites/', SatelliteListView.as_view(), name='satellite_list'),
|
||||||
path('satellite/create/', SatelliteCreateView.as_view(), name='satellite_create'),
|
path('satellite/create/', SatelliteCreateView.as_view(), name='satellite_create'),
|
||||||
path('satellite/<int:pk>/edit/', SatelliteUpdateView.as_view(), name='satellite_update'),
|
path('satellite/<int:pk>/edit/', SatelliteUpdateView.as_view(), name='satellite_update'),
|
||||||
path('delete-selected-satellites/', DeleteSelectedSatellitesView.as_view(), name='delete_selected_satellites'),
|
path('delete-selected-satellites/', DeleteSelectedSatellitesView.as_view(), name='delete_selected_satellites'),
|
||||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||||
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
||||||
path('api/locations/<int:sat_id>/geojson/', GetLocationsView.as_view(), name='locations_by_id'),
|
path('api/locations/<int:sat_id>/geojson/', GetLocationsView.as_view(), name='locations_by_id'),
|
||||||
path('transponders', AddTranspondersView.as_view(), name='add_trans'),
|
path('transponders', AddTranspondersView.as_view(), name='add_trans'),
|
||||||
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||||
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||||
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
||||||
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
||||||
path('show-source-averaging-map/<int:source_id>/', ShowSourceAveragingStepsMapView.as_view(), name='show_source_averaging_map'),
|
path('show-source-averaging-map/<int:source_id>/', ShowSourceAveragingStepsMapView.as_view(), name='show_source_averaging_map'),
|
||||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||||
path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||||
path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
||||||
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||||
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||||
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
||||||
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
||||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
path('object/<int:pk>/', ObjItemDetailView.as_view(), name='objitem_detail'),
|
path('object/<int:pk>/', ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||||
path('object/<int:pk>/delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
|
path('object/<int:pk>/delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||||
path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
||||||
path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
||||||
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
||||||
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
|
path('unlink-all-lyngsat/', UnlinkAllLyngsatSourcesView.as_view(), name='unlink_all_lyngsat'),
|
||||||
path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'),
|
path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'),
|
||||||
path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'),
|
path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'),
|
||||||
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
|
path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'),
|
||||||
path('kubsat/', KubsatView.as_view(), name='kubsat'),
|
path('kubsat/', KubsatView.as_view(), name='kubsat'),
|
||||||
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
|
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
|
||||||
path('logout/', custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
]
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,6 @@ dependencies = [
|
|||||||
"django-dynamic-raw-id>=4.4",
|
"django-dynamic-raw-id>=4.4",
|
||||||
"django-import-export>=4.3.10",
|
"django-import-export>=4.3.10",
|
||||||
"django-leaflet>=0.32.0",
|
"django-leaflet>=0.32.0",
|
||||||
"django-map-widgets>=0.5.1",
|
|
||||||
"django-more-admin-filters>=1.13",
|
"django-more-admin-filters>=1.13",
|
||||||
"dotenv>=0.9.9",
|
"dotenv>=0.9.9",
|
||||||
"flower>=2.0.1",
|
"flower>=2.0.1",
|
||||||
|
|||||||
11
dbapp/uv.lock
generated
11
dbapp/uv.lock
generated
@@ -366,7 +366,6 @@ dependencies = [
|
|||||||
{ name = "django-dynamic-raw-id" },
|
{ name = "django-dynamic-raw-id" },
|
||||||
{ name = "django-import-export" },
|
{ name = "django-import-export" },
|
||||||
{ name = "django-leaflet" },
|
{ name = "django-leaflet" },
|
||||||
{ name = "django-map-widgets" },
|
|
||||||
{ name = "django-more-admin-filters" },
|
{ name = "django-more-admin-filters" },
|
||||||
{ name = "django-redis" },
|
{ name = "django-redis" },
|
||||||
{ name = "dotenv" },
|
{ name = "dotenv" },
|
||||||
@@ -407,7 +406,6 @@ requires-dist = [
|
|||||||
{ name = "django-dynamic-raw-id", specifier = ">=4.4" },
|
{ name = "django-dynamic-raw-id", specifier = ">=4.4" },
|
||||||
{ name = "django-import-export", specifier = ">=4.3.10" },
|
{ name = "django-import-export", specifier = ">=4.3.10" },
|
||||||
{ name = "django-leaflet", specifier = ">=0.32.0" },
|
{ name = "django-leaflet", specifier = ">=0.32.0" },
|
||||||
{ name = "django-map-widgets", specifier = ">=0.5.1" },
|
|
||||||
{ name = "django-more-admin-filters", specifier = ">=1.13" },
|
{ name = "django-more-admin-filters", specifier = ">=1.13" },
|
||||||
{ name = "django-redis", specifier = ">=5.4.0" },
|
{ name = "django-redis", specifier = ">=5.4.0" },
|
||||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||||
@@ -598,15 +596,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ec/d3/bf4a46eff75a5a804fc32588696d2dcd04370008041114009f0f35a3fb42/django_leaflet-0.32.0-py3-none-any.whl", hash = "sha256:a17d8e6cc05dd98e8e543fbf198b81dabbf9f195c222e786d1686aeda91c1aa8", size = 582439, upload-time = "2025-05-14T12:49:34.151Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/d3/bf4a46eff75a5a804fc32588696d2dcd04370008041114009f0f35a3fb42/django_leaflet-0.32.0-py3-none-any.whl", hash = "sha256:a17d8e6cc05dd98e8e543fbf198b81dabbf9f195c222e786d1686aeda91c1aa8", size = 582439, upload-time = "2025-05-14T12:49:34.151Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-map-widgets"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/78/50/651dae7335fc9c6df7b1ab27c49b1cc98245ac0d61750538a192da19e671/django_map_widgets-0.5.1.tar.gz", hash = "sha256:68e81f9c58c1cd6d180421220a4d100a185c8062ae0ca7be790658fcfd4eda1d", size = 160819, upload-time = "2024-07-09T17:37:50.717Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/75/7f1782c9fa3c07c2ca63ce7b65c4838afb568a5ea71aa119aaa9dc456d8b/django_map_widgets-0.5.1-py3-none-any.whl", hash = "sha256:7307935163b46c6a2a225c85c91c7262a8b47a5c3aefbbc6d8fc7a5fda53b7cd", size = 256008, upload-time = "2024-07-09T17:37:48.941Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-more-admin-filters"
|
name = "django-more-admin-filters"
|
||||||
version = "1.13"
|
version = "1.13"
|
||||||
|
|||||||
@@ -1,95 +1,60 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
web:
|
||||||
image: postgis/postgis:17-3.4
|
build:
|
||||||
container_name: postgres-postgis-prod
|
context: ./dbapp
|
||||||
restart: always
|
dockerfile: Dockerfile
|
||||||
environment:
|
env_file:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-geodb}
|
- .env.prod
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-geralt}
|
depends_on:
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
|
- db
|
||||||
ports:
|
volumes:
|
||||||
- "5432:5432"
|
- static_volume:/app/staticfiles
|
||||||
volumes:
|
expose:
|
||||||
- postgres_data_prod:/var/lib/postgresql/data
|
- 8000
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-geralt} -d ${POSTGRES_DB:-geodb}"]
|
worker:
|
||||||
interval: 10s
|
build:
|
||||||
timeout: 5s
|
context: ./dbapp
|
||||||
retries: 5
|
dockerfile: Dockerfile
|
||||||
networks:
|
env_file:
|
||||||
- app-network
|
- .env.prod
|
||||||
|
entrypoint: []
|
||||||
web:
|
command: ["uv", "run", "celery", "-A", "dbapp", "worker", "--loglevel=INFO"]
|
||||||
build:
|
depends_on:
|
||||||
context: ./dbapp
|
- db
|
||||||
dockerfile: Dockerfile
|
- redis
|
||||||
container_name: django-app-prod
|
- web
|
||||||
restart: always
|
|
||||||
environment:
|
redis:
|
||||||
- DEBUG=False
|
image: redis:7-alpine
|
||||||
- ENVIRONMENT=production
|
restart: unless-stopped
|
||||||
- DJANGO_SETTINGS_MODULE=dbapp.settings.production
|
ports:
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
- 6379:6379
|
||||||
- DB_ENGINE=django.contrib.gis.db.backends.postgis
|
|
||||||
- DB_NAME=${DB_NAME:-geodb}
|
db:
|
||||||
- DB_USER=${DB_USER:-geralt}
|
image: postgis/postgis:18-3.6
|
||||||
- DB_PASSWORD=${DB_PASSWORD:-123456}
|
container_name: postgres-postgis
|
||||||
- DB_HOST=db
|
restart: unless-stopped
|
||||||
- DB_PORT=5432
|
env_file:
|
||||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
|
- .env.prod
|
||||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-3}
|
ports:
|
||||||
- GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120}
|
- 5432:5432
|
||||||
ports:
|
volumes:
|
||||||
- "8000:8000"
|
- pgdata:/var/lib/postgresql
|
||||||
volumes:
|
# networks:
|
||||||
- static_volume_prod:/app/staticfiles
|
# - app-network
|
||||||
- media_volume_prod:/app/media
|
|
||||||
- logs_volume_prod:/app/logs
|
nginx:
|
||||||
depends_on:
|
image: nginx:alpine
|
||||||
db:
|
depends_on:
|
||||||
condition: service_healthy
|
- web
|
||||||
networks:
|
ports:
|
||||||
- app-network
|
- 8080:80
|
||||||
|
volumes:
|
||||||
tileserver:
|
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
image: maptiler/tileserver-gl:latest
|
- static_volume:/usr/share/nginx/html/static
|
||||||
container_name: tileserver-gl-prod
|
# если у тебя медиа — можно замонтировать том media
|
||||||
restart: always
|
|
||||||
ports:
|
volumes:
|
||||||
- "8080:8080"
|
pgdata:
|
||||||
volumes:
|
static_volume:
|
||||||
- ./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
|
|
||||||
@@ -1,39 +1,39 @@
|
|||||||
events {
|
upstream django {
|
||||||
worker_connections 1024;
|
server web:8000;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
server {
|
||||||
include /etc/nginx/mime.types;
|
listen 80;
|
||||||
default_type application/octet-stream;
|
server_name _;
|
||||||
|
|
||||||
# Log format
|
proxy_connect_timeout 300s;
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
proxy_send_timeout 300s;
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
proxy_read_timeout 300s;
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
send_timeout 300s;
|
||||||
|
# Максимальный размер тела запроса, например для загрузки файлов
|
||||||
|
client_max_body_size 200m;
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
# Статические файлы (статика Django)
|
||||||
error_log /var/log/nginx/error.log;
|
location /static/ {
|
||||||
|
alias /usr/share/nginx/html/static/; # ← тут путь в контейнере nginx, куда монтируется том со static
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
}
|
||||||
|
|
||||||
# Security headers
|
# Медиа-файлы, если есть MEDIA_URL
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
location /media/ {
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
alias /usr/share/nginx/media/; # путь, куда монтируется media-том
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
expires 30d;
|
||||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
}
|
||||||
|
|
||||||
# Proxy settings
|
# Прокси для всех остальных запросов на Django (асинхронный / uvicorn или gunicorn)
|
||||||
proxy_set_header Host $http_host;
|
location / {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_pass http://django;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Host $server_name;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
# Gzip compression
|
proxy_redirect off;
|
||||||
gzip on;
|
}
|
||||||
gzip_vary on;
|
}
|
||||||
gzip_min_length 1024;
|
|
||||||
# gzip_proxied expired no-cache no-store private must-revalidate auth;
|
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
|
||||||
|
|
||||||
# Include server blocks
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user