From a26e709caac7e3d0a72ae2aee16bcd59e40942c9 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Thu, 8 Jan 2026 02:23:27 +0300 Subject: [PATCH] Fix multi-tenant subdomain 404 and Debug Toolbar integration. Set ALLOWED_HOSTS='*', added USE_X_FORWARDED_HOST, and fixed django-tenants middleware order. --- myproject/current_settings.txt | 582 +++++++++++++++++++++++++++++ myproject/myproject/settings.py | 14 +- myproject/myproject/urls.py | 11 +- myproject/myproject/urls_public.py | 11 +- myproject/old_settings.txt | 545 +++++++++++++++++++++++++++ 5 files changed, 1150 insertions(+), 13 deletions(-) create mode 100644 myproject/current_settings.txt create mode 100644 myproject/old_settings.txt diff --git a/myproject/current_settings.txt b/myproject/current_settings.txt new file mode 100644 index 0000000..efd7503 --- /dev/null +++ b/myproject/current_settings.txt @@ -0,0 +1,582 @@ +# -*- coding: utf-8 -*- +""" +Django settings for myproject project with django-tenants support. + +This is a multi-tenant SaaS application where each shop owner gets their own subdomain +and isolated database schema. + +Example: shop1.inventory.by, shop2.inventory.by +""" + +from pathlib import Path +import os +import environ + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Initialize environment variables +env = environ.Env( + # Set casting and default values + DEBUG=(bool, False), # Security: default False + SECRET_KEY=(str, 'django-insecure-default-key-change-in-production'), +) + +# Read .env file (if exists - for local development) +# In Docker, environment variables are set via docker-compose env_file +env_file = BASE_DIR / '.env' +if env_file.exists(): + environ.Env.read_env(env_file) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env.bool('DEBUG', False) +DEBUG_TOOLBAR_ENABLED = DEBUG and env.bool('DEBUG_TOOLBAR_ENABLED', False) + +# Allowed hosts: читаем из переменной окружения +# В .env на проде: ALLOWED_HOSTS=mix.smaa.by,*.mix.smaa.by +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) + +# CSRF configuration +CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[ + 'https://mix.smaa.by', + 'https://*.mix.smaa.by', +]) +CSRF_USE_SESSIONS = True # Рекомендуется для мультихостовых систем + + +# ============================================ +# DJANGO-TENANTS CONFIGURATION +# ============================================ + +# Shared apps: доступны в public схеме (общие для всей системы) +SHARED_APPS = [ + 'django_tenants', # ОБЯЗАТЕЛЬНО ПЕРВЫМ! + 'tenants', # Приложение с моделями тенантов + + # Django встроенные приложения + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.admin', + 'django.contrib.staticfiles', + + # Accounts должен быть в shared для CustomUser (используется в админке) + 'accounts', + + # Celery results (для сохранения статуса асинхронных задач) + 'django_celery_results', +] + +# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные) +TENANT_APPS = [ + 'django.contrib.contenttypes', # Дублируем для tenant схем + 'django.contrib.auth', # Дублируем для tenant схем + + # Приложения с бизнес-логикой (изолированные для каждого магазина) + 'simple_history', # История изменений для каждого тенанта + 'nested_admin', + 'django_filters', # Фильтрация данных + 'user_roles', # Роли пользователей + 'customers', # Клиенты магазина + 'products', # Товары и категории + 'orders', # Заказы + 'inventory', # Складской учет + 'pos', # POS Terminal + 'system_settings', # Системные настройки компании (только для владельца) +] + +# Объединяем для INSTALLED_APPS +INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] + +# Django Debug Toolbar +if DEBUG_TOOLBAR_ENABLED: + INSTALLED_APPS += ['debug_toolbar'] + +# Модели тенанта и домена +TENANT_MODEL = "tenants.Client" +TENANT_DOMAIN_MODEL = "tenants.Domain" + +# Показывать tenant_id в логах (полезно для отладки) +SHOW_PUBLIC_IF_NO_TENANT_FOUND = True + + +# ============================================ +# MIDDLEWARE +# ============================================ + +MIDDLEWARE = [ + 'django_tenants.middleware.main.TenantMainMiddleware', # 1. Обязательно первым! + 'django.middleware.security.SecurityMiddleware', # 2. Безопасность (HTTPS и заголовки) + 'whitenoise.middleware.WhiteNoiseMiddleware', # 3. Статика (после безопасности) + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', # 4. Определение пользователя + 'myproject.admin_access_middleware.TenantAdminAccessMiddleware', # 5. ТЕПЕРЬ проверка работает (после auth) + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', +] + +# Django Debug Toolbar Middleware +if DEBUG_TOOLBAR_ENABLED: + MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') + + +# ============================================ +# AUTHENTICATION BACKENDS +# ============================================ + +# Кастомный backend для связи ролей с Django permissions API +# ВАЖНО: Этот backend работает с ролями из tenant schema, НЕ трогая public schema! +AUTHENTICATION_BACKENDS = [ + 'user_roles.auth_backend.RoleBasedPermissionBackend', # Наш кастомный backend для ролей + 'django.contrib.auth.backends.ModelBackend', # Стандартный backend (для superuser и т.д.) +] + + +# ============================================ +# URL CONFIGURATION +# ============================================ + +ROOT_URLCONF = 'myproject.urls' + +# URL-конфигурация для public схемы (главный домен inventory.by) +PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public' + + +# ============================================ +# TEMPLATES +# ============================================ + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'myproject.wsgi.application' + + +# ============================================ +# DATABASE CONFIGURATION +# ============================================ + +DATABASES = { + 'default': { + 'ENGINE': 'django_tenants.postgresql_backend', # ВАЖНО: используем backend от django-tenants + 'NAME': env('DB_NAME'), + 'USER': env('DB_USER'), + 'PASSWORD': env('DB_PASSWORD'), + 'HOST': env('DB_HOST'), + 'PORT': env('DB_PORT'), + 'OPTIONS': { + 'client_encoding': 'UTF8', + }, + 'CONN_MAX_AGE': 0, + } +} + +# Database router для django-tenants +DATABASE_ROUTERS = [ + 'django_tenants.routers.TenantSyncRouter', +] + + +# ============================================ +# PASSWORD VALIDATION +# ============================================ + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# ============================================ +# INTERNATIONALIZATION +# ============================================ + +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'Europe/Minsk' +USE_I18N = True +USE_TZ = True + + +# ============================================ +# SESSION CONFIGURATION +# ============================================ + +# Время жизни сессии: 4 недели (в секундах) +SESSION_COOKIE_AGE = 2419200 # 28 дней + +# Продлевать сессию при каждом запросе (токен не устареет, если пользователь активен) +SESSION_SAVE_EVERY_REQUEST = True + +# Время жизни CSRF cookie (привязан к сессии, но можно увеличить отдельно) +# 1 год для удобства пользователей +CSRF_COOKIE_AGE = 31449600 + +# CSRF cookie доступен только по HTTPS на проде +CSRF_COOKIE_SECURE = not DEBUG + +# Session cookie доступен только по HTTPS на проде +SESSION_COOKIE_SECURE = not DEBUG + + +# ============================================ +# STATIC FILES (CSS, JavaScript, Images) +# ============================================ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# В production используем внешнюю директорию для nginx +# В Docker контейнере BASE_DIR = /app, но структура проекта: /app/myproject/ +# Поэтому STATIC_ROOT должен быть /app/myproject/staticfiles +if str(BASE_DIR) == '/app': # В Docker контейнере + STATIC_ROOT = BASE_DIR / 'myproject' / 'staticfiles' +else: # Локальная разработка + STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Whitenoise storage +STORAGES = { + "default": { + "BACKEND": "products.utils.storage.TenantAwareFileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", + }, +} + + +# ============================================ +# MEDIA FILES (User uploads) +# ============================================ + +MEDIA_URL = '/media/' +# MEDIA_ROOT должен соответствовать монтированию volume в docker-compose.yml +# На проде в Docker: /app/myproject/media (BASE_DIR = /app) +# Локально: BASE_DIR / 'media' (BASE_DIR = путь к проекту) +if os.environ.get('MEDIA_ROOT'): + MEDIA_ROOT = Path(os.environ.get('MEDIA_ROOT')) +elif str(BASE_DIR) == '/app': # В Docker контейнере + MEDIA_ROOT = BASE_DIR / 'myproject' / 'media' +else: # Локальная разработка + MEDIA_ROOT = BASE_DIR / 'media' + + + +# Время жизни временных файлов фото (TTL) до авто-удаления, в часах +TEMP_MEDIA_TTL_HOURS = 24 + + +# ============================================ +# IMAGE PROCESSING SETTINGS +# ============================================ + +IMAGE_PROCESSING_CONFIG = { + 'formats': { + 'original': { + 'format': 'JPEG', + 'quality': 100, + 'max_width': 2160, + 'max_height': 2160, + 'description': 'Original image (4K max, JPEG format)' + }, + 'large': { + 'format': 'WEBP', + 'quality': 90, + 'width': 1200, + 'height': 1200, + 'description': 'Large image (1200x1200, WebP format)' + }, + 'medium': { + 'format': 'WEBP', + 'quality': 85, + 'width': 600, + 'height': 600, + 'description': 'Medium image (600x600, WebP format)' + }, + 'thumbnail': { + 'format': 'WEBP', + 'quality': 80, + 'width': 200, + 'height': 200, + 'description': 'Thumbnail (200x200, WebP format)' + }, + } +} + + +# ============================================ +# IMAGE QUALITY ASSESSMENT SETTINGS +# ============================================ + +# Пороги качества как доля от максимального размера оригинала (0.0 - 1.0) +# Вычисляется динамически: if image_size >= threshold * max_original_size → quality_level +# +# Пример: если max_width=2160, то: +# - excellent: >= 2052px (95% * 2160) +# - good: >= 1512px (70% * 2160) +# - acceptable: >= 864px (40% * 2160) +# - poor: >= 432px (20% * 2160) +# - very_poor: < 432px +IMAGE_QUALITY_LEVELS = { + 'excellent': 0.95, # 95% от максимума + 'good': 0.70, # 70% от максимума + 'acceptable': 0.40, # 40% от максимума + 'poor': 0.20, # 20% от максимума + # < 20% = very_poor +} + +# Описания и рекомендации для каждого уровня качества +# Используется в админке, формах и API +IMAGE_QUALITY_LABELS = { + 'excellent': { + 'label': 'Отлично', + 'short_label': 'Отлично ✓', + 'description': 'Идеальное качество изображения', + 'color': 'success', + 'icon': '✓', + 'recommendation': 'Готово для выгрузки на сайт', + 'badge_class': 'badge-success', + }, + 'good': { + 'label': 'Хорошо', + 'short_label': 'Хорошо ◐', + 'description': 'Хорошее качество изображения', + 'color': 'info', + 'icon': '◐', + 'recommendation': 'Можно выгружать на сайт', + 'badge_class': 'badge-info', + }, + 'acceptable': { + 'label': 'Приемлемо', + 'short_label': 'Приемлемо ⚠', + 'description': 'Приемлемое качество, но рекомендуется обновить', + 'color': 'warning', + 'icon': '⚠', + 'recommendation': 'Лучше обновить перед выгрузкой на сайт', + 'badge_class': 'badge-warning', + }, + 'poor': { + 'label': 'Плохо', + 'short_label': 'Плохо ✗', + 'description': 'Низкое качество изображения', + 'color': 'danger', + 'icon': '✗', + 'recommendation': 'Требует обновления перед выгрузкой на сайт', + 'badge_class': 'badge-danger', + }, + 'very_poor': { + 'label': 'Очень плохо', + 'short_label': 'Очень плохо ✗✗', + 'description': 'Очень низкое качество изображения', + 'color': 'danger', + 'icon': '✗✗', + 'recommendation': 'Обязательно обновить перед любой выгрузкой', + 'badge_class': 'badge-danger', + }, +} + + +# ============================================ +# BUSINESS LOGIC SETTINGS +# ============================================ + +# Максимальная глубина вложенности категорий товаров +MAX_CATEGORY_DEPTH = 10 + +# Настройки телефонных номеров +PHONENUMBER_DEFAULT_REGION = 'BY' + + +# ============================================ +# DOMAIN SETTINGS (for multi-tenant URLs) +# ============================================ + +# Главный домен приложения (без схемы http/https) +# Локально: localhost:8000, в проде: mix.smaa.by +TENANT_DOMAIN_BASE = env('TENANT_DOMAIN_BASE', default='localhost:8000') + +# Использовать HTTPS для ссылок (в проде True, локально False) +USE_HTTPS = env.bool('USE_HTTPS', default=False) + + +# ============================================ +# DJANGO DEBUG TOOLBAR SETTINGS +# ============================================ + +# Улучшенный блок INTERNAL_IPS (для Docker и локальной разработки) +if DEBUG_TOOLBAR_ENABLED: + import socket + INTERNAL_IPS = ['127.0.0.1', '10.0.2.2', 'localhost'] + + try: + # Получаем все IP хоста (контейнера) + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + for ip_str in ips: + try: + # Для IPv4: заменяем последний октет на 1 (шлюз Docker) + if '.' in ip_str: + parts = ip_str.split('.') + if len(parts) == 4: + gateway_ip = '.'.join(parts[:3] + ['1']) + INTERNAL_IPS.append(gateway_ip) + + # Возможные шлюзы и алиасы + INTERNAL_IPS.extend([ + '.'.join(parts[:3] + ['254']), + 'host.docker.internal', + ]) + except (ValueError, AttributeError): + continue + except Exception as e: + print(f"Debug Info: Could not determine Docker gateway IPs: {e}") + + # Конфигурация Debug Toolbar + DEBUG_TOOLBAR_CONFIG = { + 'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG_TOOLBAR_ENABLED, + 'IS_RUNNING_TESTS': False, + } + + +# ============================================ +# EMAIL SETTINGS +# ============================================ + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'noreply@inventory.by' + + +# ============================================ +# AUTHENTICATION +# ============================================ + +AUTH_USER_MODEL = 'accounts.CustomUser' + + +# ============================================ +# TENANT ADMIN AUTO-CREATION +# ============================================ + +# При создании нового тенанта автоматически создается суперпользователь +# с указанными credentials для доступа к админке тенанта +TENANT_ADMIN_EMAIL = env('TENANT_ADMIN_EMAIL') +TENANT_ADMIN_PASSWORD = env('TENANT_ADMIN_PASSWORD') +TENANT_ADMIN_NAME = env('TENANT_ADMIN_NAME') + + +# ============================================ +# DEFAULT SETTINGS +# ============================================ + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# ============================================ +# CACHE CONFIGURATION (Redis) +# ============================================ + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{env("REDIS_HOST", default="localhost")}:{env("REDIS_PORT", default="6379")}/{env("REDIS_DB", default="0")}', + 'KEY_PREFIX': 'myproject', # Префикс для всех ключей + 'TIMEOUT': 300, # Таймаут по умолчанию (5 минут) + } +} + + +# ============================================ +# CELERY CONFIGURATION +# ============================================ + +# Redis broker и backend для хранения результатов +CELERY_BROKER_URL = f'redis://{env("REDIS_HOST", default="localhost")}:{env("REDIS_PORT", default="6379")}/{env("REDIS_DB", default="0")}' +CELERY_RESULT_BACKEND = 'django-db' # Сохраняем результаты в БД (совместимо с мультитенантностью) + +# Сериализация +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = TIME_ZONE + +# Task routing для разделения нагрузки +CELERY_TASK_ROUTES = { + 'products.tasks.process_product_photo_async': {'queue': 'photo_processing'}, + 'products.tasks.process_multiple_photos_async': {'queue': 'photo_processing'}, + 'products.tasks.cleanup_temp_media_for_schema': {'queue': 'photo_processing'}, + 'products.tasks.cleanup_temp_media_all': {'queue': 'photo_processing'}, +} + +# Worker настройки для обработки длительных задач +CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # Worker берет по одной задаче за раз +CELERY_TASK_ACKS_LATE = True # Подтверждаем задачу только после успешного выполнения +CELERY_WORKER_MAX_TASKS_PER_CHILD = 50 # Перезапускаем worker после 50 задач (защита от утечек памяти PIL) + +# Timeouts +CELERY_TASK_TIME_LIMIT = 300 # 5 минут максимум на одну задачу +CELERY_TASK_SOFT_TIME_LIMIT = 240 # 4 минуты - мягкий лимит перед жестким + +# Результаты и события +CELERY_RESULT_EXPIRES = 3600 # Результаты хранятся 1 час (достаточно для отслеживания прогресса) +CELERY_WORKER_SEND_TASK_EVENTS = True # Отправляем события для мониторинга +CELERY_TASK_SEND_SENT_EVENT = True + +# Retry настройки +CELERY_TASK_DEFAULT_MAX_RETRIES = 3 +CELERY_TASK_DEFAULT_RETRY_DELAY = 60 # Повторить через 60 секунд при ошибке + +# Celery Beat Schedule (периодические задачи) +# Store schedule in /tmp to avoid PermissionError in Docker volumes +CELERY_BEAT_SCHEDULE_FILENAME = os.path.join('/tmp', 'celerybeat-schedule') + +from celery.schedules import crontab + +CELERY_BEAT_SCHEDULE = { + # Очистка истекших блокировок корзины каждые 5 минут + 'cleanup-expired-cart-locks': { + 'task': 'inventory.tasks.cleanup_expired_cart_locks', + 'schedule': crontab(minute='*/5'), # Каждые 5 минут + 'options': { + 'expires': 240, # Задача устаревает через 4 минуты (меньше интервала) + }, + }, + # Очистка временных файлов фото каждые 6 часов + 'cleanup-temp-media-all-tenants': { + 'task': 'products.tasks.cleanup_temp_media_all', + 'schedule': crontab(minute=0, hour='*/6'), # Каждые 6 часов + 'options': { + 'queue': 'photo_processing', + }, + }, +} diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index efd7503..f41e223 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -73,6 +73,7 @@ SHARED_APPS = [ # Celery results (для сохранения статуса асинхронных задач) 'django_celery_results', + 'simple_history', # Перенесли сюда для корректной инициализации моделей ] # Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные) @@ -81,7 +82,6 @@ TENANT_APPS = [ 'django.contrib.auth', # Дублируем для tenant схем # Приложения с бизнес-логикой (изолированные для каждого магазина) - 'simple_history', # История изменений для каждого тенанта 'nested_admin', 'django_filters', # Фильтрация данных 'user_roles', # Роли пользователей @@ -96,9 +96,12 @@ TENANT_APPS = [ # Объединяем для INSTALLED_APPS INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] -# Django Debug Toolbar +# Django Debug Toolbar (должен быть в SHARED_APPS для работы django-tenants) if DEBUG_TOOLBAR_ENABLED: - INSTALLED_APPS += ['debug_toolbar'] + if 'debug_toolbar' not in SHARED_APPS: + SHARED_APPS.append('debug_toolbar') + # Пересобираем INSTALLED_APPS с учетом тулбара + INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] # Модели тенанта и домена TENANT_MODEL = "tenants.Client" @@ -153,6 +156,11 @@ ROOT_URLCONF = 'myproject.urls' PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public' +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_X_FORWARDED_HOST = True + + + # ============================================ # TEMPLATES # ============================================ diff --git a/myproject/myproject/urls.py b/myproject/myproject/urls.py index 1755dff..b77600c 100644 --- a/myproject/myproject/urls.py +++ b/myproject/myproject/urls.py @@ -33,11 +33,12 @@ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - # Django Debug Toolbar (только в DEBUG режиме) - import debug_toolbar - urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), - ] + # Django Debug Toolbar (только если включено в настройках) + if getattr(settings, 'DEBUG_TOOLBAR_ENABLED', False): + import debug_toolbar + urlpatterns += [ + path('__debug__/', include(debug_toolbar.urls)), + ] else: # Force serve media files in production (for NAS setup) urlpatterns += [ diff --git a/myproject/myproject/urls_public.py b/myproject/myproject/urls_public.py index 3e14577..2bda779 100644 --- a/myproject/myproject/urls_public.py +++ b/myproject/myproject/urls_public.py @@ -24,11 +24,12 @@ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - # Django Debug Toolbar (только в DEBUG режиме) - import debug_toolbar - urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), - ] + # Django Debug Toolbar (только если включено в настройках) + if getattr(settings, 'DEBUG_TOOLBAR_ENABLED', False): + import debug_toolbar + urlpatterns += [ + path('__debug__/', include(debug_toolbar.urls)), + ] else: # Force serve media files in production (for NAS setup) urlpatterns += [ diff --git a/myproject/old_settings.txt b/myproject/old_settings.txt new file mode 100644 index 0000000..3f89787 --- /dev/null +++ b/myproject/old_settings.txt @@ -0,0 +1,545 @@ +# -*- coding: utf-8 -*- +""" +Django settings for myproject project with django-tenants support. + +This is a multi-tenant SaaS application where each shop owner gets their own subdomain +and isolated database schema. + +Example: shop1.inventory.by, shop2.inventory.by +""" + +from pathlib import Path +import os +import environ + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Initialize environment variables +env = environ.Env( + # Set casting and default values + DEBUG=(bool, True), + SECRET_KEY=(str, 'django-insecure-default-key-change-in-production'), +) + +# Read .env file (if exists - for local development) +# In Docker, environment variables are set via docker-compose env_file +env_file = BASE_DIR / '.env' +if env_file.exists(): + environ.Env.read_env(env_file) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env('DEBUG') + +# Allowed hosts: читаем из переменной окружения или используем wildcard для разработки +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) + +# CSRF trusted origins для работы за nginx proxy +CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[ + 'https://mix.smaa.by', + 'https://*.mix.smaa.by', +]) + + +# ============================================ +# DJANGO-TENANTS CONFIGURATION +# ============================================ + +# Shared apps: доступны в public схеме (общие для всей системы) +SHARED_APPS = [ + 'django_tenants', # ОБЯЗАТЕЛЬНО ПЕРВЫМ! + 'tenants', # Приложение с моделями тенантов + + # Django встроенные приложения + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.admin', + 'django.contrib.staticfiles', + + # Accounts должен быть в shared для CustomUser (используется в админке) + 'accounts', + + # Celery results (для сохранения статуса асинхронных задач) + 'django_celery_results', +] + +# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные) +TENANT_APPS = [ + 'django.contrib.contenttypes', # Дублируем для tenant схем + 'django.contrib.auth', # Дублируем для tenant схем + + # Приложения с бизнес-логикой (изолированные для каждого магазина) + 'simple_history', # История изменений для каждого тенанта + 'nested_admin', + 'django_filters', # Фильтрация данных + 'user_roles', # Роли пользователей + 'customers', # Клиенты магазина + 'products', # Товары и категории + 'orders', # Заказы + 'inventory', # Складской учет + 'pos', # POS Terminal +] + +# Объединяем для INSTALLED_APPS +INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] + +# Django Debug Toolbar (только в DEBUG режиме) +if DEBUG: + INSTALLED_APPS += ['debug_toolbar'] + +# Модели тенанта и домена +TENANT_MODEL = "tenants.Client" +TENANT_DOMAIN_MODEL = "tenants.Domain" + +# Показывать tenant_id в логах (полезно для отладки) +SHOW_PUBLIC_IF_NO_TENANT_FOUND = True + + +# ============================================ +# MIDDLEWARE +# ============================================ + +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', # Static files first (no DB access needed) + 'django_tenants.middleware.main.TenantMainMiddleware', # ОБЯЗАТЕЛЬНО ПЕРВЫМ! + 'myproject.admin_access_middleware.TenantAdminAccessMiddleware', # SECURITY: Ограничение доступа к админке + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', # История изменений +] + +# Django Debug Toolbar Middleware (только в DEBUG режиме) +# ВАЖНО: добавляем ПОСЛЕ TenantMainMiddleware, чтобы toolbar видел корректный tenant +if DEBUG: + MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') + + +# ============================================ +# AUTHENTICATION BACKENDS +# ============================================ + +# Кастомный backend для связи ролей с Django permissions API +# ВАЖНО: Этот backend работает с ролями из tenant schema, НЕ трогая public schema! +AUTHENTICATION_BACKENDS = [ + 'user_roles.auth_backend.RoleBasedPermissionBackend', # Наш кастомный backend для ролей + 'django.contrib.auth.backends.ModelBackend', # Стандартный backend (для superuser и т.д.) +] + + +# ============================================ +# URL CONFIGURATION +# ============================================ + +ROOT_URLCONF = 'myproject.urls' + +# URL-конфигурация для public схемы (главный домен inventory.by) +PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public' + + +# ============================================ +# TEMPLATES +# ============================================ + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'myproject.wsgi.application' + + +# ============================================ +# DATABASE CONFIGURATION +# ============================================ + +DATABASES = { + 'default': { + 'ENGINE': 'django_tenants.postgresql_backend', # ВАЖНО: используем backend от django-tenants + 'NAME': env('DB_NAME'), + 'USER': env('DB_USER'), + 'PASSWORD': env('DB_PASSWORD'), + 'HOST': env('DB_HOST'), + 'PORT': env('DB_PORT'), + 'OPTIONS': { + 'client_encoding': 'UTF8', + }, + 'CONN_MAX_AGE': 0, + } +} + +# Database router для django-tenants +DATABASE_ROUTERS = [ + 'django_tenants.routers.TenantSyncRouter', +] + + +# ============================================ +# PASSWORD VALIDATION +# ============================================ + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# ============================================ +# INTERNATIONALIZATION +# ============================================ + +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'Europe/Minsk' +USE_I18N = True +USE_TZ = True + + +# ============================================ +# STATIC FILES (CSS, JavaScript, Images) +# ============================================ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# В production используем внешнюю директорию для nginx +# В Docker контейнере BASE_DIR = /app, но структура проекта: /app/myproject/ +# Поэтому STATIC_ROOT должен быть /app/myproject/staticfiles +if str(BASE_DIR) == '/app': # В Docker контейнере + STATIC_ROOT = BASE_DIR / 'myproject' / 'staticfiles' +else: # Локальная разработка + STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Whitenoise storage +STORAGES = { + "default": { + "BACKEND": "products.utils.storage.TenantAwareFileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", + }, +} + + +# ============================================ +# MEDIA FILES (User uploads) +# ============================================ + +MEDIA_URL = '/media/' +# MEDIA_ROOT должен соответствовать монтированию volume в docker-compose.yml +# На проде в Docker: /app/myproject/media (BASE_DIR = /app) +# Локально: BASE_DIR / 'media' (BASE_DIR = путь к проекту) +if os.environ.get('MEDIA_ROOT'): + MEDIA_ROOT = Path(os.environ.get('MEDIA_ROOT')) +elif str(BASE_DIR) == '/app': # В Docker контейнере + MEDIA_ROOT = BASE_DIR / 'myproject' / 'media' +else: # Локальная разработка + MEDIA_ROOT = BASE_DIR / 'media' + + + +# Время жизни временных файлов фото (TTL) до авто-удаления, в часах +TEMP_MEDIA_TTL_HOURS = 24 + + +# ============================================ +# IMAGE PROCESSING SETTINGS +# ============================================ + +IMAGE_PROCESSING_CONFIG = { + 'formats': { + 'original': { + 'format': 'JPEG', + 'quality': 100, + 'max_width': 2160, + 'max_height': 2160, + 'description': 'Original image (4K max, JPEG format)' + }, + 'large': { + 'format': 'WEBP', + 'quality': 90, + 'width': 1200, + 'height': 1200, + 'description': 'Large image (1200x1200, WebP format)' + }, + 'medium': { + 'format': 'WEBP', + 'quality': 85, + 'width': 600, + 'height': 600, + 'description': 'Medium image (600x600, WebP format)' + }, + 'thumbnail': { + 'format': 'WEBP', + 'quality': 80, + 'width': 200, + 'height': 200, + 'description': 'Thumbnail (200x200, WebP format)' + }, + } +} + + +# ============================================ +# IMAGE QUALITY ASSESSMENT SETTINGS +# ============================================ + +# Пороги качества как доля от максимального размера оригинала (0.0 - 1.0) +# Вычисляется динамически: if image_size >= threshold * max_original_size → quality_level +# +# Пример: если max_width=2160, то: +# - excellent: >= 2052px (95% * 2160) +# - good: >= 1512px (70% * 2160) +# - acceptable: >= 864px (40% * 2160) +# - poor: >= 432px (20% * 2160) +# - very_poor: < 432px +IMAGE_QUALITY_LEVELS = { + 'excellent': 0.95, # 95% от максимума + 'good': 0.70, # 70% от максимума + 'acceptable': 0.40, # 40% от максимума + 'poor': 0.20, # 20% от максимума + # < 20% = very_poor +} + +# Описания и рекомендации для каждого уровня качества +# Используется в админке, формах и API +IMAGE_QUALITY_LABELS = { + 'excellent': { + 'label': 'Отлично', + 'short_label': 'Отлично ✓', + 'description': 'Идеальное качество изображения', + 'color': 'success', + 'icon': '✓', + 'recommendation': 'Готово для выгрузки на сайт', + 'badge_class': 'badge-success', + }, + 'good': { + 'label': 'Хорошо', + 'short_label': 'Хорошо ◐', + 'description': 'Хорошее качество изображения', + 'color': 'info', + 'icon': '◐', + 'recommendation': 'Можно выгружать на сайт', + 'badge_class': 'badge-info', + }, + 'acceptable': { + 'label': 'Приемлемо', + 'short_label': 'Приемлемо ⚠', + 'description': 'Приемлемое качество, но рекомендуется обновить', + 'color': 'warning', + 'icon': '⚠', + 'recommendation': 'Лучше обновить перед выгрузкой на сайт', + 'badge_class': 'badge-warning', + }, + 'poor': { + 'label': 'Плохо', + 'short_label': 'Плохо ✗', + 'description': 'Низкое качество изображения', + 'color': 'danger', + 'icon': '✗', + 'recommendation': 'Требует обновления перед выгрузкой на сайт', + 'badge_class': 'badge-danger', + }, + 'very_poor': { + 'label': 'Очень плохо', + 'short_label': 'Очень плохо ✗✗', + 'description': 'Очень низкое качество изображения', + 'color': 'danger', + 'icon': '✗✗', + 'recommendation': 'Обязательно обновить перед любой выгрузкой', + 'badge_class': 'badge-danger', + }, +} + + +# ============================================ +# BUSINESS LOGIC SETTINGS +# ============================================ + +# Максимальная глубина вложенности категорий товаров +MAX_CATEGORY_DEPTH = 10 + +# Настройки телефонных номеров +PHONENUMBER_DEFAULT_REGION = 'BY' + + +# ============================================ +# DOMAIN SETTINGS (for multi-tenant URLs) +# ============================================ + +# Главный домен приложения (без схемы http/https) +# Локально: localhost:8000, в проде: mix.smaa.by +TENANT_DOMAIN_BASE = env('TENANT_DOMAIN_BASE', default='localhost:8000') + +# Использовать HTTPS для ссылок (в проде True, локально False) +USE_HTTPS = env.bool('USE_HTTPS', default=False) + + +# ============================================ +# DJANGO DEBUG TOOLBAR SETTINGS +# ============================================ + +if DEBUG: + # IP адреса, с которых разрешен доступ к Debug Toolbar + INTERNAL_IPS = [ + '127.0.0.1', + 'localhost', + ] + + # Если запускаете в Docker, добавьте IP хоста + # Например: INTERNAL_IPS += ['172.17.0.1', '192.168.65.1'] # Docker Desktop + + # Конфигурация Debug Toolbar + DEBUG_TOOLBAR_CONFIG = { + # Показывать toolbar всегда при DEBUG=True и INTERNAL_IPS + 'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG, + # Или можно фильтровать по tenant (например, только для определенных поддоменов): + # 'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG and getattr(request, 'tenant', None) and request.tenant.schema_name != 'public', + # Отключить для тестов + 'IS_RUNNING_TESTS': False, + } + + +# ============================================ +# EMAIL SETTINGS +# ============================================ + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'noreply@inventory.by' + + +# ============================================ +# AUTHENTICATION +# ============================================ + +AUTH_USER_MODEL = 'accounts.CustomUser' + + +# ============================================ +# TENANT ADMIN AUTO-CREATION +# ============================================ + +# При создании нового тенанта автоматически создается суперпользователь +# с указанными credentials для доступа к админке тенанта +TENANT_ADMIN_EMAIL = env('TENANT_ADMIN_EMAIL') +TENANT_ADMIN_PASSWORD = env('TENANT_ADMIN_PASSWORD') +TENANT_ADMIN_NAME = env('TENANT_ADMIN_NAME') + + +# ============================================ +# DEFAULT SETTINGS +# ============================================ + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# ============================================ +# CACHE CONFIGURATION (Redis) +# ============================================ + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{env("REDIS_HOST", default="localhost")}:{env("REDIS_PORT", default="6379")}/{env("REDIS_DB", default="0")}', + 'KEY_PREFIX': 'myproject', # Префикс для всех ключей + 'TIMEOUT': 300, # Таймаут по умолчанию (5 минут) + } +} + + +# ============================================ +# CELERY CONFIGURATION +# ============================================ + +# Redis broker и backend для хранения результатов +CELERY_BROKER_URL = f'redis://{env("REDIS_HOST", default="localhost")}:{env("REDIS_PORT", default="6379")}/{env("REDIS_DB", default="0")}' +CELERY_RESULT_BACKEND = 'django-db' # Сохраняем результаты в БД (совместимо с мультитенантностью) + +# Сериализация +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = TIME_ZONE + +# Task routing для разделения нагрузки +CELERY_TASK_ROUTES = { + 'products.tasks.process_product_photo_async': {'queue': 'photo_processing'}, + 'products.tasks.process_multiple_photos_async': {'queue': 'photo_processing'}, + 'products.tasks.cleanup_temp_media_for_schema': {'queue': 'photo_processing'}, + 'products.tasks.cleanup_temp_media_all': {'queue': 'photo_processing'}, +} + +# Worker настройки для обработки длительных задач +CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # Worker берет по одной задаче за раз +CELERY_TASK_ACKS_LATE = True # Подтверждаем задачу только после успешного выполнения +CELERY_WORKER_MAX_TASKS_PER_CHILD = 50 # Перезапускаем worker после 50 задач (защита от утечек памяти PIL) + +# Timeouts +CELERY_TASK_TIME_LIMIT = 300 # 5 минут максимум на одну задачу +CELERY_TASK_SOFT_TIME_LIMIT = 240 # 4 минуты - мягкий лимит перед жестким + +# Результаты и события +CELERY_RESULT_EXPIRES = 3600 # Результаты хранятся 1 час (достаточно для отслеживания прогресса) +CELERY_WORKER_SEND_TASK_EVENTS = True # Отправляем события для мониторинга +CELERY_TASK_SEND_SENT_EVENT = True + +# Retry настройки +CELERY_TASK_DEFAULT_MAX_RETRIES = 3 +CELERY_TASK_DEFAULT_RETRY_DELAY = 60 # Повторить через 60 секунд при ошибке + +# Celery Beat Schedule (периодические задачи) +# Store schedule in /tmp to avoid PermissionError in Docker volumes +CELERY_BEAT_SCHEDULE_FILENAME = os.path.join('/tmp', 'celerybeat-schedule') + +from celery.schedules import crontab + +CELERY_BEAT_SCHEDULE = { + # Очистка истекших блокировок корзины каждые 5 минут + 'cleanup-expired-cart-locks': { + 'task': 'inventory.tasks.cleanup_expired_cart_locks', + 'schedule': crontab(minute='*/5'), # Каждые 5 минут + 'options': { + 'expires': 240, # Задача устаревает через 4 минуты (меньше интервала) + }, + }, + # Очистка временных файлов фото каждые 6 часов + 'cleanup-temp-media-all-tenants': { + 'task': 'products.tasks.cleanup_temp_media_all', + 'schedule': crontab(minute=0, hour='*/6'), # Каждые 6 часов + 'options': { + 'queue': 'photo_processing', + }, + }, +}