Files
octopus/myproject/current_settings.txt

583 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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',
},
},
}