Files
octopus/myproject/myproject/settings.py
Andrey Smakotin 483f150e7a feat(static): improve static files handling and permissions in Docker
- Add script to set correct permissions on static files after collectstatic
- Introduce collectstatic command in entrypoint with permission fixing
- Add WhiteNoise middleware for efficient static file serving without DB access
- Configure WhiteNoise static files storage backend in settings
- Set STATIC_ROOT path properly for Docker container environment
- Add fallback static files serving in Django urls for production without nginx
- Enhance inventory_detail.html scripts to log errors if JS files or components fail to load
- Add whitenoise package to requirements for static file serving support
2025-12-22 20:45:52 +03:00

544 lines
20 KiB
Python
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, 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',
}
# ============================================
# 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',
},
},
}