Skip to content

30. تمرین عملی: نسخه ۱۲

در این فصل، ما یک برنامه وب مبتنی بر معماری MVC (مدل-نما-کنترل‌کننده) خواهیم نوشت. این برنامه قادر خواهد بود پاسخ‌ها را در سه فرمت بازگرداند: jSON، XML و HTML. افزایش قابل توجهی در پیچیدگی بین کاری که قصد انجام آن را داریم و آنچه قبلاً انجام داده‌ایم وجود دارد. ما بیشتر مفاهیم پوشش داده شده تا به اینجا را مجدداً استفاده خواهیم کرد و تمام مراحل منتهی به اپلیکیشن نهایی را به تفصیل توضیح خواهیم داد.

30.1. معماری MVC

ما مدل معماری معروف به MVC (مدل–نما–کنترل‌کننده) را به شرح زیر پیاده‌سازی خواهیم کرد:

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

  • ۱ – درخواست

URLهای درخواستی به شکل http://machine:port/action/param1/param2/… خواهند بود. [Contrôleur principal] از یک فایل پیکربندی برای «مسیردهی» درخواست به کنترل‌کننده صحیح استفاده خواهد کرد. برای این کار، از فیلد [action] در URL استفاده خواهد شد. بخش باقی‌مانده از URL و [param1/param2/…] شامل پارامترهای اختیاری است که به اکشن ارسال خواهند شد. حرف C در MVC در این مورد، رشته [Contrôleur principal, Contrôleur / Action] است. اگر هیچ کنترولری نتواند اقدام درخواستی را مدیریت کند، وب‌سرور پاسخ خواهد داد که URL درخواستی یافت نشد.

  • ۲ – پردازش
  • عمل انتخاب‌شده [2a] می‌تواند از پارامترهای parami که توسط [Contrôleur principal] به آن ارسال شده‌اند، استفاده کند. این پارامترها ممکن است از دو منبع زیر بیایند:
      • مسیر [/param1/param2/…] از URL،
      • از پارامترهای ارسال‌شده در بدنه درخواست کلاینت؛
    • هنگام پردازش درخواست کاربر، ممکن است اقدام به لایه‌های [métier] و [2b] نیاز داشته باشد. پس از پردازش درخواست مشتری، ممکن است پاسخ‌های مختلفی را ایجاد کند. یک مثال معمول عبارت است از:
      • یک پاسخ خطا اگر درخواست نتوانست به درستی پردازش شود؛
      • در غیر این صورت، یک پاسخ تأیید؛
    • [Contrôleur / Action] پاسخ خود، [2c]، را به همراه یک کد وضعیت به کنترل‌کننده اصلی بازمی‌گرداند. این کدهای وضعیت، وضعیت فعلی برنامه را به طور منحصربه‌فردی نشان می‌دهند. این کدها یا یک کد موفقیت یا یک کد خطا خواهند بود؛
  • ۳ – پاسخ
    • بسته به اینکه آیا کلاینت پاسخ jSON را درخواست کرده باشد، XML یا HTML، [Contrôleur principal] نوع پاسخ مناسب، [3a] را ایجاد کرده و به آن دستور می‌دهد که پاسخ را برای کلاینت ارسال کند. [Contrôleur principal] هم پاسخ و هم کد وضعیت ارائه‌شده توسط [Contrôleur / Action] که اجرا شده است را به آن منتقل می‌کند؛
    • اگر پاسخ مورد نظر از نوع jSON یا XML باشد، پاسخ انتخاب‌شده پاسخ ارائه‌شده از [Contrôleur / Action] را قالب‌بندی کرده و از طریق [3c] ارسال می‌کند. کلاینتی که قادر به پردازش این پاسخ است ممکن است یک اسکریپت کنسول پایتون یا یک اسکریپت جاوااسکریپت باشد که روی یک صفحه HTML میزبانی شده است؛
    • اگر پاسخ مورد نظر از نوع HTML باشد، پاسخ انتخاب‌شده با استفاده از کد وضعیت ارائه‌شده به آن، یکی از ویوهای HTML یا [Vuei] را انتخاب خواهد کرد. این نما برای MVC است. هر کد وضعیت با یک نما مطابقت دارد. این نما V پاسخ حاصل از اجرای [Contrôleur / Action] را نمایش خواهد داد. این [ویو] از HTML، CSS و جاوااسکریپت برای ارائه داده‌های این پاسخ استفاده می‌کند. به این داده‌ها، مدل ویو گفته می‌شود. این «M» در MVC است. کلاینت معمولاً یک مرورگر وب است؛

اکنون بیایید ارتباط بین معماری وب MVC و معماری لایه‌ای را روشن کنیم. بسته به نحوه تعریف مدل، این دو مفهوم ممکن است مرتبط باشند یا نباشند. بیایید یک برنامه وب تک‌لایه MVC را در نظر بگیریم:

Image

در مثال بالا، هر یک از اجزای [Contrôleur / Action] بخشی از لایه‌های [métier] و [dao] را در خود جای داده‌اند. در لایه [web]، در واقع یک معماری MVC وجود دارد، اما برنامه به طور کلی معماری لایه‌ای ندارد. در اینجا تنها یک لایه وجود دارد – لایه وب – که همه کارها را انجام می‌دهد.

اکنون، بیایید یک معماری وب چندلایه را در نظر بگیریم:

Image

لایه [web] را می‌توان بدون پیروی از مدل MVC پیاده‌سازی کرد. در این صورت، ما در واقع یک معماری چندلایه داریم، اما لایه وب مدل MVC را پیاده‌سازی نمی‌کند.

برای مثال، در محیط .NET، لایه [web]در بالا را می‌توان با استفاده از ASP.NET و MVC پیاده‌سازی کرد که منجر به یک معماری لایه‌ای با یک لایه [web] از نوع MVC می‌شود. پس از انجام این کار، این لایه ASP.NET MVC می‌تواند با یک لایه استاندارد ASP.NET (WebForms) جایگزین شود در حالی که بقیه حفظ می‌شوند (منطق کسب‌وکار، DAO، راننده) دقیقاً همان‌طور که هست. سپس ما یک معماری لایه‌ای داریم با یک لایه [web] که دیگر از نوع MVC نیست.

در MVC بیان کردیم که مدل M همان مدل نما V است، c.a.d – مجموعه‌ای از داده‌هایی که توسط نما V نمایش داده می‌شوند. تعریف دیگری از مدل M برای MVC ارائه شده است:

Image

بسیاری از نویسندگان معتقدند آنچه در سمت راست لایه [web] قرار دارد، مدل M از MVC را تشکیل می‌دهد. برای جلوگیری از ابهام، می‌توان به موارد زیر اشاره کرد:

  • مدل دامنه وقتی به همه چیز در سمت راست لایه [web] اشاره می‌شود؛
  • مدل نما هنگام ارجاع به داده‌های نمایش‌داده‌شده توسط یک نما V؛

در ادامه، هرگاه از مدل سخن می‌گوییم، همواره منظورمان مدل نما (view model) خواهد بود.

30.2. معماری اپلیکیشن کلاینت/سرور

برنامهٔ وب معماری زیر را خواهد داشت:

  • در [1]، وب‌سرور دو نوع کلاینت خواهد داشت:
    • در [2]، یک کلاینت کنسول که jSON و XML را با سرور تبادل خواهد کرد؛
    • در [3]، یک مرورگر که HTML را از سرور دریافت کرده و آن را نمایش می‌دهد؛
  • سرور وب [1] لایه‌های [métier] و [dao] را از نسخه‌های قبلی حفظ می‌کند؛
  • کلاینت وب [2] به‌روزرسانی خواهد شد تا نسخه‌های جدید سرویس URL اپلیکیشن وب را در نظر بگیرد؛
  • اپلیکیشن HTML که توسط مرورگر نمایش داده می‌شود، باید از ابتدا نوشته شود؛

ما برنامه را در چند مرحله توسعه خواهیم داد:

  • ما نسخه jSON سرور را توسعه خواهیم داد. ما انتهای نقاط سرویس سرور را یکی‌یکی با استفاده از کلاینت Postman آزمایش خواهیم کرد. این روش به ما امکان می‌دهد تا بدون نگرانی در مورد نماهای برنامه (=HTML) ستون فقرات وب‌سرور را بسازیم؛
  • پس از آزمایش سرور jSON با Postman، آن را با استفاده از یک کلاینت کنسول آزمایش خواهیم کرد؛
  • سپس به نسخه XML سرور می‌رویم. ما دیدیم که انتقال از jSON به XML ساده بود؛
  • در نهایت، به نسخه سرور HTML خواهیم پرداخت. ما یک معماری MVC خواهیم ساخت و ویوهایی را که باید نمایش داده شوند، تعریف خواهیم کرد. اپلیکیشن HTML با استفاده از هر دو کلاینت Postman و یک مرورگر وب استاندارد آزمایش خواهد شد؛

30.3. ساختار دایرکتوری کد سرور

