Skip to content

30. تمرين عملي: الإصدار 12

في هذا الفصل، سنكتب تطبيق ويب يتبع بنية MVC (نموذج-عرض-وحدة تحكم). سيكون التطبيق قادرًا على إرجاع استجابات بثلاثة تنسيقات: JSON و XML و HTML. هناك زيادة كبيرة في التعقيد بين ما نحن على وشك القيام به وما قمنا به سابقًا. سنعيد استخدام معظم المفاهيم التي تمت تغطيتها حتى الآن وسنوضح بالتفصيل جميع الخطوات المؤدية إلى التطبيق النهائي.

30.1. بنية MVC

سنقوم بتنفيذ نمط بنية MVC (Model–View–Controller) على النحو التالي:

Image

ستتم معالجة طلب العميل على النحو التالي:

  • 1 - الطلب

ستكون عناوين URL المطلوبة على النحو التالي: http://machine:port/action/param1/param2/ سيستخدم [المتحكم الرئيسي] ملف تكوين لـ"توجيه" الطلب إلى المتحكم الصحيح. للقيام بذلك، سيستخدم حقل [action] في عنوان URL. يتكون باقي عنوان URL [param1/param2/…] من معلمات اختيارية سيتم تمريرها إلى الإجراء. يشير الحرف "C" في MVC هنا إلى السلسلة [وحدة التحكم الرئيسية، وحدة التحكم / الإجراء]. إذا لم تتمكن أي وحدة تحكم من معالجة الإجراء المطلوب، فسيرد خادم الويب بأن عنوان URL المطلوب لم يتم العثور عليه.

  • 2 - المعالجة
  • يمكن للإجراء المحدد [2a] استخدام المعلمات التي مررها إليه [وحدة التحكم الرئيسية]. قد تأتي هذه المعلمات من مصدرين:
      • مسار [/param1/param2/…] لعنوان URL،
      • من المعلمات المنشورة في نص طلب العميل؛
    • عند معالجة طلب المستخدم، قد يتطلب الإجراء طبقة [الأعمال] [2b]. بمجرد معالجة طلب العميل، قد يؤدي ذلك إلى استجابات مختلفة. ومن الأمثلة الكلاسيكية على ذلك:
      • استجابة خطأ إذا تعذر معالجة الطلب بشكل صحيح؛
      • استجابة تأكيد في الحالات الأخرى؛
    • سيُرجع [المتحكم / الإجراء] استجابته [2c] إلى المتحكم الرئيسي مع رمز الحالة. وستمثل رموز الحالة هذه بشكل فريد الحالة الحالية للتطبيق. وسيكون إما رمز نجاح أو رمز خطأ؛
  • 3 - الاستجابة
    • اعتمادًا على ما إذا كان العميل قد طلب استجابة JSON أو XML أو HTML، سيقوم [المتحكم الرئيسي] بإنشاء مثيل [3a] لنوع الاستجابة المناسب وإصدار تعليمات له بإرسال الاستجابة إلى العميل. سيقوم [المتحكم الرئيسي] بتمرير كل من الاستجابة ورمز الحالة المقدم من [المتحكم/الإجراء] الذي تم تنفيذه؛
    • إذا كان الرد المطلوب من نوع JSON أو XML، فسيقوم الرد المحدد بتنسيق الرد الوارد من [وحدة التحكم/الإجراء] الذي تم تزويده به وإرساله [3c]. ويمكن أن يكون العميل القادر على معالجة هذا الرد عبارة عن برنامج نصي لـ Python على وحدة التحكم أو برنامج نصي لـ JavaScript مدمج في صفحة HTML؛
    • إذا كانت الاستجابة المطلوبة من نوع HTML، فستقوم الاستجابة المحددة باختيار [3b] إحدى طرق عرض HTML [Vuei] باستخدام رمز الحالة المقدم لها. هذا هو V في MVC. تتوافق طريقة عرض واحدة مع رمز حالة واحد. ستعرض طريقة العرض V هذه الاستجابة من [وحدة التحكم / الإجراء] التي تم تنفيذها. وهي تغلف البيانات من هذه الاستجابة في HTML و CSS و JavaScript. تسمى هذه البيانات نموذج العرض. هذا هو M في MVC. غالبًا ما يكون العميل متصفحًا؛

الآن، دعونا نوضح العلاقة بين بنية الويب MVC والبنية الطبقية. اعتمادًا على كيفية تعريف النموذج، قد يكون هذان المفهومان مرتبطين أو غير مرتبطين. دعونا نفكر في تطبيق ويب MVC أحادي الطبقة:

Image

في المثال أعلاه، يشتمل كل من [وحدة التحكم / الإجراء] على أجزاء من طبقتي [الأعمال] و[DAO]. في طبقة [الويب]، لدينا بنية MVC، لكن التطبيق ككل لا يحتوي على بنية طبقية. هنا، توجد طبقة واحدة فقط — طبقة الويب — تتولى معالجة كل شيء.

الآن، لنفكر في بنية ويب متعددة الطبقات:

Image

يمكن تنفيذ طبقة [الويب] دون اتباع نموذج MVC. عندئذٍ يكون لدينا بنية متعددة الطبقات، لكن طبقة الويب لا تنفذ نموذج MVC.

على سبيل المثال، في عالم .NET، يمكن تنفيذ طبقة [الويب] أعلاه باستخدام ASP.NET MVC، مما ينتج عنه بنية ذات طبقات مع طبقة [ويب] على غرار MVC. بعد القيام بذلك، يمكننا استبدال طبقة ASP.NET MVC هذه بطبقة ASP.NET كلاسيكية (WebForms) مع الحفاظ على بقية العناصر (منطق الأعمال، DAO، برنامج التشغيل) دون تغيير. وبذلك نحصل على بنية متعددة الطبقات مع طبقة [الويب] التي لم تعد قائمة على MVC.

في MVC، قلنا إن نموذج M هو نموذج عرض V، أي مجموعة البيانات التي يعرضها عرض V. وهناك تعريف آخر لنموذج M في MVC:

Image

يعتبر العديد من المؤلفين أن ما يقع على يمين طبقة [الويب] يشكل نموذج M في MVC. لتجنب الغموض، يمكننا الإشارة إلى:

  • نموذج المجال عند الإشارة إلى كل ما يقع على يمين طبقة [الويب]؛
  • نموذج العرض عند الإشارة إلى البيانات التي تعرضها طريقة العرض V؛

فيما يلي، عندما نشير إلى النموذج، فإننا نشير دائمًا إلى نموذج العرض.

30.2. بنية تطبيق العميل/الخادم

ستتضمن تطبيق الويب البنية التالية:

Image

  • في [1]، سيحتوي خادم الويب على نوعين من العملاء:
    • في [2]، عميل وحدة التحكم الذي سيتبادل JSON و XML مع الخادم؛
    • في [3]، متصفح سيتلقى HTML من الخادم ويعرضه؛
  • يحتفظ خادم الويب [1] بطبقتي [الأعمال] و[DAO] من الإصدارات السابقة؛
  • سيتم تحديث عميل الويب [2] لمراعاة عناوين URL الجديدة لخدمات تطبيق الويب؛
  • يجب كتابة تطبيق HTML الذي يعرضه المتصفح من البداية؛

سنقوم بتطوير التطبيق على عدة مراحل:

  • سنقوم بتطوير إصدار JSON للخادم. سنقوم باختبار عناوين URL لخدمات الخادم واحدة تلو الأخرى باستخدام عميل Postman. تسمح لنا هذه الطريقة ببناء إطار عمل خادم الويب دون القلق بشأن طرق عرض التطبيق (=HTML
  • بعد اختبار خادم JSON باستخدام Postman، سنقوم باختباره باستخدام عميل وحدة التحكم؛
  • ثم سننتقل إلى النسخة XML من الخادم. وقد رأينا أن الانتقال من JSON إلى XML أمر بسيط؛
  • وأخيرًا، سننتقل إلى إصدار HTML للخادم. سنقوم ببناء بنية MVC وتحديد طرق العرض التي سيتم عرضها. سيتم اختبار تطبيق HTML باستخدام كل من عميل Postman ومتصفح قياسي؛

30.3. هيكل دليل كود الخادم

Image

  • في [1: الخادم الويب ككل؛
  • في [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]؛
  • سيؤدي النقر على زر [التحقق] إلى تشغيل الإجراء [authenticate-user] مع معلمتين منشورتين [2-3]؛

عرض حساب الضريبة:

Image

  • في [1]، الإجراء [authenticate-user] الذي أدى إلى هذه الشاشة؛
  • في [2]، يؤدي النقر على زر [Validate] إلى تشغيل الإجراء [calculate-tax] مع ثلاثة معلمات منشورة [2-5]؛
  • يؤدي النقر على الرابط [6] إلى تشغيل الإجراء [list-simulations] بدون معلمات؛
  • يؤدي النقر على الرابط [7] إلى تشغيل الإجراء [end-session] بدون معلمات؛

تعرض العرض الثالث المحاكاة التي أجراها المستخدم المصادق عليه:

Image

  • في [3]، الإجراء [list-simulations] الذي أدى إلى ظهور هذه الصفحة؛
  • في [2]، يؤدي النقر على رابط [حذف] إلى تشغيل الإجراء [delete-simulation] مع معلمة: رقم المحاكاة المراد حذفها من القائمة؛
  • يؤدي النقر على الرابط [3] إلى تشغيل الإجراء [display-tax-calculation] بدون معلمات، مما يعيد عرض عرض حساب الضريبة؛
  • يؤدي النقر على الرابط [4] إلى تشغيل الإجراء [end-session] بدون معلمات؛

باستخدام هذه المعلومات الأولية، يمكننا تحديد عناوين URL المختلفة لخدمات الخادم:

الإجراء
الدور
سياق التنفيذ
/init-session
يُستخدم لتعيين نوع (json، xml، html) الاستجابات المطلوبة
طلب GET
يمكن إرساله في أي وقت
/authenticate-user
يوافق على تسجيل دخول المستخدم أو يرفضه
طلب POST.
يجب أن يحتوي الطلب على معلمتين مرسلتين [user, password]
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا
/calculate-tax
يقوم بمحاكاة حساب الضريبة
طلب POST.
يجب أن يحتوي الطلب على ثلاثة معلمات مرسلة [married، children، salary]
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وتم مصادقة المستخدم
/list-simulations
طلب لعرض قائمة المحاكاة التي تم إجراؤها منذ بداية الجلسة
طلب GET.
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وتم توثيق المستخدم
/delete-simulation/number
حذف محاكاة من قائمة المحاكاة
طلب GET.
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وتم توثيق المستخدم
/display-tax-calculation
يعرض صفحة HTML لحساب الضريبة
طلب GET.
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وتم مصادقة المستخدم
/end-session
ينهي جلسة المحاكاة.
من الناحية الفنية، يتم حذف جلسة الويب القديمة وإنشاء جلسة جديدة
لا يمكن إصداره إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وتم توثيق المستخدم

سيتم استخدام عناوين URL المختلفة للخدمة لكل من خادم HTML وخوادم JSON أو XML. سيتم استخدام عنوانين URL حصريًا للخادمين الأخيرين: وهما عنوانا URL من الإصدار السابق لخادم العميل/الويب الذي نعيد استخدامه هنا:

الإجراء
الدور
سياق التنفيذ
/get-admindata
تُرجع البيانات الضريبية المستخدمة لحساب الضريبة
GET.
يُستخدم فقط إذا كان نوع الجلسة json أو xml. يجب مصادقة المستخدم
/calculate-taxes
يحسب الضريبة لقائمة من دافعي الضرائب المنشورة في طلب JSON
.
يُستخدم فقط إذا كان نوع الجلسة json أو xml. يجب أن يكون المستخدم قد تمت مصادقته

ستعمل جميع وحدات التحكم المرتبطة بهذه الإجراءات بنفس الطريقة:

  • ستتحقق من معلماتها. توجد هذه المعلمات في الكائن:
    • [request.path] للمعلمات الموجودة في عنوان URL بالشكل [/action/param1/param2/…]؛
    • في الكائن [request.form] للمعلمات التي يتم إرسالها كـ [x-www-form-urlencoded] في نص الطلب؛
    • في كائن [request.data] بالنسبة للقيم المرسلة بتنسيق JSON في نص الطلب؛
  • يشبه وحدة التحكم وظيفة أو طريقة تتحقق من صحة معلماتها. لكن الأمر أكثر تعقيدًا قليلاً بالنسبة لوحدة التحكم:
    • قد تكون المعلمات المتوقعة مفقودة؛
    • المعلمات التي يستردها المتحكم هي سلاسل نصية. إذا كانت المعلمة المتوقعة رقمًا، فيجب على المتحكم التحقق من أن السلسلة النصية للمعلمة تمثل رقمًا بالفعل؛
    • بمجرد التحقق من وجود المعلمات المتوقعة وصحة صياغتها، يجب عليك التحقق من صحتها في سياق التنفيذ الحالي. هذا السياق موجود في الجلسة. مثال المصادقة هو مثال على سياق التنفيذ. يجب معالجة إجراءات معينة فقط بعد مصادقة العميل. بشكل عام، يشير مفتاح في الجلسة إلى ما إذا كانت هذه المصادقة قد تمت أم لا؛
    • بمجرد اكتمال الفحوصات السابقة، يمكن للوحدة الثانوية المضي قدمًا. عملية التحقق من المعلمات هذه مهمة جدًا. لا يمكننا قبول أن يرسل لنا العميل بيانات عشوائية في أي مرحلة من مراحل دورة حياة التطبيق. يجب أن نحافظ على السيطرة الكاملة على دورة حياة التطبيق؛
    • بمجرد الانتهاء من عملها، تعيد وحدة التحكم الثانوية قاموسًا بمفاتيح [action, state, response] إلى وحدة التحكم الرئيسية التي استدعتها:
      • [action] هو الإجراء الذي تم تنفيذه للتو؛
      • [state] هو رقم مكون من ثلاثة أرقام يشير إلى نتيجة معالجة الإجراء:
    • [x00] يشير إلى نجاح المعالجة؛
    • [x01] يشير إلى فشل المعالجة؛
  • [response] هو قاموس النتائج في شكل {‘response’:object}. سيكون للكائن هياكل مختلفة اعتمادًا على الإجراء الذي تتم معالجته؛

سنستعرض الآن مختلف وحدات التحكم — أو بعبارة أخرى، الإجراءات المختلفة التي تتولى هذه الوحدات معالجتها — والتي تقود سير عمل تطبيق الويب.

30.5. تكوين الخادم

Image

تكوين قاعدة البيانات [config_database] وتكوين طبقة الخادم [config_layers] مطابقان لتلك الموجودة في الإصدارات السابقة. يتضمن ملف [config] الآن معلومات جديدة:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        #  scripts [config_database, config_layers]
        script_dir,
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  web server dependencies

    #  controllers
    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

    #  answers HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    #  view models
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],

        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },

        #  thread pause time in seconds
        "sleep_time": 0,

        #  authorized shares and their auditors
        "controllers": {
            #  initialization of a calculation session
            "init-session": InitSessionController(),
            #  user authentication
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            #  tax calculation in individual mode
            "calculer-impot": CalculerImpotController(),
            #  batch mode tax calculation
            "calculer-impots": CalculerImpotsController(),
            #  list of simulations
            "lister-simulations": ListerSimulationsController(),
            #  deleting a simulation
            "supprimer-simulation": SupprimerSimulationController(),
            #  end of calculation session
            "fin-session": FinSessionController(),
            #  display tax calculation view
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #  obtaining data from tax authorities
            "get-admindata": GetAdminDataController(),
            #  main controller
            "main-controller": MainController()
        },

        #  different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        #  HTML views and their models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                "états": [
                    #  /init-session success
                    700,
                    #  /authentifier-user failure
                    201
                ],
                "view_name": "views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                #  tax calculation
                "états": [
                    #  /authentifier-user success
                    200,
                    #  /calculate-tax-success
                    300,
                    #  /calculate-tax failure
                    301,
                    #  /show-tax-calculation
                    800
                ],
                "view_name": "views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                #  view of simulation list
                "états": [
                    #  /lister-simulations
                    500,
                    #  /suppress-simulation
                    600
                ],
                "view_name": "views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        #  view of unexpected errors
        "view-erreurs": {
            "view_name": "views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        #  redirections
        "redirections": [
            {
                "états": [
                    400,  #  /end-session successful
                ],
                #  redirection to
                "to": "/init-session/html",
            }
        ],
    }
    )

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • حتى السطر 41، نرى عناصر قياسية؛
  • الأسطر 43–66: بحلول السطر 43، يتم تعريف مسار Python الخاص بالخادم. يمكننا بعد ذلك استيراد تبعيات المشروع:
    • الأسطر 45–55: قائمة وحدات التحكم؛
    • الأسطر 57–60: قائمة استجابات HTTP؛
    • الأسطر 62-66: قائمة قوالب العرض؛
  • الأسطر 68-189: تكوين التطبيق مع سلسلة من الثوابت؛
    • الأسطر 71–98: نحن على دراية بهذه الأسطر بالفعل من الإصدارات السابقة؛
    • الأسطر 101–122: قاموس وحدات التحكم:
      • المفاتيح هي أسماء الإجراءات؛
      • القيم هي مثيل للمتحكم المسؤول عن معالجة هذا الإجراء. يتم إنشاء مثيل لكل متحكم كمثيل واحد (singleton). سيتم تنفيذ نفس المثيل بواسطة خيوط خادم مختلفة. لذلك، يجب توخي الحذر مع البيانات المشتركة التي قد يرغب كل متحكم في تعديلها؛
    • الأسطر 125–129: قاموس الاستجابات الثلاثة المحتملة لـ HTTP:
      • المفاتيح هي نوع الاستجابة المطلوبة من قبل العميل (JSON، XML، HTML
      • القيم هي مثيل لاستجابة HTTP. يتم إنشاء مثيل لكل مولد استجابة كمثيل واحد (singleton). سيتم تنفيذ نفس المولد بواسطة خيوط خادم مختلفة. لذلك يجب توخي الحذر مع البيانات المشتركة التي قد يرغب كل مولد في تعديلها؛
    • الأسطر 132–186: تكوين طرق عرض HTML. في الوقت الحالي، سنتجاهل هذه الأسطر؛
  • الأسطر 191–202: لقد صادفنا هذه الأسطر بالفعل في الإصدارات السابقة؛

30.6. مسار طلب العميل داخل الخادم

Image

سنتتبع مسار طلب العميل الذي يصل إلى الخادم وصولاً إلى استجابة HTTP المرسلة. ويتبع ذلك تدفق خادم MVC.

30.6.1. النص البرمجي [main]

النص البرمجي [main] مطابق في نواحٍ عديدة للنص البرمجي في الإصدارات السابقة. ومع ذلك، فإننا نقدمه بالكامل لضمان البدء على أساس سليم:

#  a mysql or pgres parameter is expected
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()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
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

#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

#  start-up log
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  flask application
app = Flask(__name__, template_folder="templates", static_folder="static")
#  session secret key
app.secret_key = os.urandom(12).hex()

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /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:
    #  execute the controller associated with the action
    return front_controller()

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-calculation-tax
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • الأسطر 1–92: تمت تغطية جميع هذه الأسطر وشرحها بالفعل؛
  • السطر 92: سيقوم الخادم بإدارة جلسة العمل. لذلك نحتاج إلى مفتاح سري. بالنسبة لكل مستخدم، سنقوم بتخزين معلومتين في جلسة العمل:
    • ما إذا كان المستخدم قد نجح في المصادقة؛
    • في كل مرة يقوم فيها بحساب الضريبة، سيتم وضع نتائج هذا الحساب في قائمة تسمى قائمة محاكاة المستخدم. سيتم تخزين هذه القائمة في الجلسة؛
  • الأسطر 100-151: قائمة عناوين URL لخدمات الخادم. تعمل الوظائف المرتبطة بها كمرشح: أي عناوين URL غير موجودة في هذه القائمة سيتم رفضها من قبل خادم Flask مع ظهور خطأ [404 NOT FOUND]. بمجرد اكتمال هذا التصفية، يتم توجيه الطلب بشكل منهجي إلى "Front Controller" الذي يتم تنفيذه بواسطة وظيفة [front_controller] في الأسطر 94–98، والتي سنناقشها بعد قليل؛
  • الأسطر 100–103: معالجة مسار [/]. ستكون نقطة الدخول لتطبيق الويب هي عنوان URL الموجود في السطر 107. لذلك، في السطر 103، نقوم بإعادة توجيه العميل إلى عنوان URL هذا:
  • يتم استيراد الدالة [url_for] في السطر 18. ولها معلمتان هنا:
      • المعلمة الأولى هي اسم إحدى وظائف التوجيه، وفي هذه الحالة هي الوظيفة الموجودة في السطر 107. يمكننا أن نرى أن هذه الوظيفة تتوقع معلمة [type_responseوهي نوع الاستجابة (json، xml، html) التي يطلبها العميل؛
      • المعلمة الثانية تأخذ اسم المعلمة من السطر 107، [type_responseوتعيّن لها قيمة. لو كانت هناك معلمات أخرى، لكنا كررنا العملية لكل منها؛
      • تُرجع الدالة عنوان URL المرتبط بالدالة المحددة بواسطة المعلمتين المقدمتين إليها. هنا، ستُرجع الدالة عنوان URL من السطر 106، حيث يتم استبدال المعلمة بقيمتها [/init-session/html]؛
    • تم استيراد الدالة [redirect] في السطر 18. وتتمثل مهمتها في إرسال رأس إعادة توجيه HTTP إلى العميل:
      • المعلمة الأولى هي عنوان URL الذي يجب إعادة توجيه العميل إليه؛
      • المعلمة الثانية هي رمز حالة استجابة HTTP المرسلة إلى العميل. الرمز [status.HTTP_302_FOUND] يتوافق مع إعادة توجيه HTTP؛

تقوم الدالة [ front_controller] في الأسطر 94–98 بإجراء المعالجة الأولية لطلب العميل:

#  the front controller
def front_controller() -> tuple:
    #  we process the request
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • الأسطر 1–57: نحن على دراية بهذا الكود. على سبيل المثال، كان هذا هو كود الدالة المسماة [main] في البرنامج النصي [main] في الإصدار السابق. هناك أمر واحد جدير بالملاحظة وهو وحدة التحكم المستخدمة في الأسطر 25–26:
  • السطر 25: نسترد مثيل وحدة التحكم المرتبط بالاسم [main-controller] من التكوين. هذه هي الأسطر التالية:
    #  web server dependencies
    #  controllers
    
    from MainController import MainController

    #  authorized shares and their controllers
        "controllers": {
            ,
            #  main controller
            "main-controller": MainController()
        },
  • (تابع)
    • السطر 10 أعلاه، لاحظ أننا نسترد مثيلًا للفئة؛
  • السطر 26: نطلب من وحدة التحكم [MainController] معالجة الطلب؛
  • الأسطر 30–45: يتم إرسال الاستجابة التي يعيدها [MainController] إلى العميل. سنعود إلى هذه الأسطر لاحقًا؛

تتمثل مهمة دالة [front_controller] ثم فئة [MainController] في معالجة المهام المشتركة بين جميع الطلبات:

في الرسم البياني أعلاه، ما زلنا في المرحلة 1 من معالجة الطلب. سيواصل وحدة التحكم الرئيسية [MainController] الخطوة 1.

Image

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] الوحيدة في السطر 8. تأخذ هذه الطريقة ثلاثة معلمات:
    • [request]: طلب العميل؛
    • [session]: جلسة عمل العميل؛
    • [config]: تكوين التطبيق؛

تُرجع الطريقة [execute] مجموعة مكونة من عنصرين:

  • الأول هو قاموس النتائج بالشكل {‘action’: action, ‘status’: status, ‘response’: results};
  • والثاني هو رمز حالة HTTP الذي سيتم إرجاعه إلى العميل؛

تقوم وحدة التحكم الرئيسية [MainController] [1] بتنفيذ واجهة [InterfaceController] على النحو التالي:

#  import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

#  web application controllers
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  retrieve path elements
        params = request.path.split('/')
        action = params[1]

        #  errors
        erreur = False
        #  session type must be known prior to certain actions
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        #  some actions require authentication
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        #  are there any mistakes?
        if erreur:
            #  an error msg is returned
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            #  execute the controller associated with the action
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

يقوم [MainController] بإجراء الفحوصات الأولية للتحقق من صحة الطلب.

  • الأسطر 11–13: يبدأ وحدة التحكم باسترداد الإجراء الذي طلبه العميل. تذكر أن عناوين URL للخدمة تكون بالشكل [/action/param1/param2/…] وأن عنوان URL هذا موجود في [request.path]؛
  • الأسطر 17–23: تُستخدم الإجراء [init-session] لتهيئة نوع الاستجابة (json، xml، html) المطلوب من قبل العميل. يتم تخزين هذه المعلومات في الجلسة تحت المفتاح [responseType]. لذلك، إذا لم يكن الإجراء هو [init-session]، يجب أن تحتوي الجلسة على المفتاح [responseType]؛ وإلا، فإن الطلب غير صالح؛
  • السطور 21-22: هيكل النتيجة التي تعيدها كل وحدة تحكم، وفي هذه الحالة نتيجة خطأ:
    • [action]: هو اسم الإجراء الحالي. سيسمح لنا هذا باسترداد اسمه عند تسجيل نتيجة الطلب؛
    • [status]: هو رمز حالة مكون من ثلاثة أرقام:
        • [x00] للنجاح؛
        • [x01] للفشل؛
  • [response]: هو الرد على الطلب. وتختلف طبيعته باختلاف كل طلب؛
  • الأسطر 24–30: يُستخدم الإجراء [authenticate-user] لمصادقة المستخدم. في حالة النجاح، تتم إضافة مفتاح [user=True] إلى جلسة عمل المستخدم. لا يمكن الوصول إلى عناوين URL لبعض الخدمات إلا للمستخدم المصادق عليه. وهذا ما يتم التحقق منه هنا؛
  • السطر 26: لا يمكن للمستخدم الذي لم يتم مصادقته بعد تنفيذ سوى الإجراءين [init-session] و [authenticate-user]؛
  • الأسطر 28-29: الاستجابة التي يتم إرسالها في حالة حدوث خطأ؛
  • الأسطر 32-34: إذا حدث أي من الخطأين السابقين، يتم إرسال استجابة الخطأ إلى العميل مع حالة HTTP 400 BAD REQUEST؛
  • الأسطر 35-39: إذا لم يحدث أي خطأ، يتم تمرير التحكم إلى وحدة التحكم المسؤولة عن معالجة الإجراء الحالي. توجد مثيلتها في تكوين التطبيق؛

تواصل فئة [MainController] عمل دالة [front_controller]: تعملان معًا على معالجة كل ما يمكن استبعاده من معالجة الطلب، في انتظار اللحظة الأخيرة لتمرير الطلب إلى وحدة تحكم محددة. إن تقسيم الكود بين دالة [front_controller] وفئة [MainController] أمر شخصي تمامًا. هنا أردت الحفاظ على بنية الإصدار السابق: كانت الدالة [front_controller] موجودة بالفعل تحت اسم [main]. في الممارسة العملية، يمكن للمرء:

  • وضع كل شيء في دالة [front_controller] وإلغاء فئة [MainController]؛
  • وضع كل شيء في فئة [MainController] وإلغاء وظيفة [front_controller]. أميل إلى اختيار هذا الحل لأنه يتميز بتبسيط كود البرنامج النصي الرئيسي [main]؛

30.7. المعالجة الخاصة بالإجراءات

لنعد إلى بنية MVC للتطبيق:

Image

ما زلنا في الخطوة 1 أعلاه. إذا لم تكن هناك أخطاء، فستبدأ الخطوة 2. تم توجيه الطلب إلى وحدة التحكم الخاصة بالإجراء المطلوب في الطلب. لنفترض أن هذا الإجراء هو [/init-session] المحدد بواسطة المسار:

1
2
3
4
5
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

هذا الإجراء مرتبط بوحدة تحكم في التكوين [config]:


        # actions autorisées et leurs contrôleurs
        "controllers": {
            # initialisation d'une session de calcul
            "init-session": InitSessionController(),
            
        },

ثم يتولى [InitSessionController] (السطر 4) زمام الأمور. وفيما يلي كوده:

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):
        #  path elements are retrieved
        dummy, action, type_response = request.path.split('/')

        #  initially no error
        erreur = False
        #  check response type
        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 no error
        if not erreur:
            #  set the session type in the flask session
            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
  • السطر 6: مثل وحدات التحكم الأخرى، تنفذ [InitSessionController] واجهة [InterfaceController]؛
  • السطر 10: عنوان URL من النوع [/init-session/type_response]. نسترد الإجراء [init-session] ونوع الاستجابة المطلوب؛
  • السطر 15: لا يمكن أن يكون نوع الاستجابة المطلوب سوى أحد الأنواع الموجودة في تكوين الاستجابة:

        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • إذا لم يكن الأمر كذلك، يتم إعداد استجابة خطأ 701 (السطر 17)؛
  • الأسطر 20-25: الحالة التي يكون فيها نوع الاستجابة المطلوب صالحًا؛
  • السطر 22: يتم تخزين نوع الاستجابة المطلوب في الجلسة. وذلك لأننا سنحتاج إلى تذكره للطلبات اللاحقة؛
  • الأسطر 23–24: إعداد استجابة نجاح 700؛
  • السطر 25: يتم إرجاع استجابة النجاح إلى المتصل؛
  • السطر 27: إذا حدث خطأ، يتم إرجاع استجابة الخطأ إلى المتصل؛

30.8. إنشاء استجابة HTTP للخادم

لنعد إلى بنية MVC للتطبيق:

Image

لقد غطينا للتو الخطوتين 1 و 2. واجهنا ثلاثة رموز حالة:

  • 700: نجحت عملية /init-session؛
  • 701: فشل /init-session؛
  • 101: طلب غير صالح، إما لأن الجلسة لم يتم تهيئتها أو لأن المستخدم لم يتم توثيقه؛

دعونا ندرس كيف سيتم إرسال استجابة الخادم إلى العميل خلال الخطوة 3 أعلاه. يحدث هذا في دالة [front_controller] في البرنامج النصي [main]:

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • نحن الآن في السطر 26: أرجعت وحدة التحكم الرئيسية استجابة الخطأ الخاصة بها؛
  • الأسطر 27–29: بغض النظر عن استجابة وحدة التحكم الرئيسية (نجاح أو فشل)، يتم تسجيل هذه الاستجابة في ملف السجل؛
  • الأسطر 30-33: كما في الإصدارات السابقة، إذا كانت حالة HTTP هي [500 INTERNAL SERVER ERROR]، فإننا نرسل بريدًا إلكترونيًا إلى مسؤول التطبيق مع سجل الأخطاء؛
  • الأسطر 34–39: نرسل استجابة HTTP، ويتم وضع النتيجة التي أعادها وحدة التحكم في نص هذه الاستجابة. نحتاج إلى معرفة التنسيق (JSON، XML، HTML) الذي يريده العميل لهذه الاستجابة. نبحث عن نوع الاستجابة المطلوب في الجلسة. إذا لم يكن موجودًا، فإننا نحدد هذا النوع بشكل تعسفي إلى JSON؛
  • الأسطر 40–43: يتم إنشاء استجابة HTTP؛

في ملف التكوين، تم ربط كل نوع استجابة (json، xml، html) بمثيل فئة:


        # les différents types de réponse (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
  • الأسطر 8–11: تحدد واجهة [InterfaceResponse] طريقة واحدة [build_http_response] بالمعلمات التالية:
    • [request, session, config]: هذه هي المعلمات التي يتلقاها وحدة التحكم في الإجراءات؛
    • [result, 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):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  we return the answer 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؛
  • 101: طلب غير صالح، إما لأن الجلسة لم يتم تهيئتها أو لأن المستخدم لم يتم توثيقه؛

سنحاول تشغيل هذه الأوامر باستخدام جلسة JSON.

  • نقوم بتشغيل خادم الويب ونظام إدارة قواعد البيانات وخادم البريد؛
  • نقوم بتشغيل عميل Postman؛

الاختبار 1

أولاً، سنعرض طلبًا غير صالح لأن الجلسة لم يتم تهيئتها بعد:

Image

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

ولكن لا يتم قبولها إلا إذا تم تهيئة الجلسة مسبقًا باستخدام الإجراء [/init-session].

دعونا ننفذ الطلب ونرى النتيجة التي أرسلها الخادم:

Image

  • [1-2]: تلقينا استجابة JSON. عندما لا يكون نوع الاستجابة قد تم تحديده بعد من قبل العميل، يستخدم الخادم JSON للرد؛
  • [3-5]: قاموس JSON للاستجابة؛
    • [action]: الإجراء الذي تم تنفيذه؛
    • [status]: رمز حالة الاستجابة. يشير الرمز [x01] إلى وجود خطأ؛
    • [response]: يتم تخصيصه لكل إجراء. ويحتوي هنا على رسالة خطأ؛

الآن دعونا نبدأ جلسة عمل بنوع استجابة غير صحيح:

Image

  • [1-2] هو مسار صالح:
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    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):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  result: the dictionary to be transformed into the XML string
        xml_string = xmltodict.unparse({"root": résultat})
        #  we return the answer 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. الإجراء [authenticate-user]

