Ilo/ilo/settings/base.py
2026-05-04 20:47:23 +03:30

276 lines
9.8 KiB
Python

from datetime import timedelta
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env(
DJANGO_DEBUG=(bool, False),
DJANGO_ALLOWED_HOSTS=(list, []),
)
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env("DJANGO_DEBUG")
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS")
DJANGO_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.admin", # listed last so modeltranslation can register before it
]
THIRD_PARTY_APPS = [
"rest_framework",
"rest_framework_simplejwt.token_blacklist",
"drf_spectacular",
"django_filters",
"corsheaders",
]
LOCAL_APPS = [
"apps.core",
"apps.accounts",
"apps.courses",
"apps.enrollments",
"apps.announcements",
"apps.payments",
]
# modeltranslation MUST be installed before django.contrib.admin (and before any
# app whose models it patches) so its admin tab integration kicks in.
INSTALLED_APPS = ["modeltranslation", *DJANGO_APPS, *THIRD_PARTY_APPS, *LOCAL_APPS]
# modeltranslation
MODELTRANSLATION_LANGUAGES = ("en", "fa")
MODELTRANSLATION_DEFAULT_LANGUAGE = "fa"
MODELTRANSLATION_FALLBACK_LANGUAGES = ("fa", "en")
MIDDLEWARE = [
# CORS must come before CommonMiddleware so it can short-circuit preflight (OPTIONS) requests.
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# CORS — frontend (Next.js) calls this from the browser. Origins come from env so prod
# can lock it down to the deployed frontend domain. Dev defaults to localhost:3000.
CORS_ALLOWED_ORIGINS = env.list(
"CORS_ALLOWED_ORIGINS",
default=["http://localhost:3000", "http://127.0.0.1:3000"],
)
CORS_ALLOW_CREDENTIALS = True
ROOT_URLCONF = "ilo.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.template.context_processors.i18n",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "ilo.wsgi.application"
ASGI_APPLICATION = "ilo.asgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": env("POSTGRES_DB"),
"USER": env("POSTGRES_USER"),
"PASSWORD": env("POSTGRES_PASSWORD"),
"HOST": env("POSTGRES_HOST", default="db"),
"PORT": env("POSTGRES_PORT", default="5432"),
}
}
AUTH_USER_MODEL = "accounts.User"
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"},
]
LANGUAGE_CODE = "en"
TIME_ZONE = "Asia/Tehran"
USE_I18N = True
USE_TZ = True
LANGUAGES = [
("en", "English"),
("fa", "Persian"),
]
LOCALE_PATHS = [BASE_DIR / "locale"]
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else []
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# Pluggable storage. Default is local FS in dev. To switch to S3-compatible
# (MinIO / ArvanCloud Object Storage) in production, set
# DEFAULT_FILE_STORAGE=storages.backends.s3.S3Storage in .env, add the
# `django-storages[s3]` dependency, and set the AWS_* env vars.
STORAGES = {
"default": {
"BACKEND": env(
"DEFAULT_FILE_STORAGE",
default="django.core.files.storage.FileSystemStorage",
),
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# --- DRF + JWT ----------------------------------------------------------------
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"apps.accounts.auth.TrackingJWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_THROTTLE_CLASSES": [],
"DEFAULT_THROTTLE_RATES": {
"otp_send_phone": "5/hour",
"otp_send_ip": "30/min",
},
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_FILTER_BACKENDS": (
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
),
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
SPECTACULAR_SETTINGS = {
"TITLE": "ilo API",
"DESCRIPTION": "SaaS platform for instructors. SMS-OTP auth, JWT tokens, role-based access (student / teacher / admin).",
"VERSION": "0.1.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"TAGS": [
{"name": "auth", "description": "OTP signup/login, JWT lifecycle, current-user."},
{"name": "courses-public", "description": "Public catalog: browse and inspect published courses."},
{"name": "courses-instructor", "description": "Instructor authoring: create / edit / publish courses, sections, lessons."},
{"name": "categories", "description": "Course categories."},
{"name": "enrollments", "description": "Student enrollment, my-courses dashboard, lesson progress."},
{"name": "announcements", "description": "Course announcements: instructors post, enrolled students read."},
{"name": "payments", "description": "Checkout, gateway callback, order history, instructor sales."},
{"name": "instructors-public", "description": "Public instructor profiles + featured-instructors strip."},
{"name": "analytics", "description": "Instructor KPIs, revenue chart, activity feed."},
{"name": "search", "description": "Global / unified search across courses + instructors (+ lessons for enrolled students)."},
],
"SWAGGER_UI_SETTINGS": {
"persistAuthorization": True,
},
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"SIGNING_KEY": SECRET_KEY,
"ALGORITHM": "HS256",
"AUTH_HEADER_TYPES": ("Bearer",),
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
# --- OTP ----------------------------------------------------------------------
OTP_CODE_LENGTH = 4
OTP_TTL_SECONDS = 120
OTP_SEND_COOLDOWN_SECONDS = 60
# --- SMS provider -------------------------------------------------------------
# Default: console backend (logs the OTP) — never use in production.
# For sms.ir, set SMS_BACKEND=apps.accounts.services.sms.SMSIRBackend and configure SMSIR_*
SMS_BACKEND = env("SMS_BACKEND", default="apps.accounts.services.sms.ConsoleSMSBackend")
SMSIR_API_KEY = env("SMSIR_API_KEY", default="")
SMSIR_LINE_NUMBER = env("SMSIR_LINE_NUMBER", default="")
SMSIR_TEMPLATE_ID = env("SMSIR_TEMPLATE_ID", default="")
SMSIR_TEMPLATE_PARAM_NAME = env("SMSIR_TEMPLATE_PARAM_NAME", default="Code")
SMSIR_BASE_URL = env("SMSIR_BASE_URL", default="https://api.sms.ir")
# --- Payments ----------------------------------------------------------------
# Default: console backend (auto-completes orders for dev). Switch to ZarinPal in prod.
PAYMENT_BACKEND = env(
"PAYMENT_BACKEND",
default="apps.payments.services.gateway.ConsolePaymentBackend",
)
PAYMENT_CALLBACK_BASE_URL = env(
"PAYMENT_CALLBACK_BASE_URL", default="http://localhost:8000"
)
# Frontend pages the user lands on after the gateway redirect:
PAYMENT_SUCCESS_REDIRECT_URL = env(
"PAYMENT_SUCCESS_REDIRECT_URL", default="/payment/success"
)
PAYMENT_FAILURE_REDIRECT_URL = env(
"PAYMENT_FAILURE_REDIRECT_URL", default="/payment/failed"
)
# Platform commission split (whole percent, e.g. 15 = 15%).
ILO_COMMISSION_PERCENT = env.int("ILO_COMMISSION_PERCENT", default=15)
# Catalog badge thresholds.
COURSE_NEW_WINDOW_DAYS = env.int("COURSE_NEW_WINDOW_DAYS", default=30)
COURSE_BESTSELLER_THRESHOLD = env.int("COURSE_BESTSELLER_THRESHOLD", default=10)
COURSE_BESTSELLER_WINDOW_DAYS = env.int("COURSE_BESTSELLER_WINDOW_DAYS", default=90)
# Referral split — percentages of the post-checkout amount.
REFERRAL_BUYER_DISCOUNT_PERCENT = env.int("REFERRAL_BUYER_DISCOUNT_PERCENT", default=10)
REFERRAL_REFERRER_CREDIT_PERCENT = env.int("REFERRAL_REFERRER_CREDIT_PERCENT", default=10)
# ZarinPal (only used when PAYMENT_BACKEND points to ZarinPalBackend)
ZARINPAL_MERCHANT_ID = env("ZARINPAL_MERCHANT_ID", default="")
ZARINPAL_SANDBOX = env.bool("ZARINPAL_SANDBOX", default=True)
# IDPay (only used when PAYMENT_BACKEND points to IDPayBackend)
IDPAY_API_KEY = env("IDPAY_API_KEY", default="")
IDPAY_SANDBOX = env.bool("IDPAY_SANDBOX", default=True)
# --- Logging ------------------------------------------------------------------
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"root": {"handlers": ["console"], "level": "INFO"},
"loggers": {
"apps.accounts.services.sms": {"handlers": ["console"], "level": "WARNING", "propagate": False},
},
}