افزودن فیلد به مدل با کمترین impact

یه مدل داریم تو جنگو که پروفایل شرکت های مختلف توی اون ذخیره میشه:

لازم شد که لوگو شرکت هم اضافه بشه اما از اونجایی که این مدل جاهای مختلف استفاده شده بود و توی فرمها و view های مختلف ازش استفاده شده اگه یه فیلد برای لوگو بهش اضافه کنیم قطعا باید خیلی از جاهای کد هم تغییر بدیم و احتمالا باگ هایی تولید بشن و از اونجایی که این سیستم براش تست نوشته نشده ممکن اطفاقات بدی بیوفته.

بنابراین رفتم دنبال اینکه چطور میشه بدون تغییر این مدل لوگو رو اضافه کنم. حداقل طوری که اگر جایی از این مدل داره استفاده میشه دیگه فیلدهاش عوض نشه. راه های مختلفی وجود داره ولی بهترینش بنظرم اینه که یه مدل جدید برای لوگو بسازیم و ارتباط OneToOne بینشون برقرار کنیم.

و اینطوری ازش استفاده کنم

Coroutine و Awaitables

Coroutine در واقع یک تابع هست که میتونیم اونو pause کنیم. فرض کنیم دو تا تابع داریم و میخایم کنترل اجرا بین این دو تا جابجا بشه مثل ping pong مثلا تابع اول چند خط رو اجرا کنه بعد کنترل رو به تابع دوم بده و اون چند خط اجرا کنه و … در واقع دو تا routine که اینجا بهشون میگیم تابع با هم cooperate کنن.

برای تریفش توی پایتون از async کلیدواژه استفاده میکنیم. که اون وقت اون تابع coroutine میشه. میتونیم هر جای کد coroutine ازش بخایم وایسه تا اجرای یه تابع دیگه تموم شه برای این کار await میکنیم.

مقایسه Celery و Channels و Singnal

در جدول زیر مقایسه Celery و Django Channels و Django Signals رو میبینیم

Here’s a comparison of Django Channels, Django Signals, and Celery, presented in a tabular format, focusing on their use cases, features, and suitable scenarios:

Feature/AspectDjango ChannelsDjango SignalsCelery
Primary Use CaseReal-time communication, WebSockets, and long-lived connectionsDecoupled event handling within Django applicationsAsynchronous task queue/job queue for running background tasks
CommunicationWebSockets, HTTP2, server-sent eventsIn-process communication within the same Django appDistributed communication, allowing task distribution across multiple workers
ConcurrencyHandles concurrent connections and async processingSynchronous, works within the Django request-response cycleHandles concurrent tasks, designed for parallel processing
ScalabilityScalable for handling many concurrent connectionsLimited by the Django processHighly scalable, can distribute tasks across multiple servers
Real-time UpdatesYes, supports real-time updatesNoNo, tasks are run asynchronously but not in real-time
Event HandlingHandles real-time eventsHandles events within the Django lifecycle (e.g., model save, user login)Handles events as background tasks
Persistent ConnectionsYes, supports long-lived connections (e.g., chat apps)No, operates within the request-response cycleNo, designed for discrete, short-lived tasks
IntegrationIntegrates with WebSockets, WebRTC, etc.Integrates with Django ORM and lifecycle hooksIntegrates with message brokers (RabbitMQ, Redis, etc.)
Typical Use CasesChat applications, live notifications, live dashboardsSending signals on model changes, user actionsSending emails, processing data, generating reports, scheduled tasks
ExampleLive chat app with instant messagingLogging changes to models when savedSending periodic email reports, processing user-uploaded files in the background
Fault ToleranceBasic fault tolerance, relies on infrastructureBasic, depends on Django’s fault toleranceHigh fault tolerance, can retry failed tasks, distributed task execution
DependenciesRedis, RabbitMQ (for channel layers)None beyond DjangoMessage broker (e.g., RabbitMQ, Redis), result backend (optional)
Ease of UseRequires understanding of asynchronous programmingSimple to use with DjangoRequires setup of a message broker and understanding of task queues

Detailed Use Case Examples

  1. Django Channels:
    • Chat Application: Users can send and receive messages in real-time, with updates being pushed to all connected clients instantly.
    • Live Notifications: Real-time notifications for events such as new comments, likes, or mentions in a social media application.
    • Live Dashboard: Real-time updates of metrics and analytics in a monitoring dashboard.
  2. Django Signals:
    • Model Change Notifications: Automatically trigger a function to log changes or send an email when a model instance is saved or deleted.
    • User Activity Tracking: Capture user login, logout, or profile update events to maintain an activity log.
    • Post-Processing: Perform additional actions after saving a model, such as updating related records or sending a confirmation email.
  3. Celery:
    • Email Sending: Send emails asynchronously to avoid blocking the main thread during the request-response cycle.
    • Data Processing: Handle large data processing tasks, such as generating reports or processing user-uploaded files, in the background.
    • Scheduled Tasks: Run periodic tasks like clearing expired sessions, generating nightly reports, or updating external APIs.

