Fix multi-tenant subdomain 404 and Debug Toolbar integration. Set ALLOWED_HOSTS='*', added USE_X_FORWARDED_HOST, and fixed django-tenants middleware order.
This commit is contained in:
582
myproject/current_settings.txt
Normal file
582
myproject/current_settings.txt
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ SHARED_APPS = [
|
|||||||
|
|
||||||
# Celery results (для сохранения статуса асинхронных задач)
|
# Celery results (для сохранения статуса асинхронных задач)
|
||||||
'django_celery_results',
|
'django_celery_results',
|
||||||
|
'simple_history', # Перенесли сюда для корректной инициализации моделей
|
||||||
]
|
]
|
||||||
|
|
||||||
# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные)
|
# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные)
|
||||||
@@ -81,7 +82,6 @@ TENANT_APPS = [
|
|||||||
'django.contrib.auth', # Дублируем для tenant схем
|
'django.contrib.auth', # Дублируем для tenant схем
|
||||||
|
|
||||||
# Приложения с бизнес-логикой (изолированные для каждого магазина)
|
# Приложения с бизнес-логикой (изолированные для каждого магазина)
|
||||||
'simple_history', # История изменений для каждого тенанта
|
|
||||||
'nested_admin',
|
'nested_admin',
|
||||||
'django_filters', # Фильтрация данных
|
'django_filters', # Фильтрация данных
|
||||||
'user_roles', # Роли пользователей
|
'user_roles', # Роли пользователей
|
||||||
@@ -96,9 +96,12 @@ TENANT_APPS = [
|
|||||||
# Объединяем для INSTALLED_APPS
|
# Объединяем для INSTALLED_APPS
|
||||||
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_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:
|
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"
|
TENANT_MODEL = "tenants.Client"
|
||||||
@@ -153,6 +156,11 @@ ROOT_URLCONF = 'myproject.urls'
|
|||||||
PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public'
|
PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public'
|
||||||
|
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# TEMPLATES
|
# TEMPLATES
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ if settings.DEBUG:
|
|||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
||||||
# Django Debug Toolbar (только в DEBUG режиме)
|
# Django Debug Toolbar (только если включено в настройках)
|
||||||
|
if getattr(settings, 'DEBUG_TOOLBAR_ENABLED', False):
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
path('__debug__/', include(debug_toolbar.urls)),
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ if settings.DEBUG:
|
|||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
||||||
# Django Debug Toolbar (только в DEBUG режиме)
|
# Django Debug Toolbar (только если включено в настройках)
|
||||||
|
if getattr(settings, 'DEBUG_TOOLBAR_ENABLED', False):
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
path('__debug__/', include(debug_toolbar.urls)),
|
||||||
|
|||||||
545
myproject/old_settings.txt
Normal file
545
myproject/old_settings.txt
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user