يتيح لك الإجراء [authenticate-user] مصادقة مستخدم يرغب في استخدام تطبيق حساب الضرائب. يتم تعريف مساره على النحو التالي في البرنامج النصي [main]:

1
2
3
4
5
#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

يتوقع الخادم معلمتين POST:

  • [user]: معرف المستخدم؛
  • [password]: كلمة المرور الخاصة به؛

يتم تحديد قائمة المستخدمين المصرح لهم في التكوين [config]:


        # utilisateurs autorisés à utiliser l'application
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

هنا، لدينا قائمة تحتوي على عنصر واحد.

يتم التعامل مع الإجراء [authenticate-user] بواسطة وحدة التحكم [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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  POST parameters
        post_params = request.form
        #  response status code HTTP
        status_code = None
        #  initially no errors
        erreur = False
        erreurs = []
        #  you need a POST with two parameters
        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:
            #  retrieve POST parameters
            #  parameter [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            #  parameter [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            #  mistake?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        #  mistake?
        if not erreur:
            #  check the validity of the (user, password) pair
            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
            #  found?
            if not trouvé:
                #  we note the error
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                #  note in the session that the user has been found
                session["user"] = True
        #  it's over
        if not erreur:
            #  error-free return
            résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            #  return with error
            return {"action": action, "état": 201, "réponse": erreurs}, status_code
  • السطر 14: استرداد معلمات POST؛
  • السطر 19: قائمة الأخطاء الموجودة في الطلب؛
  • الأسطر 20–24: نتحقق من وجود معلمتين مرسلتين بالفعل؛
  • الأسطر 27–31: التحقق من وجود معلمة [users]؛
  • الأسطر 32–36: التحقق من وجود معلمة [password]؛
  • الأسطر 38-39: إذا كانت المعلمات المرسلة غير صحيحة، قم بإعداد استجابة HTTP 400 BAD REQUEST؛
  • الأسطر 40–58: التحقق من أن بيانات الاعتماد [user, password] تخص مستخدمًا مخولًا باستخدام التطبيق؛
  • الأسطر 51-55: إذا لم يكن المستخدم (user, password) مخولًا لاستخدام التطبيق، قم بإعداد استجابة HTTP 401 UNAUTHORIZED؛
  • الأسطر 56–58: إذا كان المستخدم مخولًا، نسجل في الجلسة باستخدام مفتاح [user] أنه قد تم توثيقه؛

لاحظ أنه إذا تمت مصادقة المستخدم باستخدام بيانات الاعتماد [credentials1] وفشل في المصادقة باستخدام بيانات الاعتماد [credentials2]، فإنه يظل مصادقًا عليه باستخدام بيانات الاعتماد [credentials1].

دعونا نجري بعض اختبارات Postman:

  • نقوم بتشغيل خادم الويب ونظام إدارة قواعد البيانات وخادم البريد؛
  • باستخدام عميل Postman:
    • ابدأ جلسة JSON؛
    • ثم المصادقة؛

فيما يلي سيناريوهات مختلفة.

الحالة 1: POST بدون معلمات مرسلة

Image

  • في [3-5]، لا يحتوي طلب POST على نص؛

وكانت نتيجة الطلب كما يلي:

Image

  • في [2]، تلقينا استجابة HTTP 400 BAD REQUEST؛
  • في [5]، تلقينا رمز خطأ [201]؛

الحالة 2: POST مع بيانات اعتماد غير صحيحة

Image

  • في [6]، بيانات الاعتماد غير صحيحة؛

يرسل الخادم الاستجابة التالية:

Image

  • في [2]، استجابة HTTP 401 UNAUTHORIZED؛
  • في [5]، رد الخطأ؛

الحالة 2: POST مع بيانات اعتماد صحيحة

Image

  • في [6]، بيانات الاعتماد صحيحة؛

استجابة الخادم هي كما يلي:

  • في [2]، استجابة HTTP 200 OK؛ Image
  • في [5]، استجابة النجاح؛

30.11. الإجراء [calculate_tax]

يحسب الإجراء [calculate_tax] ضريبة دافع الضرائب. يتم تعريف مساره على النحو التالي في البرنامج النصي [main]:

1
2
3
4
5
#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

يتوقع الخادم ثلاثة معلمات POST:

  • [married]: نعم / لا؛
  • [children]: عدد أطفال دافع الضرائب؛
  • [الراتب]: الراتب السنوي للمكلف؛

تتولى وحدة التحكم [CalculateTaxController] معالجة الإجراء [calculate_tax]:

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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  no error at start
        erreur = False
        erreurs = []
        #  POST parameters
        post_params = request.form
        #  you need a POST with three parameters
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        #  analyze posted parameters
        if not erreur:
            #  married parameter
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                #  is the parameter valid?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            #  children] parameter
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                #  is the parameter valid?
                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)]")
            #  salary parameter
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                #  is the parameter valid?
                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)]")
        #  mistake?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état": 301, "réponse": erreurs}
            #  we return the result
            return résultat, status_code

        #  tAX CALCULATION
        #  retrieve the [business] layer and the [adminData] dictionary
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        #  simulation no
        id_simulation = session.get('id_simulation', 0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        #  we put the result in session in the form of a TaxPayer dictionary
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        #  we add the result to the list of simulations already carried out and put it in session
        simulations = session.get("simulations", [])
        simulations.append(simulation)
        session["simulations"] = simulations
        #  result
        résultat = {"action": action, "état": 300, "réponse": simulation}
        status_code = status.HTTP_200_OK

        #  we return the result
        return résultat, status_code
  • السطر 13: نسترد اسم الإجراء الحالي؛
  • السطر 17: نجمع الأخطاء في قائمة؛
  • السطر 19: استرداد المعلمات المرسلة. يتم إرسال هذه المعلمات بتنسيق [x-www-form-urlencoded]، ولهذا السبب نستردها من [request.form]. لو كانت قد أُرسلت بتنسيق JSON، لكنا استردناها من [request.data]؛
  • الأسطر 21–24: نتحقق من وجود ثلاثة معلمات مرسلة بالفعل؛
  • الأسطر 27–36: التحقق من وجود وصحة المعلمة المنشورة [married]؛
  • الأسطر 37–48: التحقق من وجود وصحة المعلمة المرسلة [children]؛
  • الأسطر 49–60: التحقق من وجود وصحة المعلمة المنشورة [salary]؛
  • الأسطر 62-66: إذا كان هناك خطأ، يتم إرسال استجابة خطأ 400 BAD REQUEST مع رمز الحالة [301]؛
  • الأسطر 69–71: في حالة عدم وجود خطأ، الاستعداد لحساب الضريبة. للقيام بذلك،
    • السطر 70: استرداد مرجع من طبقة [business]؛
    • السطر 71: استرداد البيانات من سلطة الضرائب في تكوين الخادم؛
  • الأسطر 72-74: يتم حساب ضريبة دافع الضرائب؛
  • الأسطر 75-77: نحسب عدد حسابات الضريبة التي أجراها المستخدم؛
    • السطر 76: استرجاع رقم آخر عملية حسابية تم إجراؤها من الجلسة. هنا، نشير إلى نتيجة عملية الحساب باسم [محاكاة]؛
    • السطر 77: زيادة رقم آخر محاكاة؛
    • السطر 78: يتم حفظ هذا الرقم في الجلسة؛
  • الأسطر 79-84: لتتبع الحسابات التي أجراها المستخدم، سنقوم بتخزين قائمة المحاكاة التي أجراها في جلسته؛
  • السطر 80: ستكون المحاكاة عبارة عن قاموس لكائن TaxPayer الذي ستكون قيمة خاصية [id] الخاصة به هي رقم المحاكاة؛
  • الأسطر 82-84: تتم إضافة المحاكاة الحالية إلى قائمة المحاكاة في الجلسة؛
  • السطران 86-87: نقوم بإعداد استجابة HTTP ناجحة؛
  • السطر 90: نُرجع النتيجة؛

دعونا نجري بعض الاختبارات: يتم تشغيل خادم الويب ونظام إدارة قواعد البيانات وخادم البريد وعميل Postman.

الحالة 1: إجراء حساب ضريبي بينما الجلسة غير مهيأة

Image

الرد كما يلي:

Image

الحالة 2: إجراء حساب الضريبة دون المصادقة

أولاً، نبدأ جلسة JSON باستخدام [/init-session/json]. ثم نرسل نفس الطلب كما في السابق. والاستجابة هي كما يلي:

Image

الحالة 3: إجراء حساب الضريبة مع وجود معلمات مفقودة

نقوم بتهيئة جلسة JSON، والمصادقة، ثم نرسل الطلب التالي:

Image

  • في [5]، المعلمة [married] مفقودة؛

الرد كما يلي:

الحالة 4: حساب الضريبة باستخدام معلمات غير صحيحة

Image

Image

رد الخادم كما يلي:

Image

الحالة 4: إجراء حساب الضريبة باستخدام معلمات صحيحة

Image

رد الخادم كما يلي:

Image

30.12. إجراء [list-simulations]

يتيح الإجراء [list-simulations] للمستخدم عرض قائمة المحاكاة التي أجراها منذ بدء الجلسة. يتم تعريف مساره على النحو التالي في البرنامج النصي [main]:

1
2
3
4
5
#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  retrieve the list of simulations in the session
        simulations = session.get("simulations", [])
        #  we return the result
        return {"action": action, "état": 500,
                "réponse": simulations}, status.HTTP_200_OK
  • السطر 13: يتم استرداد قائمة المحاكاة من الجلسة؛
  • السطران 15-16: يتم إرجاع استجابة نجاح؛

دعونا نجري اختبار Postman التالي:

  • نبدأ جلسة JSON؛
  • نقوم بالمصادقة؛
  • نقوم بإجراء حسابين ضريبيين؛
  • نطلب قائمة المحاكاة؛

الطلب كما يلي:

  • في [3]، لا توجد معلمات؛ Image

رد الخادم كما يلي:

Image

  • في [4]، قائمة عمليات المحاكاة الخاصة بالمستخدم؛

30.13. إجراء [delete-simulation]

يسمح الإجراء [delete-simulation] للمستخدم بحذف إحدى عمليات المحاكاة من قائمة عمليات المحاكاة الخاصة به. يتم تعريف مساره على النحو التالي في البرنامج النصي [main]:

1
2
3
4
5
#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

يتوقع الخادم معلمة واحدة: رقم المحاكاة المراد حذفها. يتم التعامل مع الإجراء [delete-simulation] بواسطة [DeleteSimulationController] التالي:

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):
        #  path elements are retrieved
        dummy, action, numéro = request.path.split('/')

        #  parameter [number] is a positive integer or zero according to its route
        numéro = int(numéro)
        #  the simulation id=number must exist in the simulation list
        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"
            #  we return the error
            return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
        #  delete simulation id=number
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        #  put the simulations back in the session
        session["simulations"] = simulations
        #  we return the result
        return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
  • السطر 10: استرجاع عنصري مسار الطلب. يتم استرجاعهما كسلاسل نصية؛
  • السطر 13: يتم تحويل المعلمة [number] إلى عدد صحيح. ونعلم أن هذا ممكن بفضل توقيع المسار،

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

