- Удалена лишняя модель IntegrationConfig из system_settings - Singleton-паттерн: одна запись на интеграцию с is_active тумблером - Добавлено шифрование токенов (EncryptedCharField с Fernet AES-128) - UI: тумблеры слева, форма настроек справа - API endpoints: toggle, settings, form_data - Модель Recommerce: store_url + api_token (x-auth-token) - Модель WooCommerce: store_url + consumer_key/secret Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
634 lines
24 KiB
Python
634 lines
24 KiB
Python
# -*- 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: принудительно разрешаем все для корректной работы поддоменов
|
||
ALLOWED_HOSTS = ['*']
|
||
|
||
# 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', # Приложение с моделями тенантов
|
||
'platform_admin', # Администраторы платформы (отдельная модель от CustomUser)
|
||
|
||
# Django встроенные приложения
|
||
'django.contrib.contenttypes',
|
||
'django.contrib.auth',
|
||
'django.contrib.sessions',
|
||
'django.contrib.messages',
|
||
'django.contrib.admin',
|
||
'django.contrib.staticfiles',
|
||
|
||
# Celery results (для сохранения статуса асинхронных задач)
|
||
'django_celery_results',
|
||
]
|
||
|
||
# Tenant apps: создаются в отдельной схеме для каждого тенанта (изолированные данные)
|
||
TENANT_APPS = [
|
||
'django.contrib.contenttypes', # Дублируем для tenant схем
|
||
'django.contrib.auth', # Дублируем для tenant схем
|
||
|
||
# CustomUser - TENANT-ONLY модель!
|
||
# Каждый тенант имеет свою таблицу accounts_customuser в своей схеме
|
||
# Один email в разных тенантах = разные пользователи (полная изоляция)
|
||
'accounts',
|
||
|
||
# Приложения с бизнес-логикой (изолированные для каждого магазина)
|
||
'nested_admin',
|
||
'django_filters', # Фильтрация данных
|
||
'user_roles', # Роли пользователей
|
||
'customers', # Клиенты магазина
|
||
'products', # Товары и категории
|
||
'orders', # Заказы
|
||
'inventory', # Складской учет
|
||
'pos', # POS Terminal
|
||
'discounts', # Скидки и промокоды
|
||
'system_settings', # Системные настройки компании (только для владельца)
|
||
'integrations', # Интеграции с внешними сервисами
|
||
# TODO: 'simple_history' - вернуть позже для истории изменений
|
||
]
|
||
|
||
# Объединяем для INSTALLED_APPS
|
||
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
|
||
|
||
# Django Debug Toolbar (должен быть в SHARED_APPS для работы django-tenants)
|
||
if DEBUG_TOOLBAR_ENABLED:
|
||
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_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. Определение пользователя
|
||
'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
|
||
# ============================================
|
||
|
||
# Authentication backends для изолированной аутентификации
|
||
# ВАЖНО: Порядок важен! Django пробует backends по очереди до первого успеха
|
||
AUTHENTICATION_BACKENDS = [
|
||
# 1. PlatformAdmin - администраторы платформы (public schema)
|
||
# Работает на public домене, а также на tenant доменах для суперадминов (поддержка)
|
||
'platform_admin.backends.PlatformAdminBackend',
|
||
|
||
# 2. CustomUser - пользователи тенантов (tenant schema)
|
||
# Работает ТОЛЬКО на tenant доменах, ищет пользователя в текущей schema
|
||
'accounts.backends.TenantUserBackend',
|
||
|
||
# 3. Ролевой backend для проверки permissions (работает с RoleService)
|
||
'user_roles.auth_backend.RoleBasedPermissionBackend',
|
||
]
|
||
|
||
|
||
# ============================================
|
||
# URL CONFIGURATION
|
||
# ============================================
|
||
|
||
ROOT_URLCONF = 'myproject.urls'
|
||
|
||
# URL-конфигурация для public схемы (главный домен inventory.by)
|
||
PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public'
|
||
|
||
|
||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||
USE_X_FORWARDED_HOST = True
|
||
|
||
|
||
|
||
# ============================================
|
||
# 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
|
||
|
||
|
||
# ============================================
|
||
# 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)
|
||
|
||
|
||
# ============================================
|
||
# SESSION CONFIGURATION
|
||
# ============================================
|
||
|
||
# Время жизни сессии: 7 дней (в секундах)
|
||
# SECURITY: Сокращено с 28 до 7 дней для уменьшения окна атаки
|
||
SESSION_COOKIE_AGE = 604800 # 7 дней
|
||
|
||
# Продлевать сессию при каждом запросе (токен не устареет, если пользователь активен)
|
||
SESSION_SAVE_EVERY_REQUEST = True
|
||
|
||
# Время жизни CSRF cookie (привязан к сессии, но можно увеличить отдельно)
|
||
# 1 год для удобства пользователей
|
||
CSRF_COOKIE_AGE = 31449600
|
||
|
||
# Настройки безопасности куки (универсально для локала и сервера)
|
||
CSRF_COOKIE_SECURE = USE_HTTPS
|
||
SESSION_COOKIE_SECURE = USE_HTTPS
|
||
CSRF_COOKIE_HTTPONLY = True
|
||
SESSION_COOKIE_HTTPONLY = True
|
||
|
||
# SECURITY: SameSite защита от CSRF атак
|
||
# Lax - позволяет работать с поддоменами, но защищает от внешних сайтов
|
||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||
CSRF_COOKIE_SAMESITE = 'Lax'
|
||
|
||
|
||
# ============================================
|
||
# 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'
|
||
|
||
|
||
|
||
|
||
|
||
# ============================================
|
||
# 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 указывает на PlatformAdmin (public schema)
|
||
# Это нужно для django.contrib.admin.LogEntry и других Django-компонентов
|
||
# CustomUser живёт в TENANT_APPS и НЕ является AUTH_USER_MODEL
|
||
AUTH_USER_MODEL = 'platform_admin.PlatformAdmin'
|
||
|
||
|
||
# ============================================
|
||
# 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',
|
||
},
|
||
},
|
||
# Очистка устаревших файлов ошибок импорта клиентов каждый день в полночь
|
||
'delete-old-import-error-files-every-24-hours': {
|
||
'task': 'customers.tasks.delete_old_import_error_files',
|
||
'schedule': crontab(hour=0, minute=0), # Запускать каждый день в полночь
|
||
},
|
||
}
|
||
|
||
# ============================================
|
||
# DATA UPLOAD SETTINGS
|
||
# ============================================
|
||
|
||
# Увеличиваем лимиты для загрузки больших списков (10 000+ клиентов)
|
||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 50000
|
||
DATA_UPLOAD_MAX_MEMORY_SIZE = 104857600 # 100MB
|
||
|
||
|
||
# ============================================
|
||
# ENCRYPTION SETTINGS (for sensitive data like API tokens)
|
||
# ============================================
|
||
|
||
# Ключ шифрования для EncryptedCharField (Fernet AES-128)
|
||
# ВАЖНО: Сгенерировать один раз и сохранить в .env!
|
||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||
ENCRYPTION_KEY = env('ENCRYPTION_KEY', default=None)
|
||
|
||
# Проверка наличия ключа в production
|
||
if not DEBUG and not ENCRYPTION_KEY:
|
||
import warnings
|
||
warnings.warn(
|
||
"ENCRYPTION_KEY not set! Encrypted fields will fail. "
|
||
"Generate with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
||
)
|