Сделал деплой

This commit is contained in:
2025-11-23 22:55:32 +03:00
parent 0d239ef1de
commit 9a9900cfa6
15 changed files with 4191 additions and 4217 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

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