Initial commit - clean project structure

This commit is contained in:
2026-08-18 16:48:15 +03:30
commit 331148037e
147 changed files with 5543 additions and 0 deletions

0
config/__init__.py Normal file
View File

16
config/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()

253
config/settings.py Normal file
View File

@@ -0,0 +1,253 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 5.2.17.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# 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 = 'django-insecure-jn!s*25lz^+bjm101tzmlz8jlj70i0qka++*h-(!0s^^vk-0ss'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
'drf_spectacular',
# Tadrisino apps (فاز اول - ۴ از ۱۱ اپ)
'accounts',
'content',
'progress',
'ai_tutor',
# Tadrisino apps (فاز دوم - بخش ۱ از ۳: subscription, payment, discount)
'subscription',
'payment',
'discount',
# Tadrisino apps (فاز دوم - بخش ۲: notifications)
'notifications',
# Tadrisino apps (فاز دوم - بخش ۳: affiliate)
'affiliate',
# Tadrisino apps (فاز دوم - بخش ۴: support)
'support',
# Tadrisino apps (فاز دوم - بخش ۵: family)
'family',
# Tadrisino apps (سیستم پرداخت بازار + آفر/تخفیف + لاگ عمومی تغییر Status)
'statuslog',
]
MIDDLEWARE = [
'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',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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 = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
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
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'fa-ir'
TIME_ZONE = 'Asia/Tehran'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# -----------------------------------------------------------------------
# Tadrisino — تنظیمات پروژه
# -----------------------------------------------------------------------
# این پروژه از مدل User جنگو (auth) استفاده نمی‌کنه؛ طبق طراحی سینیورت
# accounts.User یک مدل مستقل (business model) هست، نه AUTH_USER_MODEL.
# احراز هویت با یک TokenAuthentication سفارشی (accounts/authentication.py)
# روی همین مدل accounts.User انجام می‌شه؛ نه سشن/کوکی جنگو.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'accounts.authentication.TadrisinoTokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Tadrisino API',
'DESCRIPTION': (
'بک‌اند اپلیکیشن آموزشی Tadrisino — احراز هویت با OTP، محتوای درسی '
'و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت، تخفیف، '
'اعلان‌ها، همکاری در فروش، پشتیبانی و حساب خانواده.'
),
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'COMPONENT_SPLIT_REQUEST': True,
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
'displayRequestDuration': True,
'defaultModelsExpandDepth': -1,
'docExpansion': 'list',
'filter': True,
'persistAuthorization': True,
},
}
# اعتبار توکن دسترسی (ثانیه) و رفرش‌توکن (روز)
ACCESS_TOKEN_TTL_SECONDS = 3600
REFRESH_TOKEN_TTL_DAYS = 30
# در محیط توسعه/تست، این کد OTP همیشه معتبره تا بدون نیاز به SMS واقعی
# بشه لاگین رو با Swagger تست کرد. در پروداکشن حتماً حذف بشه.
DEV_MASTER_OTP = '0000'
# -----------------------------------------------------------------------
# Content progress (NodeProgress status)
# -----------------------------------------------------------------------
# بعد از چند روز بدون فعالیت روی یک Node که status اون in_progress هست،
# management command مربوطه (content/management/commands/
# mark_abandoned_node_progress.py) اونو abandoned علامت می‌زنه. ۱۴ روز
# (نه ۳-۷ روزِ رایج در اپ‌های مشابه) عمداً انتخاب شده چون قطعی/محدودیت
# اینترنت و دیتای موبایل در ایران باعث وقفه‌های چندروزه‌ی غیرارادی
# می‌شه که نباید فوراً به‌عنوان «رهاکردن درس» حساب بشه.
NODE_PROGRESS_ABANDON_DAYS = 14
# -----------------------------------------------------------------------
# Payment security
# -----------------------------------------------------------------------
# POST /api/payment/orders طبق سند یک فراخوانی سرور-به-سرور از اپ
# Subscription‌ه (نه از کلاینت). در عمل subscription/views.py مستقیماً
# تابع payment.services.create_order رو import و صدا می‌زنه، پس این
# endpoint اصلاً لازم نیست از بیرون صدا زده بشه؛ اما چون در urls.py
# رجیستر و AllowAny‌ه، هر کلاینتی می‌تونه با یک userId دلخواه سفارش بسازه.
# برای بستن این حفره، درخواست باید هدر X-Internal-Key رو برابر همین مقدار
# بفرسته؛ در پروداکشن حتماً از env خونده بشه و یک مقدار تصادفی/طولانی باشه.
INTERNAL_SERVICE_KEY = os.environ.get('INTERNAL_SERVICE_KEY', 'dev-internal-service-key-change-me')
# POST /api/payment/webhook قراره از درگاه پرداخت بیاد، نه از یک کلاینت
# دلخواه؛ بدون امضا هر کسی می‌تونه با یک orderId معتبر status=paid بفرسته
# و بدون پرداخت واقعی، اشتراک رو فعال کنه. درگاه‌های واقعی (زرین‌پال و غیره)
# امضای HMAC واقعی خودشون رو دارن؛ اینجا چون درگاه واقعی وصل نیست، به‌جای
# HMAC یک shared-secret ساده به اسم X-Webhook-Secret استفاده شده — همون
# سطح حفاظتی که در سند API برای این endpoint خواسته شده. برای اتصال واقعی
# این باید با بررسی امضای HMAC درگاه جایگزین بشه.
PAYMENT_WEBHOOK_SECRET = os.environ.get('PAYMENT_WEBHOOK_SECRET', 'dev-webhook-secret-change-me')
# -----------------------------------------------------------------------
# Bazaar In-App Billing (پرداخت درون‌برنامه‌ای کافه‌بازار — REST API v2)
# -----------------------------------------------------------------------
# طبق داکیومنت رسمی (developers.cafebazaar.ir/fa/guidelines/in-app-billing)،
# صحت‌سنجی پرداخت بازار یک OAuth2 سرور-به-سرور‌ه: یک بار با CLIENT_ID/
# CLIENT_SECRET و کد بازگشتی از پنل devapi، REFRESH_TOKEN گرفته می‌شه (یک‌بار،
# دستی)؛ از اون به بعد بک‌اند با همین REFRESH_TOKEN، ACCESS_TOKEN تازه
# می‌گیره و با اون به endpoint وضعیت خرید/اشتراک درخواست می‌زنه.
# ⚠️ کافه‌بازار اواخر ۱۴۰۳ روش صحت‌سنجی رو تغییر داد؛ قبل از اتصال واقعی،
# حتماً آخرین مسیر endpointها رو از همون صفحه‌ی رسمی/پنل devapi خودتون
# تایید کنید (BAZAAR_API_BASE پایینی رو بر همون اساس ست کنید).
BAZAAR_PACKAGE_NAME = os.environ.get('BAZAAR_PACKAGE_NAME', 'ir.tadrisino.app')
BAZAAR_CLIENT_ID = os.environ.get('BAZAAR_CLIENT_ID', '')
BAZAAR_CLIENT_SECRET = os.environ.get('BAZAAR_CLIENT_SECRET', '')
BAZAAR_REFRESH_TOKEN = os.environ.get('BAZAAR_REFRESH_TOKEN', '')
BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar.ir/devapi/v2')
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

