Добавлена система ролей пользователей для управления доступом в multi-tenant приложении. Новые роли: - Владелец (Owner): полный доступ, управление пользователями - Менеджер (Manager): управление заказами, клиентами, товарами, складом - Флорист (Florist): работа с заказами и складскими операциями - Курьер (Courier): роль создана, права будут определены позже Архитектура: - Роли автоматически изолируются по тенантам через django-tenants (TENANT_APPS) - Не требуется FK на Client/Tenant - изоляция через PostgreSQL schemas - Роли автоматически создаются при создании нового тенанта Компоненты: - user_roles/models.py: модели Role и UserRole - user_roles/services.py: RoleService для управления ролями - user_roles/decorators.py: @role_required, @owner_required - user_roles/mixins.py: RoleBasedAdminMixin, OwnerOnlyAdminMixin - user_roles/admin.py: админка для управления ролями - user_roles/management/commands/init_roles.py: команда для инициализации Изменения: - accounts/models.py: добавлены helper методы (is_owner, has_role, etc) - settings.py: добавлен user_roles в TENANT_APPS - tenants/admin.py: автосоздание ролей при создании тенанта 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
448 lines
16 KiB
Python
448 lines
16 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 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
|
||
environ.Env.read_env(BASE_DIR / '.env')
|
||
|
||
|
||
# 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 = ['*'] # Для разработки. В продакшене указать конкретные домены
|
||
|
||
|
||
# ============================================
|
||
# 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]
|
||
|
||
# Модели тенанта и домена
|
||
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', # ОБЯЗАТЕЛЬНО ПЕРВЫМ!
|
||
'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', # История изменений
|
||
]
|
||
|
||
|
||
# ============================================
|
||
# 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/Moscow'
|
||
USE_I18N = True
|
||
USE_TZ = True
|
||
|
||
|
||
# ============================================
|
||
# STATIC FILES (CSS, JavaScript, Images)
|
||
# ============================================
|
||
|
||
STATIC_URL = '/static/'
|
||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||
|
||
|
||
# ============================================
|
||
# MEDIA FILES (User uploads)
|
||
# ============================================
|
||
|
||
MEDIA_URL = '/media/'
|
||
MEDIA_ROOT = BASE_DIR / 'media'
|
||
|
||
# Custom file storage for tenant-aware file organization
|
||
DEFAULT_FILE_STORAGE = 'products.utils.storage.TenantAwareFileSystemStorage'
|
||
|
||
# Время жизни временных файлов фото (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'
|
||
|
||
|
||
# ============================================
|
||
# 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 (периодические задачи)
|
||
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',
|
||
},
|
||
},
|
||
}
|