By comparing these tools, you can choose the most appropriate solution based on the specific requirements and characteristics of your project.

database_sync_to_async و async_to_sync

کاربرد این دوتابع در django channels هست. به طور کلی وقتی توی یه consumer که به صورت Asynchronous تعریف شده بخایم از یک تابع Synchronous استفاده کنیم باید اون رو به Asynchronous تبدیل کنیم و برعکس و کاربرد این دو تابع همینه.

میدونیم که ORM جنگو Synchronous هست بنابراین وقتی توی یه کانسومر Asynchronous ازش استفاده میکنیم باید اونو توی database_sync_to_async بزاریمو

میدونیم که Channel Layer ها ذاتا Asynchronous هستند و همه متدهای اون مثل self.send group_send و group_add همگی Async هستند بنابراین اگر بخایم توی یه کلاس sync ازشون استفاده کنیم باید از async_to_syn استفاده بشه.

لازم به ذکر است که channels.db.database_sync_to_async یک wrapper از asgiref.sync.sync_to_async هست که بعد از کانکشن دیتابیس رو هم clean میکنه

برای استفاده از اون بهتره اول خود query رو توی یه تابع بنویسیم بعد اون تابع رو بدیم به این تابع مثل:

یا

Gunicorn یا Daphne?

Gunicorn و Daphne هر دو وب سرور برای فریمورکهای پایتون هستن که روی لینوکس نصب میشن.

فرقشون چیه؟

Gunicorn برای درخواست های Syncronous و HTTP request ها استفاده میشه. تنظیمات WSGI باید ست شه.

Daphne برای درخواست های Asynchronous و Websocket Request ها کاربرد داره و تنظیمات ASGI باید روشون انجام شه.

از کدوم استفاده کنیم؟

از Daphne میشه برای هر دو استفاده کرد. اگر درخواست های Async ندارید میتونید از Gunicorn استفاده کنید. یه راه حل دیگه هم برای سولوشنهای بزرگ اینه که Nginx رو استفاده کنید و تنظیم کنید که درخواست های Websocker رو به Daphne و درخواست های HTTP رو به Gunicorn بفرسته.

تغییر توی دیتابیس برای زدن query بهینه تر تو ORM جنگو

توی یه مدل جنگو میخاستم یه JSON ذخیره کنم، طبیعتا رفتم تو کار استفاده از JSONField. تعریف مدل اینجوری شد:

توی یه view میخاستم kwargs ای که به view پاس داده شده رو توی این فیلد ذخیره کنم بنابراین با ORM زیر اون رو آپدیت میکردم:

دیتایی که توی این فیلد ذخیره میشه حالات مختلفی داره مثل

نکته اینجاست که من میخام یه جاهای query بزنم و اون آبجکتهایی رو داشته باشم که این فلدشون پارامتر doc_id رو توش داره (همشون ندارن) و مجبور از روش زیر query بزنم:

مشکل این چیه؟ اینکه بهینه نیست باید راه حلی باشه که با یه query جواب رو بگیرم. در واقع یک ORM جنگو یکجا به SQL تبدیل بشه و یک هیت دیتابیس داشته باشه. فهمیدم که با تغییر زیر میتونم این کار رو بکنم

اول اینکه باید JSONField رو هست بگم که دیفالتش دیکشنری باشه

و حالا دیگه از json.dumps برای وارد کردن دیتا استفاده نمیکنیم:

و حالا دیتایی که توی دیتابیس ذخیره میشه این فیلدش دیکشنریه و میتونیم با یه query دیتایی که میخاستیم رو بگیریم:

چطور توی جنگو Django ترکیب چند فیلد رو یکتا کنیم و بعدش باید به چه نکاتی توجه کنیم

فرض کنید میخایم توی یک Model تو جنگو بگیم که مجموع سه تا فیلد باید یکتا باشه. برای این کار توی django.models.Model یک قابلیت تعریف شده:

ولی مساله اینه که اینجا ارور در سطح دیتابیس نشون میده و جاهای مختلف باید این Excelption رو هندل کنید. اینطوری نیست که خودش هندل کنه. مثلا

همینطور باید توی view این موضوع رو خودمون هندل کنیم و قبل از ارسال درخواست به دیتابیس حتما چک کنیم ترکیب این سه فیلد تکراری توی دیتابیس نباشه

و برای اینکه ارور رو توی template قشنگتر نشون بده مثل زیر عمل کنیم

استفاده بد از تکنولوژی

گاهی پیش میاد که توی کدهایی که میخونم از برنامه نویسهای دیگه مشکلات زیر رو توشون میبینم:

1.از یک Framework استفاده شده ولی از تمامی قابلیتهاش استفاده نشده و گاهی میبینیم قابلیتی که توی فریمورک به خوبی و استاندارد پیاده سازی شده رو استفاده نکرده و خود شخص اون قابلیت رو کد زده