Image

  • در [۱: وب سرور به طور کلی؛
  • در [2]: فعلاً، پوشه‌های [static, templates, tests_views] را که مربوط به نسخه HTML سرور هستند، نادیده می‌گیریم. خارج از این پوشه، اسکریپت اصلی [main] و پیکربندی آن را پیدا خواهیم کرد؛
  • در [3]، کنترل‌کننده‌های وب سرور. این‌ها نمونه‌هایی از کلاس خواهند بود؛
 
  • در [4]، پاسخ سرور HTTP توسط کلاس‌ها مدیریت خواهد شد؛
  • در [5]، فایل لاگ سرورهای قبلی را حفظ می‌کنیم؛

هنگامی که نسخه HTML سرور را می‌سازیم، پوشه‌های دیگری نیز درگیر خواهند شد:

 
  • در [6]، عناصر ایستا اپلیکیشن HTML؛
  • در [7]، قالب‌های برنامه از HTML، که به ویوها [9] و قطعات ویو [8] تفکیک شده‌اند؛
  • در [9]، کلاس‌هایی که مدل‌های نما را پیاده‌سازی می‌کنند؛

30.4. سرویس کاربردی URL

برای ساخت سرور وب، به شرح زیر عمل خواهیم کرد:

  • با استفاده از ویوها (نمای‌ها) از برنامه HTML، اقداماتی را که برنامه وب باید پیاده‌سازی کند، تعریف خواهیم کرد. در اینجا از ویوهای واقعی استفاده می‌کنیم، اما این ویوها می‌توانند صرفاً ویوهایی روی کاغذ باشند؛
  • بر اساس این اقدامات، ما کامپوننت‌های سرویس URL را برای برنامه HTML تعریف خواهیم کرد؛
  • ما این سرویس URL را با استفاده از سروری که jSON را ارائه می‌دهد، پیاده‌سازی خواهیم کرد. این به ما امکان می‌دهد تا چارچوب وب‌سرور را بدون نگرانی در مورد صفحات HTML که باید ارائه شوند، تعریف کنیم. ما این سرویس‌های URL را با استفاده از Postman آزمایش خواهیم کرد؛
  • سپس سرور jSON خود را با استفاده از یک کلاینت کنسول آزمایش خواهیم کرد؛
  • پس از اعتبارسنجی سرور jSON، به نوشتن برنامه HTML می‌پردازیم؛

اولین نما، نمای احراز هویت خواهد بود:

Image

  • عملی که منجر به این نمای اول می‌شود، [init-session] [1] نامیده خواهد شد؛
  • کلیک روی دکمه [Valider]، اقدام [authentifier-utilisateur] را با دو پارامتر ارسال‌شده [2-3] فعال می‌کند؛

نمایان محاسبه مالیات:

Image

  • در [1]، اقدام [authentifier-utilisateur] که منجر به این نما شد؛
  • در [2]، کلیک کردن روی دکمه [Valider] باعث اجرای اقدام [calculer-impot] با سه پارامتر ارسال‌شده [2-5] می‌شود؛
  • کلیک روی لینک [6]، اقدام [lister-simulations] را بدون هیچ پارامتری اجرا می‌کند؛
  • کلیک بر روی لینک [7]، اقدام [fin-session] را بدون هیچ پارامتری اجرا می‌کند؛

نمای سوم شبیه‌سازی‌های انجام‌شده توسط کاربر احرازشده را نشان می‌دهد:

Image

  • در [3]، اقدام [lister-simulations] که به این نما انجامید؛
  • در [2]، کلیک بر روی لینک [Supprimer]، اقدام [supprimer-simulation] را با یک پارامتر: شماره شبیه‌سازی که باید از لیست حذف شود، فعال می‌کند؛
  • کلیک بر روی لینک [3]، اقدام [afficher-calcul-impot] را بدون هیچ پارامتری فعال می‌کند که نمای محاسبه مالیات را مجدداً نمایش می‌دهد؛
  • کلیک بر روی لینک [4]، اقدام [fin-session] را بدون هیچ پارامتری فعال می‌کند؛

با این اطلاعات اولیه، می‌توانیم عملیات‌های مختلف سرویس سرور URL را تعریف کنیم:

اقدام
نقش
زمینهٔ اجرا
/init-session
برای مشخص کردن نوع (json, xml, html) پاسخ‌های مورد نظر استفاده می‌شود
درخواست GET
می‌توان در هر زمانی صادر شود
/احراز-هویت-کاربر
ورود کاربر را مجاز یا رد می‌کند
درخواست POST.
درخواست باید دارای دو پارامتر POST به نام‌های [user, password] باشد
فقط در صورتی قابل ارسال است که نوع جلسه (json, xml, html) مشخص باشد
/محاسبه-مالیات
شبیه‌سازی محاسبه مالیات را انجام می‌دهد
درخواست POST.
درخواست باید سه پارامتر POST داشته باشد: [marié, enfants, salaire]
فقط در صورتی قابل اجرا است که نوع جلسه (json, xml, html) مشخص باشد و کاربر احراز هویت شده باشد
/فهرست-شبیه‌سازی‌ها
درخواست فهرستی از شبیه‌سازی‌های انجام‌شده از ابتدای جلسه
درخواست GET.
فقط در صورتی قابل اجرا است که نوع جلسه (json, xml, html) مشخص باشد و کاربر احراز هویت شده باشد
/delete-simulation/number
حذف یک شبیه‌سازی از فهرست شبیه‌سازی‌ها
درخواست GET.
تنها در صورتی صادر می‌شود که نوع جلسه (json، xml، html) مشخص باشد و کاربر احراز هویت شده باشد
/نمایش-محاسبه-مالیات
صفحه محاسبه مالیات HTML را نمایش می‌دهد
درخواست GET.
فقط در صورتی قابل اجرا است که نوع جلسه (json، xml، html) مشخص باشد و کاربر احراز هویت شده باشد
/end-session
پایان جلسه شبیه‌سازی.
از نظر فنی، جلسه وب قدیمی حذف شده و یک جلسه جدید ایجاد می‌شود
فقط در صورتی قابل صدور است که نوع جلسه (json، xml، html) مشخص باشد و کاربر احراز هویت شده باشد

این کدهای سرویس مختلف URL برای سرور HTML و همچنین برای سرورهای jSON و XML استفاده خواهند شد. دو فایل URL تنها برای این دو سرور آخر استفاده خواهند شد: این‌ها فایل‌های URL از نسخه قبلی کلاینت/سرور وب هستند که در اینجا مجدداً استفاده می‌کنیم:

اقدام
نقش
زمینهٔ اجرا
/get-admindata
داده‌های مالیاتی مورد نیاز برای محاسبه مالیات را بازمی‌گرداند
پرس‌وجوی GET.
فقط در صورتی استفاده می‌شود که نوع جلسه json یا xml باشد. کاربر باید احراز هویت شود
/محاسبه-مالیات‌ها
مالیات را برای فهرستی از مودیان که از طریق jSON ارسال شده است، محاسبه می‌کند
درخواست GET.
فقط در صورتی استفاده می‌شود که نوع جلسه json یا xml باشد. کاربر باید احراز هویت شود

تمام کنترلرهای مرتبط با این عملیات به یک شکل پیش خواهند رفت:

  • آنها پارامترهای خود را بررسی خواهند کرد. این پارامترها در شیء یافت می‌شوند:
    • [request.path] برای پارامترهای موجود در URL به صورت [/action/param1/param2/…]؛
    • در شیء [request.form] برای مواردی که در [x-www-form-urlencoded] در داخل بدنه درخواست منتقل شده‌اند؛
    • در شیء [request.data] برای مواردی که در jSON در بدنه درخواست منتقل شده‌اند؛
  • یک کنترلر مشابه یک تابع یا متد است که اعتبار پارامترهای خود را بررسی می‌کند. با این حال، برای کنترلر کمی پیچیده‌تر است:
    • ممکن است پارامترهای مورد انتظار وجود نداشته باشند؛
    • پارامترهای بازیابی‌شده توسط کنترلر رشته‌ها هستند. اگر پارامتر مورد انتظار یک عدد باشد، آنگاه کنترلر باید بررسی کند که رشته پارامتر واقعاً نمایانگر یک عدد است؛
    • پس از آنکه تأیید شد که پارامترهای مورد انتظار موجود و از نظر دستوری صحیح هستند، باید بررسی شود که آیا آنها در زمینه اجرایی فعلی معتبر هستند یا خیر. این زمینه در جلسه (session) موجود است. مثال احراز هویت، نمونه‌ای از یک زمینه اجرایی است. برخی اقدامات باید تنها پس از احراز هویت مشتری پردازش شوند. به طور کلی، یک کلید در جلسه نشان می‌دهد که آیا این احراز هویت انجام شده است یا خیر؛
    • پس از انجام بررسی‌های پیشین، کنترل‌کننده ثانویه می‌تواند کار خود را آغاز کند. این فرآیند تأیید پارامترها بسیار مهم است. ما نمی‌توانیم در هیچ نقطه‌ای از چرخه عمر برنامه، هر چیزی را که مشتری ارسال می‌کند، بپذیریم. ما باید کنترل کامل چرخه عمر برنامه را حفظ کنیم؛
    • پس از اتمام کار، کنترل‌کننده ثانویه دیکشنری‌ای حاوی کلیدهای [action, état, réponse] را به کنترل‌کننده اصلی که آن را فراخوانده است، بازمی‌گرداند:
      • [action] عملی است که به تازگی اجرا شده است؛
      • [état] یک عدد سه‌رقمی است که نتیجه پردازش عمل را نشان می‌دهد:
    • [x00] نشان‌دهنده موفقیت‌آمیز بودن پردازش است؛
    • [x01] نشان می‌دهد که عملیات با شکست مواجه شده است؛
  • [réponse] فرهنگ نتایج در قالب {'response':object} است. این شیء بسته به عملی که پردازش شده است ساختارهای متفاوتی خواهد داشت؛

اکنون کنترل‌کننده‌های مختلف – یا به عبارت دیگر، عملیات متفاوتی که این کنترل‌کننده‌ها مدیریت می‌کنند و رفتار اپلیکیشن وب را تعیین می‌کنند – را بررسی خواهیم کرد.

30.5. پیکربندی سرور

Image

پیکربندی پایگاه داده [config_database] و لایه‌های سرور [config_layers] با نسخه‌های قبلی یکسان است. فایل [config] اکنون حاوی اطلاعات جدید است:


def configure(config: dict) -> dict:
    import os

    # مرحله ۱ ------

    # پوشه این فایل
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # مسیر ریشه
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #وابستگی‌ها
    absolute_dependencies = [
        # پوشه‌های پروژه
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # ثوابت، بازه‌ها
        f"{root_dir}/impots/v05/entities",
        # لاگر، SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # اسکریپت‌ها [config_database, config_layers]
        script_dir,
        # کنترل‌کننده‌ها
        f"{script_dir}/../controllers",
        # پاسخ‌ها HTTP
        f"{script_dir}/../responses",
        #قالب‌های نما
        f"{script_dir}/../models_for_views",
    ]

    # تنظیم syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #وابستگی‌های سرور وب

    # کنترل‌کننده‌ها
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    #پاسخ‌ها HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # قالب‌های نما
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # مرحله ۲ ------
    #پیکربندی برنامه
    config.update({
        # کاربران مجاز به استفاده از برنامه
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

        # فایل لاگ
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",

        # پیکربندی سرور SMTP
        "adminMail": {
            # سرور SMTP
            "smtp-server""localhost",
            # پورت سرور SMTP
            "smtp-port""25",
            # مدیر
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # موضوع ایمیل
            "subject""plantage du serveur de calcul d'impôts",
            # TLS را روی True تنظیم کنید اگر سرور SMTP احراز هویت را می‌طلبد، در غیر این صورت روی False تنظیم کنید
            "tls"False
        },

        #مدت زمان مکث نخ در ثانیه
        "sleep_time"0,

        # اقدامات مجاز و کنترل‌کننده‌های آن‌ها
        "controllers": {
            # ابتدای یک جلسه محاسباتی
            "init-session": InitSessionController(),
            #احراز هویت کاربر
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            # محاسبه مالیات در حالت فردی
            "calculer-impot": CalculerImpotController(),
            # محاسبه مالیات در حالت دسته‌ای
            "calculer-impots": CalculerImpotsController(),
            # فهرست شبیه‌سازی‌ها
            "lister-simulations": ListerSimulationsController(),
            # حذف یک شبیه‌سازی
            "supprimer-simulation": SupprimerSimulationController(),
            #پایان جلسه محاسبه
            "fin-session": FinSessionController(),
            #نمایش نمای محاسبه مالیات
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #بازیابی داده‌ها از مراجع مالیاتی
            "get-admindata": GetAdminDataController(),
            # کنترل‌کننده اصلی
            "main-controller": MainController()
        },

        #انواع مختلف پاسخ (json، xml، html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        #نماهای HTML و قالب‌های آن‌ها به وضعیت بازگردانده‌شده توسط کنترلر بستگی دارند
        "views"[
            {
                # نمایه احراز هویت
                "états": [
                    # /init-session موفقیت‌آمیز
                    700,
                    # /authentification-کاربر شکست
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                #نمای محاسبه مالیات
                "états"[
                    #/ احراز هویت کاربر موفق بود
                    200,
                    #/محاسبه-مالیات موفق
                    300,
                    # /محاسبه-مالیات شکست
                    301,
                    # /مشاهده-محاسبه-مالیات
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                #مشاهده فهرست شبیه‌سازی‌ها
                "états"[
                    # /لیست-شبیه‌سازی‌ها
                    500,
                    # /حذف-شبیه‌سازی
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        #مشاهده خطاهای غیرمنتظره
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        # ارسال مجدد
        "redirections"[
            {
                "états": [
                    400,  # /جلسه با موفقیت پایان یافت
                ],
                # ارسال مجدد به
                "to""/init-session/html",
            }
        ],
    }
    )

    # مرحله ۳ ------
    # پیکربندی پایگاه داده
    import config_database
    config["database"] = config_database.configure(config)

    # مرحله ۴ ------
    # مصداق‌سازی لایه‌های کاربردی
    import config_layers
    config['layers'] = config_layers.configure(config)

    # ذخیره پیکربندی
    return config
  • تا خط ۴۱، محتوا استاندارد است؛
  • خطوط ۴۳–۶۶: تا خط ۴۳، مسیر پایتون سرور تعریف می‌شود. سپس وابستگی‌های پروژه را می‌توان وارد کرد:
    • خطوط ۴۵–۵۵: لیست کنترل‌کننده‌ها؛
    • خطوط ۵۷–۶۰: لیست پاسخ‌ها HTTP;
    • خطوط ۶۲–۶۶: فهرست قالب‌های نما؛
  • خطوط ۶۸–۱۸۹: پیکربندی برنامه با مجموعه‌ای از ثابت‌ها؛
    • خطوط ۷۱–۹۸: ما از نسخه‌های قبلی با این خطوط آشنا هستیم؛
    • خطوط ۱۰۱–۱۲۲: فرهنگ لغت کنترل‌کننده‌ها:
      • کلیدها نام اکشن‌ها هستند؛
      • مقادیر، نمونه‌ای از کنترلری است که مسئول رسیدگی به آن عمل است. هر کنترلر به صورت یک نمونه واحد (singleton) ایجاد می‌شود. همان نمونه توسط نخ‌های مختلف سرور اجرا خواهد شد. بنابراین باید در مورد داده‌های مشترکی که هر کنترلر ممکن است بخواهد آن را تغییر دهد، دقت کرد؛
    • خطوط ۱۲۵–۱۲۹: فرهنگ لغت سه پاسخ ممکن HTTP:
      • کلیدها نوع پاسخی هستند که توسط کلاینت درخواست شده است (jSON, xml, html);
      • مقادیر، یک نمونه از پاسخ HTTP هستند. هر ژنراتور پاسخ به صورت یک نمونه واحد (singleton) ایجاد می‌شود. همان ژنراتور توسط نخ‌های مختلف سرور اجرا خواهد شد. بنابراین باید در مورد داده‌های مشترک که هر ژنراتور ممکن است بخواهد آن را تغییر دهد، دقت کرد؛
    • خطوط ۱۳۲–۱۸۶: پیکربندی ویوهای HTML. فعلاً، این خطوط را نادیده می‌گیریم؛
  • خطوط ۱۹۱–۲۰۲: ما قبلاً در نسخه‌های قبلی با این خطوط مواجه شده‌ایم؛

30.6. مسیر یک درخواست مشتری در داخل سرور

Image

ما مسیر درخواست مشتری را که به سرور می‌رسد تا پاسخ HTTP که بازگردانده می‌شود، دنبال خواهیم کرد. این مسیر از طریق سرور MVC دنبال می‌شود.

30.6.1. اسکریپت [main]

Image

اسکریپت [main] از جهات بسیاری با نسخه‌های قبلی یکسان است. با این حال، ما آن را به طور کامل ارائه می‌دهیم تا اطمینان حاصل کنیم که کار را با شروعی صحیح آغاز می‌کنیم:


# منتظر یک پارامتر MySQL یا PostgreSQL
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#پیکربندی برنامه
import config
config = config.configure({'sgbd': sgbd})

#وابستگی‌ها
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os

# ارسال ایمیل به مدیر
def send_adminmail(config: dict, message: str):
    # ارسال ایمیل به مدیر برنامه
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

#بررسی فایل لاگ
logger = None
erreur = False
message_erreur = None
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # لاگ کنسول
    print(f"L'erreur suivante s'est produite : {exception}")
    #ثبت خطا
    erreur = True
    message_erreur = f"{exception}"
#لاگ‌گیر در پیکربندی ذخیره می‌شود
config['logger'] = logger
# مدیریت خطا
if erreur:
    # ایمیل به مدیر
    send_adminmail(config, message_erreur)
    # برنامه خاتمه می‌یابد
    sys.exit(1)

# فایل لاگ راه‌اندازی
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#بازیابی داده‌ها از مراجع مالیاتی
erreur = False
try:
    #داده‌های مدیریتی به صورت فقط-خواندنی در سطح برنامه خواهند بود
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    # لاگ موفقیت
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # خطا ثبت شد
    erreur = True
    # لاگ خطا
    log = f"L'erreur suivante s'est produite : {ex}"
    #کنسول
    print(log)
    # فایل گزارش
    logger.write(f"{log}\n")
    # ایمیل به مدیر
    send_adminmail(config, log)

# رشته اصلی دیگر به لاگ‌گیر نیاز ندارد
logger.close()

# اگر خطایی رخ داده باشد، فرآیند متوقف می‌شود
if erreur:
    sys.exit(2)

# برنامه Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
#کلید مخفی جلسه
app.secret_key = os.urandom(12).hex()

# کنترل‌کنندهٔ جلویی
def front_controller() -> tuple:
    # پردازش درخواست
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    # ارسال مجدد به /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # اجرای کنترلر مرتبط با اکشن
    return front_controller()

# احراز هویت کاربر
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#محاسبه-مالیات
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# فهرست-شبیه‌سازی‌ها
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# حذف-شبیه‌سازی
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#پایان-جلسه
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#نمایش-محاسبه-مالیات
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    #کنترلر مرتبط با اقدام اجرا می‌شود
    return front_controller()

# فقط اصلی
if __name__ == '__main__':
    # سرور راه‌اندازی می‌شود
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • خطوط ۱–۹۲: تمام این خطوط قبلاً پوشش داده شده و توضیح داده شده‌اند؛
  • خط ۹۲: سرور یک جلسه را مدیریت خواهد کرد. بنابراین به یک کلید مخفی نیاز داریم. برای هر کاربر، دو مورد اطلاعات را در جلسه ذخیره خواهیم کرد:
    • اینکه آیا کاربر با موفقیت احراز هویت شده است؛
    • هر زمان که کاربر محاسبه مالیات را انجام دهد، نتایج آن محاسبه در لیستی قرار داده خواهد شد که آن را «فهرست شبیه‌سازی کاربر» می‌نامیم. این فهرست در جلسه (session) ذخیره خواهد شد؛
  • خطوط 100–151: لیست توابع سمت سرور URL. توابع مرتبط به عنوان یک فیلتر عمل می‌کنند: هر تابع URL که در این لیست وجود نداشته باشد، توسط سرور Flask با خطای [404 NOT FOUND] رد خواهد شد. پس از عبور از این فیلتر، درخواست به طور سیستماتیک به یک «فرانت کنترلر» (Front Controller) ارسال می‌شود که توسط تابع [front_controller] در خطوط 94–98 پیاده‌سازی شده است و به زودی در مورد آن بحث خواهیم کرد؛
  • خطوط 100–103: رسیدگی به مسیر [/]. نقطه ورود به برنامه وب، URL در خط ۱۰۷ خواهد بود. بنابراین، در خط ۱۰۳، ما کلاینت را به این URL هدایت می‌کنیم:
  • تابع [url_for] در خط ۱۸ وارد شده است. این تابع در اینجا دو پارامتر دارد:
      • پارامتر اول نام یکی از توابع مسیریابی است، در این مورد، آنی که در خط ۱۰۷ قرار دارد. ما می‌توانیم ببینیم که این تابع منتظر یک پارامتر به نام [type_response] است که نوع پاسخ (json، xml، html) درخواستی توسط کلاینت را مشخص می‌کند؛
      • پارامتر دوم نام پارامتر از خط ۱۰۷، [type_response را می‌گیرد و یک مقدار به آن اختصاص می‌دهد. اگر پارامترهای دیگری وجود داشتند، این عملیات برای هر یک از آنها تکرار می‌شد؛
      • این تابع، URL مرتبط با تابع مشخص‌شده توسط دو پارامتر ارائه‌شده به آن را برمی‌گرداند. در اینجا، این تابع، URL از خط ۱۰۶ را برمی‌گرداند، که در آن پارامتر با مقدار خود، [/init-session/html]، جایگزین می‌شود؛
    • تابع [redirect] در خط ۱۸ وارد شده است. نقش آن ارسال یک هدر تغییر مسیر HTTP به کلاینت است:
      • پارامتر اول، URL است که کلاینت باید به آن هدایت شود؛
      • پارامتر دوم کد وضعیت پاسخ HTTP است که به کلاینت ارسال می‌شود. کد [status.HTTP_302_FOUND] معادل یک هدایت HTTP است؛

تابع [front_controller] در خطوط ۹۴–۹۸ پردازش اولیه درخواست مشتری را انجام می‌دهد:


#کنترل‌کنندهٔ جلویی
def front_controller() -> tuple:
    # پردازش درخواست
    logger = None
    try:
        # لاگ‌گیر
        logger = Logger(config["logsFilename"])
        # ذخیره شده در پیکربندی مرتبط با نخ
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # درخواست را ثبت کنید
        logger.write(f"[ front_controller] requête : {request}\n")
        # در صورت درخواست، نخ متوقف می‌شود
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #وقفه به‌صورت تصادفی انتخاب می‌شود، به‌طوری که برخی رشته‌ها قطع می‌شوند و برخی دیگر قطع نمی‌شوند
            aléa = randint(01)
            if aléa == 1:
                # ثبت قبل از مکث
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #مکث
                time.sleep(sleep_time)
        # درخواست به کنترل‌کننده اصلی ارسال می‌شود
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # ثبت نتیجه ارسال‌شده به کلاینت
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # آیا خطای مرگبار رخ داده است؟
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # ایمیلی برای مدیر برنامه ارسال می‌شود
            send_adminmail(config, log)
        # نوع پاسخ مورد نظر را تعیین کنید
        if session.get('typeResponse'is None:
            # نوع جلسه هنوز مشخص نشده است – این jSON خواهد بود
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #پاسخ قابل ارسال ساخته می‌شود
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #ارسال پاسخ
        return response, status_code
    except BaseException as erreur:
        #این یک خطای غیرمنتظره است – خطا در صورت امکان ثبت می‌شود
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #در حال آماده‌سازی پاسخ برای مشتری است
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        #یک پاسخ در jSON ارسال می‌شود
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #اگر فایل لاگ باز شده باشد، بسته می‌شود
        if logger:
            logger.close()
  • خطوط ۱–۵۷: ما با این کد آشنا هستیم. این، برای مثال، کد تابع با نام [main] در اسکریپت [main] از نسخه قبلی بود. تنها یک نکته وجود دارد: کنترلر مورد استفاده در خطوط ۲۵–۲۶:
  • خط 25: نمونهٔ کنترلر با نام [main-controller] از پیکربندی بازیابی می‌شود. این خطوط زیر هستند:

    #وابستگی‌های سرور وب
    #کنترل‌کننده‌ها
    
    from MainController import MainController

     # اقدامات مجاز و کنترل‌کننده‌های آن‌ها
        "controllers": {
            ,
            # کنترل‌کنندهٔ اصلی
            "main-controller": MainController()
        },
  • (ادامه)
    • خط ۱۰ بالا؛ توجه کنید که یک نمونه کلاس بازیابی می‌شود؛
  • خط ۲۶: به کنترلر [MainController] دستور داده می‌شود که درخواست را پردازش کند؛
  • خطوط ۳۰–۴۵: پاسخ بازگردانده شده توسط کنترل‌کننده [MainController] به کلاینت ارسال می‌شود. کمی بعد به این خطوط باز خواهیم گشت؛

نقش تابع [front_controller] و سپس کلاس [MainController] انجام وظایف مشترک در همه درخواست‌ها است:

در نمودار بالا، ما هنوز در فاز ۱ پردازش درخواست هستیم. کنترل‌کننده اصلی [MainController] با مرحله ۱ ادامه خواهد داد.

30.6.2. کنترل‌کننده اصلی [MainController]

کنترل‌کننده اصلی [MainController] کار آغازشده توسط تابع [front_controller] را ادامه می‌دهد:

تمام کنترل‌کننده‌ها رابط زیر را پیاده‌سازی می‌کنند: [InterfaceController] [2]:

Image


from abc import ABC, abstractmethod

from werkzeug.local import LocalProxy

class InterfaceController(ABC):

    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • رابط [InterfaceController] تنها متد [execute] را در خط ۸ تعریف می‌کند. این متد سه پارامتر می‌گیرد:
    • [request]: درخواست مشتری؛
    • [session]: جلسهٔ مشتری؛
    • [config]: پیکربندی برنامه؛

متد [execute] یک تپل دو عنصری بازمی‌گرداند:

  • اولین مورد، دیکشنری نتایج در قالب {'action': action, 'status': status, 'response': results} است؛
  • عنصر دوم کد وضعیت HTTP است که باید به کلاینت بازگردانده شود؛

کنترل‌کننده اصلی [MainController] [1] رابط [InterfaceController] را به شرح زیر پیاده‌سازی می‌کند:


# وارد کردن وابستگی‌ها

from flask_api import status
from werkzeug.local import LocalProxy

#کنترل‌کننده‌های وب‌اپلیکیشن
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #بازیابی عناصر مسیر
        params = request.path.split('/')
        action = params[1]

        # خطاها
        erreur = False
        # نوع جلسه باید قبل از انجام برخی اقدامات مشخص باشد
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            #خطا ثبت می‌شود
            résultat = {"action": action, "état"101,
                        "réponse"["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        # برای انجام برخی عملیات، باید احراز هویت شوید
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
            #خطا ثبت شد
            résultat = {"action": action, "état"101,
                        "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        #آیا خطایی وجود دارد؟
        if erreur:
            # یک پیام خطا بازگردانده می‌شود
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            # اجرای کنترلر مرتبط با اقدام
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

کنترل‌کننده [MainController] بررسی‌های اولیه را برای اعتبار درخواست انجام می‌دهد.

  • خطوط ۱۱–۱۳: کنترل‌کننده با بازیابی عملی که توسط مشتری درخواست شده است، شروع می‌کند. شایان ذکر است که سرویس‌های URL به شکل [/action/param1/param2/…] هستند و این URL در داخل [request.path] قرار دارد؛
  • خطوط 17–23: عمل [init-session] برای مقداردهی اولیه نوع پاسخ (json, xml, html) درخواستی توسط کلاینت استفاده می‌شود. این اطلاعات در جلسه تحت کلید [typeRéponse] ذخیره می‌شود. بنابراین، اگر اقدام، [init-session] نباشد، جلسه باید شامل کلید [typeRéponse] باشد؛ در غیر این صورت، درخواست نامعتبر است؛
  • خطوط ۲۱–۲۲: ساختار نتیجه‌ای که توسط هر کنترلر بازگردانده می‌شود، در این مورد یک نتیجه خطا:
    • [action]: نام اقدام فعلی است. این به ما امکان می‌دهد تا هنگام ثبت نتیجه درخواست، نام آن را بازیابی کنیم؛
    • [état]: یک کد وضعیت سه‌رقمی است:
        • [x00] برای موفقیت؛
        • [x01] برای خطا؛
  • [réponse]: پاسخ به درخواست است. ماهیت آن برای هر درخواست خاص است؛
  • خطوط ۲۴–۳۰: عمل [authentifier-utilisateur] برای احراز هویت کاربر استفاده می‌شود. در صورت موفقیت، یک کلید [user=True] در جلسه کاربر قرار می‌گیرد. برخی از عملیات سرویس URL تنها برای کاربر احراز هویت‌شده قابل دسترسی هستند. این چیزی است که در اینجا بررسی می‌شود؛
  • خط ۲۶: تنها عملیات [init-session] و [authentifier-utilisateur] می‌توانند توسط کاربری که هنوز احراز هویت نشده است، انجام شوند؛
  • خطوط ۲۸–۲۹: پاسخی که در صورت بروز خطا ارسال می‌شود؛
  • خطوط ۳۲–۳۴: اگر هر یک از دو خطای قبلی رخ داده باشد، آنگاه پاسخ خطا با کدهای وضعیت HTTP، 400، BAD و REQUEST به کلاینت ارسال می‌شود؛
  • خطوط ۳۵–۳۹: اگر هیچ خطایی رخ نداده باشد، کنترل به کنترلری که مسئول رسیدگی به اقدام فعلی است، واگذار می‌شود. نمونهٔ آن در پیکربندی برنامه یافت می‌شود؛

کلاس [MainController] کار تابع [front_controller] را ادامه می‌دهد: این دو با هم همه چیزهایی را که می‌توان از پردازش درخواست استخراج کرد، مدیریت می‌کنند و تا آخرین لحظه صبر می‌کنند تا درخواست را به یک کنترل‌کنندهٔ خاص ارسال کنند. تقسیم کد بین تابع [front_controller] و کلاس [MainController] کاملاً سلیقه‌ای است. در اینجا، می‌خواستم کار انجام‌شده در نسخه قبلی را حفظ کنم: تابع [front_controller] قبلاً با نام [main] وجود داشت. در عمل، می‌توانست:

  • همه چیز را در تابع [front_controller] قرار داد و کلاس [MainController] را حذف کرد؛
  • همه چیز را در کلاس [MainController] قرار داد و تابع [front_controller] را حذف کرد. من این راه‌حل را انتخاب می‌کردم زیرا این مزیت را دارد که کد اسکریپت اصلی [main] را ساده‌تر می‌کند؛

30.7. پردازش اختصاصی اقدام

بیایید به معماری برنامه MVC بازگردیم:

Image

ما هنوز در مرحلهٔ ۱ بالا هستیم. اگر هیچ خطایی رخ نداده باشد، مرحلهٔ ۲ آغاز خواهد شد. درخواست به کنترل‌کننده‌ای که مخصوص عملی است که در درخواست خواسته شده، ارسال شده است. فرض کنیم این عمل [/init-session] است که توسط مسیر زیر تعریف شده است:


# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # کنترلر مرتبط با اقدام اجرا می‌شود
    return front_controller()

این اقدام به یک کنترل‌کننده در پیکربندی [config] مرتبط است:


        # اقدامات مجاز و کنترل‌کننده‌های آن‌ها
        "controllers": {
            # ابتدای یک جلسه محاسباتی
            "init-session": InitSessionController(),
            
        },

بنابراین کنترلر [InitSessionController] (خط ۴) کنترل را بر عهده می‌گیرد. کد آن به شرح زیر است:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # بازیابی عناصر مسیر
        dummy, action, type_response = request.path.split('/')

        # بدون خطا در شروع
        erreur = False
        #بررسی نوع پاسخ
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état"701,
                        "réponse"[f"paramètre [type={type_response}] invalide"]}
        # اگر خطایی وجود نداشته باشد
        if not erreur:
            # تنظیم نوع جلسه در جلسه Flask
            session['typeResponse'] = type_response
            résultat = {"action": action, "état"700,
                        "réponse"[f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, status.HTTP_400_BAD_REQUEST
  • خط ۶: مانند سایر کنترل‌کننده‌ها، کنترل‌کننده [InitSessionController] رابط [InterfaceController] را پیاده‌سازی می‌کند؛
  • خط ۱۰: کنترل‌کننده URL از نوع [/init-session/type_response] است. ما اقدام [init-session] و نوع پاسخ مورد نظر را بازیابی می‌کنیم؛
  • خط ۱۵: نوع پاسخ مورد نظر تنها می‌تواند یکی از موارد فهرست‌شده در پیکربندی پاسخ باشد:

        #انواع مختلف پاسخ (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • در غیر این صورت، پاسخ خطای 701 تولید می‌شود (خط 17)؛
  • خطوط ۲۰–۲۵: جایی که نوع پاسخ مورد نظر معتبر است؛
  • خط ۲۲: نوع پاسخ مورد نظر در جلسه ذخیره می‌شود. این به این دلیل است که ما باید آن را برای درخواست‌های بعدی به خاطر بسپاریم؛
  • خطوط ۲۳–۲۴: یک پاسخ موفقیت ۷۰۰ آماده می‌شود؛
  • خط ۲۵: پاسخ موفقیت به کد فراخوانی بازگردانده می‌شود؛
  • خط ۲۷: اگر خطایی رخ داده باشد، پاسخ خطا به کد فراخوانی بازگردانده می‌شود؛

30.8. تولید پاسخ HTTP سرور

بیایید به معماری MVC برنامه بازگردیم:

Image

ما همین‌اکنون به مراحل ۱ و ۲ نگاهی انداختیم. با سه کد وضعیت مواجه شدیم:

  • 700: /init-session با موفقیت انجام شد؛
  • 701: /init-session ناموفق بود؛
  • ۱۰۱: درخواست نامعتبر، یا به این دلیل که جلسه (session) راه‌اندازی نشده است یا به این دلیل که کاربر احراز هویت نشده است؛

بیایید بررسی کنیم که چگونه پاسخ سرور در مرحله ۳ فوق به کلاینت ارسال می‌شود. این کار در تابع [front_controller] درون اسکریپت [main] انجام می‌شود:


# کنترل‌کنندهٔ جلویی
def front_controller() -> tuple:
    #پردازش درخواست
    logger = None
    try:
        #ثبت گزارش
        logger = Logger(config["logsFilename"])
        # ذخیره آن در پیکربندی مرتبط با نخ
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # درخواست را ثبت کنید
        logger.write(f"[ front_controller] requête : {request}\n")
        # در صورت درخواست، نخ متوقف می‌شود
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #وقفه به‌صورت تصادفی تنظیم می‌شود، به‌طوری که برخی رشته‌ها قطع می‌شوند و برخی دیگر قطع نمی‌شوند
            aléa = randint(01)
            if aléa == 1:
                #ثبت قبل از مکث
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #مکث
                time.sleep(sleep_time)
        # درخواست به کنترل‌کننده اصلی ارسال می‌شود
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #نتیجه ارسال‌شده به کلاینت ثبت می‌شود
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #آیا خطای مرگبار رخ داده است؟
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # ایمیلی برای مدیر برنامه ارسال می‌شود
            send_adminmail(config, log)
        # نوع پاسخ مورد نظر را تعیین کنید
        if session.get('typeResponse'is None:
            # نوع جلسه هنوز مشخص نشده است – این خواهد بود jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #پاسخ قابل ارسال ساخته می‌شود
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #ارسال پاسخ
        return response, status_code
    except BaseException as erreur:
        #این یک خطای غیرمنتظره است – خطا در صورت امکان ثبت می‌شود
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #پاسخ را برای مشتری آماده کنید
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        #یک پاسخ در jSON ارسال می‌شود
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # در صورت باز بودن، فایل لاگ را ببندید
        if logger:
            logger.close()
  • اکنون در خط ۲۶ هستیم: کنترلر اصلی پاسخ خطای خود را بازگردانده است؛
  • خطوط ۲۷–۲۹: صرف‌نظر از پاسخ کنترلر اصلی (موفقیت یا شکست)، این پاسخ در فایل لاگ ثبت می‌شود؛
  • خطوط ۳۰–۳۳: همانند نسخه‌های قبلی، اگر وضعیت HTTP برابر [500 INTERNAL SERVER ERROR] باشد، ایمیلی حاوی گزارش خطا برای مدیر برنامه ارسال می‌شود؛
  • خطوط ۳۴–۳۹: پاسخ HTTP ارسال می‌شود و نتیجه‌ای که توسط کنترلر بازگردانده شده در بدنه این پاسخ قرار می‌گیرد. ما باید بدانیم که کلاینت این پاسخ را در کدام فرمت (json, xml, html) می‌خواهد. ما سشن را برای نوع پاسخ مورد نظر بررسی می‌کنیم. اگر این نوع موجود نباشد، ما به صورت دلخواه این نوع را روی jSON تنظیم می‌کنیم؛
  • خطوط ۴۰–۴۳: پاسخ HTTP ساخته می‌شود؛

در فایل پیکربندی، هر نوع پاسخ (json, xml, html) با یک نمونه کلاس مرتبط شده است:


        #انواع مختلف پاسخ (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

کلاس‌های پاسخ در پوشه [responses] در ساختار دایرکتوری سرور قرار دارند:

Image

هر کلاس پاسخ، رابط زیر را پیاده‌سازی می‌کند: [InterfaceResponse]


from abc import ABC, abstractmethod

from flask.wrappers import Response
from werkzeug.local import LocalProxy

class InterfaceResponse(ABC):

    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        pass
  • خطوط ۸–۱۱: رابط [InterfaceResponse] یک متد واحد، [build_http_responseرا با پارامترهای زیر تعریف می‌کند:
    • [request, session, config]: این‌ها پارامترهای دریافت‌شده توسط کنترل‌کننده اقدام هستند؛
    • [résultat, status_code]: این‌ها نتایجی هستند که توسط پردازشگر اقدام تولید می‌شوند؛

اکنون پاسخ jSON را ارائه می‌دهیم. این پاسخ توسط کلاس زیر [JsonResponse] تولید می‌شود:


import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # نتایج: فرهنگ نتایج
        # status_code: کد وضعیت پاسخ HTTP

        # پاسخ به صورت HTTP بازگردانده می‌شود
        response = make_response(json.dumps(résultat, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

ما با این کد آشنا هستیم، زیرا قبلاً بارها با آن مواجه شده‌ایم. این کد مربوط به تابع [json_response] در ماژول [myutils] است.

30.9. آزمایش‌های اولیه

در کدی که بررسی کردیم، با سه کد وضعیت مواجه شدیم:

  • 700: /init-session موفق بود؛
  • 701: /init-session ناموفق بود؛
  • ۱۰۱: درخواست نامعتبر، یا به این دلیل که جلسه راه‌اندازی نشده است یا به این دلیل که کاربر احراز هویت نشده است؛

ما سعی خواهیم کرد این وضعیت‌ها را با استفاده از یک جلسه به نام jSON تحریک کنیم.

  • وب‌سرور SGBD و سرور ایمیل را راه‌اندازی می‌کنیم؛
  • یک کلاینت Postman را راه‌اندازی می‌کنیم؛

آزمون ۱

ابتدا، یک درخواست نامعتبر را به دلیل راه‌اندازی نشدن جلسه نشان می‌دهیم:

Image

  • [1-2]: پرس‌وجوی [POST http://localhost:5000/authentifier-utilisateur] یک مسیر معتبر است:

# احراز-هویت-کاربر
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #کنترلر مرتبط با اکشن اجرا می‌شود
    return front_controller()

اما این تنها در صورتی پذیرفته می‌شود که جلسه قبلاً با استفاده از اقدام [/init-session] آغاز شده باشد.

درخواست را اجرا کنیم و نتیجه ارسال‌شده توسط سرور را ببینیم:

Image

  • [1-2]: ما یک پاسخ jSON دریافت کردیم. هنگامی که نوع پاسخ هنوز توسط کلاینت مشخص نشده باشد، سرور برای پاسخ دادن از jSON استفاده می‌کند؛
  • [3-5]: فرهنگ لغت jSON از پاسخ؛
    • [action]: عملی که اجرا شد؛
    • [état]: کد وضعیت پاسخ. کد [x01] نشان‌دهنده یک خطا است؛
    • [réponse]: مختص هر اقدام است. در اینجا، حاوی یک پیام خطا است؛

اکنون یک جلسه را با نوع پاسخ نادرست راه‌اندازی کنیم:

Image

  • [1-2] یک مسیر معتبر است:

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

بنابراین وارد خط لوله پردازش درخواست سرور MVC خواهد شد. با این حال، احتمالاً در این فرایند رد می‌شود زیرا نوع جلسه درخواستی نادرست است.

پاسخ به شرح زیر است:

Image

  • در [4]، یک کد خطا [x01]؛
  • در [5]، توضیح خطا؛

اکنون، بیایید یک جلسه را با jSON آغاز کنیم:

Image

پاسخ به شرح زیر است:

Image

اکنون، بیایید یک جلسه XML را آغاز کنیم. پاسخ jSON با پاسخی XML که توسط کلاس زیر [XmlResponse] تولید شده است، جایگزین خواهد شد:


import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # نتایج: دیکشنری نتایج
        #status_code: کد وضعیت پاسخ HTTP

        # نتیجه: دیکشنری که باید به رشته تبدیل شود XML
        xml_string = xmltodict.unparse({"root": résultat})
        # پاسخ به صورت HTTP بازگردانده می‌شود
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

این کدی است که با آن آشنا هستیم: کد تابع [xml_response] از ماژول مشترک [myutils].

ما یک جلسه XML را آغاز می‌کنیم:

Image

پاسخ سرور سپس به شرح زیر است:

Image

ما همان پاسخ را مانند jSON دریافت می‌کنیم، اما این بار پاسخ به صورت XML قالب‌بندی شده است.

30.10. عمل [authentifier-utilisateur]

عمل [authentifier-utilisateur] برای احراز هویت کاربری که قصد استفاده از برنامه محاسبه مالیات را دارد، به کار می‌رود. مسیر آن در اسکریپت [main] به صورت زیر تعریف شده است:


#احراز هویت کاربر
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # اجرای کنترلر مرتبط با اکشن
    return front_controller()

سرور منتظر دو پارامتر POST است:

  • [user]: شناسهٔ کاربر؛
  • [password]: رمز عبورشان؛

فهرست کاربران مجاز در پیکربندی [config] تعریف شده است:


        # کاربران مجاز به استفاده از برنامه
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

در اینجا، ما یک لیست داریم که شامل یک عنصر واحد است.

عمل [authentifier-utilisateur] توسط کنترلر زیر [AuthentifierUtilisateurController] پردازش می‌شود:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class AuthentifierUtilisateurController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #بازیابی عناصر مسیر
        dummy, action = request.path.split('/')

        #پارامترها برای POST
        post_params = request.form
        #کد وضعیت پاسخ برای HTTP
        status_code = None
        # در ابتدا هیچ خطایی وجود ندارد
        erreur = False
        erreurs = []
        # یک POST با دو پارامتر مورد نیاز است
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            #پارامترها از POST بازیابی می‌شوند
            #پارامتر [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            #پارامتر [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            # خطا؟
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        # خطا؟
        if not erreur:
            # در حال بررسی اعتبار نام کاربری و رمز عبور
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            # یافت شد؟
            if not trouvé:
                #خطا ثبت می‌شود
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                # جلسه به‌روزرسانی می‌شود تا نشان دهد کاربر پیدا شده است
                session["user"] = True
        # تمام
        if not erreur:
            # بازگشت بدون خطا
            résultat = {"action": action, "état"200"réponse"f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            # بازگشت با خطا
            return {"action": action, "état"201"réponse": erreurs}, status_code

  • خط ۱۴: پارامترها از POST بازیابی می‌شوند؛
  • خط ۱۹: فهرست خطاهای یافت‌شده در درخواست؛
  • خطوط ۲۰–۲۴: بررسی انجام می‌شود تا اطمینان حاصل شود که دو پارامتر واقعاً ارسال شده‌اند؛
  • خطوط ۲۷–۳۱: بررسی وجود پارامتر [users]؛
  • خطوط ۳۲–۳۶: بررسی وجود پارامتر [password]؛
  • خطوط ۳۸–۳۹: اگر پارامترهای ارسال‌شده نادرست باشند، یک پاسخ آماده کنید: HTTP 400 BAD REQUEST;
  • خطوط ۴۰–۵۸: ما بررسی می‌کنیم که اعتبارنامه‌های [user, password] متعلق به کاربری باشند که مجاز به استفاده از برنامه است؛
  • خطوط ۵۱–۵۵: اگر کاربر (نام کاربری، رمز عبور) مجاز به استفاده از برنامه نباشد، پاسخی آماده می‌شود: HTTP 401 UNAUTHORIZED;
  • خطوط ۵۶–۵۸: اگر مجاز باشند، آنگاه با استفاده از کلید [user] در جلسه رکوردی ثبت می‌شود تا نشان دهد که احراز هویت شده‌اند؛

توجه داشته باشید که اگر کاربر با اعتبارنامه‌های [identifiants1] احراز هویت شده باشد و در احراز هویت با اعتبارنامه‌های [identifiants2] ناموفق باشد، همچنان با اعتبارنامه‌های [identifiants1] احراز هویت شده باقی می‌ماند.

بیایید چند تست Postman را اجرا کنیم:

  • سرور وب، سرور SGBD و سرور ایمیل را راه‌اندازی کنید؛
  • با استفاده از کلاینت Postman:
    • یک جلسه را با jSON آغاز کنید؛
    • سپس احراز هویت می‌کنیم؛

در اینجا چند سناریوی مختلف آورده شده است.

مورد ۱: POST بدون پارامترهای ارسال‌شده

Image

  • در [3-5]، POST فاقد بدنه است؛

نتیجه درخواست به شرح زیر است:

Image

  • در [2]، ما یک پاسخ HTTP 400 BAD REQUEST دریافت کردیم؛
  • برای [5]، کد خطا [201] دریافت شد؛

مورد ۲: POST با اعتبارنامه‌های نادرست

Image

  • در [6]، اعتبارنامه‌ها نادرست هستند؛

سرور پاسخ زیر را ارسال می‌کند:

Image

  • در [2]، پاسخ HTTP 401 UNAUTHORIZED;
  • برای [5]، پاسخ خطا؛

مورد ۲: POST با اعتبارنامه‌های صحیح

Image

  • به [6]، اعتبارنامه‌ها صحیح هستند؛

پاسخ سرور به شرح زیر است:

  • در [2]، پاسخی از HTTP با کد ۲۰۰ و OK؛ Image
  • در [5]، پاسخ موفقیت؛

30.11. اقدام [calculer_impot]

عمل [calculer_impot] برای محاسبه مالیات مودی استفاده می‌شود. مسیر آن در اسکریپت [main] به شرح زیر تعریف شده است:


#محاسبه-مالیات
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # کنترل‌کننده‌ی مرتبط با اقدام اجرا می‌شود
    return front_controller()

سرور انتظار سه پارامتر POST را دارد:

  • [marié]: بله / نه;
  • [enfants]: تعداد فرزندان مؤدی؛
  • [salaire]: حقوق سالانه مودی؛

کنترل‌کننده [CalculerImpotController] اقدام [calculer_impot] را پردازش می‌کند:


import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #عناصر مسیر را بازیابی می‌کند
        dummy, action = request.path.split('/')

        #در ابتدا بدون خطا
        erreur = False
        erreurs = []
        #پارامترها برای POST
        post_params = request.form
        #یک POST با سه پارامتر مورد نیاز است
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        # تحلیل پارامترهای ارسال‌شده
        if not erreur:
            #پارامتر مطابقت داد
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                # آیا پارامتر معتبر است؟
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            #پارامتر [enfants]
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                #آیا این پارامتر معتبر است؟
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            #پارامتر حقوق
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                #آیا این پارامتر معتبر است؟
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        # خطا؟
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état"301"réponse": erreurs}
            # بازگرداندن نتیجه
            return résultat, status_code

        #محاسبه مالیات
        #بازیابی لایه [métier] و فرهنگ لغت [adminData]
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        #محاسبه مالیات
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        # شماره شبیه‌سازی
        id_simulation = session.get('id_simulation'0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        #نتیجه به صورت یک مدخل فرهنگ لغت در جلسه وارد می‌شود: TaxPayer
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        # نتیجه به فهرست شبیه‌سازی‌های انجام‌شده اضافه می‌شود و این فهرست در جلسه ذخیره می‌شود
        simulations = session.get("simulations"[])
        simulations.append(simulation)
        session["simulations"] = simulations
        # نتیجه
        résultat = {"action": action, "état"300"réponse": simulation}
        status_code = status.HTTP_200_OK

        # نتیجه بازگردانده می‌شود
        return résultat, status_code
  • خط ۱۳: بازیابی نام اقدام فعلی؛
  • خط ۱۷: خطاها در یک لیست جمع‌آوری می‌شوند؛
  • خط ۱۹: پارامترهای ارسال‌شده بازیابی می‌شوند. این‌ها در فرم [x-www-form-urlencoded] ارسال شده‌اند، به همین دلیل در [request.form] بازیابی می‌شوند. اگر آن‌ها به صورت jSON ارسال شده بودند، ما آن‌ها را به صورت [request.data] بازیابی می‌کردیم؛
  • خطوط ۲۱–۲۴: ما بررسی می‌کنیم که واقعاً سه پارامتر ارسال شده‌اند؛
  • خطوط ۲۷–۳۶: بررسی وجود و اعتبار پارامتر ارسال‌شده [marié];
  • خطوط ۳۷–۴۸: بررسی می‌کنیم که پارامتر ارسال‌شده [enfants] موجود و معتبر است؛
  • خطوط ۴۹–۶۰: بررسی می‌کنیم که پارامتر ارسال‌شده [salaire] موجود و معتبر است؛
  • خطوط ۶۲–۶۶: اگر خطایی رخ داده باشد، یک پاسخ خطا با کد وضعیت [301] ارسال می‌شود؛
  • خطوط ۶۹–۷۱: اگر خطایی رخ نداده باشد، سیستم برای محاسبه مالیات آماده می‌شود. برای این کار،
    • خط ۷۰: یک مرجع از لایه [métier] بازیابی می‌شود؛
    • خط ۷۱: داده‌های مرجع مالیاتی از پیکربندی سرور بازیابی می‌شود؛
  • خطوط ۷۲–۷۴: مالیات مودی محاسبه می‌شود؛
  • خطوط ۷۵–۷۷: ما تعداد محاسبات مالیاتی انجام‌شده توسط کاربر را می‌شماریم؛
    • خط ۷۶: شماره آخرین محاسبه انجام‌شده از جلسه بازیابی می‌شود. در اینجا، نتیجه یک محاسبه با نام [simulation] ارجاع داده می‌شود؛
    • خط ۷۷: شماره آخرین شبیه‌سازی افزایش می‌یابد؛
    • خط ۷۸: این عدد در جلسه ذخیره می‌شود؛
  • خطوط ۷۹–۸۴: برای پیگیری محاسبات انجام‌شده توسط کاربر، ما لیست شبیه‌سازی‌هایی را که او انجام داده است در جلسه (session) او ذخیره خواهیم کرد؛
  • خط ۸۰: یک شبیه‌سازی واژه‌نامه یک شی TaxPayer خواهد بود که ویژگی [id] آن عدد شبیه‌سازی را به عنوان مقدار خود خواهد داشت؛
  • خطوط ۸۲–۸۴: شبیه‌سازی فعلی به فهرست شبیه‌سازی‌ها در جلسه اضافه می‌شود؛
  • خطوط ۸۶–۸۷: یک پاسخ موفق HTTP آماده می‌شود؛
  • خط ۹۰: نتیجه بازگردانده می‌شود؛

بیایید چند تست اجرا کنیم: وب‌سرور، SGBD، سرور ایمیل و یک کلاینت Postman همگی در حال اجرا هستند.

مورد ۱: انجام محاسبه مالیات در حالی که جلسه راه‌اندازی نشده است

Image

پاسخ به شرح زیر است:

Image

مورد ۲: انجام محاسبه مالیات بدون احراز هویت

ابتدا یک جلسه با استفاده از [/init-session/json] آغاز می‌شود (jSON). سپس همان پرس‌وجوی قبلی اجرا می‌شود. پاسخ به شرح زیر است:

Image

مورد ۳: انجام محاسبه مالیات با پارامترهای ناقص

یک جلسه jSON را آغاز می‌کنیم، احراز هویت می‌کنیم و سپس درخواست زیر را ارسال می‌کنیم:

Image

  • در [5]، پارامتر [marié] وجود ندارد؛

پاسخ به شرح زیر است:

مورد ۴: انجام محاسبه مالیات با پارامترهای نادرست

Image

Image

پاسخ سرور به شرح زیر است:

Image

مورد ۴: انجام محاسبه مالیات با پارامترهای صحیح

Image

پاسخ سرور به شرح زیر است:

Image

30.12. اقدام [lister-simulations]

عمل [lister-simulations] به کاربر اجازه می‌دهد فهرست شبیه‌سازی‌هایی را که از ابتدای جلسه انجام داده است مشاهده کند. مسیر آن در اسکریپت [main] به شرح زیر تعریف شده است:


# فهرست شبیه‌سازی‌ها
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # اجرای کنترلر مرتبط با عمل
    return front_controller()

سرور انتظار هیچ پارامتری را ندارد. اقدام [lister-simulations] توسط کنترل‌کننده زیر [ListerSimulationsController] مدیریت می‌شود:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # بازیابی عناصر مسیر
        dummy, action = request.path.split('/')

        # فهرست شبیه‌سازی‌ها در جلسه را بازیابی می‌کند
        simulations = session.get("simulations"[])
        # نتیجه بازگردانده می‌شود
        return {"action": action, "état"500,
                "réponse": simulations}, status.HTTP_200_OK
  • خط ۱۳: فهرست شبیه‌سازی‌ها از جلسه بازیابی می‌شود؛
  • خطوط ۱۵–۱۶: یک پاسخ موفقیت بازگردانده می‌شود؛

بیایید تست زیر را در Postman اجرا کنیم:

  • یک جلسه jSON را راه‌اندازی کنید؛
  • ما احراز هویت می‌کنیم؛
  • دو محاسبه مالیات انجام دهید؛
  • فهرست شبیه‌سازی‌ها را درخواست می‌کنیم؛

درخواست به شرح زیر است:

  • در [3]، هیچ پارامتری وجود ندارد؛ Image

پاسخ سرور به شرح زیر است:

Image

  • در [4]، فهرست شبیه‌سازی‌های کاربر؛

30.13. عمل [supprimer-simulation]

عمل [supprimer-simulation] به کاربر اجازه می‌دهد یکی از شبیه‌سازی‌ها را از فهرست شبیه‌سازی‌های خود حذف کند. مسیر آن در اسکریپت [main] به شرح زیر تعریف شده است:


# حذف-شبیه‌سازی
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

سرور منتظر یک پارامتر است: شماره شبیه‌سازی که باید حذف شود. اکشن [supprimer-simulation] توسط کنترلر زیر [SupprimerSimulationController] مدیریت می‌شود:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # بازیابی عناصر مسیر
        dummy, action, numéro = request.path.split('/')

        #پارامتر [numéro] یک عدد صحیح مثبت یا صفر است که بر اساس مسیر آن تعیین می‌شود
        numéro = int(numéro)
        #شبیه‌سازی با شناسه=number باید در فهرست شبیه‌سازی‌ها وجود داشته باشد
        simulations = session.get("simulations"[])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            #یک خطا بازگردانده می‌شود
            return {"action": action, "état"601"réponse"[msg_erreur]}, status.HTTP_400_BAD_REQUEST
        #شبیه‌سازی با شناسه = شماره حذف شد
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        #شبیه‌سازی‌ها به جلسه بازگردانده می‌شوند
        session["simulations"] = simulations
        # نتیجه را بازمی‌گرداند
        return {"action": action, "état"600"réponse": simulations}, status.HTTP_200_OK
  • خط ۱۰: دو عنصر مسیر درخواست بازیابی می‌شوند. آنها به صورت رشته‌ها بازیابی می‌شوند؛
  • خط ۱۳: پارامتر [numéro] به یک عدد صحیح تبدیل می‌شود. می‌دانیم این کار ممکن است به دلیل امضای مسیر آن،

@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])

ما همچنین می‌دانیم که این یک عدد صحیح ≥ 0 است. در واقع، ما نمی‌توانیم یک URL یا [/supprimer-simulation/-4] داشته باشیم. این موارد توسط سرور Flask رد می‌شوند؛

  • خط ۱۵: ما لیست شبیه‌سازی‌ها را از جلسه بازیابی می‌کنیم؛
  • خط 16: با استفاده از تابع [filter]، شبیه‌سازی با id==number را جستجو می‌کنیم. یک شیء [filter] به دست می‌آوریم که آن را به نوع [list] تبدیل می‌کنیم؛
  • خطوط 17–20: اگر فیلتر هیچ نتیجه‌ای بازنگرداند، آنگاه شبیه‌سازی مورد نظر برای حذف وجود ندارد. یک پاسخ خطا برای نشان دادن این موضوع بازگردانده می‌شود؛
  • خطوط ۲۱–۲۳: شبیه‌سازی بازگردانده‌شده توسط فیلتر حذف می‌شود؛
  • خط ۲۵: لیست جدید شبیه‌سازی‌ها مجدداً به جلسه اضافه می‌شود؛
  • خط ۲۷: فهرست جدید شبیه‌سازی‌ها در پاسخ بازگردانده می‌شود؛

ما یک تست موفقیت و یک تست شکست انجام می‌دهیم. ما شبیه‌سازی‌ها را اجرا می‌کنیم و سپس لیست شبیه‌سازی‌ها را درخواست می‌کنیم:

Image

  • شبیه‌سازی‌های اینجا با شماره‌های ۲ و ۳ هستند؛

درخواست می‌کنیم که شبیه‌سازی شمارهٔ ۳ حذف شود.

Image

پاسخ به شرح زیر است:

اکنون، بیایید همان عملیات را (حذف شبیه‌سازی با شناسه=3) تکرار کنیم. پاسخ سپس به شرح زیر است:

Image

Image

30.14. عمل [fin-session]

عمل [fin-session] به کاربر اجازه می‌دهد جلسه شبیه‌سازی خود را پایان دهد. مسیر آن در اسکریپت [main] به شرح زیر تعریف شده است:


# پایان جلسه
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

سرور انتظار هیچ پارامتری را ندارد. این اقدام توسط کنترل‌کننده زیر [FinSessionController] مدیریت می‌شود:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # بازیابی عناصر مسیر
        dummy, action = request.path.split('/')

        # تمام کلیدها در جلسهٔ جاری حذف می‌شوند
        session.clear()
        # بازگرداندن نتیجه
        return {"action": action, "état"400"réponse""session réinitialisée"}, status.HTTP_200_OK
  • خط ۱۳: تمام کلیدهای جلسه حذف می‌شوند. این کار موارد زیر را حذف می‌کند:
    • [typeResponse]: انواع پاسخ برای HTTP (json, xml, html);
    • [id_simulation]: شماره آخرین شبیه‌سازی انجام‌شده؛
    • [simulations]: فهرست شبیه‌سازی‌های کاربر؛
    • [user]: شاخصی که نشان می‌دهد کاربر احراز هویت شده است؛
  • پاسخ بازگردانده می‌شود؛

ممکن است این سؤال پیش بیاید که پاسخ HTTP در خط ۱۵ چگونه بازگردانده خواهد شد، در حالی که نوع پاسخ دیگر در جلسه (session) وجود ندارد. برای پی بردن به این موضوع، باید به تابع |front_controller| در اسکریپت اصلی [main] بازگردیم و آن را به شرح زیر اصلاح کنیم:


…        
         # on not# نوع پاسخ مورد نظر ثبت می‌شود اگر این اطلاعات در جلسه موجود باشد
        type_response1 = session.get('typeResponse'None)
        # درخواست را به کنترل‌کننده اصلی ارسال کنید
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # نتیجه ارسال‌شده به کلاینت را ثبت کنید
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #آیا خطای فاتال رخ داده است؟
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # ایمیلی برای مدیر برنامه ارسال می‌شود
            send_adminmail(config, log)
        # نوع پاسخ مورد نظر تعیین می‌شود
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # نوع جلسه هنوز مشخص نشده است – این خواهد بود jSON
            type_response = 'json'
        elif type_response2 is not None:
            # نوع پاسخ مشخص است و بخشی از جلسه می‌باشد
            type_response = type_response2
        else:
            type_response=type_response1
        #پاسخ قابل ارسال در حال ساخت است
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #پاسخ ارسال می‌شود
        return response, status_code
  • خط ۳: نوع پاسخ فعلی در جلسه ذخیره می‌شود؛
  • خط ۶: عمل اجرا می‌شود. اگر این باشد:
    • [fin-session]، کلید [typeResponse] دیگر در جلسه وجود ندارد؛
    • [init-session]، مقدار کلید جلسه [typeResponse] ممکن است تغییر کرده باشد؛
  • خطوط ۱۴–۲۰: پاسخ HTTP باید ارسال شود. باید بدانیم به چه شکلی:
    • خطوط 16–18: اگر نوع پاسخ توسط [type_response1] در خط 3 یا [type_response2] در خط 15 تعریف نشده باشد، در این صورت نوع پاسخ نه قبل و نه بعد از اقدام تعریف نشده است. در این صورت، از jSON (خط ۱۸) استفاده می‌کنیم؛
    • خطوط ۱۹–۲۱: اگر [type_response2] وجود داشته باشد—نوع در جلسه پس از اقدام—در این صورت این همان نوع است که باید استفاده شود؛
    • خطوط ۲۲–۲۳: در غیر این صورت، [type_response1]، نوع پاسخ قبل از اقدام (که باید [fin-session] باشد)، مورد استفاده قرار می‌گیرد؛

30.15. عمل [get-admindata]

اکنون به دو کد URL که برای خدمات jSON و XML رزرو شده‌اند، می‌پردازیم:

اقدام
نقش
زمینهٔ اجرا
/get-admindata
داده‌های مالیاتی مورد نیاز برای محاسبه مالیات را بازمی‌گرداند
پرس‌وجوی GET.
فقط در صورتی استفاده می‌شود که نوع جلسه json یا xml باشد. کاربر باید احراز هویت شود
/محاسبه-مالیات‌ها
مالیات را برای فهرستی از مودیان که در jSON ارسال شده است، محاسبه می‌کند
پرس‌وجوی GET.
فقط در صورتی استفاده شود که نوع جلسه json یا xml باشد. کاربر باید احراز هویت شود.

URL و [/get-admindata] در مسیرهای اسکریپت اصلی [main] به شرح زیر تعریف شده‌اند:


# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    #کنترل‌کننده‌ی مرتبط با اقدام اجرا می‌شود
    return front_controller()

مسیر [/get-admindata] توسط کنترلر زیر، [GetAdminDataController]، مدیریت می‌شود:


# وارد کردن وابستگی‌ها

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # بازیابی عناصر از مسیر
        dummy, action = request.path.split('/')
        # فقط جلسات JSON و XML پذیرفته می‌شوند
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # پاسخ خطا را بازمی‌گرداند
            return {
                       "action": action,
                       "état"1001,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            # پاسخ موفقیت بازگردانده می‌شود
            return {"action": action, "état"1000"réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • خطوط ۱۳–۲۱: بررسی می‌شود تا اطمینان حاصل شود که درخواست در قالب JSON یا XML است؛
  • خط ۲۴: فرهنگ لغت داده سازمان مالیاتی رندر می‌شود که هنگام راه‌اندازی سرور در پیکربندی قرار گرفته بود:

    #داده‌های admindata به‌صورت فقط-خواندنی در سطح برنامه خواهند بود
    config["admindata"] = config["layers"]["dao"].get_admindata()

بیایید با استفاده از کلاینت Postman، پس از شروع یک جلسه با jSON و احراز هویت، URL و [/get-admindata] را درخواست کنیم:

Image

پاسخ سرور به شرح زیر است:

Image

30.16. عمل [calculer-impots]

عمل [calculer-impots] مالیات را برای فهرستی از مودیان که در بدنه درخواست به صورت یک رشته jSON یافت می‌شود، محاسبه می‌کند. ما قبلاً با این عمل آشنا هستیم: در نسخه قبلی [calculate_tax_in_bulk_mode] نامیده می‌شد.

مسیر آن به شرح زیر است:


#محاسبه دسته‌ای مالیات
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    #کنترلر مرتبط با اکشن اجرا می‌شود
    return front_controller()

این اقدام توسط کنترلر زیر مدیریت می‌شود: [CalculerImpotsController]:


import json

from flask_api import status
from werkzeug.local import LocalProxy

from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #بازیابی عناصر مسیر
        dummy, action = request.path.split('/')

        # فقط جلسات JSON و XML پذیرفته می‌شوند
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # یک پاسخ خطا بازگردانده می‌شود
            return {
                       "action": action,
                       "état"1501,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        #بدنه POST را بازیابی می‌کند – انتظار یک لیست از دیکشنری‌ها را دارد
        msg_erreur = None
        list_dict_taxpayers = None
        #بدنه jSON از POST
        request_text = request.data
        try:
            #که به یک لیست از دیکشنری‌ها تبدیل می‌شود
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            # ما خطا را ثبت می‌کنیم
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        #آیا یک لیست غیرخالی داریم؟
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            # ما خطا را ثبت می‌کنیم
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        #آیا ما یک لیست از فرهنگ لغت‌ها داریم؟
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            # خطا؟
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        # خطا؟
        if msg_erreur:
            # یک پاسخ خطا به کلاینت ارسال می‌شود
            résultats = {"action": action, "état"1501"réponse"[msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        #بررسی TaxPayers به‌صورت جداگانه
        #در ابتدا هیچ خطایی وجود ندارد
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            #یک TaxPayer از dict_taxpayer ایجاد می‌شود
            msg_erreur = None
            try:
                # عملیات زیر مواردی را که پارامترها نیستند حذف می‌کند
                # ویژگی‌های کلاس TaxPayer، و همچنین مواردی که مقادیر آنها
                #نادرست هستند
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            # کلیدهای خاصی باید در دیکشنری موجود باشند
            if not msg_erreur:
                #کلیدهای [marié, enfants, salaire] باید در فرهنگ لغت موجود باشند
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            # آیا خطایی وجود دارد؟
            if msg_erreur:
                #خطا در خود TaxPayer ثبت شده است
                dict_taxpayer['erreur'] = msg_erreur
                # TaxPayer به فهرست خطاها اضافه می‌شود
                list_erreurs.append(dict_taxpayer)

        # تمام مالیات‌دهندگان پردازش شده‌اند – آیا خطایی وجود دارد؟
        if list_erreurs:
            # یک پاسخ خطا برای مشتری ارسال می‌شود
            résultats = {"action": action, "état"1501"réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        # بدون خطا؛ می‌توانیم ادامه دهیم
        # استخراج داده‌ها از سازمان مالیاتی
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            #پردازش سوابق TaxPayer به‌صورت جداگانه
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                # محاسبه مالیات
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié']'enfants': dict_taxpayer['enfants'],
                     'salary': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                # نتیجه را به صورت یک دیکشنری ذخیره کنید
                list_taxpayers.append(taxpayer.asdict())
            # افزودن list_taxpayers به شبیه‌سازی‌های فعلی و اختصاص یک شماره به هر شبیه‌سازی
            simulations = session.get("simulations"[])
            id_simulation = session.get("id_simulation"0)
            for simulation in list_taxpayers:
                # ما به هر شبیه‌سازی یک عدد اختصاص می‌دهیم
                id_simulation += 1
                simulation['id'] = id_simulation
                #آن را به فهرست فعلی شبیه‌سازی‌ها اضافه کنید
                simulations.append(simulation)
            #کل ماجرا دوباره ارسال می‌شود
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            # پاسخ به مشتری ارسال می‌شود
            return {"action": action, "état"1500"réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            #یک پاسخ خطا به مشتری ارسال می‌شود
            return {"action": action, "état"1501"réponse"[f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • خطوط ۱۶–۲۴: یک بررسی انجام می‌شود تا اطمینان حاصل شود که داده‌ها در قالب JSON یا XML هستند
  • خطوط ۲۶–۱۲۰: این کد برای ما تا حدودی آشناست. این کد از تابع |index_controller| در نسخه ۱۰ برنامه گرفته شده است که برای مطابقت با مشخصات رابط پیاده‌سازی‌شده [InterfaceController] تطبیق داده شده است؛
  • خطوط ۱۰۴–۱۱۵: کدی که برای در نظر گرفتن محیط جدید این کنترل‌کننده اضافه شده است. ما به تازگی محاسبات مالیاتی را انجام داده‌ایم. باید نتایج را در لیست شبیه‌سازی‌های نگهداری‌شده در جلسه ذخیره کنیم؛
  • خط ۱۰۵: ما لیست شبیه‌سازی‌های فعلی جلسه را بازیابی می‌کنیم؛
  • خط ۱۰۶: ما شماره جدیدترین شبیه‌سازی انجام‌شده را بازیابی می‌کنیم؛
  • خطوط ۱۰۷–۱۱۲: ما لیست دیکشنری‌های حاوی نتایج محاسبه مالیات را مرور می‌کنیم؛ به هر یک یک شماره شبیه‌سازی [id] اختصاص می‌دهیم و هر دیکشنری به لیست شبیه‌سازی‌ها اضافه می‌شود؛
  • خطوط ۱۱۳–۱۱۵: لیست جدید شبیه‌سازی‌ها و شماره آخرین شبیه‌سازی انجام‌شده به جلسه بازگردانده می‌شوند؛

پس از راه‌اندازی یک جلسه jSON و احراز هویت، تست Postman زیر را انجام می‌دهیم:

Image

Image

پاسخ سرور به شرح زیر است:

Image

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

می‌توانیم ببینیم که در لیست نتایج برای [/calcul-impots]، مالیات‌دهندگان فاقد ویژگی [id] هستند، در حالی که در لیست شبیه‌سازی‌ها، هر شبیه‌سازی دارای یک شماره شناسایی منحصر به فرد است.

Image