3965 lines
214 KiB
Plaintext
3965 lines
214 KiB
Plaintext
|
|
{
|
|||
|
|
"cells": [
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# مقدمه\n",
|
|||
|
|
"\n",
|
|||
|
|
"توی این استایلگاید، قراره با هم یاد بگیریم چطور پروژههای Django رو\n",
|
|||
|
|
"طوری طراحی کنیم که **هم تمیزتر، هم مقیاسپذیرتر و هم لذتبخشتر** باشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"ایدهی اصلی اینه که بجای اینکه همهچیز رو توی View یا Model بنویسیم،\n",
|
|||
|
|
"بیایم پروژه رو به چند **لایهی منطقی (Logic Layers)** تقسیم کنیم،\n",
|
|||
|
|
"که هرکدوم وظیفهی خودش رو داره و کدها از هم **جدا و قابل تست** میشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## هدف نهایی\n",
|
|||
|
|
"قراره به جایی برسیم که:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- بدونیم هر بخش از منطق برنامه کجا باید نوشته بشه.\n",
|
|||
|
|
"- بتونیم با خیال راحت تست بنویسیم و خطاها رو هندل کنیم.\n",
|
|||
|
|
"- وظیفهی هر لایه (Service, Selector, API, Serializer و ...) مشخص باشه.\n",
|
|||
|
|
"- اگه یه بخشی از سیستم بزرگ شد، بتونیم راحت جداش کنیم و مدیریت کنیم.\n",
|
|||
|
|
"- کدها هم برای خودمون، هم برای همتیمیهامون **خوانا، امن و توسعهپذیر** باشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ساختار کلی این راهنما\n",
|
|||
|
|
"\n",
|
|||
|
|
"در ادامه قراره با این مباحث آشنا بشیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **API Layer** – نقطهی ورود درخواستها، مسئول دریافت و پاسخ HTTP.\n",
|
|||
|
|
"2. **Service Layer** – مسئول منطق اصلی کسبوکار (Business Logic).\n",
|
|||
|
|
"3. **Selector Layer** – لایهی مخصوص خواندن داده از دیتابیس.\n",
|
|||
|
|
"4. **Serializer Layer** – آمادهسازی دادهها برای API، با prefetch و caching هوشمند.\n",
|
|||
|
|
"5. **URLs Structure** – سازماندهی مسیرها به روش قابل نگهداری در پروژههای بزرگ.\n",
|
|||
|
|
"6. **Settings Structure** – تقسیم تنظیمات Django و ۳rd partyها برای محیطهای مختلف.\n",
|
|||
|
|
"7. **Error Handling** – چطور خطاها رو یکدست و قابل پیشبینی مدیریت کنیم.\n",
|
|||
|
|
"8. **Celery Integration** – اجرای کارهای پسزمینه و Async به روش تمیز.\n",
|
|||
|
|
"9. **Testing Strategy** – چطور هر لایه رو جداگانه و مطمئن تست کنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"در نهایت، هدف ما اینه که:\n",
|
|||
|
|
"> «کدی بنویسیم که هم زیبا باشه، هم قابل فهم، هم انعطافپذیر —\n",
|
|||
|
|
"> و موقع خوندنش حس نکنیم تو جهنم افتادیم »\n"
|
|||
|
|
],
|
|||
|
|
"id": "2f68eed541fac7be"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Django Styleguide — Explained My Way\n",
|
|||
|
|
"\n",
|
|||
|
|
"این خلاصهایه از **Django Styleguide** نوشتهی HackSoft — ولی به زبون خودمونیتر.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اینجا با نتیجهی سالها پروژههای کوچیک و بزرگ طرفی و سه تا راه داری برای برخورد باهاش:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. مو به مو ازش پیروی کن.\n",
|
|||
|
|
"2. هرچی برات معنیدار بود انتخاب کن .\n",
|
|||
|
|
"3. یا کلاً بیخیالش شو — ولی اگه گزینهی سهای… چرا اصلاً اینجایی؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ایدهی اصلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"هرچی ابزار و امکانات پروژه بیشتر بشن، نیازت به ظرفهای منظمتر هم بیشتر میشه.\n",
|
|||
|
|
"هرچی پروژه بزرگتر، لایهها هم باید واضحتر باشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"تو Django منطق پروژه (Business Logic) باید داخل اینا باشه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **Services** → توابعی که بیشتر کار نوشتن توی دیتابیس رو انجام میدن.\n",
|
|||
|
|
"- **Selectors** → توابعی که بیشتر کار خوندن از دیتابیس رو میکنن.\n",
|
|||
|
|
"- **Model Properties** (در موارد خاص).\n",
|
|||
|
|
"- **متد `clean()`** برای اعتبارسنجی اضافه (در موارد خاص).\n",
|
|||
|
|
"\n",
|
|||
|
|
"و نباید داخل اینا باشه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- APIها و Viewها\n",
|
|||
|
|
"- Serializerها و Formها\n",
|
|||
|
|
"- تگهای Form\n",
|
|||
|
|
"- متد `save()` مدل\n",
|
|||
|
|
"- Custom Managerها یا QuerySetها\n",
|
|||
|
|
"- Signalها\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ️ Model Property یا Selector؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"- اگه Property مربوط به چند رابطه (multi-relation) باشه → ببرش تو Selector.\n",
|
|||
|
|
"- اگه Property پیچیدهست و ممکنه باعث **N + 1 query problem** بشه → باز هم Selector.\n",
|
|||
|
|
"\n",
|
|||
|
|
"هدف کلی:\n",
|
|||
|
|
"**\"Separate Concerns\"** — یعنی هر بخش فقط کار خودش رو انجام بده تا بشه راحتتر نگهش داشت و تستش کرد.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## 🤔 چرا نه View یا Serializer؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"چون وقتی منطق رو پخش کنی بین View و Serializer و Form،\n",
|
|||
|
|
"کد تیکهتیکه میشه، جریان دیتا گم میشه،\n",
|
|||
|
|
"و برای یه تغییر ساده باید بری دل abstractionها ببینی چی به چیه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"بله، generic View و serializerها برای CRUD ساده عالیان،\n",
|
|||
|
|
"ولی وقتی از مسیر خوش و خرم CRUD خارج شدی، وارد باسفایت میشی\n",
|
|||
|
|
"اونجاست که اوضاع شلخته میشه و کمکم یه **لگسی ریپو** ازت جا میمونه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"پس این Styleguide اومده تا برات **جعبه** تعریف کنه:\n",
|
|||
|
|
"تا بتونی منطق رو توی جای درستش بچینی، برای پروژهی خودت.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## Core در برابر Interface\n",
|
|||
|
|
"\n",
|
|||
|
|
"رفتار اصلی برنامه (business logic) نباید قاطی لایهی ارتباطش با بیرون بشه.\n",
|
|||
|
|
"API، CLI، management command — اینا فقط Interface ان.\n",
|
|||
|
|
"مرز بین **core** و **interface** باید واضح باشه.\n",
|
|||
|
|
"بعضی وقتا قاطی میشن، ولی داشتن این مرز یعنی پروژهات تمیز، تستپذیر و قابلگسترشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## چرا نه Custom Manager یا QuerySet؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"ایدهی بدی نیست، اما کافی نیست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"ممکنه Manager/QuerySet بهتر برای مدل بسازی،\n",
|
|||
|
|
"ولی نمیتونی کل منطق پروژه رو اونجا بچپونی، چون:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- منطق پروژه همیشه دقیقاً با مدل یکی نیست.\n",
|
|||
|
|
"- معمولاً بین چند تا مدل پخش میشه (A و B و C و D و …).\n",
|
|||
|
|
"- بعضی وقتا باید با سرویسهای خارجی کار کنه — و اونجا جاش توی Manager نیست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"پس منطق رو از لایهی دیتا جدا کن و بذار توی **Service Layer**.\n",
|
|||
|
|
"\n",
|
|||
|
|
"Service ممکنه تابع، کلاس، یا حتی ماژول باشه — هرچی منطقیتره برای مورد تو.\n",
|
|||
|
|
"\n",
|
|||
|
|
"Manager و QuerySet ابزارهای قدرتمندن،\n",
|
|||
|
|
"ولی کارشون فقط ارائهی **interface بهتر** برای مدلهاست، نه جای منطق اصلی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ⚠️ چرا نه Signal؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"Signalها ابزار خوبیان برای **چیزایی که نباید همدیگه رو بشناسن**\n",
|
|||
|
|
"ولی میخوای وصلشون کنی — مثلاً برای invalidate کردن cache.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اما اگه منطق اصلی رو ببری اونجا،\n",
|
|||
|
|
"اتصالها **پنهون و غیرقابلردیابی** میشن.\n",
|
|||
|
|
"در نتیجه، دیباگش به جهنم ختم میشه 🔥\n",
|
|||
|
|
"\n",
|
|||
|
|
"بنابراین Signal فقط برای موارد خاص،\n",
|
|||
|
|
"نه برای ساختار دادن به لایهی domain/business.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## خلاصهی داستان\n",
|
|||
|
|
"\n",
|
|||
|
|
"هرچی پروژه بزرگتر، ساختار باید حرفهایتر.\n",
|
|||
|
|
"منطق رو بذار توی Service و Selector، نه توی View و Serializer.\n",
|
|||
|
|
"Core رو از Interface جدا کن.\n",
|
|||
|
|
"و لطفاً، لطفاً Signal رو فقط وقتی استفاده کن که واقعاً لازمه. 😅\n"
|
|||
|
|
],
|
|||
|
|
"id": "a0d4b30c7f3312b9"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# 🍪 Cookiecutter — پروژه رو با ساختار درست شروع کن\n",
|
|||
|
|
"\n",
|
|||
|
|
"اوه، در ضمن...\n",
|
|||
|
|
"بیشتر دردسرهای اول پروژه معمولاً سر **فولدربندی و فایلمنیجمنت** شروع میشن\n",
|
|||
|
|
"ولی خدا رو شکر یه چیز آماده هست به اسم **Cookiecutter** که کارت رو راحت میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## Cookiecutter چیه؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"یه ابزار برای ساختن **اسکلت اولیه پروژه** ـه.\n",
|
|||
|
|
"به جای اینکه هر بار بری دستی فولدر بسازی و `settings.py` و `config` و `tests` رو از صفر درست کنی،\n",
|
|||
|
|
"با یه دستور ساده برات یه ساختار حرفهای و آماده میسازه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"مثلاً:\n",
|
|||
|
|
"\n",
|
|||
|
|
"```bash\n",
|
|||
|
|
"cookiecutter https://github.com/cookiecutter/cookiecutter-django\n"
|
|||
|
|
],
|
|||
|
|
"id": "1d42ac476244445c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Model\n",
|
|||
|
|
"\n",
|
|||
|
|
"مدل فقط باید مسئول **داده (Data Model)** باشه — نه چیز دیگه.\n",
|
|||
|
|
"یعنی کاری به منطق (Business Logic)، API یا سایر لایهها نداره.\n",
|
|||
|
|
"مدل فقط باید تعریف کنه **چی ذخیره میکنیم** و **چطور ذخیره میکنیم**.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## BaseModel\n",
|
|||
|
|
"\n",
|
|||
|
|
"ما معمولاً از **تکرار کد** خوشمون نمیاد (شما هم احتمالاً همینطور ).\n",
|
|||
|
|
"پس بهترین کار اینه که برای همهی مدلها **یه مدل پایه (Parent Model)** تعریف کنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"این مدل پایه، فیلدهایی رو نگه میداره که تقریباً همهی مدلها بهش نیاز دارن.\n",
|
|||
|
|
"معمولترینش هم ایناست:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- `created_at` → تاریخ ساخت\n",
|
|||
|
|
"- `updated_at` → تاریخ آخرین تغییر\n",
|
|||
|
|
"\n",
|
|||
|
|
"به این ترتیب لازم نیست توی هر مدل جداگانه این فیلدها رو تکرار کنیم.\n",
|
|||
|
|
"کد تمیزتر، کوتاهتر و قابلنگهداریتر میمونه.\n",
|
|||
|
|
"\n"
|
|||
|
|
],
|
|||
|
|
"id": "f45895968b2f2150"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"source": [
|
|||
|
|
"from django.db import models\n",
|
|||
|
|
"from django.utils import timezone\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class BaseModel(models.Model):\n",
|
|||
|
|
" created_at = models.DateTimeField(db_index=True, default=timezone.now)\n",
|
|||
|
|
" updated_at = models.DateTimeField(auto_now=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" class Meta:\n",
|
|||
|
|
" abstract = True"
|
|||
|
|
],
|
|||
|
|
"id": "863c92fa490a623",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " هر موقع هم مودل جدید خواستی فقط از `BaseModel` ارث بری کن",
|
|||
|
|
"id": "b4ba7889997addfb"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class SomeModel(BaseModel):\n",
|
|||
|
|
" pass"
|
|||
|
|
],
|
|||
|
|
"id": "57747f8d45cf2c67"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# ✅ Validation — `clean()` & `full_clean()`\n",
|
|||
|
|
" گاهی نیازه که در مودل خود مطمعن بشیم که دیتایی که در حال ارسال ذخیره سازی هست درسته یا نه"
|
|||
|
|
],
|
|||
|
|
"id": "c35ef5d471064d6a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class Course(BaseModel):\n",
|
|||
|
|
" name = models.CharField(unique=True, max_length=255)\n",
|
|||
|
|
" start_date = models.DateField()\n",
|
|||
|
|
" end_date = models.DateField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def clean(self):\n",
|
|||
|
|
" if self.start_date >= self.end_date:\n",
|
|||
|
|
" raise ValidationError(\"End date cannot be before start date\")\n"
|
|||
|
|
],
|
|||
|
|
"id": "7f2183666c9579ee"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"اینجا با متد `clean()` داریم مطمئن میشیم که دادهی اشتباه وارد دیتابیس نشه.\n",
|
|||
|
|
"اما دقت کن! برای اینکه `clean()` صدا زده بشه باید قبل از `save()`،\n",
|
|||
|
|
"یه بار `full_clean()` روی شیء اجرا بشه 👇"
|
|||
|
|
],
|
|||
|
|
"id": "2a40301792435642"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def course_create(*, name: str, start_date: date, end_date: date) -> Course:\n",
|
|||
|
|
" obj = Course(name=name, start_date=start_date, end_date=end_date)\n",
|
|||
|
|
"\n",
|
|||
|
|
" obj.full_clean() # اعتبارسنجی\n",
|
|||
|
|
" obj.save() # ذخیره در دیتابیس\n",
|
|||
|
|
"\n",
|
|||
|
|
" return obj\n"
|
|||
|
|
],
|
|||
|
|
"id": "8e9df84a2740ada5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"این روش هم با Django Admin هماهنگه،\n",
|
|||
|
|
"چون اونجا فرمها خودشون `full_clean()` رو صدا میزنن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## 📋 قانون کلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"✅ وقتی اعتبارسنجی فقط روی چند تا **فیلد سادهی مدل** هست → توی `clean()` بنویس.\n",
|
|||
|
|
"⚙️ وقتی اعتبارسنجی پیچیدهست یا نیاز به **داده از مدلهای دیگه** داری → ببرش توی **service layer**.\n"
|
|||
|
|
],
|
|||
|
|
"id": "4349f0b2b2e08b97"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Validation — Constraints\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگر بشه با Constraint انجامش داد، همیشه اون راه بهتره 👇\n",
|
|||
|
|
"چون دیتابیس خودش جلوی دادهی اشتباه رو میگیره (حتی از بیرون Django).\n"
|
|||
|
|
],
|
|||
|
|
"id": "8a6e4f232c7ae222"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class Course(BaseModel):\n",
|
|||
|
|
" name = models.CharField(unique=True, max_length=255)\n",
|
|||
|
|
" start_date = models.DateField()\n",
|
|||
|
|
" end_date = models.DateField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" class Meta:\n",
|
|||
|
|
" constraints = [\n",
|
|||
|
|
" models.CheckConstraint(\n",
|
|||
|
|
" name=\"start_date_before_end_date\",\n",
|
|||
|
|
" check=Q(start_date__lt=F(\"end_date\"))\n",
|
|||
|
|
" )\n",
|
|||
|
|
" ]\n"
|
|||
|
|
],
|
|||
|
|
"id": "ff2dab1ff2abd89"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"از Django 4.1 به بعد،\n",
|
|||
|
|
"`full_clean()` حتی **constraints**ها رو هم چک میکنه 👀\n",
|
|||
|
|
"پس به جای `IntegrityError`، `ValidationError` میگیری.\n",
|
|||
|
|
"\n",
|
|||
|
|
"📚 [Docs](https://docs.djangoproject.com/en/4.1/ref/models/instances/#validating-objects)\n"
|
|||
|
|
],
|
|||
|
|
"id": "da2002f4149ecdc7"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Properties\n",
|
|||
|
|
"\n",
|
|||
|
|
"گاهی لازمه مقدارهای سادهی مشتقشده از فیلدها رو سریع بگیریم.\n",
|
|||
|
|
"اونجا بهترین گزینه، `@property` هست.\n"
|
|||
|
|
],
|
|||
|
|
"id": "c28b41382d9cc6f3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.utils import timezone\n",
|
|||
|
|
"\n",
|
|||
|
|
"class Course(BaseModel):\n",
|
|||
|
|
" ...\n",
|
|||
|
|
" @property\n",
|
|||
|
|
" def has_started(self) -> bool:\n",
|
|||
|
|
" now = timezone.now()\n",
|
|||
|
|
" return self.start_date <= now.date()\n",
|
|||
|
|
"\n",
|
|||
|
|
" @property\n",
|
|||
|
|
" def has_finished(self) -> bool:\n",
|
|||
|
|
" now = timezone.now()\n",
|
|||
|
|
" return self.end_date <= now.date()\n"
|
|||
|
|
],
|
|||
|
|
"id": "46be994e2cb5c266"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"الان میتونی توی serializer یا template بنویسی:\n",
|
|||
|
|
"\n",
|
|||
|
|
"```python\n",
|
|||
|
|
"course.has_started\n",
|
|||
|
|
"course.has_finished\n"
|
|||
|
|
],
|
|||
|
|
"id": "8237eece2f794240"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Methods\n",
|
|||
|
|
"\n",
|
|||
|
|
"متدهای مدل ابزار قدرتمندی هستن که میتونن روی properties ساخته بشن یا دادههای مدل رو تغییر بدن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## 🧩 مثال ساده\n",
|
|||
|
|
"\n",
|
|||
|
|
"در این مثال، متد `is_within(self, x)` بررسی میکنه که آیا تاریخ `x` بین `start_date` و `end_date` هست یا نه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "464c1da129b3874"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import ValidationError\n",
|
|||
|
|
"from django.utils import timezone\n",
|
|||
|
|
"from datetime import date\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class Course(BaseModel):\n",
|
|||
|
|
" name = models.CharField(unique=True, max_length=255)\n",
|
|||
|
|
" start_date = models.DateField()\n",
|
|||
|
|
" end_date = models.DateField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def clean(self):\n",
|
|||
|
|
" if self.start_date >= self.end_date:\n",
|
|||
|
|
" raise ValidationError(\"End date cannot be before start date\")\n",
|
|||
|
|
"\n",
|
|||
|
|
" @property\n",
|
|||
|
|
" def has_started(self) -> bool:\n",
|
|||
|
|
" now = timezone.now()\n",
|
|||
|
|
" return self.start_date <= now.date()\n",
|
|||
|
|
"\n",
|
|||
|
|
" @property\n",
|
|||
|
|
" def has_finished(self) -> bool:\n",
|
|||
|
|
" now = timezone.now()\n",
|
|||
|
|
" return self.end_date <= now.date()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def is_within(self, x: date) -> bool:\n",
|
|||
|
|
" return self.start_date <= x <= self.end_date\n"
|
|||
|
|
],
|
|||
|
|
"id": "9f0b439a9f9d0884"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"متد `is_within` نمیتونه property باشه چون نیاز به آرگومان داره، پس بهصورت متد تعریف میشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## 🔁 مثال دوم: ستکردن چند مقدار با هم\n",
|
|||
|
|
"\n",
|
|||
|
|
"گاهی وقتا وقتی یک attribute تغییر میکنه، باید attribute دیگهای هم بر اساس اون آپدیت بشه.\n",
|
|||
|
|
"در این مواقع میتونیم از یک متد استفاده کنیم.\n",
|
|||
|
|
"اینجا چون `x` ورودی داره، دیگه property نیست، method حساب میشه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "87ee9f1eb34fa889"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.utils.crypto import get_random_string\n",
|
|||
|
|
"from django.conf import settings\n",
|
|||
|
|
"from django.utils import timezone\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class Token(BaseModel):\n",
|
|||
|
|
" secret = models.CharField(max_length=255, unique=True)\n",
|
|||
|
|
" expiry = models.DateTimeField(blank=True, null=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" def set_new_secret(self):\n",
|
|||
|
|
" now = timezone.now()\n",
|
|||
|
|
" self.secret = get_random_string(255)\n",
|
|||
|
|
" self.expiry = now + settings.TOKEN_EXPIRY_TIMEDELTA\n",
|
|||
|
|
" return self\n"
|
|||
|
|
],
|
|||
|
|
"id": "f3f24d65fb578fc2"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"با صدا زدن `set_new_secret()`، هر دو مقدار `secret` و `expiry` بهدرستی مقداردهی میشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## 📜 قانون کلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"✅ اگه مقدار جدید فقط از خود فیلدهای مدل حساب میشه → متد.\n",
|
|||
|
|
"⚙️ اگه باید از مدلهای دیگه داده بگیری یا محاسبه پیچیدهست → سرویس (Service).\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"# 🧪 Testing\n",
|
|||
|
|
"\n",
|
|||
|
|
"مدلها فقط وقتی تست نیاز دارن که منطق خاصی داشته باشن (مثل validation یا property یا method).\n"
|
|||
|
|
],
|
|||
|
|
"id": "6ea8b2ecd4ec994b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from datetime import timedelta\n",
|
|||
|
|
"from django.test import TestCase\n",
|
|||
|
|
"from django.core.exceptions import ValidationError\n",
|
|||
|
|
"from django.utils import timezone\n",
|
|||
|
|
"from project.some_app.models import Course\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class CourseTests(TestCase):\n",
|
|||
|
|
" def test_course_end_date_cannot_be_before_start_date(self):\n",
|
|||
|
|
" start_date = timezone.now()\n",
|
|||
|
|
" end_date = timezone.now() - timedelta(days=1)\n",
|
|||
|
|
"\n",
|
|||
|
|
" course = Course(start_date=start_date, end_date=end_date)\n",
|
|||
|
|
"\n",
|
|||
|
|
" with self.assertRaises(ValidationError):\n",
|
|||
|
|
" course.full_clean()\n"
|
|||
|
|
],
|
|||
|
|
"id": "592b86eae5ab75f8"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"✅ نکات مهم:\n",
|
|||
|
|
"- `full_clean()` بررسی میکنه که اعتبار دادهها درست باشه.\n",
|
|||
|
|
"- نیازی نیست به دیتابیس وصل بشیم، چون فقط منطق مدل تست میشه → تست سریعتر میشه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "d7153c4801ac18f9"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Services\n",
|
|||
|
|
"\n",
|
|||
|
|
"سرویسها جایی هستن که **منطق تجاری (Business Logic)** پروژه در اونها قرار میگیره.\n",
|
|||
|
|
"یعنی هر چیزی که به قوانین یا رفتار اصلی سیستم مربوطه — نه صرفاً ذخیرهسازی یا نمایش داده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"لایهی Service زبانی متناسب با **دامنهی پروژه (Domain Language)** صحبت میکنه،\n",
|
|||
|
|
"به دیتابیس و منابع خارجی دسترسی داره،\n",
|
|||
|
|
"و میتونه با بخشهای دیگهی سیستم تعامل کنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"## جایگاه در ساختار پروژه\n",
|
|||
|
|
"\n",
|
|||
|
|
"لایهی Service معمولاً بین **View** و **Model** قرار میگیره:\n",
|
|||
|
|
"\n",
|
|||
|
|
"[ Views ] → [ Services ] → [ Models ]"
|
|||
|
|
],
|
|||
|
|
"id": "47bfa5655b35cfcd"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "",
|
|||
|
|
"id": "810e866eefd4da64",
|
|||
|
|
"attachments": {
|
|||
|
|
"a56fbb8b-78e7-4442-bb46-e3974577e458.png": {
|
|||
|
|
"image/png": "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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## ساختار کلی Service\n",
|
|||
|
|
"\n",
|
|||
|
|
"یک سرویس میتونه هرکدوم از اینا باشه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- یک تابع ساده\n",
|
|||
|
|
"- یک کلاس\n",
|
|||
|
|
"- حتی یک ماژول کامل\n",
|
|||
|
|
"- یا هر چیزی که **برای پروژهت منطقی باشه**\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"## معمولترین حالت\n",
|
|||
|
|
"\n",
|
|||
|
|
"در بیشتر موارد، سرویسها فقط **توابع ساده** هستن که:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- داخل فایل `<your_app>/services.py` قرار میگیرن\n",
|
|||
|
|
"- ورودیهاشون **keyword-only** هست (مگر اینکه فقط یه آرگومان نیاز داشته باشن)\n",
|
|||
|
|
"- با **type hint** نوشته میشن، حتی اگه فعلاً از `mypy` استفاده نکنی\n",
|
|||
|
|
"- با دیتابیس، فایلها، APIها یا سرویسهای دیگه ارتباط دارن\n",
|
|||
|
|
"- منطق اصلی بیزینس رو انجام میدن — از ساخت سادهی مدلها گرفته تا عملیات پیچیده یا فراخوانی سرویسهای خارجی\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"Example - function-based service"
|
|||
|
|
],
|
|||
|
|
"id": "7217107a64af0d96"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def user_create(\n",
|
|||
|
|
" *,\n",
|
|||
|
|
" email: str,\n",
|
|||
|
|
" name: str\n",
|
|||
|
|
") -> User:\n",
|
|||
|
|
" user = User(email=email)\n",
|
|||
|
|
" user.full_clean()\n",
|
|||
|
|
" user.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" profile_create(user=user, name=name)\n",
|
|||
|
|
" confirmation_email_send(user=user)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return user"
|
|||
|
|
],
|
|||
|
|
"id": "b6bd9661823ed54c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "Example - class-based service\n",
|
|||
|
|
"id": "8975bbd5ce5df8a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class FileStandardUploadService:\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" This also serves as an example of a service class,\n",
|
|||
|
|
" which encapsulates 2 different behaviors (create & update) under a namespace.\n",
|
|||
|
|
"\n",
|
|||
|
|
" Meaning, we use the class here for:\n",
|
|||
|
|
"\n",
|
|||
|
|
" 1. The namespace\n",
|
|||
|
|
" 2. The ability to reuse `_infer_file_name_and_type` (which can also be an util)\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" def __init__(self, user: BaseUser, file_obj):\n",
|
|||
|
|
" self.user = user\n",
|
|||
|
|
" self.file_obj = file_obj\n",
|
|||
|
|
"\n",
|
|||
|
|
" def _infer_file_name_and_type(self, file_name: str = \"\", file_type: str = \"\") -> Tuple[str, str]:\n",
|
|||
|
|
" file_name = file_name or self.file_obj.name\n",
|
|||
|
|
"\n",
|
|||
|
|
" if not file_type:\n",
|
|||
|
|
" guessed_file_type, encoding = mimetypes.guess_type(file_name)\n",
|
|||
|
|
" file_type = guessed_file_type or \"\"\n",
|
|||
|
|
"\n",
|
|||
|
|
" return file_name, file_type\n",
|
|||
|
|
"\n",
|
|||
|
|
" @transaction.atomic\n",
|
|||
|
|
" def create(self, file_name: str = \"\", file_type: str = \"\") -> File:\n",
|
|||
|
|
" _validate_file_size(self.file_obj)\n",
|
|||
|
|
"\n",
|
|||
|
|
" file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n",
|
|||
|
|
"\n",
|
|||
|
|
" obj = File(\n",
|
|||
|
|
" file=self.file_obj,\n",
|
|||
|
|
" original_file_name=file_name,\n",
|
|||
|
|
" file_name=file_generate_name(file_name),\n",
|
|||
|
|
" file_type=file_type,\n",
|
|||
|
|
" uploaded_by=self.user,\n",
|
|||
|
|
" upload_finished_at=timezone.now()\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" obj.full_clean()\n",
|
|||
|
|
" obj.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return obj\n",
|
|||
|
|
"\n",
|
|||
|
|
" @transaction.atomic\n",
|
|||
|
|
" def update(self, file: File, file_name: str = \"\", file_type: str = \"\") -> File:\n",
|
|||
|
|
" _validate_file_size(self.file_obj)\n",
|
|||
|
|
"\n",
|
|||
|
|
" file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n",
|
|||
|
|
"\n",
|
|||
|
|
" file.file = self.file_obj\n",
|
|||
|
|
" file.original_file_name = file_name\n",
|
|||
|
|
" file.file_name = file_generate_name(file_name)\n",
|
|||
|
|
" file.file_type = file_type\n",
|
|||
|
|
" file.uploaded_by = self.user\n",
|
|||
|
|
" file.upload_finished_at = timezone.now()\n",
|
|||
|
|
"\n",
|
|||
|
|
" file.full_clean()\n",
|
|||
|
|
" file.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return file"
|
|||
|
|
],
|
|||
|
|
"id": "c8e3aaa27bfb4ee3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"همونطور که تو کامنت گفته شد، ما این روش رو به دو دلیل اصلی استفاده میکنیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **Namespace**\n",
|
|||
|
|
" - یک فضای نام واحد داریم برای `create` و `update`.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **Reuse**\n",
|
|||
|
|
" - میخوایم از منطق `_infer_file_name_and_type` دوباره استفاده کنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"در ادامه، نمونهای از نحوهی استفادهی این سرویس رو میبینیم:\n"
|
|||
|
|
],
|
|||
|
|
"id": "cecbfe320f4741ae"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/apis.py\n",
|
|||
|
|
"\n",
|
|||
|
|
"class FileDirectUploadApi(ApiAuthMixin, APIView):\n",
|
|||
|
|
" def post(self, request):\n",
|
|||
|
|
" service = FileDirectUploadService(\n",
|
|||
|
|
" user=request.user,\n",
|
|||
|
|
" file_obj=request.FILES[\"file\"]\n",
|
|||
|
|
" )\n",
|
|||
|
|
" file = service.create()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data={\"id\": file.id}, status=status.HTTP_201_CREATED)"
|
|||
|
|
],
|
|||
|
|
"id": "e067d685d21197af"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"@admin.register(File)\n",
|
|||
|
|
"class FileAdmin(admin.ModelAdmin):\n",
|
|||
|
|
" # ... other code here ...\n",
|
|||
|
|
" # https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py\n",
|
|||
|
|
"\n",
|
|||
|
|
" def save_model(self, request, obj, form, change):\n",
|
|||
|
|
" try:\n",
|
|||
|
|
" cleaned_data = form.cleaned_data\n",
|
|||
|
|
"\n",
|
|||
|
|
" service = FileDirectUploadService(\n",
|
|||
|
|
" file_obj=cleaned_data[\"file\"],\n",
|
|||
|
|
" user=cleaned_data[\"uploaded_by\"]\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" if change:\n",
|
|||
|
|
" service.update(file=obj)\n",
|
|||
|
|
" else:\n",
|
|||
|
|
" service.create()\n",
|
|||
|
|
" except ValidationError as exc:\n",
|
|||
|
|
" self.message_user(request, str(exc), messages.ERROR)"
|
|||
|
|
],
|
|||
|
|
"id": "52d5d1cf07308729"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " استفاده از classbased خیلی برای کار ها دارای جریان مفیده مثل کار های مولتی استیج",
|
|||
|
|
"id": "47a806c2ff87b03a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class FileDirectUploadService:\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" This also serves as an example of a service class,\n",
|
|||
|
|
" which encapsulates a flow (start & finish) + one-off action (upload_local) into a namespace.\n",
|
|||
|
|
"\n",
|
|||
|
|
" Meaning, we use the class here for:\n",
|
|||
|
|
"\n",
|
|||
|
|
" 1. The namespace\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" def __init__(self, user: BaseUser):\n",
|
|||
|
|
" self.user = user\n",
|
|||
|
|
"\n",
|
|||
|
|
" @transaction.atomic\n",
|
|||
|
|
" def start(self, *, file_name: str, file_type: str) -> Dict[str, Any]:\n",
|
|||
|
|
" file = File(\n",
|
|||
|
|
" original_file_name=file_name,\n",
|
|||
|
|
" file_name=file_generate_name(file_name),\n",
|
|||
|
|
" file_type=file_type,\n",
|
|||
|
|
" uploaded_by=self.user,\n",
|
|||
|
|
" file=None\n",
|
|||
|
|
" )\n",
|
|||
|
|
" file.full_clean()\n",
|
|||
|
|
" file.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" upload_path = file_generate_upload_path(file, file.file_name)\n",
|
|||
|
|
"\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" We are doing this in order to have an associated file for the field.\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" file.file = file.file.field.attr_class(file, file.file.field, upload_path)\n",
|
|||
|
|
" file.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" presigned_data: Dict[str, Any] = {}\n",
|
|||
|
|
"\n",
|
|||
|
|
" if settings.FILE_UPLOAD_STORAGE == FileUploadStorage.S3:\n",
|
|||
|
|
" presigned_data = s3_generate_presigned_post(\n",
|
|||
|
|
" file_path=upload_path, file_type=file.file_type\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" else:\n",
|
|||
|
|
" presigned_data = {\n",
|
|||
|
|
" \"url\": file_generate_local_upload_url(file_id=str(file.id)),\n",
|
|||
|
|
" }\n",
|
|||
|
|
"\n",
|
|||
|
|
" return {\"id\": file.id, **presigned_data}\n",
|
|||
|
|
"\n",
|
|||
|
|
" @transaction.atomic\n",
|
|||
|
|
" def finish(self, *, file: File) -> File:\n",
|
|||
|
|
" # Potentially, check against user\n",
|
|||
|
|
" file.upload_finished_at = timezone.now()\n",
|
|||
|
|
" file.full_clean()\n",
|
|||
|
|
" file.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return file"
|
|||
|
|
],
|
|||
|
|
"id": "f44e1ac4b30189b1"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"\n",
|
|||
|
|
"نامگذاری سرویسها به سلیقهی شما بستگی داره، اما مهمه که **یکدست و قابل پیشبینی** باشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگر به مثال قبلی نگاه کنیم، سرویس ما `user_create` نام داره.\n",
|
|||
|
|
"الگوی پیشنهادی: `<entity>_<action>`.\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **Namespace**\n",
|
|||
|
|
" - راحت میتونیم همهی سرویسهایی که با `user_` شروع میشن رو پیدا کنیم.\n",
|
|||
|
|
" - همچنین میتونیم اونها رو در یک ماژول جداگانه مثل `users.py` قرار بدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **Greppability**\n",
|
|||
|
|
" - اگر بخوایم همهی عملیات مرتبط با یک موجودیت خاص رو ببینیم، کافیه grep کنیم روی `user_`."
|
|||
|
|
],
|
|||
|
|
"id": "f9066ce8d21bd233"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Modules\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگه یک اپ سادهی Django داری و تعداد کمی سرویس داری، میتونی همه رو داخل یک فایل `services.py` نگه داری.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ️ وقتی پروژه بزرگ میشه\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی سرویسها زیاد شدن، بهتره `services.py` رو به یک **پکیج (فولدر) با زیرماژولها** تبدیل کنی،\n",
|
|||
|
|
"بسته به **سابدامینها** یا بخشهای مختلفی که در اپ داری.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"فرض کنیم یک اپ Authentication داریم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- یک زیرماژول برای **JWT**\n",
|
|||
|
|
"- یک زیرماژول برای **OAuth**\n",
|
|||
|
|
"\n",
|
|||
|
|
"```text\n",
|
|||
|
|
"services\n",
|
|||
|
|
"├── __init__.py\n",
|
|||
|
|
"├── jwt.py\n",
|
|||
|
|
"└── oauth.py"
|
|||
|
|
],
|
|||
|
|
"id": "bf2e2bda65226330"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Flavors of Service Modules\n",
|
|||
|
|
"\n",
|
|||
|
|
"چند روش مختلف برای ساختاردهی سرویسها وجود داره:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **Import-export dance**\n",
|
|||
|
|
" - میتونی داخل `services/__init__.py` همهی سرویسها رو ایمپورت کنی،\n",
|
|||
|
|
" - تا بتونی از هر جای پروژه، فقط با `from project.authentication.services import ...` دسترسی داشته باشی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **Folder-module**\n",
|
|||
|
|
" - میتونی یک فولدر مثل `jwt/` بسازی و `__init__.py` داشته باشه،\n",
|
|||
|
|
" - و تمام کدهای مرتبط با JWT رو اونجا بذاری.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. **Flexibility**\n",
|
|||
|
|
" - در نهایت، ساختار کاملاً به خودت بستگی داره.\n",
|
|||
|
|
" - اگه حس کردی وقتشه بازسازی و ریفکتور انجام بدی — همین کار رو بکن"
|
|||
|
|
],
|
|||
|
|
"id": "f43a82fe12fd62c0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Selectors\n",
|
|||
|
|
"\n",
|
|||
|
|
"در بیشتر پروژههامون، بین **پوش دادن (Push)** و **پول کشیدن (Pull)** دادهها از دیتابیس تفاوت قائل میشیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- 🧩 **Services** مسئول \"Push\" هستن — یعنی وارد کردن، تغییر دادن یا حذف دادهها در دیتابیس.\n",
|
|||
|
|
"- 🔍 **Selectors** مسئول \"Pull\" هستن — یعنی واکشی و خواندن دادهها از دیتابیس.\n",
|
|||
|
|
"\n",
|
|||
|
|
"به عبارتی، **Selectors** یک «زیرلایه» از سرویسها هستن که تخصصش در گرفتن دادههاست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگه با این ایده خیلی ارتباط نمیگیری، هیچ اشکالی نداره — میتونی برای هر دو نوع عملیات فقط از سرویسها استفاده کنی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## قواعد Selector\n",
|
|||
|
|
"\n",
|
|||
|
|
"یک Selector دقیقاً از همان قوانین Service پیروی میکند:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- میتونه تابع، کلاس یا ماژول باشه.\n",
|
|||
|
|
"- در `<your_app>/selectors.py` قرار میگیره.\n",
|
|||
|
|
"- ورودیهاش باید شفاف و keyword-only باشن.\n",
|
|||
|
|
"- خروجی معمولاً queryset یا object برمیگردونه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---"
|
|||
|
|
],
|
|||
|
|
"id": "14fffe05635a6b5a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def user_list(*, fetched_by: User) -> Iterable[User]:\n",
|
|||
|
|
" user_ids = user_get_visible_for(user=fetched_by)\n",
|
|||
|
|
"\n",
|
|||
|
|
" query = Q(id__in=user_ids)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return User.objects.filter(query)"
|
|||
|
|
],
|
|||
|
|
"id": "11a6a260e9e93772"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Testing\n",
|
|||
|
|
"\n",
|
|||
|
|
"از اونجایی که **Service Layer** منطق اصلی کسبوکار (Business Logic) رو در خودش داره،\n",
|
|||
|
|
"بهترین نقطه برای نوشتن تستهای دقیق و هدفمند هم هست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## قوانین کلی برای تست سرویسها\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگر تصمیم گرفتی **service layer** رو با تست پوشش بدی، این چند قانون کلی رو در نظر بگیر:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **تستها باید تمام منطق کسبوکار رو بهصورت کامل پوشش بدن.**\n",
|
|||
|
|
" یعنی تمام مسیرهای ممکن در تابع یا کلاس تست بشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **تستها باید به دیتابیس واقعی (در محیط تست) دسترسی داشته باشن.**\n",
|
|||
|
|
" یعنی داده بسازن، ذخیره کنن و دوباره بخونن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. **هر چیزی که از پروژه خارج میشه باید Mock بشه.**\n",
|
|||
|
|
" مثل:\n",
|
|||
|
|
" - فراخوانی async taskها\n",
|
|||
|
|
" - درخواست به APIهای خارجی\n",
|
|||
|
|
" - ایمیلزدن یا پیامفرستادن\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ساخت وضعیت مورد نیاز برای تست\n",
|
|||
|
|
"\n",
|
|||
|
|
"برای ساخت حالت اولیه (state) مورد نیاز هر تست، میتونی از ترکیب روشهای زیر استفاده کنی:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **Fakes** → (مثل کتابخانه `faker`) برای ساخت دادهی تصادفی و طبیعی.\n",
|
|||
|
|
"- **سرویسهای دیگر** → از سرویسهای موجود برای ساخت آبجکتهای مورد نیاز استفاده کن.\n",
|
|||
|
|
"- **Test utilities & helpers** → متدهای کمکی مخصوص تستها.\n",
|
|||
|
|
"- **Factories** → پیشنهاد میکنیم از `factory_boy` استفاده کنی.\n",
|
|||
|
|
"- **Plain `.objects.create()` calls** → اگر هنوز factory نداری، مستقیم با ORM بساز.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"در نهایت، هر روشی که بهت کمک کنه تستهایت تمیز، خوانا و پایدار باشن، بهترین انتخابه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "46c62fdd47f9f14e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.contrib.auth.models import User\n",
|
|||
|
|
"from django.core.exceptions import ValidationError\n",
|
|||
|
|
"from django.db import transaction\n",
|
|||
|
|
"\n",
|
|||
|
|
"from project.payments.selectors import items_get_for_user\n",
|
|||
|
|
"from project.payments.models import Item, Payment\n",
|
|||
|
|
"from project.payments.tasks import payment_charge\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@transaction.atomic\n",
|
|||
|
|
"def item_buy(\n",
|
|||
|
|
" *,\n",
|
|||
|
|
" item: Item,\n",
|
|||
|
|
" user: User,\n",
|
|||
|
|
") -> Payment:\n",
|
|||
|
|
" if item in items_get_for_user(user=user):\n",
|
|||
|
|
" raise ValidationError(f'Item {item} already in {user} items.')\n",
|
|||
|
|
"\n",
|
|||
|
|
" payment = Payment(\n",
|
|||
|
|
" item=item,\n",
|
|||
|
|
" user=user,\n",
|
|||
|
|
" successful=False\n",
|
|||
|
|
" )\n",
|
|||
|
|
" payment.full_clean()\n",
|
|||
|
|
" payment.save()\n",
|
|||
|
|
"\n",
|
|||
|
|
" # Run the task once the transaction has commited,\n",
|
|||
|
|
" # guaranteeing the object has been created.\n",
|
|||
|
|
" transaction.on_commit(\n",
|
|||
|
|
" lambda: payment_charge.delay(payment_id=payment.id)\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" return payment"
|
|||
|
|
],
|
|||
|
|
"id": "cafdccd628863788"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"1. **فراخوانی یک Selector برای اعتبارسنجی (Validation)**\n",
|
|||
|
|
" ابتدا دادهها رو از دیتابیس واکشی میکنه یا بررسی میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **ایجاد یک آبجکت**\n",
|
|||
|
|
" بعد از اعتبارسنجی، یک آبجکت جدید ایجاد میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. **تاخیر یک تسک**\n",
|
|||
|
|
" در نهایت، یک تسک async رو به صورت delayed فراخوانی میکنه برای انجام کارهای پسزمینه."
|
|||
|
|
],
|
|||
|
|
"id": "e516af97cdcd05aa"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from unittest.mock import patch, Mock\n",
|
|||
|
|
"\n",
|
|||
|
|
"from django.test import TestCase\n",
|
|||
|
|
"from django.contrib.auth.models import User\n",
|
|||
|
|
"from django.core.exceptions import ValidationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"from django_styleguide.payments.services import item_buy\n",
|
|||
|
|
"from django_styleguide.payments.models import Payment, Item\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class ItemBuyTests(TestCase):\n",
|
|||
|
|
" @patch('project.payments.services.items_get_for_user')\n",
|
|||
|
|
" def test_buying_item_that_is_already_bought_fails(\n",
|
|||
|
|
" self, items_get_for_user_mock: Mock\n",
|
|||
|
|
" ):\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" Since we already have tests for `items_get_for_user`,\n",
|
|||
|
|
" we can safely mock it here and give it a proper return value.\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" user = User(username='Test User')\n",
|
|||
|
|
" item = Item(\n",
|
|||
|
|
" name='Test Item',\n",
|
|||
|
|
" description='Test Item description',\n",
|
|||
|
|
" price=10.15\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" items_get_for_user_mock.return_value = [item]\n",
|
|||
|
|
"\n",
|
|||
|
|
" with self.assertRaises(ValidationError):\n",
|
|||
|
|
" item_buy(user=user, item=item)\n",
|
|||
|
|
"\n",
|
|||
|
|
" @patch('project.payments.services.payment_charge.delay')\n",
|
|||
|
|
" def test_buying_item_creates_a_payment_and_calls_charge_task(\n",
|
|||
|
|
" self,\n",
|
|||
|
|
" payment_charge_mock: Mock\n",
|
|||
|
|
" ):\n",
|
|||
|
|
" # How we prepare our tests is a topic for a different discussion\n",
|
|||
|
|
" user = given_a_user(username=\"Test user\")\n",
|
|||
|
|
" item = given_a_item(\n",
|
|||
|
|
" name='Test Item',\n",
|
|||
|
|
" description='Test Item description',\n",
|
|||
|
|
" price=10.15\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" self.assertEqual(0, Payment.objects.count())\n",
|
|||
|
|
"\n",
|
|||
|
|
" payment = item_buy(user=user, item=item)\n",
|
|||
|
|
"\n",
|
|||
|
|
" self.assertEqual(1, Payment.objects.count())\n",
|
|||
|
|
" self.assertEqual(payment, Payment.objects.first())\n",
|
|||
|
|
"\n",
|
|||
|
|
" self.assertFalse(payment.successful)\n",
|
|||
|
|
"\n",
|
|||
|
|
" payment_charge_mock.assert_called_once()"
|
|||
|
|
],
|
|||
|
|
"id": "18e48e3a427946f3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# APIs & Serializers\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی از **Services** و **Selectors** استفاده میکنیم، تمام APIهامون باید ظاهری ساده و یکسان داشته باشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## اصول کلی در ساخت APIها\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **هر عملیات = یک API**\n",
|
|||
|
|
" برای مثال در CRUD، چهار API جدا برای Create, Read, Update, Delete داریم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **ارثبری از سادهترین کلاس ممکن**\n",
|
|||
|
|
" از `APIView` یا `GenericAPIView` استفاده کنید.\n",
|
|||
|
|
" کلاسهای خیلی انتزاعیتر (مثل ViewSets) کارها رو با Serializer هندل میکنن، در حالی که ما میخوایم منطق اصلی رو در Service و Selector نگه داریم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. **عدم وجود Business Logic در API**\n",
|
|||
|
|
" نباید منطق اصلی در API نوشته بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"4. **مدیریت داده درون API مجازه**\n",
|
|||
|
|
" کارهایی مثل واکشی آبجکت یا دستکاری داده میتونه داخل API انجام بشه (البته میتونید اون رو به تابع یا Service جداگانه منتقل کنید).\n",
|
|||
|
|
"\n",
|
|||
|
|
"5. **سادگی در طراحی APIها**\n",
|
|||
|
|
" API فقط باید رابطی باشه بین کاربر و منطق اصلی برنامه.\n",
|
|||
|
|
" در صورت نیاز، میتونید از توابعی مثل `some_service_parse` برای جداسازی مرحلهی آمادهسازی دادهها استفاده کنید.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## Serializers\n",
|
|||
|
|
"\n",
|
|||
|
|
"در کنار APIها، ما برای کار با دادهها — چه **ورودی** و چه **خروجی** — به Serializer نیاز داریم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"Serializer وظیفهی زیر رو داره:\n",
|
|||
|
|
"- اعتبارسنجی دادههای ورودی (Validation)\n",
|
|||
|
|
"- تبدیل دادهها به ساختارهای Python برای استفاده در Service یا Database\n",
|
|||
|
|
"- آمادهسازی دادههای خروجی برای ارسال به Client\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" در حالت کلی:\n",
|
|||
|
|
"- **Service** و **Selector** منطق اصلی (Business Logic) رو انجام میدن.\n",
|
|||
|
|
"- **Serializer** فقط وظیفهی تبدیل و اعتبارسنجی داده رو داره.\n",
|
|||
|
|
"- **API** نقش واسطهی بین این دو رو بازی میکنه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "74789ff48a6ed3a0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# API Serialization Rules\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی صحبت از **سریالایزرها** میشه، چند قانون کلی داریم که کار رو تمیز و قابل پیشبینی نگه میدارن:\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## اصول پایه\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. برای هر API باید **دو سریالایزر جداگانه** وجود داشته باشه:\n",
|
|||
|
|
" - `InputSerializer` → دادههای ورودی رو اعتبارسنجی و آماده میکنه.\n",
|
|||
|
|
" - `OutputSerializer` → دادههای خروجی رو برای نمایش آماده میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. از هر انتزاعی (abstraction) که دوست داری استفاده کن — هر چیزی که برات کارآمده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ️ اگر از DRF (Django REST Framework) استفاده میکنی\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **سریالایزرها باید داخل API تعریف بشن**\n",
|
|||
|
|
" و با نامهای `InputSerializer` و `OutputSerializer` شناخته بشن.\n",
|
|||
|
|
" این باعث میشه ساختار کد واضحتر و محلیتر باشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **ترجیحاً از `Serializer` بهجای `ModelSerializer` استفاده کن**\n",
|
|||
|
|
" `Serializer` کنترل بیشتری بهت میده و API رو از مدل جدا نگه میداره.\n",
|
|||
|
|
" اما اگر `ModelSerializer` برای پروژهت جواب میده، هیچ اشکالی نداره ازش استفاده کنی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. **در صورت نیاز به nested serializer**\n",
|
|||
|
|
" از ابزار `inline_serializer` استفاده کن تا ساختار کد تمیز و ایزوله بمونه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"4. **از Reuse بیشازحد سریالایزرها خودداری کن**\n",
|
|||
|
|
" چون تغییر در یک سریالایزر پایه ممکنه رفتار APIهای دیگه رو هم ناخواسته تغییر بده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"خلاصه:\n",
|
|||
|
|
"> هر API = ۱ سریالایزر ورودی + ۱ سریالایزر خروجی\n",
|
|||
|
|
"> و هر دو داخل همان View تعریف بشن تا ساختار کد شفاف و محلی باقی بمونه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "a717874e312d26ad"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Class-based vs. Function-based APIs\n",
|
|||
|
|
"\n",
|
|||
|
|
"انتخاب بین **Class-based** و **Function-based** معمولاً به سلیقه و عادت تیم بستگی داره،\n",
|
|||
|
|
"چون هر دو روش میتونن دقیقا همون نتایج رو بدن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"- بهصورت پیشفرض از **Class-based APIs / Views** استفاده میکنیم.\n",
|
|||
|
|
"- اگر بقیه اعضای تیم با **Function-based** راحتتر هستن، از اون روش استفاده کنید.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## مزایای استفاده از Class-based\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. میتونی از `BaseApi` ارثبری کنی یا از **mixins** استفاده کنی.\n",
|
|||
|
|
"2. اگر Function-based باشی، باید همین رفتار رو با **decorator**ها بسازی.\n",
|
|||
|
|
"3. کلاس خودش یک **namespace** ایجاد میکنه، که میتونی داخلش\n",
|
|||
|
|
" متدها، اتربیوتها و تنظیمات مختلف رو بنویسی.\n",
|
|||
|
|
"4. در کلاسها میتونی پیکربندی API رو از طریق **class attributes** انجام بدی.\n",
|
|||
|
|
" ولی در حالت تابعی، باید **decorator**ها رو روی هم بچینی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" **خلاصه:**\n",
|
|||
|
|
"کلاسها ساختار تمیزتری برای گسترش، تست و پیکربندی فراهم میکنن،\n",
|
|||
|
|
"اما اگر تیم با توابع احساس راحتی بیشتری داره، مهمتر از همه **یکنواختی و هماهنگی در کد**ه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "2c6c3c80abaf825b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "با کلاس مثال `BaseApi`",
|
|||
|
|
"id": "9f4ad8234dabaf2a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class SomeApi(BaseApi):\n",
|
|||
|
|
" def get(self, request):\n",
|
|||
|
|
" data = something()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data)"
|
|||
|
|
],
|
|||
|
|
"id": "9f94608de6174efa"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " یک مثال `functionBase`",
|
|||
|
|
"id": "e94df60d317d3e3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class SomeApi(BaseApi):\n",
|
|||
|
|
" def get(self, request):\n",
|
|||
|
|
" data = something()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data)"
|
|||
|
|
],
|
|||
|
|
"id": "b49c79da5f621ecb"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " با `BaseApi decorator`",
|
|||
|
|
"id": "4b5346f62f553f0e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"@base_api([\"GET\"])\n",
|
|||
|
|
"def some_api(request):\n",
|
|||
|
|
" data = something()\n",
|
|||
|
|
" return Response(data)"
|
|||
|
|
],
|
|||
|
|
"id": "671ce724fddcdd05"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# List APIs\n",
|
|||
|
|
"\n",
|
|||
|
|
"بریم سراغ سادهترین نوع API یعنی **List API** — اونایی که فقط یه لیست از دیتا برمیگردونن، بدون دردسر خاص.\n",
|
|||
|
|
"\n",
|
|||
|
|
"خب، یه **plain list API** معمولاً خیلی تمیز و جمعوجوره. چیزی شبیه به مثال زیره"
|
|||
|
|
],
|
|||
|
|
"id": "159ba4d84086654f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework.views import APIView\n",
|
|||
|
|
"from rest_framework import serializers\n",
|
|||
|
|
"from rest_framework.response import Response\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.users.selectors import user_list\n",
|
|||
|
|
"from styleguide_example.users.models import BaseUser\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class UserListApi(APIView):\n",
|
|||
|
|
" class OutputSerializer(serializers.Serializer):\n",
|
|||
|
|
" id = serializers.CharField()\n",
|
|||
|
|
" email = serializers.CharField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def get(self, request):\n",
|
|||
|
|
" users = user_list()\n",
|
|||
|
|
"\n",
|
|||
|
|
" data = self.OutputSerializer(users, many=True).data\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data)"
|
|||
|
|
],
|
|||
|
|
"id": "beb124af9c72d8b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " >یادت باشه این پابلیکه Authentication اش با خودته",
|
|||
|
|
"id": "dc5f66e599ce8eca"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Filters + Pagination\n",
|
|||
|
|
"\n",
|
|||
|
|
"اولش شاید به نظر سخت بیاد، چون API ما از همون plain `APIView` تو DRF ارث میبره،\n",
|
|||
|
|
"در حالی که filtering و pagination تو کلاسهای generic خودشون تعبیه شدن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **DRF Filtering**\n",
|
|||
|
|
"- **DRF Pagination**\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ️ رویکرد ما\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **Selectors** مسئول فیلتر واقعی هستن.\n",
|
|||
|
|
" یعنی کد اصلی که دیتا رو فیلتر میکنه، توی selector میره.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **APIs** مسئول **serialize کردن پارامترهای فیلتر** هستن.\n",
|
|||
|
|
" یعنی ورودی رو میگیرن و آماده میکنن که selector بتونه استفاده کنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. اگر میخوای از paginationهای آماده DRF استفاده کنی،\n",
|
|||
|
|
" خود API اون رو هندل میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"4. اگر دنبال یه pagination متفاوتی، یا پیادهسازی خودت هستی:\n",
|
|||
|
|
" - میتونی یه لایه جدید بسازی برای pagination،\n",
|
|||
|
|
" - یا بذاری selector این کار رو انجام بده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"نکته اصلی:\n",
|
|||
|
|
"**API فقط رابط باشه، منطق و دیتا رو بذار توی selector و service** —\n",
|
|||
|
|
"نه اینکه همه چیز توی API جمع بشه و کد شلخته شه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "6be4e4c7a3811b29"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework.views import APIView\n",
|
|||
|
|
"from rest_framework import serializers\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.api.mixins import ApiErrorsMixin\n",
|
|||
|
|
"from styleguide_example.api.pagination import get_paginated_response, LimitOffsetPagination\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.users.selectors import user_list\n",
|
|||
|
|
"from styleguide_example.users.models import BaseUser\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class UserListApi(ApiErrorsMixin, APIView):\n",
|
|||
|
|
" class Pagination(LimitOffsetPagination):\n",
|
|||
|
|
" default_limit = 1\n",
|
|||
|
|
"\n",
|
|||
|
|
" class FilterSerializer(serializers.Serializer):\n",
|
|||
|
|
" id = serializers.IntegerField(required=False)\n",
|
|||
|
|
" # Important: If we use BooleanField, it will default to False\n",
|
|||
|
|
" is_admin = serializers.NullBooleanField(required=False)\n",
|
|||
|
|
" email = serializers.EmailField(required=False)\n",
|
|||
|
|
"\n",
|
|||
|
|
" class OutputSerializer(serializers.Serializer):\n",
|
|||
|
|
" id = serializers.CharField()\n",
|
|||
|
|
" email = serializers.CharField()\n",
|
|||
|
|
" is_admin = serializers.BooleanField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def get(self, request):\n",
|
|||
|
|
" # Make sure the filters are valid, if passed\n",
|
|||
|
|
" filters_serializer = self.FilterSerializer(data=request.query_params)\n",
|
|||
|
|
" filters_serializer.is_valid(raise_exception=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" users = user_list(filters=filters_serializer.validated_data)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return get_paginated_response(\n",
|
|||
|
|
" pagination_class=self.Pagination,\n",
|
|||
|
|
" serializer_class=self.OutputSerializer,\n",
|
|||
|
|
" queryset=users,\n",
|
|||
|
|
" request=request,\n",
|
|||
|
|
" view=self\n",
|
|||
|
|
" )"
|
|||
|
|
],
|
|||
|
|
"id": "7c855fe41f81d7b0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# API Walkthrough\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی به API نگاه میکنیم، چند تا چیز مشخص میشه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **FilterSerializer** داریم، که پارامترهای query رو هندل میکنه.\n",
|
|||
|
|
" اگه این کارو اینجا نکنیم، باید جای دیگه هندل بشه و خب،\n",
|
|||
|
|
" DRF serializers تو این کار عالی هستن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. بعد از serialize کردن، فیلترها رو میفرستیم به **user_list selector**.\n",
|
|||
|
|
" یعنی دیتا واقعا از اینجا فیلتر میشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"3. برای خروجی، از **get_paginated_response** استفاده میکنیم،\n",
|
|||
|
|
" تا یه پاسخ paginated (صفحهبندی شده) به کلاینت بدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" خلاصه:\n",
|
|||
|
|
"API نقش **واسطه** بین پارامترهای کاربر و منطق اصلی (selector) رو داره،\n",
|
|||
|
|
"نه اینکه خودش دیتا رو دستکاری کنه یا فیلترهای پیچیده بسازه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "307e1ab2f3730fcd"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"import django_filters\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.users.models import BaseUser\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class BaseUserFilter(django_filters.FilterSet):\n",
|
|||
|
|
" class Meta:\n",
|
|||
|
|
" model = BaseUser\n",
|
|||
|
|
" fields = ('id', 'email', 'is_admin')\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def user_list(*, filters=None):\n",
|
|||
|
|
" filters = filters or {}\n",
|
|||
|
|
"\n",
|
|||
|
|
" qs = BaseUser.objects.all()\n",
|
|||
|
|
"\n",
|
|||
|
|
" return BaseUserFilter(filters, qs).qs"
|
|||
|
|
],
|
|||
|
|
"id": "997a2cc32be54ba2"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Using django-filter\n",
|
|||
|
|
"\n",
|
|||
|
|
"همونطور که میبینی، ما داریم از **کتابخونه قدرتمند `django-filter`** استفاده میکنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" نکته اصلی:\n",
|
|||
|
|
"**selector مسئول فیلتر واقعی هستش**.\n",
|
|||
|
|
"میتونی همیشه از چیز دیگهای به عنوان abstraction برای فیلتر استفاده کنی،\n",
|
|||
|
|
"اما برای بیشتر موارد، `django-filter` کامل کافی و راحت هست.\n"
|
|||
|
|
],
|
|||
|
|
"id": "2fbd676f6c42fa9"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " get_paginated_response :",
|
|||
|
|
"id": "e5843d5c23bf4c94"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework.response import Response\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def get_paginated_response(*, pagination_class, serializer_class, queryset, request, view):\n",
|
|||
|
|
" paginator = pagination_class()\n",
|
|||
|
|
"\n",
|
|||
|
|
" page = paginator.paginate_queryset(queryset, request, view=view)\n",
|
|||
|
|
"\n",
|
|||
|
|
" if page is not None:\n",
|
|||
|
|
" serializer = serializer_class(page, many=True)\n",
|
|||
|
|
" return paginator.get_paginated_response(serializer.data)\n",
|
|||
|
|
"\n",
|
|||
|
|
" serializer = serializer_class(queryset, many=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data=serializer.data)"
|
|||
|
|
],
|
|||
|
|
"id": "e735db438d1a09ea"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "LimitOffsetPagination :",
|
|||
|
|
"id": "daae4b408a8d2df"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from collections import OrderedDict\n",
|
|||
|
|
"\n",
|
|||
|
|
"from rest_framework.pagination import LimitOffsetPagination as _LimitOffsetPagination\n",
|
|||
|
|
"from rest_framework.response import Response\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class LimitOffsetPagination(_LimitOffsetPagination):\n",
|
|||
|
|
" default_limit = 10\n",
|
|||
|
|
" max_limit = 50\n",
|
|||
|
|
"\n",
|
|||
|
|
" def get_paginated_data(self, data):\n",
|
|||
|
|
" return OrderedDict([\n",
|
|||
|
|
" ('limit', self.limit),\n",
|
|||
|
|
" ('offset', self.offset),\n",
|
|||
|
|
" ('count', self.count),\n",
|
|||
|
|
" ('next', self.get_next_link()),\n",
|
|||
|
|
" ('previous', self.get_previous_link()),\n",
|
|||
|
|
" ('results', data)\n",
|
|||
|
|
" ])\n",
|
|||
|
|
"\n",
|
|||
|
|
" def get_paginated_response(self, data):\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" We redefine this method in order to return `limit` and `offset`.\n",
|
|||
|
|
" This is used by the frontend to construct the pagination itself.\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" return Response(OrderedDict([\n",
|
|||
|
|
" ('limit', self.limit),\n",
|
|||
|
|
" ('offset', self.offset),\n",
|
|||
|
|
" ('count', self.count),\n",
|
|||
|
|
" ('next', self.get_next_link()),\n",
|
|||
|
|
" ('previous', self.get_previous_link()),\n",
|
|||
|
|
" ('results', data)\n",
|
|||
|
|
" ]))"
|
|||
|
|
],
|
|||
|
|
"id": "2f088f36fd6c1a25"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Reverse-Engineering Generic APIs\n",
|
|||
|
|
"\n",
|
|||
|
|
"در واقع کاری که کردیم اینه که **generic APIs رو reverse-engineer کردیم** و فهمیدیم چطور کار میکنن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" نکته مهم:\n",
|
|||
|
|
"اگر دنبال یه pagination متفاوت هستی، میتونی همیشه خودت پیادهسازی کنی و همون روش رو استفاده کنی.\n",
|
|||
|
|
"گاهی اوقات لازم میشه selector خودش pagination رو هندل کنه،\n",
|
|||
|
|
"و ما با همین روش filtering اون رو انجام میدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" برای مثال عملی:\n",
|
|||
|
|
"کد یک **List API با filters و pagination** رو میتونی توی پروژه **Styleguide Example** پیدا کنی.\n"
|
|||
|
|
],
|
|||
|
|
"id": "f7f562c9f6eed1b2"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "# Detail API",
|
|||
|
|
"id": "7f7dbf4ea0ef2fb5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class CourseDetailApi(SomeAuthenticationMixin, APIView):\n",
|
|||
|
|
" class OutputSerializer(serializers.Serializer):\n",
|
|||
|
|
" id = serializers.CharField()\n",
|
|||
|
|
" name = serializers.CharField()\n",
|
|||
|
|
" start_date = serializers.DateField()\n",
|
|||
|
|
" end_date = serializers.DateField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def get(self, request, course_id):\n",
|
|||
|
|
" course = course_get(id=course_id)\n",
|
|||
|
|
"\n",
|
|||
|
|
" serializer = self.OutputSerializer(course)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(serializer.data)"
|
|||
|
|
],
|
|||
|
|
"id": "2ef277dfbab647d"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "# Create API",
|
|||
|
|
"id": "302129ac4d79bb9b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class CourseCreateApi(SomeAuthenticationMixin, APIView):\n",
|
|||
|
|
" class InputSerializer(serializers.Serializer):\n",
|
|||
|
|
" name = serializers.CharField()\n",
|
|||
|
|
" start_date = serializers.DateField()\n",
|
|||
|
|
" end_date = serializers.DateField()\n",
|
|||
|
|
"\n",
|
|||
|
|
" def post(self, request):\n",
|
|||
|
|
" serializer = self.InputSerializer(data=request.data)\n",
|
|||
|
|
" serializer.is_valid(raise_exception=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" course_create(**serializer.validated_data)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(status=status.HTTP_201_CREATED)"
|
|||
|
|
],
|
|||
|
|
"id": "a1d089fc6aefa951"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "# Update API",
|
|||
|
|
"id": "c62cb632347346a0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class CourseUpdateApi(SomeAuthenticationMixin, APIView):\n",
|
|||
|
|
" class InputSerializer(serializers.Serializer):\n",
|
|||
|
|
" name = serializers.CharField(required=False)\n",
|
|||
|
|
" start_date = serializers.DateField(required=False)\n",
|
|||
|
|
" end_date = serializers.DateField(required=False)\n",
|
|||
|
|
"\n",
|
|||
|
|
" def post(self, request, course_id):\n",
|
|||
|
|
" serializer = self.InputSerializer(data=request.data)\n",
|
|||
|
|
" serializer.is_valid(raise_exception=True)\n",
|
|||
|
|
"\n",
|
|||
|
|
" course_update(course_id=course_id, **serializer.validated_data)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(status=status.HTTP_200_OK)"
|
|||
|
|
],
|
|||
|
|
"id": "13675cdd9219e1a9"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Fetching Objects\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی API ما یه `object_id` میگیره، سوال اینه: **کجا باید اون object رو fetch کنیم؟**\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## گزینهها\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. میتونیم اون object رو به **serializer** بدیم، که خودش یه `PrimaryKeyRelatedField` یا حتی `SlugRelatedField` داره.\n",
|
|||
|
|
"2. میتونیم یه جور object fetching توی **API** انجام بدیم و بعد object رو به **service یا selector** بدیم.\n",
|
|||
|
|
"3. میتونیم همون `id` رو به **service/selector** بدیم و fetch اونجا انجام بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" تصمیم اینکه کدوم روش رو استفاده کنیم، **بستگی به context پروژه و سلیقه تیم** داره.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## روشی که ما معمولا استفاده میکنیم\n",
|
|||
|
|
"\n",
|
|||
|
|
"معمولا objectها رو تو سطح **API** fetch میکنیم، با استفاده از یه util مخصوص به اسم `get_object`.\n"
|
|||
|
|
],
|
|||
|
|
"id": "81c67183d7f2c7f5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def get_object(model_or_queryset, **kwargs):\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" Reuse get_object_or_404 since the implementation supports both Model && queryset.\n",
|
|||
|
|
" Catch Http404 & return None\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" try:\n",
|
|||
|
|
" return get_object_or_404(model_or_queryset, **kwargs)\n",
|
|||
|
|
" except Http404:\n",
|
|||
|
|
" return None"
|
|||
|
|
],
|
|||
|
|
"id": "6a1bfe827e5afc3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " این یک یوتیلیتی ساده است که استثنا رو هندل میکنه فقط حواست باشه که جوابا یک دست باشه",
|
|||
|
|
"id": "57f45ca644fbac86"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "# Nested serializers",
|
|||
|
|
"id": "89cceae953a1532c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class Serializer(serializers.Serializer):\n",
|
|||
|
|
" weeks = inline_serializer(many=True, fields={\n",
|
|||
|
|
" 'id': serializers.IntegerField(),\n",
|
|||
|
|
" 'number': serializers.IntegerField(),\n",
|
|||
|
|
" })"
|
|||
|
|
],
|
|||
|
|
"id": "a8fe5e4ad714459a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Advanced Serialization\n",
|
|||
|
|
"\n",
|
|||
|
|
"گاهی اوقات، نتیجه نهایی یک API میتونه خیلی پیچیده باشه.\n",
|
|||
|
|
"گاهی وقتا میخواهیم **queryها رو بهینه کنیم** و خود این بهینهسازیها میتونه خیلی پیچیده بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"**اگر فقط بخواهیم از یه OutputSerializer استفاده کنیم، ممکنه انتخابها و گزینههایمون محدود بشه.**\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"در این مواقع، میتونیم **serialization خروجی رو به صورت یک تابع پیادهسازی کنیم** و تمام بهینهسازیهایی که نیاز داریم رو اونجا انجام بدیم، به جای اینکه همهی بهینهسازیها رو توی selector انجام بدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n"
|
|||
|
|
],
|
|||
|
|
"id": "5b00ef8732d49c97"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class SomeGenericFeedApi(BaseApi):\n",
|
|||
|
|
" def get(self, request):\n",
|
|||
|
|
" feed = some_feed_get(\n",
|
|||
|
|
" user=request.user,\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" data = some_feed_serialize(feed)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return Response(data)"
|
|||
|
|
],
|
|||
|
|
"id": "a19c6a1f49270cee"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## Example: Advanced Serialization Function\n",
|
|||
|
|
"\n",
|
|||
|
|
"در این سناریو، `some_feed_get` مسئول بازگرداندن یک لیست از آیتمهای feed هستش (ممکنه ORM objectها باشن، ممکنه فقط IDها باشن، یا هر چی که برای شما مناسب باشه).\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"ما میخواهیم پیچیدگی **serialization** این feed رو، به روشی بهینه، به تابع serializer بسپاریم - یعنی `some_feed_serialize`.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"این به این معنیه که دیگه نیازی نیست که **پیشفرضها (prefetch)** و بهینهسازیهای غیرضروری رو توی `some_feed_get` انجام بدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n"
|
|||
|
|
],
|
|||
|
|
"id": "40d0e10f57d465bc"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class FeedItemSerializer(serializers.Serializer):\n",
|
|||
|
|
" ... some fields here ...\n",
|
|||
|
|
" calculated_field = serializers.IntegerField(source=\"_calculated_field\")\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_feed_serialize(feed: List[FeedItem]):\n",
|
|||
|
|
" feed_ids = [feed_item.id for feed_item in feed]\n",
|
|||
|
|
"\n",
|
|||
|
|
" # Refetch items with more optimizations\n",
|
|||
|
|
" # Based on the relations that are going in\n",
|
|||
|
|
" objects = FeedItem.objects.select_related(\n",
|
|||
|
|
" # ... as complex as you want ...\n",
|
|||
|
|
" ).prefetch_related(\n",
|
|||
|
|
" # ... as complex as you want ...\n",
|
|||
|
|
" ).filter(\n",
|
|||
|
|
" id__in=feed_ids\n",
|
|||
|
|
" ).order_by(\n",
|
|||
|
|
" \"-some_timestamp\"\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" some_cache = get_some_cache(feed_ids)\n",
|
|||
|
|
"\n",
|
|||
|
|
" result = []\n",
|
|||
|
|
"\n",
|
|||
|
|
" for feed_item in objects:\n",
|
|||
|
|
" # An example, adding additional fields for the serializer\n",
|
|||
|
|
" # That are based on values outside of our current object\n",
|
|||
|
|
" # This may be some optimization to save queries\n",
|
|||
|
|
" feed_item._calculated_field = some_cache.get(feed_item.id)\n",
|
|||
|
|
"\n",
|
|||
|
|
" result.append(FeedItemSerializer(feed_item).data)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return result"
|
|||
|
|
],
|
|||
|
|
"id": "40921757eeb3e20d"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"همونطور که میبینید، این یه مثال خیلی **همگانی** هستش، ولی ایده خیلی سادهست:\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"- دادههاتون رو دوباره با **joins** و **prefetch** لازم میگیرید.\n",
|
|||
|
|
"- کشهای درونحافظهای (in-memory caches) رو میسازید که برای مقادیر محاسبه شده، تعداد queryها رو کاهش میده.\n",
|
|||
|
|
"- در نهایت یه **نتیجهای که آمادهی پاسخدهی API باشه** برمیگردونید.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"حتی با اینکه به این روش اسم \"**advanced serialization**\" داده شده، این الگو خیلی قدرتمنده و میتونید برای همهی انواع serialization ها ازش استفاده کنید.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"این تابعهای serializer معمولاً در **ماژول serializers.py** در اپ مربوطهی **Django** قرار میگیرن.\n"
|
|||
|
|
],
|
|||
|
|
"id": "23edc698dc71ffbc"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def user_list(*, fetched_by: User) -> Iterable[User]:\n",
|
|||
|
|
" user_ids = user_get_visible_for(user=fetched_by)\n",
|
|||
|
|
"\n",
|
|||
|
|
" query = Q(id__in=user_ids)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return User.objects.filter(query)"
|
|||
|
|
],
|
|||
|
|
"id": "2f05efa0947ede97"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# URLs\n",
|
|||
|
|
"\n",
|
|||
|
|
"ما معمولاً **URLها رو دقیقاً مثل APIها** سازماندهی میکنیم — یعنی **هر API یک URL مخصوص خودش داره**.\n",
|
|||
|
|
"به زبون سادهتر:\n",
|
|||
|
|
"> هر \"action\" توی سیستم، یه مسیر (route) جداگانه داره.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## نکتهی مهم\n",
|
|||
|
|
"بهتره URLهای مربوط به هر دامین (domain یا بخش از پروژه) رو داخل یه لیست جدا مثل `domain_patterns` بنویسیم\n",
|
|||
|
|
"و بعد با `include()` از `urlpatterns` اصلی فراخوانی کنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n"
|
|||
|
|
],
|
|||
|
|
"id": "95a62e4d7f565154"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.urls import path, include\n",
|
|||
|
|
"\n",
|
|||
|
|
"from project.education.apis import (\n",
|
|||
|
|
" CourseCreateApi,\n",
|
|||
|
|
" CourseUpdateApi,\n",
|
|||
|
|
" CourseListApi,\n",
|
|||
|
|
" CourseDetailApi,\n",
|
|||
|
|
" CourseSpecificActionApi,\n",
|
|||
|
|
")\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"course_patterns = [\n",
|
|||
|
|
" path('', CourseListApi.as_view(), name='list'),\n",
|
|||
|
|
" path('<int:course_id>/', CourseDetailApi.as_view(), name='detail'),\n",
|
|||
|
|
" path('create/', CourseCreateApi.as_view(), name='create'),\n",
|
|||
|
|
" path('<int:course_id>/update/', CourseUpdateApi.as_view(), name='update'),\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" '<int:course_id>/specific-action/',\n",
|
|||
|
|
" CourseSpecificActionApi.as_view(),\n",
|
|||
|
|
" name='specific-action'\n",
|
|||
|
|
" ),\n",
|
|||
|
|
"]\n",
|
|||
|
|
"\n",
|
|||
|
|
"urlpatterns = [\n",
|
|||
|
|
" path('courses/', include((course_patterns, 'courses'))),\n",
|
|||
|
|
"]"
|
|||
|
|
],
|
|||
|
|
"id": "45b1d0ffeadb4a80"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Splitting URLs\n",
|
|||
|
|
"\n",
|
|||
|
|
"تقسیم کردن URLها به این شکل، بهت **انعطاف بیشتری** میده.\n",
|
|||
|
|
"بهخصوص توی پروژههای بزرگ که فایل `urls.py` شلوغ میشه و حتی ممکنه دائم **conflict موقع merge** پیش بیاد.\n",
|
|||
|
|
"با جدا کردن هر domain به یه ماژول مستقل، میتونی راحتتر کنترلش کنی یا حتی بعداً جابهجاش کنی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## دیدن کل درخت URL\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگه دوست داری **ساختار کامل درخت URL** پروژه رو یکجا ببینی،\n",
|
|||
|
|
"کافیه متغیرهای جداگانه برای `include()` تعریف نکنی و مستقیماً اونها رو بنویسی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n"
|
|||
|
|
],
|
|||
|
|
"id": "8b910c7f578207ea"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.urls import path, include\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.files.apis import (\n",
|
|||
|
|
" FileDirectUploadApi,\n",
|
|||
|
|
"\n",
|
|||
|
|
" FilePassThruUploadStartApi,\n",
|
|||
|
|
" FilePassThruUploadFinishApi,\n",
|
|||
|
|
" FilePassThruUploadLocalApi,\n",
|
|||
|
|
")\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"urlpatterns = [\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"upload/\",\n",
|
|||
|
|
" include(([\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"direct/\",\n",
|
|||
|
|
" FileDirectUploadApi.as_view(),\n",
|
|||
|
|
" name=\"direct\"\n",
|
|||
|
|
" ),\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"pass-thru/\",\n",
|
|||
|
|
" include(([\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"start/\",\n",
|
|||
|
|
" FilePassThruUploadStartApi.as_view(),\n",
|
|||
|
|
" name=\"start\"\n",
|
|||
|
|
" ),\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"finish/\",\n",
|
|||
|
|
" FilePassThruUploadFinishApi.as_view(),\n",
|
|||
|
|
" name=\"finish\"\n",
|
|||
|
|
" ),\n",
|
|||
|
|
" path(\n",
|
|||
|
|
" \"local/<str:file_id>/\",\n",
|
|||
|
|
" FilePassThruUploadLocalApi.as_view(),\n",
|
|||
|
|
" name=\"local\"\n",
|
|||
|
|
" )\n",
|
|||
|
|
" ], \"pass-thru\"))\n",
|
|||
|
|
" )\n",
|
|||
|
|
" ], \"upload\"))\n",
|
|||
|
|
" )\n",
|
|||
|
|
"]"
|
|||
|
|
],
|
|||
|
|
"id": "73c1f15760ae7ea8"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# ️ Settings Structure\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی صحبت از تنظیمات (Settings) جنگو میشه، ما معمولاً از ساختار پوشهای **cookiecutter-django** پیروی میکنیم،\n",
|
|||
|
|
"با چند تغییر کوچک که کار رو تمیزتر میکنه\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## اصول کلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"- تنظیمات خاص جنگو رو از بقیه تنظیمات جدا میکنیم.\n",
|
|||
|
|
"- همهچیز باید در `base.py` تعریف و **در اون فایل import** بشه.\n",
|
|||
|
|
"- فایل `production.py` نباید تنظیمات خاص خودش رو داشته باشه.\n",
|
|||
|
|
"- هرچیزی که فقط باید تو محیط production فعال باشه،\n",
|
|||
|
|
" باید **با environment variable** کنترل بشه، نه با شرط مستقیم داخل کد.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## ساختار پیشنهادی پوشهها\n",
|
|||
|
|
"\n",
|
|||
|
|
"یه نمونه از ساختار پروژهی ما (برگرفته از *Styleguide Example*):\n",
|
|||
|
|
"\n",
|
|||
|
|
"```bash\n",
|
|||
|
|
"config\n",
|
|||
|
|
"├── __init__.py\n",
|
|||
|
|
"├── django\n",
|
|||
|
|
"│ ├── __init__.py\n",
|
|||
|
|
"│ ├── base.py\n",
|
|||
|
|
"│ ├── local.py\n",
|
|||
|
|
"│ ├── production.py\n",
|
|||
|
|
"│ └── test.py\n",
|
|||
|
|
"├── settings\n",
|
|||
|
|
"│ ├── __init__.py\n",
|
|||
|
|
"│ ├── celery.py\n",
|
|||
|
|
"│ ├── cors.py\n",
|
|||
|
|
"│ ├── sentry.py\n",
|
|||
|
|
"│ └── sessions.py\n",
|
|||
|
|
"├── urls.py\n",
|
|||
|
|
"├── env.py\n",
|
|||
|
|
"└── wsgi.py\n",
|
|||
|
|
"├── asgi.py"
|
|||
|
|
],
|
|||
|
|
"id": "215f2485c32a69ca"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Django Settings Layout\n",
|
|||
|
|
"\n",
|
|||
|
|
"وقتی ساختار تنظیمات رو طراحی میکنیم، هدف اینه که **چیزهای مربوط به جنگو** از **بقیه تنظیمات پروژه** جدا باشن.\n",
|
|||
|
|
"ما معمولاً این تفکیک رو به دو بخش انجام میدیم:\n",
|
|||
|
|
"`config/django` برای تنظیمات خود جنگو و `config/settings` برای تنظیمات جانبی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## داخل `config/django`\n",
|
|||
|
|
"\n",
|
|||
|
|
"اینجا همهچیز مستقیماً به جنگو مربوطه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **`base.py`**\n",
|
|||
|
|
" شامل بیشتر تنظیماته و بقیه فایلها ازش import میگیرن.\n",
|
|||
|
|
" (قلب تنظیمات اصلی پروژهست )\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **`production.py`**\n",
|
|||
|
|
" از `base.py` import میکنه و فقط مقادیر خاص محیط production رو **بازنویسی (override)** میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **`test.py`**\n",
|
|||
|
|
" از `base.py` import میکنه و تنظیمات مخصوص تستها رو تغییر میده.\n",
|
|||
|
|
" این فایل باید به عنوان ماژول تنظیمات توی `pytest.ini` ست بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **`local.py`**\n",
|
|||
|
|
" از `base.py` import میکنه و برای توسعهی لوکال استفاده میشه.\n",
|
|||
|
|
" اگه خواستی ازش استفاده کنی، باید توی `manage.py` بهش اشاره کنی،\n",
|
|||
|
|
" وگرنه همون `base.py` کفایت میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## داخل `config/settings`\n",
|
|||
|
|
"\n",
|
|||
|
|
"اینجا تنظیمات **غیرمستقیم جنگویی** قرار میگیرن — مثل:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- تنظیمات **Celery**\n",
|
|||
|
|
"- تنظیمات **third-party packages**\n",
|
|||
|
|
"- هر نوع تنظیم عمومی دیگهای که به خود جنگو وابسته نیست\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" **نتیجه:**\n",
|
|||
|
|
"با این ساختار، یه تفکیک تمیز بین هستهی جنگو و تنظیمات خارجی داری.\n",
|
|||
|
|
"یعنی هر بخش دقیقاً میدونه جاش کجاست و چی رو باید کنترل کنه\n"
|
|||
|
|
],
|
|||
|
|
"id": "d5cd6a887170fd0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " به علاوه ما از `cogig/env.py`",
|
|||
|
|
"id": "ea4618b8cf736e41"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"import environ\n",
|
|||
|
|
"\n",
|
|||
|
|
"env = environ.Env()"
|
|||
|
|
],
|
|||
|
|
"id": "eaa69de4400fe7cd"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " و اگر ما از محیط بخواهیم چیزی دریافت کنیم",
|
|||
|
|
"id": "831eed74d468d413"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": "from config.env import env",
|
|||
|
|
"id": "8c49910f9c5639ad"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"معمولاً در انتهای فایل **`base.py`**،\n",
|
|||
|
|
"تمام تنظیمات دیگه رو از مسیر **`config/settings`** ایمپورت میکنیم.\n",
|
|||
|
|
"اینجوری همهی کانفیگهای جانبی (مثل Celery یا تنظیمات تردپارتیها)\n",
|
|||
|
|
"بهصورت خودکار لود میشن و توی محیط جنگو قابل استفادهان."
|
|||
|
|
],
|
|||
|
|
"id": "31306ba2b26f769c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from config.settings.cors import * # noqa\n",
|
|||
|
|
"from config.settings.sessions import * # noqa\n",
|
|||
|
|
"from config.settings.celery import * # noqa\n",
|
|||
|
|
"from config.settings.sentry import * # noqa"
|
|||
|
|
],
|
|||
|
|
"id": "f743350624d75c39"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Prefix کردن Environment Variables با DJANGO_\n",
|
|||
|
|
"\n",
|
|||
|
|
"شاید توی خیلی مثالها دیده باشی که Environment Variables رو با **DJANGO_** شروع میکنن.\n",
|
|||
|
|
"این کار مخصوصاً وقتی مفیده که چند تا برنامهی دیگه هم روی همون محیط اجرا میشن.\n",
|
|||
|
|
"با این prefix، راحت میتونی تشخیص بدی کدوم متغیر برای جنگوئه و کدوم برای بقیه برنامهها.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"تو **HackSoft** معمولاً چند تا اپ روی یه محیط نداریم،\n",
|
|||
|
|
"پس ما معمولاً فقط متغیرهای خاص جنگو رو با **DJANGO_** شروع میکنیم،\n",
|
|||
|
|
"و بقیه چیزها مثل AWS یا Celery بدون prefix میمونن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"مثال:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- Prefix میکنیم:\n",
|
|||
|
|
" `DJANGO_SETTINGS_MODULE`, `DJANGO_DEBUG`, `DJANGO_ALLOWED_HOSTS`, `DJANGO_CORS_ORIGIN_WHITELIST`\n",
|
|||
|
|
"- Prefix نمیکنیم:\n",
|
|||
|
|
" `AWS_SECRET_KEY`, `CELERY_BROKER_URL`, `EMAILS_ENABLED`\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"⚠️ این کاملاً سلیقهایه، ولی **یکسان بودنش تو پروژه مهمه**.\n",
|
|||
|
|
"یک قانون مشخص داشته باش و همه جا رعایتش کن.\n"
|
|||
|
|
],
|
|||
|
|
"id": "16b9c1a5440f5f00"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Integrations\n",
|
|||
|
|
"\n",
|
|||
|
|
"خب، از اونجایی که همه چیز باید توی **base.py** ایمپورت بشه،\n",
|
|||
|
|
"ولی بعضی وقتها نمیخوایم یه integration خاص رو برای توسعه لوکال کانفیگ کنیم،\n",
|
|||
|
|
"ما این روش رو گرفتیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. تنظیمات مربوط به هر integration رو میذاریم توی\n",
|
|||
|
|
" `config/settings/some_integration.py`\n",
|
|||
|
|
"2. همیشه یه boolean داریم به اسم **`USE_SOME_INTEGRATION`**\n",
|
|||
|
|
" که از environment میخونه و پیشفرضش **False** هست.\n",
|
|||
|
|
"3. اگه مقدارش **True** بود، بقیهی تنظیمات رو هم میخونه و اگه چیزی کم بود، خطا میده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" خلاصهش اینکه:\n",
|
|||
|
|
"- لوکال راحت باشه، همه چیز default باشه.\n",
|
|||
|
|
"- وقتی بخوای production یا staging یه integration فعال کنی، همه چیز آماده باشه و خطاها واضح باشن.\n"
|
|||
|
|
],
|
|||
|
|
"id": "ea341bb03febfa6"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from config.env import env\n",
|
|||
|
|
"\n",
|
|||
|
|
"SENTRY_DSN = env('SENTRY_DSN', default='')\n",
|
|||
|
|
"\n",
|
|||
|
|
"if SENTRY_DSN:\n",
|
|||
|
|
" import sentry_sdk\n",
|
|||
|
|
" from sentry_sdk.integrations.django import DjangoIntegration\n",
|
|||
|
|
" from sentry_sdk.integrations.celery import CeleryIntegration\n",
|
|||
|
|
"\n",
|
|||
|
|
" # ... we proceed with sentry settings here ...\n",
|
|||
|
|
" # View the full file here - https://github.com/HackSoftware/Styleguide-Example/blob/master/config/settings/sentry.py"
|
|||
|
|
],
|
|||
|
|
"id": "5475466b1bb09713"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# .env & Environment Variables\n",
|
|||
|
|
"\n",
|
|||
|
|
"حالا میتونی یه فایل **`.env`** توی روت پروژه داشته باشی\n",
|
|||
|
|
"و مقادیر تنظیماتت رو اونجا بذاری (ولی اجباری نیست).\n",
|
|||
|
|
"\n",
|
|||
|
|
"چند نکته مهم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **هیچوقت `.env` رو توی source control نذار**\n",
|
|||
|
|
" چون اطلاعات حساس و credentials میره بیرون\n",
|
|||
|
|
"2. بهتره یه **`.env.example`** داشته باشی\n",
|
|||
|
|
" با مقادیر خالی برای همه چیز، تا توسعهدهندههای جدید بفهمن چی استفاده میشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"# Errors & Exception Handling\n",
|
|||
|
|
"\n",
|
|||
|
|
"اوه پسر، این موضوع بزرگه\n",
|
|||
|
|
"جزئیاتش معمولاً مخصوص هر پروژه هست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"به همین دلیل ما دو بخش میکنیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. **Guidelines عمومی**\n",
|
|||
|
|
" - بدون که exception handling چطور کار میکنه (با DRF مثال میزنیم)\n",
|
|||
|
|
" - مشخص کن که خطاهای API قراره چطوری به کاربر نشون داده بشن\n",
|
|||
|
|
" - بدون چطور میتونی رفتار پیشفرض exception handling رو تغییر بدی\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. **روشهای خاص**\n",
|
|||
|
|
" - از default exceptions خود DRF استفاده کن، با کمترین تغییر\n",
|
|||
|
|
" - روش پیشنهادی HackSoft\n",
|
|||
|
|
" - اگر دنبال یه استاندارد جهانی هستی، RFC7807 رو نگاه کن:\n",
|
|||
|
|
" [https://datatracker.ietf.org/doc/html/rfc7807](https://datatracker.ietf.org/doc/html/rfc7807)\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"👀 برای کد عملی، میتونی به این لینک نگاه کنی:\n",
|
|||
|
|
"[Styleguide-Example exception handlers](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/exception_handlers.py)\n"
|
|||
|
|
],
|
|||
|
|
"id": "86f17356585f990a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# How Exception Handling Works (DRF Style)\n",
|
|||
|
|
"\n",
|
|||
|
|
"خب پسر، DRF یه **راهنمای خیلی خوب** داره برای اینکه بفهمیم exceptionها چطور هندل میشن:\n",
|
|||
|
|
"[DRF Exceptions Guide](https://www.django-rest-framework.org/api-guide/exceptions/)\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"علاوه بر راهنما، یه **diagram مرتب** داریم که روند کار رو نشون میده:\n",
|
|||
|
|
"(میتونی اینو توی notebook با تصویر یا SVG بذاری)\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
" نکته: ایده اینه که اول بفهمیم جریان exceptionها چطوریه،\n",
|
|||
|
|
"بعد تازه تصمیم بگیریم چه تغییراتی لازمه برای پروژه خودمون.\n"
|
|||
|
|
],
|
|||
|
|
"id": "cc6b0fa3336db0dc"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "",
|
|||
|
|
"id": "61aab097368adbbf",
|
|||
|
|
"attachments": {
|
|||
|
|
"0538973d-4fb2-44e8-968e-444832f70ac6.png": {
|
|||
|
|
"image/png": "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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"خب، اگر **exception handler نتونه exception مورد نظر رو هندل کنه** و `None` برگردونه،\n",
|
|||
|
|
"این باعث میشه که یه **Unhandled Exception** رخ بده و نتیجهش **500 Server Error** باشه 😬\n",
|
|||
|
|
"\n",
|
|||
|
|
"این معمولاً خوبه، چون حداقل **خطاها رو سایلنس نمیکنه** و مجبور میشی بهشون توجه کنی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"حالا، چند **quirk و نکته ریز** هست که باید مراقبش باشیم:"
|
|||
|
|
],
|
|||
|
|
"id": "fa8d6844a3db7151"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# DRF's ValidationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"مثلاً، اگر ساده یه `rest_framework.exceptions.ValidationError` پر کنیم مثل این:\n",
|
|||
|
|
"\n",
|
|||
|
|
"```python\n",
|
|||
|
|
"from rest_framework.exceptions import ValidationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"raise ValidationError(\"این یک خطای ولیدیشن ساده است\")\n"
|
|||
|
|
],
|
|||
|
|
"id": "74d9605d847ad80d"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise exceptions.ValidationError(\"Some message\")"
|
|||
|
|
],
|
|||
|
|
"id": "e38176cab9be0570"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " محتوای پاسخ قراره یه چی شبیه به این بشه",
|
|||
|
|
"id": "ee1de05a74d35bf"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": "[\"Some message\"]",
|
|||
|
|
"id": "26c1681f0a63cfe2"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " و اگه اینجوری بنویسیمش",
|
|||
|
|
"id": "929e80c337cd090d"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise exceptions.ValidationError({\"error\": \"Some message\"})"
|
|||
|
|
],
|
|||
|
|
"id": "94adc2896853431"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جوابش قراره این شکلی باشه",
|
|||
|
|
"id": "736cefa52474d89"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"error\": \"Some message\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "35a1bb24fe817d9c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
" این همون `detail` هست که تحویل `validation error` دادیم ولی با نوع داده متفاوت\n",
|
|||
|
|
"\n",
|
|||
|
|
"و حالا یخوایم از استثناعات دیکه استقاده کنیم مثل نمونه های داخل خود DRF"
|
|||
|
|
],
|
|||
|
|
"id": "d0102e7e20c2bdcc"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise exceptions.NotFound()"
|
|||
|
|
],
|
|||
|
|
"id": "f2e3745c32e37d9e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " نمونه جواب قراره شبیه به این بشه",
|
|||
|
|
"id": "f5236e5c8fb631af"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": \"Not found.\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "9def3c6d953bf4d7"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"این کاملا با چیزی که از رفتار ValidationError دیدیم فرق داره و میتونه مشکلاتی ایجاد کنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"تا الان، رفتار پیشفرض DRF میتونه برامون تولید کنه:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- یه آرایه.\n",
|
|||
|
|
"- یه دیکشنری.\n",
|
|||
|
|
"- یه نتیجه خاص به شکل {\"detail\": \"something\"}.\n",
|
|||
|
|
"\n",
|
|||
|
|
"پس اگه بخوایم از رفتار پیشفرض DRF استفاده کنیم، باید حواسمون به این ناسازگاری باشه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "b017d18fbb868baa"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"### ValidationError خود Django\n",
|
|||
|
|
"\n",
|
|||
|
|
"حالا، رفتار پیشفرض DRF با ValidationError خود Django زیاد خوشاخلاق نیست.\n",
|
|||
|
|
"\n",
|
|||
|
|
"این قطعه کد:\n"
|
|||
|
|
],
|
|||
|
|
"id": "94964a1331d33618"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import ValidationError as DjangoValidationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise DjangoValidationError(\"Some error message\")"
|
|||
|
|
],
|
|||
|
|
"id": "ae173904c412798e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"این باعث میشه یه استثنا بدون هندل شدن رخ بده و یه خطای ۵۰۰ سرور بدیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"همین اتفاق وقتی میافته که این ValidationError از اعتبارسنجی مدل اومده باشه، مثلا:\n"
|
|||
|
|
],
|
|||
|
|
"id": "7cf5e261f31bc6e8"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" user = BaseUser()\n",
|
|||
|
|
" user.full_clean() # Throws ValidationError\n",
|
|||
|
|
" user.save()"
|
|||
|
|
],
|
|||
|
|
"id": "8af6ba3e918c32ff"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"این هم باعث میشه ۵۰۰ سرور بده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگه بخوایم شروع کنیم به هندل کردنش، مثل rest_framework.exceptions.ValidationError، باید یه exception handler سفارشی خودمون بسازیم:\n"
|
|||
|
|
],
|
|||
|
|
"id": "89d3e0ef3c12da21"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import ValidationError as DjangoValidationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"from rest_framework.views import exception_handler\n",
|
|||
|
|
"from rest_framework.serializers import as_serializer_error\n",
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def custom_exception_handler(exc, ctx):\n",
|
|||
|
|
" if isinstance(exc, DjangoValidationError):\n",
|
|||
|
|
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
|
|||
|
|
"\n",
|
|||
|
|
" response = exception_handler(exc, ctx)\n",
|
|||
|
|
"\n",
|
|||
|
|
" # If unexpected error occurs (server error, etc.)\n",
|
|||
|
|
" if response is None:\n",
|
|||
|
|
" return response\n",
|
|||
|
|
"\n",
|
|||
|
|
" return response"
|
|||
|
|
],
|
|||
|
|
"id": "62985ff85e2cf595"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": "این عملاً پیادهسازی پیشفرضه، با اضافه شدن این قطعه کد:\n",
|
|||
|
|
"id": "fa6a43b1aca6593a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"if isinstance(exc, DjangoValidationError):\n",
|
|||
|
|
" exc = exceptions.ValidationError(as_serializer_error(exc))"
|
|||
|
|
],
|
|||
|
|
"id": "5711a2bf024527d3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"چون ما باید بین `django.core.exceptions.ValidationError` و `rest_framework.exceptions.ValidationError` نگاشت انجام بدیم، داریم از `as_serializer_error` خود DRF استفاده میکنیم، که توی serializerها هم داخلی استفاده میشه، فقط برای همین کار.\n",
|
|||
|
|
"\n",
|
|||
|
|
"با این کار، حالا میتونیم ValidationError خود Django رو خوشرفتار کنیم و با exception handler خود DRF کار کنه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "b24224f93d41ed23"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# توصیف کنید که خطاهای API شما قراره چه شکلی باشن.\n",
|
|||
|
|
"این خیلی مهمه و بهتره هر چه زودتر توی پروژه مشخص بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"به زبان ساده یعنی: با هم توافق کنیم که اینترفیس خطاهای APIمون چه شکلیه – وقتی خطایی پیش اومد، خروجی API چه شکلی باشه؟\n",
|
|||
|
|
"\n",
|
|||
|
|
"این خیلی وابسته به پروژه است، میتونید از APIهای معروف برای الهام گرفتن استفاده کنید:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- Stripe - [Docs](https://stripe.com/docs/api/errors)\n",
|
|||
|
|
"\n",
|
|||
|
|
"مثلا میتونیم تصمیم بگیریم که خطاهای ما اینطوری باشن:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- کد وضعیت 4** و 5** برای انواع مختلف خطاها.\n",
|
|||
|
|
"- هر خطا یه دیکشنری باشه با یه کلید `message` که پیام خطا داخلش باشه.\n"
|
|||
|
|
],
|
|||
|
|
"id": "12ba78fec9982631"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Some error message here\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "70e26bf54bdb10bc"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"خیلی سادهست:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- 400 برای خطاهای اعتبارسنجی (validation errors)\n",
|
|||
|
|
"- 401 برای خطاهای احراز هویت (auth errors)\n",
|
|||
|
|
"- 403 برای خطاهای دسترسی (permission errors)\n",
|
|||
|
|
"- 404 برای خطاهای پیدا نشدن (not found errors)\n",
|
|||
|
|
"- 429 برای خطاهای محدودسازی درخواستها (throttling errors)\n",
|
|||
|
|
"- 500 برای خطاهای سرور (server errors) – اینو باید مواظب باشیم که هیچ استثنایی که باعث 500 میشه رو ساکت نکنیم و همیشه گزارش بدیم، مثلا تو سرویسهایی مثل Sentry\n",
|
|||
|
|
"\n",
|
|||
|
|
"باز هم میگم، این بستگی به پروژه داره و شما میتونید تغییرش بدید. برای یکی از رویکردهای خاص، یه پیشنهاد مشابه هم ارائه میکنیم.\n"
|
|||
|
|
],
|
|||
|
|
"id": "26e4435c50b87b73"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# تغییر رفتار پیشفرض مدیریت خطاها\n",
|
|||
|
|
"\n",
|
|||
|
|
"خب ، این یه بخش مهمه: وقتی که تصمیم میگیرید خطاهای API شما چه شکلی باشن، باید بتونید رفتار پیشفرض مدیریت خطاها رو تغییر بدید.\n",
|
|||
|
|
"\n",
|
|||
|
|
"تو مثال قبل که درباره **ValidationError در Django** صحبت کردیم، یه نمونهی custom exception handler گذاشتیم که دقیقاً همین کارو میکنه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"همچنین، تو بخشهای بعدی چند مثال دیگه هم داریم که میتونید ببینید چطوری میشه خطاها رو مدیریت کرد.\n"
|
|||
|
|
],
|
|||
|
|
"id": "4783d4ae3b78dc4b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Approach 1: استفاده از استثناهای پیشفرض DRF با تغییرات کم\n",
|
|||
|
|
"\n",
|
|||
|
|
" DRF خودش مدیریت خطاهاش خوبه. ولی یه مشکلی هست: خروجی همیشه consistent نیست.\n",
|
|||
|
|
"پس ما یه سری تغییرات کوچیک میزنیم تا همه چیز یکدست بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"هدفمون اینه که خطاها همیشه یه شکلی شبیه به این باشن:\n"
|
|||
|
|
],
|
|||
|
|
"id": "81e4e5b0007f52cd"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": \"Some error\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "ba94876d894cb827"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " یا",
|
|||
|
|
"id": "2249263e32b295f4"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": [\"Some error\", \"Another error\"]\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "89d17de795e5abb8"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " یا",
|
|||
|
|
"id": "bc9ea365a5ad64c3"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": { \"key\": \"... some arbitrary nested structure ...\" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "edbdc83c654e913f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
" به هرحال مطمعن بشو که ما یک دیکچنری با کلید `detail` داریم\n",
|
|||
|
|
"به علاوه اون ما میخوایم دجنگو `validation error` رو هم حل بکنیم\n",
|
|||
|
|
"برا حل اون مثال کاستوم ما باید این شکلی باشه"
|
|||
|
|
],
|
|||
|
|
"id": "fa0278135c5c78f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\n",
|
|||
|
|
"from django.http import Http404\n",
|
|||
|
|
"\n",
|
|||
|
|
"from rest_framework.views import exception_handler\n",
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"from rest_framework.serializers import as_serializer_error\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def drf_default_with_modifications_exception_handler(exc, ctx):\n",
|
|||
|
|
" if isinstance(exc, DjangoValidationError):\n",
|
|||
|
|
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc, Http404):\n",
|
|||
|
|
" exc = exceptions.NotFound()\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc, PermissionDenied):\n",
|
|||
|
|
" exc = exceptions.PermissionDenied()\n",
|
|||
|
|
"\n",
|
|||
|
|
" response = exception_handler(exc, ctx)\n",
|
|||
|
|
"\n",
|
|||
|
|
" # If unexpected error occurs (server error, etc.)\n",
|
|||
|
|
" if response is None:\n",
|
|||
|
|
" return response\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc.detail, (list, dict)):\n",
|
|||
|
|
" response.data = {\n",
|
|||
|
|
" \"detail\": response.data\n",
|
|||
|
|
" }\n",
|
|||
|
|
"\n",
|
|||
|
|
" return response"
|
|||
|
|
],
|
|||
|
|
"id": "5c2d3bb7cde30bd1"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"خب، ما عملاً یه چیزی شبیه همون exception handler اصلی DRF رو بازسازی میکنیم،\n",
|
|||
|
|
"تا بعدش بتونیم با APIException درست کار کنیم (مثلاً دنبال `detail` باشیم).\n",
|
|||
|
|
"\n",
|
|||
|
|
"حالا بزنیم یه سری تستها رو اجرا کنیم:\n"
|
|||
|
|
],
|
|||
|
|
"id": "acb0559469a24997"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise DjangoValidationError(\"Some error message\")"
|
|||
|
|
],
|
|||
|
|
"id": "a919a07d7ae367ab"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب",
|
|||
|
|
"id": "9cfc71df3ec7715b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": {\n",
|
|||
|
|
" \"non_field_errors\": [\"Some error message\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "ec89bc4639bac76a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "f227bc5464940514"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import PermissionDenied\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise PermissionDenied()"
|
|||
|
|
],
|
|||
|
|
"id": "1c7c107c73c29f86"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب:",
|
|||
|
|
"id": "274a6489eb0727fa"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": \"You do not have permission to perform this action.\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "3a94f4871bb34"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "47b8822c1b55164f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.http import Http404\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise Http404()"
|
|||
|
|
],
|
|||
|
|
"id": "c4062fca5646c5d0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "38d0de7c93edaf10"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": \"Not found.\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "757df861f03caa79"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "fa809e12cd4eed71"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise RestValidationError(\"Some error message\")"
|
|||
|
|
],
|
|||
|
|
"id": "2e538c011629ab88"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "bd409ed104b9c15a"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": [\"Some error message\"]\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "83bd5a0aee82662c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "80b01be1ae215e5c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise RestValidationError(detail={\"error\": \"Some error message\"})"
|
|||
|
|
],
|
|||
|
|
"id": "6b835d50db6bc099"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "38d71423cc829577"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": {\n",
|
|||
|
|
" \"error\": \"Some error message\"\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "2a1056196d4cd527"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "95c063f93c357a9f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class NestedSerializer(serializers.Serializer):\n",
|
|||
|
|
" bar = serializers.CharField()\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class PlainSerializer(serializers.Serializer):\n",
|
|||
|
|
" foo = serializers.CharField()\n",
|
|||
|
|
" email = serializers.EmailField(min_length=200)\n",
|
|||
|
|
"\n",
|
|||
|
|
" nested = NestedSerializer()\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" serializer = PlainSerializer(data={\n",
|
|||
|
|
" \"email\": \"foo\",\n",
|
|||
|
|
" \"nested\": {}\n",
|
|||
|
|
" })\n",
|
|||
|
|
" serializer.is_valid(raise_exception=True)\n"
|
|||
|
|
],
|
|||
|
|
"id": "14e08afb10a1b39c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "96caaabe2eac48b1"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": {\n",
|
|||
|
|
" \"foo\": [\"This field is required.\"],\n",
|
|||
|
|
" \"email\": [\n",
|
|||
|
|
" \"Ensure this field has at least 200 characters.\",\n",
|
|||
|
|
" \"Enter a valid email address.\"\n",
|
|||
|
|
" ],\n",
|
|||
|
|
" \"nested\": {\n",
|
|||
|
|
" \"bar\": [\"This field is required.\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "12c5732281d27322"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "9ecf43a33dba5f7b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise exceptions.Throttled()"
|
|||
|
|
],
|
|||
|
|
"id": "db5507641f949da5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "df93ed692f322a95"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": \"Request was throttled.\"\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "73653427ce868ee0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " کد :",
|
|||
|
|
"id": "a3aa36d0e92e8ae0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" user = BaseUser()\n",
|
|||
|
|
" user.full_clean()"
|
|||
|
|
],
|
|||
|
|
"id": "581c582fe3a391f0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "f78ce4fcdb582fdb"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"detail\": {\n",
|
|||
|
|
" \"password\": [\"This field cannot be blank.\"],\n",
|
|||
|
|
" \"email\": [\"This field cannot be blank.\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "5b5fa54a20959264"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## Approach 2 - روش پیشنهادی HackSoft\n",
|
|||
|
|
"\n",
|
|||
|
|
"خب، میخوایم یه روش معرفی کنیم که راحت قابل توسعه باشه و با پروژه شما جور در بیاد.\n",
|
|||
|
|
"\n",
|
|||
|
|
"ایدههای کلیدی:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- اپلیکیشن شما یه سلسلهمراتب از استثناها (exceptions) داره که توسط منطق کسبوکار پرتاب میشن.\n",
|
|||
|
|
"- برای ساده بودن، فرض کنیم فقط یه خطا داریم: `ApplicationError`.\n",
|
|||
|
|
"- این خطا تو یه اپ core تعریف میشه، داخل ماژول exceptions، یعنی چیزی شبیه `project.core.exceptions.ApplicationError`.\n",
|
|||
|
|
"- بقیه موارد رو بذارید DRF به طور پیشفرض هندل کنه.\n",
|
|||
|
|
"- `ValidationError` حالا خاصه و قراره متفاوت هندل بشه.\n",
|
|||
|
|
"- `ValidationError` فقط باید از serializer یا اعتبارسنجی مدل بیاد.\n"
|
|||
|
|
],
|
|||
|
|
"id": "815336649f316a13"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " قراره یه ساختار به شکل زیر برای ارور ها به وجود بیاریم",
|
|||
|
|
"id": "4ddd46fff45b17ae"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"The error message here\",\n",
|
|||
|
|
" \"extra\": {}\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "9bce28ef60ccd1de"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"\n",
|
|||
|
|
"کلید اضافه (`extra`) میتونه هر دیتایی رو نگه داره — هر چیزی که لازم داریم به فرانت منتقل بشه.\n",
|
|||
|
|
"برای مثال، وقتی با یه `ValidationError` سروکار داریم (که معمولاً از یه **Serializer** یا **Model** میاد)،\n",
|
|||
|
|
"میخوایم ارور رو به این شکل نمایش بدیم:\n"
|
|||
|
|
],
|
|||
|
|
"id": "b5752591b23367ea"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error.\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": {\n",
|
|||
|
|
" \"password\": [\"This field cannot be blank.\"],\n",
|
|||
|
|
" \"email\": [\"This field cannot be blank.\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "c878c0d2b254c324"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"\n",
|
|||
|
|
"اینجا هدف اینه که فرانتاند بتونه بفهمه کدوم فیلدها خطا دارن،\n",
|
|||
|
|
"مثلاً از طریق `extra.fields` تا بتونه ارور هر فیلد رو جدا جدا به کاربر نشون بده.\n",
|
|||
|
|
"\n",
|
|||
|
|
"برای رسیدن به این هدف، یه **exception handler سفارشی** میسازیم.\n",
|
|||
|
|
"کدی شبیه به این:"
|
|||
|
|
],
|
|||
|
|
"id": "155b1e5cbaea876f"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\n",
|
|||
|
|
"from django.http import Http404\n",
|
|||
|
|
"\n",
|
|||
|
|
"from rest_framework.views import exception_handler\n",
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"from rest_framework.serializers import as_serializer_error\n",
|
|||
|
|
"from rest_framework.response import Response\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.core.exceptions import ApplicationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def hacksoft_proposed_exception_handler(exc, ctx):\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" {\n",
|
|||
|
|
" \"message\": \"Error message\",\n",
|
|||
|
|
" \"extra\": {}\n",
|
|||
|
|
" }\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
" if isinstance(exc, DjangoValidationError):\n",
|
|||
|
|
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc, Http404):\n",
|
|||
|
|
" exc = exceptions.NotFound()\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc, PermissionDenied):\n",
|
|||
|
|
" exc = exceptions.PermissionDenied()\n",
|
|||
|
|
"\n",
|
|||
|
|
" response = exception_handler(exc, ctx)\n",
|
|||
|
|
"\n",
|
|||
|
|
" # If unexpected error occurs (server error, etc.)\n",
|
|||
|
|
" if response is None:\n",
|
|||
|
|
" if isinstance(exc, ApplicationError):\n",
|
|||
|
|
" data = {\n",
|
|||
|
|
" \"message\": exc.message,\n",
|
|||
|
|
" \"extra\": exc.extra\n",
|
|||
|
|
" }\n",
|
|||
|
|
" return Response(data, status=400)\n",
|
|||
|
|
"\n",
|
|||
|
|
" return response\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc.detail, (list, dict)):\n",
|
|||
|
|
" response.data = {\n",
|
|||
|
|
" \"detail\": response.data\n",
|
|||
|
|
" }\n",
|
|||
|
|
"\n",
|
|||
|
|
" if isinstance(exc, exceptions.ValidationError):\n",
|
|||
|
|
" response.data[\"message\"] = \"Validation error\"\n",
|
|||
|
|
" response.data[\"extra\"] = {\n",
|
|||
|
|
" \"fields\": response.data[\"detail\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" else:\n",
|
|||
|
|
" response.data[\"message\"] = response.data[\"detail\"]\n",
|
|||
|
|
" response.data[\"extra\"] = {}\n",
|
|||
|
|
"\n",
|
|||
|
|
" del response.data[\"detail\"]\n",
|
|||
|
|
"\n",
|
|||
|
|
" return response"
|
|||
|
|
],
|
|||
|
|
"id": "e71e81f779674049"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " استراتژی ما اینجا استفاده کردن از DRF تا جای ممکن و تغییر اونه",
|
|||
|
|
"id": "6c26c56fdd27247e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from styleguide_example.core.exceptions import ApplicationError\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def trigger_application_error():\n",
|
|||
|
|
" raise ApplicationError(message=\"Something is not correct\", extra={\"type\": \"RANDOM\"})"
|
|||
|
|
],
|
|||
|
|
"id": "6f75653b5d35375e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " جواب :",
|
|||
|
|
"id": "aeb2828ae1397936"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Something is not correct\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"type\": \"RANDOM\"\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "998ed2fc11844799"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise DjangoValidationError(\"Some error message\")"
|
|||
|
|
],
|
|||
|
|
"id": "61759c185aa72e38"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": {\n",
|
|||
|
|
" \"non_field_errors\": [\"Some error message\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "4c26f5f575fa4dd9"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.exceptions import PermissionDenied\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise PermissionDenied()"
|
|||
|
|
],
|
|||
|
|
"id": "e7636b8f054e207b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"You do not have permission to perform this action.\",\n",
|
|||
|
|
" \"extra\": {}\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "a6fe417cd1060d6b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.http import Http404\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise Http404()"
|
|||
|
|
],
|
|||
|
|
"id": "e6044ff129b213e2"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Not found.\",\n",
|
|||
|
|
" \"extra\": {}\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "343ef03b1bfb194e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise RestValidationError(\"Some error message\")"
|
|||
|
|
],
|
|||
|
|
"id": "55956678fb2ff180"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": [\"Some error message\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "58da4ed3ef327982"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise RestValidationError(detail={\"error\": \"Some error message\"})"
|
|||
|
|
],
|
|||
|
|
"id": "3d7dedd3453f6249"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": {\n",
|
|||
|
|
" \"error\": \"Some error message\"\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "bbdc9c4e79b9ae0c"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"class NestedSerializer(serializers.Serializer):\n",
|
|||
|
|
" bar = serializers.CharField()\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class PlainSerializer(serializers.Serializer):\n",
|
|||
|
|
" foo = serializers.CharField()\n",
|
|||
|
|
" email = serializers.EmailField(min_length=200)\n",
|
|||
|
|
"\n",
|
|||
|
|
" nested = NestedSerializer()\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" serializer = PlainSerializer(data={\n",
|
|||
|
|
" \"email\": \"foo\",\n",
|
|||
|
|
" \"nested\": {}\n",
|
|||
|
|
" })\n",
|
|||
|
|
" serializer.is_valid(raise_exception=True)\n"
|
|||
|
|
],
|
|||
|
|
"id": "748221458f29ffd5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": {\n",
|
|||
|
|
" \"foo\": [\"This field is required.\"],\n",
|
|||
|
|
" \"email\": [\n",
|
|||
|
|
" \"Ensure this field has at least 200 characters.\",\n",
|
|||
|
|
" \"Enter a valid email address.\"\n",
|
|||
|
|
" ],\n",
|
|||
|
|
" \"nested\": {\n",
|
|||
|
|
" \"bar\": [\"This field is required.\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "8d54fd9e3332fec"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from rest_framework import exceptions\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" raise exceptions.Throttled()"
|
|||
|
|
],
|
|||
|
|
"id": "b1aa8fb83fd13572"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Request was throttled.\",\n",
|
|||
|
|
" \"extra\": {}\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "5d9561f655ffaa91"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"def some_service():\n",
|
|||
|
|
" user = BaseUser()\n",
|
|||
|
|
" user.full_clean()"
|
|||
|
|
],
|
|||
|
|
"id": "1647b7f6c5f81444"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"{\n",
|
|||
|
|
" \"message\": \"Validation error\",\n",
|
|||
|
|
" \"extra\": {\n",
|
|||
|
|
" \"fields\": {\n",
|
|||
|
|
" \"password\": [\"This field cannot be blank.\"],\n",
|
|||
|
|
" \"email\": [\"This field cannot be blank.\"]\n",
|
|||
|
|
" }\n",
|
|||
|
|
" }\n",
|
|||
|
|
"}"
|
|||
|
|
],
|
|||
|
|
"id": "b2b3a68f0ce46d1e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"# Celery — مغز پشت صحنهی پروژه\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## کاربردهای اصلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"سلری رو معمولاً واسه ۳ مدل کار استفاده میکنیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. حرف زدن با سرویسهای خارجی (ایمیل، نوتیف، پیام و از این داستانا)\n",
|
|||
|
|
"2. انداختن کارای سنگین بیرون از چرخهی HTTP (تا یوزر منتظر نمونه)\n",
|
|||
|
|
"3. اجرای کارای زمانبندیشده با **Celery Beat**\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## منطق کلی\n",
|
|||
|
|
"\n",
|
|||
|
|
"ما با **Celery** مثل یه رابط رفتار میکنیم، نه یه جای منطق اصلی.\n",
|
|||
|
|
"یعنی چی؟ یعنی:\n",
|
|||
|
|
"\n",
|
|||
|
|
"> هیچ کاری که مربوط به منطق پروژهست نباید مستقیم داخل تسک نوشته بشه.\n",
|
|||
|
|
"\n",
|
|||
|
|
"سلری فقط یه مجریه. منطق اصلی باید توی سرویسها (services) نوشته بشه، نه تو تسکها.\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n"
|
|||
|
|
],
|
|||
|
|
"id": "d5e01e664b4c6edb"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.db import transaction\n",
|
|||
|
|
"from django.core.mail import EmailMultiAlternatives\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.core.exceptions import ApplicationError\n",
|
|||
|
|
"from styleguide_example.common.services import model_update\n",
|
|||
|
|
"from styleguide_example.emails.models import Email\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@transaction.atomic\n",
|
|||
|
|
"def email_send(email: Email) -> Email:\n",
|
|||
|
|
" if email.status != Email.Status.SENDING:\n",
|
|||
|
|
" raise ApplicationError(f\"Cannot send non-ready emails. Current status is {email.status}\")\n",
|
|||
|
|
"\n",
|
|||
|
|
" subject = email.subject\n",
|
|||
|
|
" from_email = \"styleguide-example@hacksoft.io\"\n",
|
|||
|
|
" to = email.to\n",
|
|||
|
|
"\n",
|
|||
|
|
" html = email.html\n",
|
|||
|
|
" plain_text = email.plain_text\n",
|
|||
|
|
"\n",
|
|||
|
|
" msg = EmailMultiAlternatives(subject, plain_text, from_email, [to])\n",
|
|||
|
|
" msg.attach_alternative(html, \"text/html\")\n",
|
|||
|
|
"\n",
|
|||
|
|
" msg.send()\n",
|
|||
|
|
"\n",
|
|||
|
|
" email, _ = model_update(\n",
|
|||
|
|
" instance=email,\n",
|
|||
|
|
" fields=[\"status\", \"sent_at\"],\n",
|
|||
|
|
" data={\n",
|
|||
|
|
" \"status\": Email.Status.SENT,\n",
|
|||
|
|
" \"sent_at\": timezone.now()\n",
|
|||
|
|
" }\n",
|
|||
|
|
" )\n",
|
|||
|
|
" return email"
|
|||
|
|
],
|
|||
|
|
"id": "e3fb09af2d6d3a9b"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": " ارسال ایمیل لاجیک بیزینسی داره ولی ما میخوایم این رو با سلری تسک انجام بدیم و یک سرویس رو تریگر کنیم",
|
|||
|
|
"id": "163e071644009da0"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from celery import shared_task\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.emails.models import Email\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@shared_task\n",
|
|||
|
|
"def email_send(email_id):\n",
|
|||
|
|
" email = Email.objects.get(id=email_id)\n",
|
|||
|
|
"\n",
|
|||
|
|
" from styleguide_example.emails.services import email_send\n",
|
|||
|
|
" email_send(email)"
|
|||
|
|
],
|
|||
|
|
"id": "8c0b1f1d9ca626c5"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"همونطور که میبینی، ما با **تسک (task)** مثل یه **API** برخورد میکنیم:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. دادههای لازم رو میگیریم.\n",
|
|||
|
|
"2. سرویس مورد نظر رو صدا میزنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"حالا تصور کن یه **سرویس دیگه** داریم که خودش قراره **ارسال ایمیل** رو فعال کنه.\n",
|
|||
|
|
"کدش ممکنه شبیه این باشه:"
|
|||
|
|
],
|
|||
|
|
"id": "9f90a92ea9055870"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.db import transaction\n",
|
|||
|
|
"\n",
|
|||
|
|
"# ... more imports here ...\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.emails.tasks import email_send as email_send_task\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@transaction.atomic\n",
|
|||
|
|
"def user_complete_onboarding(user: User) -> User:\n",
|
|||
|
|
" # ... some code here\n",
|
|||
|
|
"\n",
|
|||
|
|
" email = email_get_onboarding_template(user=user)\n",
|
|||
|
|
"\n",
|
|||
|
|
" transaction.on_commit(lambda: email_send_task.delay(email.id))\n",
|
|||
|
|
"\n",
|
|||
|
|
" return user"
|
|||
|
|
],
|
|||
|
|
"id": "e723aeab9f78f962"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"\n",
|
|||
|
|
"1. ما تسک (task) رو که **اسمش دقیقاً مثل سرویسه** ایمپورت میکنیم،\n",
|
|||
|
|
" ولی یه پسوند `_task` بهش اضافه میکنیم تا از هم تفکیک بشن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"2. وقتی **تراکنش (transaction)** تموم میشه (commit میکنه)،\n",
|
|||
|
|
" اون موقع تسک رو صدا میزنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"### در کل، طرز کار ما با Celery به این شکله:\n",
|
|||
|
|
"\n",
|
|||
|
|
"- **تسکها سرویسها رو صدا میزنن.**\n",
|
|||
|
|
"- **سرویس** رو داخل بدنهی تابع تسک ایمپورت میکنیم (نه در بالای فایل).\n",
|
|||
|
|
"- **وقتی میخوایم یه تسک رو اجرا کنیم،** اون رو در سطح ماژول ایمپورت میکنیم و بهش پسوند `_task` میدیم.\n",
|
|||
|
|
"- **تسکها رو بعد از commit تراکنش اجرا میکنیم،** بهعنوان یه اثر جانبی از منطق اصلی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"### نکتهی طراحی\n",
|
|||
|
|
"این ترکیب بین **tasks و services** باعث میشه **دایرهی ایمپورتها (circular imports)** اتفاق نیفته،\n",
|
|||
|
|
"که یکی از مشکلات رایج موقع استفاده از Celery هست.\n"
|
|||
|
|
],
|
|||
|
|
"id": "576f7039d9619769"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## Error Handling\n",
|
|||
|
|
"\n",
|
|||
|
|
"گاهی وقتا ممکنه **سرویس ما fail کنه**،\n",
|
|||
|
|
"و بخوایم اون خطا رو در سطح **تسک (task)** هندل کنیم.\n",
|
|||
|
|
"مثلاً بخوایم **تسک رو دوباره retry کنیم**.\n",
|
|||
|
|
"\n",
|
|||
|
|
"اینجور منطق مربوط به خطا **باید داخل خود تسک نوشته بشه**،\n",
|
|||
|
|
"نه داخل سرویس.\n"
|
|||
|
|
],
|
|||
|
|
"id": "706ae47d0958814e"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from celery import shared_task\n",
|
|||
|
|
"from celery.utils.log import get_task_logger\n",
|
|||
|
|
"\n",
|
|||
|
|
"from styleguide_example.emails.models import Email\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"logger = get_task_logger(__name__)\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"def _email_send_failure(self, exc, task_id, args, kwargs, einfo):\n",
|
|||
|
|
" email_id = args[0]\n",
|
|||
|
|
" email = Email.objects.get(id=email_id)\n",
|
|||
|
|
"\n",
|
|||
|
|
" from styleguide_example.emails.services import email_failed\n",
|
|||
|
|
"\n",
|
|||
|
|
" email_failed(email)\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@shared_task(bind=True, on_failure=_email_send_failure)\n",
|
|||
|
|
"def email_send(self, email_id):\n",
|
|||
|
|
" email = Email.objects.get(id=email_id)\n",
|
|||
|
|
"\n",
|
|||
|
|
" from styleguide_example.emails.services import email_send\n",
|
|||
|
|
"\n",
|
|||
|
|
" try:\n",
|
|||
|
|
" email_send(email)\n",
|
|||
|
|
" except Exception as exc:\n",
|
|||
|
|
" # https://docs.celeryq.dev/en/stable/userguide/tasks.html#retrying\n",
|
|||
|
|
" logger.warning(f\"Exception occurred while sending email: {exc}\")\n",
|
|||
|
|
" self.retry(exc=exc, countdown=5)"
|
|||
|
|
],
|
|||
|
|
"id": "7ae1c321d101abe"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## Handling Failure\n",
|
|||
|
|
"\n",
|
|||
|
|
"همونطور که میبینی، تسک چند بار **retry** میکنه،\n",
|
|||
|
|
"ولی اگه **همهی تلاشها fail بشن**، ما این وضعیت رو\n",
|
|||
|
|
"در **callback مخصوص شکست (on_failure)** هندل میکنیم.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"### منطقش اینجوریه:\n",
|
|||
|
|
"اسم callback بر اساس یه الگو ساخته میشه\n",
|
|||
|
|
"> `_ {task_name}_failure`\n",
|
|||
|
|
"\n",
|
|||
|
|
"و این تابع هم مثل یه تسک معمولی،\n",
|
|||
|
|
"**سرویس مربوطه رو صدا میزنه** (service layer).\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"### مثال ساده:\n",
|
|||
|
|
"\n",
|
|||
|
|
"```python\n",
|
|||
|
|
"from celery import shared_task\n",
|
|||
|
|
"from styleguide_example.emails.models import Email\n",
|
|||
|
|
"\n",
|
|||
|
|
"@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=5)\n",
|
|||
|
|
"def email_send(self, email_id):\n",
|
|||
|
|
" try:\n",
|
|||
|
|
" email = Email.objects.get(id=email_id)\n",
|
|||
|
|
" from styleguide_example.emails.services import email_send\n",
|
|||
|
|
" email_send(email)\n",
|
|||
|
|
" except Exception as exc:\n",
|
|||
|
|
" raise self.retry(exc=exc)\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"@shared_task\n",
|
|||
|
|
"def email_send_failure(email_id):\n",
|
|||
|
|
" # اینجا منطق مربوط به fail شدن رو هندل میکنیم\n",
|
|||
|
|
" email = Email.objects.get(id=email_id)\n",
|
|||
|
|
" from styleguide_example.emails.services import email_mark_failed\n",
|
|||
|
|
" email_mark_failed(email)\n"
|
|||
|
|
],
|
|||
|
|
"id": "9848bf1f9a58f460"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"## Structure — ساختار تسکها\n",
|
|||
|
|
"\n",
|
|||
|
|
"تسکها معمولا توی **tasks.py** هر اپ قرار میگیرن.\n",
|
|||
|
|
"\n",
|
|||
|
|
"قوانین همون قوانین بقیه چیزها هستن (APIs, services, selectors):\n",
|
|||
|
|
"اگه تعداد تسکها برای یه اپ زیاد شد، بهتره اونها رو **بر اساس domain جدا کنیم**.\n",
|
|||
|
|
"\n",
|
|||
|
|
"مثلا:\n",
|
|||
|
|
"- `tasks/domain_a.py`\n",
|
|||
|
|
"- `tasks/domain_b.py`\n",
|
|||
|
|
"\n",
|
|||
|
|
"و کافیه اونا رو توی `tasks/__init__.py` ایمپورت کنیم تا **Celery autodiscover** انجام بده.\n",
|
|||
|
|
"\n",
|
|||
|
|
" نکتهی کلی: تسکها رو طوری تقسیم کن که **برای خودت منطقی باشه**.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"## Periodic Tasks — تسکهای دورهای\n",
|
|||
|
|
"\n",
|
|||
|
|
"مدیریت تسکهای دورهای خیلی مهمه، مخصوصا وقتی **دهها یا صدها تسک** داشته باشی.\n",
|
|||
|
|
"\n",
|
|||
|
|
"ما از ترکیب زیر استفاده میکنیم:\n",
|
|||
|
|
"- `Celery Beat`\n",
|
|||
|
|
"- `django_celery_beat.schedulers:DatabaseScheduler`\n",
|
|||
|
|
"- `django-celery-beat`\n",
|
|||
|
|
"\n",
|
|||
|
|
"اضافه بر این، یه **management command** داریم به اسم:\n",
|
|||
|
|
"`setup_periodic_tasks`\n",
|
|||
|
|
"\n",
|
|||
|
|
"این کامند **تعریف همهی تسکهای دورهای سیستم** رو نگه میداره و داخل اپ tasks قرار داره.\n",
|
|||
|
|
"\n",
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"### مسیر فایل مثال:\n",
|
|||
|
|
"`project/tasks/management/commands/setup_periodic_tasks.py`\n"
|
|||
|
|
],
|
|||
|
|
"id": "10a20c6ad31a61fe"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"outputs": [],
|
|||
|
|
"execution_count": null,
|
|||
|
|
"source": [
|
|||
|
|
"from django.core.management.base import BaseCommand\n",
|
|||
|
|
"from django.db import transaction\n",
|
|||
|
|
"\n",
|
|||
|
|
"from django_celery_beat.models import IntervalSchedule, CrontabSchedule, PeriodicTask\n",
|
|||
|
|
"\n",
|
|||
|
|
"from project.app.tasks import some_periodic_task\n",
|
|||
|
|
"\n",
|
|||
|
|
"\n",
|
|||
|
|
"class Command(BaseCommand):\n",
|
|||
|
|
" help = f\"\"\"\n",
|
|||
|
|
" Setup celery beat periodic tasks.\n",
|
|||
|
|
"\n",
|
|||
|
|
" Following tasks will be created:\n",
|
|||
|
|
"\n",
|
|||
|
|
" - {some_periodic_task.name}\n",
|
|||
|
|
" \"\"\"\n",
|
|||
|
|
"\n",
|
|||
|
|
" @transaction.atomic\n",
|
|||
|
|
" def handle(self, *args, **kwargs):\n",
|
|||
|
|
" print('Deleting all periodic tasks and schedules...\\n')\n",
|
|||
|
|
"\n",
|
|||
|
|
" IntervalSchedule.objects.all().delete()\n",
|
|||
|
|
" CrontabSchedule.objects.all().delete()\n",
|
|||
|
|
" PeriodicTask.objects.all().delete()\n",
|
|||
|
|
"\n",
|
|||
|
|
" periodic_tasks_data = [\n",
|
|||
|
|
" {\n",
|
|||
|
|
" 'task': some_periodic_task\n",
|
|||
|
|
" 'name': 'Do some peridoic stuff',\n",
|
|||
|
|
" # https://crontab.guru/#15_*_*_*_*\n",
|
|||
|
|
" 'cron': {\n",
|
|||
|
|
" 'minute': '15',\n",
|
|||
|
|
" 'hour': '*',\n",
|
|||
|
|
" 'day_of_week': '*',\n",
|
|||
|
|
" 'day_of_month': '*',\n",
|
|||
|
|
" 'month_of_year': '*',\n",
|
|||
|
|
" },\n",
|
|||
|
|
" 'enabled': True\n",
|
|||
|
|
" },\n",
|
|||
|
|
" ]\n",
|
|||
|
|
"\n",
|
|||
|
|
" for periodic_task in periodic_tasks_data:\n",
|
|||
|
|
" print(f'Setting up {periodic_task[\"task\"].name}')\n",
|
|||
|
|
"\n",
|
|||
|
|
" cron = CrontabSchedule.objects.create(\n",
|
|||
|
|
" **periodic_task['cron']\n",
|
|||
|
|
" )\n",
|
|||
|
|
"\n",
|
|||
|
|
" PeriodicTask.objects.create(\n",
|
|||
|
|
" name=periodic_task['name'],\n",
|
|||
|
|
" task=periodic_task['task'].name,\n",
|
|||
|
|
" crontab=cron,\n",
|
|||
|
|
" enabled=periodic_task['enabled']\n",
|
|||
|
|
" )"
|
|||
|
|
],
|
|||
|
|
"id": "e100be6acfe2dfef"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metadata": {},
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"source": [
|
|||
|
|
"---\n",
|
|||
|
|
"\n",
|
|||
|
|
"# جمعبندی\n",
|
|||
|
|
"\n",
|
|||
|
|
"این استایلگاید قرار نیست یه قانون خشک باشه،\n",
|
|||
|
|
"بلکه قراره بهت کمک کنه که **کدهایی بنویسی که خودت ازش لذت ببری.**\n",
|
|||
|
|
"\n",
|
|||
|
|
"دنیا پر از فریمورکها و ابزارهای مختلفه،\n",
|
|||
|
|
"اما در نهایت چیزی که یه پروژه رو ماندگار میکنه\n",
|
|||
|
|
"**طرز فکر و نظم ذهنی توسعهدهندههاشه.**\n",
|
|||
|
|
"\n",
|
|||
|
|
"اگر این ساختار رو تو پروژههات پیاده کنی،\n",
|
|||
|
|
"هم کار گروهی راحتتر میشه، هم رفع باگها و توسعههای بعدی تبدیل میشه به یه بازی\n",
|
|||
|
|
"\n",
|
|||
|
|
"پس اینو به عنوان یه «نقشه راه شخصی» برای جنگو در نظر بگیر —\n",
|
|||
|
|
"یه سبکی که قراره زندگیتو سادهتر کنه، نه سختتر.\n",
|
|||
|
|
"\n",
|
|||
|
|
"موفق باشی ایلیا قادری آبان 404\n"
|
|||
|
|
],
|
|||
|
|
"id": "286c22de50c62a62"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"metadata": {
|
|||
|
|
"kernelspec": {
|
|||
|
|
"display_name": "Python 3",
|
|||
|
|
"language": "python",
|
|||
|
|
"name": "python3"
|
|||
|
|
},
|
|||
|
|
"language_info": {
|
|||
|
|
"codemirror_mode": {
|
|||
|
|
"name": "ipython",
|
|||
|
|
"version": 2
|
|||
|
|
},
|
|||
|
|
"file_extension": ".py",
|
|||
|
|
"mimetype": "text/x-python",
|
|||
|
|
"name": "python",
|
|||
|
|
"nbconvert_exporter": "python",
|
|||
|
|
"pygments_lexer": "ipython2",
|
|||
|
|
"version": "2.7.6"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"nbformat": 4,
|
|||
|
|
"nbformat_minor": 5
|
|||
|
|
}
|