2.کتابخانه یا فریمورکی نصب شده و قابلیتی داره ولی از اون استفاده نشده و برای اون قابلیت کتابخانه دیگری نصب شده

دلیل این اشتباه اینه که برنامه نویس مستندات اون فریمورک و کتابخانه رو نخونده. کد زیر مثالی از حالت اوله. ببینید در ادامه یک فرم جنگو Django میبینیم:

و کد زیر view هست که توی اون از این فرم استفاده شده:

مشکل کجاست؟ برنامه نویس اومده مقدار فیلدهایی که از template ارسال شده POST با استفاده از request.POST.get گرفته شده. بعد برنامه نویس اومده field validation (اعتبارسنجی فیلدها) رو انجام داده. بعد اگر توی ورودی فیلد ایرادی وجود داشت اومده یک پیام ارور به کاربر نشون داده. تمامی این کاری که برنامه نویس براش کد زده توی django.forms.Formپیاده سازی شده. به بهترین شکل توی جنگو پیاده سازی شده. در حالی که با تست این view مشکلات خیلی زیادی هست.

با استفاده از ServiceForm.is_valid() تمامی فیلدها validate و اعتبار سنجی میشن و اگر ایرادی داشته باشه همونجا فیلد قرمز رنگ میشه و ارور مرتبط نشان داده میشه اما در این حالت اگر یک فیلد required توی فرم خالی بمونه ارور مرتبط به کاربر نشان داده نمیشه. مشکل دوم اینه که مثلا اگر یک فیلد ایمیل یا پسورد باشه ایمیل با روش های استاندارد اعتبارسنجی میشه و پسورد هم همینطور.

مشکل بعدی این کد اینه که کد کثیفه و از طرفی همین الان ممکنه چنتا باگ داشته باشه که با ورودی های مختلف خودشو نشون میده و ما الان نمیدونیم وجود داره یا نه.

همینطور که توی این لینک میبینید فرم جنگو چند لایه صحت سنجی داره.

همیشه اگر از فریمورک یا کتابخانه ای استفاده میکنید حتما تمامی مستنداتش رو بخونید.

درگیری جدید با django-ckeditor-5

برای اضافه کردن فونت به CKEditor توی Django راه حل طبق این لینکه. منتها مساله اینه که من با نصب ورژن 5 نمیتونستم این مدلی فونت اضافه کنم.

توی django_ckeditor_5 تنظیمات فونت توی django_ckeditor_5/dist/bundle/js اضافه میشه. البته فقط تنظیمات فونت اینجوریه.

بخاطر همین فایل bundle.js رو prettify کردم و فونت ها رو سرچ کردم و Bnazanin رو بهش اضافه کردم مثل کد زیر

و یه فایل ساختم توی django_ckeditor_5/css/font.css و کد زیر رو زدم:

و البته دایرکتوری django_ckeditor_5/farsifonts/ رو ساختم و فایل ttf فونت رو کپی کردم توش.

و تمام

راه حل غیر استاندارده چون هر بار اگر بخام تغییر بدم باید bundle.js رو تغییر بدم درستش این بود که ورژن جدید CKEditor رو نصب کنم. ولی امکانش نبود.

استفاده از CKEditor توی پروژه (جنگو)

اگر توی جنگو نیاز دارید که به جای یک Text Area خشک و بی روح یک ادیتور کامل داشته باشید (مثلا برای نوشتن بلاگ که به بولت چونت و هایلایت و …. نیاز دارید) میتونید از django-ckeditor-5 استفاده کنید. مستندات خود کتابخونه کامل نحوه نصب و تنظیمات رو گفته و نیازی به توضیح دادن کدش نیست. من قسمت هایی که برای جالب بود رو میگم:

یه کتابخونه کامله که بعد از نصبش و migrate کردن دیتابیس میتونید از فیلدهای جدیدی که اضافه میکنه توی ORM استفاده کنید مثلا به جای استفاده از فیلدهای استاندارد جنگو میشه از CKEditor5Field استفاده کرد

لازم نیست توی template کاری بکنید چون خود فایلهای استاتیک رو اضافه میکنه و اتوماتیک رندر میکنه.

توی تعریف فرمها میشه از widget هایی که اضافه کرده استفاده کرد. ولی یه بخشی که برای من خیلی جالب بود اینه که من نیاز داشتم از اون فرمهای رندر شده گاهی یه درخواست AJAX به یک route بفرستم و متوجه شدم مثل حالت معمولی نمیشه دیتای textarea رو با JQuery بگیرم و ازش توی request استفاده کنم. گشتم و متوجه شدم که توی تمپلیت ادیتور قابل دسترسی هست و از API هاش میتونم استفاده کنم. مثل کد زیر از editors لیست فیلدهایی که ckeditor بودن رو داشته باشم و با تابع getData() دیتای رندر شده شو بگیرم یعنی دیتایی که فرمت HTML داره و باید توی دیتابیس ذخیره بشه.