45
config/urls.py Normal file
View File

@@ -0,0 +1,45 @@
"""
مسیرهای اصلی پروژه‌ی Tadrisino — فاز ۱ (۴ از ۱۱ اپ)
هر اپ زیر پیشوند /api/ خودش mount شده. مسیرهای هر endpoint دقیقاً طبق
سند «Endpointهای API» طراحی‌شده تا مقایسه با Swagger راحت باشه.
"""
from django.contrib import admin
from django.urls import include, path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
urlpatterns = [
path('admin/', admin.site.urls),
# OpenAPI schema + Swagger UI + Redoc
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
# اپ‌های فاز ۱
path('api/', include('accounts.urls')),
path('api/', include('content.urls')),
path('api/', include('progress.urls')),
path('api/', include('ai_tutor.urls')),
# اپ‌های فاز ۲ / بخش ۱ (subscription, payment, discount)
path('api/', include('subscription.urls')),
path('api/', include('payment.urls')),
path('api/', include('discount.urls')),
# اپ‌های فاز ۲ / بخش ۲ (notifications)
path('api/', include('notifications.urls')),
# اپ‌های فاز ۲ / بخش ۳ (affiliate)
path('api/', include('affiliate.urls')),
# اپ‌های فاز ۲ / بخش ۴ (support)
path('api/', include('support.urls')),
# اپ‌های فاز ۲ / بخش ۵ (family)
path('api/', include('family.urls')),
]

16
config/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()