ونعلم أيضًا أنه عدد صحيح >=0. في الواقع، لا يمكن أن يكون لدينا عنوان URL مثل [/delete-simulation/-4]. فهذا العنوان يرفضه خادم Flask؛

  • السطر 15: نسترد قائمة عمليات المحاكاة من الجلسة؛
  • السطر 16: باستخدام دالة [filter]، نبحث عن المحاكاة التي لها id==number. نحصل على كائن [filter] الذي نحوله إلى [list]؛
  • الأسطر 17–20: إذا لم يُرجع المرشح أي شيء، فهذا يعني أن المحاكاة المراد حذفها غير موجودة. نُرجع استجابة خطأ تشير إلى ذلك؛
  • الأسطر 21–23: نحذف المحاكاة التي أعادها المرشح؛
  • السطر 25: نعيد قائمة المحاكاة الجديدة إلى الجلسة؛
  • السطر 27: نُرجع قائمة المحاكاة الجديدة في الاستجابة؛

نجري اختبار النجاح واختبار الفشل. نقوم بتشغيل المحاكاة ثم نطلب قائمة المحاكاة:

Image

  • المحاكاة هنا تحمل الرقمين 2 و 3؛

نطلب إزالة المحاكاة التي تحمل الرقم 3.

Image

الرد كما يلي:

الآن، دعونا نكرر نفس العملية (حذف المحاكاة التي تحمل الرقم التعريفي 3). يكون الرد عندئذ كما يلي:

Image

Image

30.14. إجراء [end-session]

يسمح الإجراء [end-session] للمستخدم بإنهاء جلسة المحاكاة الخاصة به. يتم تعريف مساره على النحو التالي في البرنامج النصي [main]:

1
2
3
4
5
#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  delete all keys in the current session
        session.clear()
        #  we return the result
        return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
  • السطر 13: حذف جميع المفاتيح من الجلسة. يؤدي هذا إلى حذف:
    • [typeResponse]: نوع استجابات HTTP (json، xml، html
    • [simulation_id]: معرف آخر محاكاة تم إجراؤها؛
    • [simulations]: قائمة محاكاة المستخدم؛
    • [user]: مؤشر على أن المستخدم قد تمت مصادقته؛
  • إرجاع الاستجابة؛

قد يتساءل المرء عن كيفية إرجاع استجابة HTTP من السطر 15، الآن بعد أن لم يعد نوع الاستجابة موجودًا في الجلسة. لمعرفة ذلك، نحتاج إلى العودة إلى دالة |front_controller| في البرنامج النصي الرئيسي [main] وتعديلها على النحو التالي:


…        
         # on not# note the type of response required if this information is in the session
        type_response1 = session.get('typeResponse'None)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # send an e-mail to the application administrator
            send_adminmail(config, log)
        # determine the desired type of response
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # the session type has not yet been set - it will be jSON
            type_response = 'json'
        elif type_response2 is not None:
            # the type of response is known and in the session
            type_response = type_response2
        else:
            type_response=type_response1
        # build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # we send the answer
        return response, status_code
  • السطر 3: يتم تخزين نوع الاستجابة الموجودة حاليًا في الجلسة؛
  • السطر 6: يتم تنفيذ الإجراء. إذا كان:
    • [end-session]، فإن مفتاح [typeResponse] لم يعد موجودًا في الجلسة؛
    • [init-session]، فقد تتغير قيمة مفتاح [typeResponse] في الجلسة؛
  • الأسطر 14-20: يجب إرسال استجابة HTTP. نحتاج إلى معرفة الشكل الذي ستكون عليه:
    • الأسطر 16-18: إذا لم يتم تعريف نوع الاستجابة بواسطة [type_response1] في السطر 3 أو [type_response2] في السطر 15، فإن نوع الاستجابة لم يتم تعريفه قبل الإجراء أو بعده. ثم نستخدم JSON (السطر 18)؛
    • الأسطر 19-21: إذا كان [type_response2] موجودًا — نوع الاستجابة في الجلسة بعد الإجراء — فهذا هو النوع الذي يجب استخدامه؛
    • الأسطر 22-23: خلاف ذلك، فإن [type_response1]، أي نوع الاستجابة قبل الإجراء (الذي يجب أن يكون [end-session])، هو النوع الذي يجب استخدامه؛

30.15. الإجراء [get-admindata]

سنناقش الآن عنواني URL المخصصين لخدمات JSON و XML:

الإجراء
الدور
سياق التنفيذ
/get-admindata
يعرض البيانات الضريبية المستخدمة لحساب الضريبة
GET.
يُستخدم فقط إذا كان نوع الجلسة json أو xml. يجب مصادقة المستخدم
/calculate-taxes
يحسب الضريبة لقائمة من دافعي الضرائب المنشورة بتنسيق JSON
طلب GET.
يُستخدم فقط إذا كان نوع الجلسة json أو xml. يجب مصادقة المستخدم

يتم تعريف عنوان URL [/get-admindata] في مسارات البرنامج النصي الرئيسي [main] على النحو التالي:

1
2
3
4
5
#  get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

يتم التعامل مع المسار [/get-admindata] بواسطة [GetAdminDataController] التالي:

#  import dependencies

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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')
        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            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:
            #  a success answer is returned
            return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • الأسطر 13-21: نتحقق من أننا في جلسة JSON أو XML؛
  • السطر 24: إرجاع قاموس بيانات إدارة الضرائب، الذي تم وضعه في التكوين عند بدء تشغيل الخادم:

    # admindata sera une donnée de portée application en lecture seule
    config["admindata"] = config["layers"]["dao"].get_admindata()

دعونا نستخدم عميل Postman ونطلب عنوان URL [/get-admindata]، بعد بدء جلسة JSON والمصادقة:

Image

استجابة الخادم هي كما يلي:

Image

30.16. الإجراء [calculate-taxes]

يحسب الإجراء [calculate-taxes] الضرائب لقائمة من دافعي الضرائب الموجودة في نص الطلب كسلسلة JSON. نحن على دراية بهذا الإجراء بالفعل: كان يُسمى [calculate_tax_in_bulk_mode] في الإصدار السابق.

مسارها كما يلي:

1
2
3
4
5
#  batch tax calculation
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    #  execute the controller associated with the action
    return front_controller()

يتم التعامل مع هذا الإجراء بواسطة [CalculateTaxesController] التالي:

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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1501,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        #  retrieve the body of the post - wait for a list of dictionaries
        msg_erreur = None
        list_dict_taxpayers = None
        #  the jSON body of POST
        request_text = request.data
        try:
            #  which we transform into a list of dictionaries
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            #  we note the error
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        #  do we have a non-empty list?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            #  we note the error
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        #  do we have a list of dictionaries?
        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
            #  mistake?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        #  mistake?
        if msg_erreur:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  check TaxPayers one by one
        #  initially no errors
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            #  we create a TaxPayer from dict_taxpayer
            msg_erreur = None
            try:
                #  the following operation will eliminate cases where the parameters are not
                #  properties of the TaxPayer class as well as the cases where their values
                #  are incorrect
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            #  certain keys must be present in the dictionary
            if not msg_erreur:
                #  the keys [married, children, salary] must be present in the dictionary
                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]"
            #  mistakes?
            if msg_erreur:
                #  we note the error in the TaxPayer itself
                dict_taxpayer['erreur'] = msg_erreur
                #  add the TaxPayer to the error list
                list_erreurs.append(dict_taxpayer)

        #  we've processed all the taxpayers - are there any mistakes?
        if list_erreurs:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  no mistakes, we can work
        #  data recovery from tax authorities
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            #  process the TaxPayer one by one
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                #  tAX CALCULATION
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                     'salaire': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                #  the result is stored as a dictionary
                list_taxpayers.append(taxpayer.asdict())
            #  we add list_taxpayers to the current simulations, giving each simulation a number
            simulations = session.get("simulations", [])
            id_simulation = session.get("id_simulation", 0)
            for simulation in list_taxpayers:
                #  each simulation is given a number
                id_simulation += 1
                simulation['id'] = id_simulation
                #  we add it to the current list of simulations
                simulations.append(simulation)
            #  we put everything back in session
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            #  we send the response to the client
            return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            #  an error response is sent to the client
            return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • الأسطر 16-24: نتحقق من أننا بالفعل في جلسة JSON أو XML
  • الأسطر 26–120: هذا الكود مألوف لنا بشكل عام. إنه مأخوذ من دالة |index_controller| في الإصدار 10 من التطبيق، والتي تم تكييفها لتتوافق مع مواصفات واجهة [InterfaceController] التي تم تنفيذها؛
  • الأسطر 104–115: كود تمت إضافته لمراعاة البيئة الجديدة لهذا المتحكم. لقد أجرينا للتو حسابات الضرائب. نحتاج إلى تخزين النتائج في قائمة المحاكاة التي يتم الاحتفاظ بها في الجلسة؛
  • السطر 105: نسترد قائمة المحاكاة في الجلسة؛
  • السطر 106: نسترد رقم آخر محاكاة تم إجراؤها؛
  • الأسطر 107-112: نكرر قائمة القواميس التي تحتوي على نتائج حساب الضرائب؛ ونخصص معرف محاكاة لكل منها، ويتم إضافة كل قاموس إلى قائمة المحاكاة؛
  • الأسطر 113–115: يتم إرجاع قائمة المحاكاة الجديدة ورقم آخر محاكاة تم إجراؤها إلى الجلسة؛

نقوم بإجراء اختبار Postman التالي بعد تهيئة جلسة JSON والمصادقة:

Image

Image

استجابة الخادم هي كما يلي:

Image

إذا طلبنا الآن قائمة المحاكاة:

لاحظ أنه في قائمة النتائج لـ [/calcul-impots]، لا يمتلك دافعو الضرائب سمة [id]، بينما في قائمة المحاكاة، تحتوي كل محاكاة على رقم يحددها.

Image