Compare commits

...

2 Commits

16 changed files with 4512 additions and 4342 deletions

View File

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

View File

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

View File

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

View File

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

@@ -1,37 +1,37 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Определяем окружение (по умолчанию production) # Определяем окружение (по умолчанию production)
ENVIRONMENT=${ENVIRONMENT:-production} ENVIRONMENT=${ENVIRONMENT:-production}
echo "Starting in $ENVIRONMENT mode..." echo "Starting in $ENVIRONMENT mode..."
# Ждем PostgreSQL # Ждем PostgreSQL
echo "Waiting for PostgreSQL..." echo "Waiting for PostgreSQL..."
while ! nc -z $DB_HOST $DB_PORT; do 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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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