Skip to content

32. وضع HTML في الإصدار 12

ذكرنا في بداية الإصدار 12 أننا سنقوم بتطوير التطبيق على عدة مراحل. كتبنا:

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

لدينا خادم JSON و XML جاهز للعمل. يمكننا الآن الانتقال إلى خادم HTML. سنرى أنه يعيد استخدام البنية الكاملة التي تم تطويرها لخادم JSON/XML ويضيف إليها إدارة عرض HTML.

32.1. بنية MVC

سنقوم بتنفيذ نمط بنية MVC (النموذج – العرض – وحدة التحكم) على النحو التالي:

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. غالبًا ما يكون العميل متصفحًا؛

32.2. هيكل دليل نصوص خادم HTML

Image

  • في [1]، العناصر الثابتة لخادم HTML؛
  • في [2-3]، طرق عرض خادم HTML V. الأجزاء [2] هي عناصر قابلة لإعادة الاستخدام داخل طرق العرض [3]؛
  • في [4]، مجلد يستخدم لاختبار طرق العرض بشكل ثابت؛
  • في [5]، المجلد الخاص بنماذج M لعروض V، حيث M في MVC؛

32.3. نظرة عامة على طرق العرض

يستخدم تطبيق الويب HTML أربع طرق عرض. طريقة العرض الأولى هي طريقة عرض المصادقة:

  • الإجراء الذي يؤدي إلى هذا العرض الأول هو الإجراء [/init-session] [1]؛ Image
  • يؤدي النقر على زر [Validate] إلى تشغيل الإجراء [/authenticate-user] مع معلمتين منشورتين [2-3]؛

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

Image

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

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

Image

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

سيُطلق على العرض الرابع اسم عرض الأخطاء غير المتوقعة:

  • في [1]: قام المستخدم بكتابة عنوان URL بنفسه. ومع ذلك، في هذا المثال، لم تكن هناك أي محاكاة. لذلك نتلقى رسالة الخطأ [2]. نحن على دراية بهذه الرسالة. لقد ظهرت لنا في JSON/XML. سنسمي هذا النوع من الأخطاء "خطأ غير متوقع" لأنه لا يمكن أن يحدث أثناء الاستخدام العادي للتطبيق. ولا يمكن أن تحدث إلا عندما يكتب المستخدم عناوين URL بنفسه؛ Image
  • في حالة حدوث خطأ غير متوقع، تتيح لك الروابط [3-5] العودة إلى إحدى العروض الثلاثة الأخرى؛

دعونا نستعرض عناوين URL المختلفة لخدمات خادم JSON/XML:

الإجراء
الدور
سياق التنفيذ
/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.

32.4. تكوين طرق العرض

يتم التعامل مع الإجراء بواسطة وحدة تحكم. تعرض وحدة التحكم هذه مجموعة (result, status_code) حيث:

  • [result] عبارة عن قاموس يحتوي على مفاتيح [action, status, response]؛
  • [status_code] هو رمز الحالة لاستجابة HTTP التي سيتم إرسالها إلى العميل؛

في جلسة HTML، تعتمد الصفحة المعروضة بعد الإجراء على رمز الحالة الذي تعيده وحدة التحكم. تنعكس هذه التبعية في تكوين [config] على النحو التالي:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
 
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
 
        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussite
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],
    }
  • الأسطر 2–40: [views] هي قائمة بالطرق. لننظر إلى الطريقة في الأسطر 3–13:
    • السطر 11: العرض المعروض V؛
    • السطر 12: مثيل الفئة المسؤول عن إنشاء النموذج M لهذا العرض؛
    • الأسطر 5–10: الحالات التي تؤدي إلى هذه العرضة؛
  • الأسطر 3–13: عرض المصادقة؛
  • الأسطر 14–28: عرض حساب الضرائب؛
  • الأسطر 29-39: عرض قائمة المحاكاة؛
  • الأسطر 42-46: عرض الأخطاء غير المتوقعة؛
  • الأسطر 49-57: تؤدي بعض الحالات إلى عرض عبر إعادة توجيه. هذا هو الحال بالنسبة للحالة 400، التي تتوافق مع الإجراء الناجح [/fin-session]. يجب بعد ذلك إعادة توجيه العميل إلى الإجراء [http://machine:port/chemin/init-session/html]؛

سنقدم الآن العروض المختلفة.

32.5. عرض المصادقة

Image

32.5.1. نظرة عامة على العرض

عرض المصادقة هو كما يلي:

Image

يتكون العرض من عنصرين سنسميهما "أجزاء":

  • يتم إنشاء الجزء [1] بواسطة الجزء [v-banner.html]؛
  • يتم إنشاء الجزء [2] بواسطة الجزء [v-authentication.html]؛

يتم إنشاء عرض المصادقة بواسطة الصفحة التالية [vue-authentification.html]:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <div class="col-md-9">
            {% include "fragments/v-authentification.html" %}
        </div>
    </div>
    <!-- if error - displays an error alert -->
    {% if modèle.error %}
    <div class="row">
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs suivantes se sont produites :
                <ul>{{modèle.erreurs|safe}}</ul>
            </div>
        </div>
    </div>
    {% endif %}
</div>
</body>
</html>

تعليقات

  • السطر 2: يبدأ مستند HTML بهذا السطر؛
  • الأسطر 3–36: صفحة HTML محاطة بعلامتي <html> و </html
  • الأسطر 4–11: رأس مستند HTML (head
  • السطر 6: تشير علامة <meta charset> هنا إلى أن المستند مشفر بـ UTF-8؛
  • السطر 7: تحدد علامة <meta name='viewport'> عرض نافذة العرض الأولي: عبر العرض الكامل للشاشة التي تعرضها (width) بحجمها الأولي (initial-scale) دون تغيير الحجم لتناسب شاشة أصغر (shrink-to-fit
  • السطر 9: تحدد علامة <link rel='stylesheet'> ملف CSS الذي يتحكم في مظهر نافذة العرض. هنا، نستخدم إطار عمل Bootstrap 4.4.1 CSS [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ؛
  • السطر 10: تحدد علامة title عنوان الصفحة:

Image

  • الأسطر 13–35: يتم تضمين نص الصفحة الإلكترونية بين علامتي body و /body؛
  • الأسطر 14–34: تحدد علامة <div> قسمًا من الصفحة المعروضة. تشير سمات [class] المستخدمة في العرض جميعها إلى إطار عمل Bootstrap CSS. تحدد علامة <div class=’container> (السطر 14) حاوية Bootstrap؛
  • السطر 26: يتم تضمين الجزء [v-banner.html]. يولد هذا الجزء شعار الصفحة [1]. سنصفه بعد قليل؛
  • الأسطر 18–22: تحدد علامة <div class=’row’> صفًا في Bootstrap. تتكون هذه الصفوف من 12 عمودًا؛
  • السطر 19: تحدد العلامة قسمًا مكونًا من 9 أعمدة؛
  • السطر 20: نضمّن الجزء [v-authentification.htmlالذي يعرض نموذج المصادقة للصفحة [2]. سنصفه بعد قليل؛
  • الأسطر 24–33: لا يُستخدم كود HTML في هذه الأسطر إلا إذا كانت قيمة [model.error] هي True. سنقوم دائمًا بالعمل بهذه الطريقة: سيتم تغليف نموذج عرض HTML في قاموس [model]؛
  • الأسطر 24–33: تفشل المصادقة إذا أدخل المستخدم بيانات اعتماد غير صحيحة. في هذه الحالة، يتم إعادة عرض عرض المصادقة مع رسالة خطأ. تشير السمة [model.error] إلى ما إذا كان يجب عرض رسالة الخطأ هذه أم لا؛
  • الأسطر 27–30: تعريف منطقة بخلفية وردية (class="alert alert-danger") (السطر 27)؛

Image

  • السطر 28: بعض النصوص؛
  • السطر 29: تعرض علامة HTML <ul> (قائمة غير مرتبة) قائمة نقطية. يجب أن يكون لكل عنصر في القائمة الصيغة <li>item</li>. هنا، نعرض قيمة [model.errors]. يتم تصفية هذه القيمة (للتحقق من وجود |) بواسطة مرشح [safe]. بشكل افتراضي، عندما يتم إرسال سلسلة إلى المتصفح، يقوم Flask بـ"تجاوز" أي علامات HTML قد تحتوي عليها حتى لا يفسرها المتصفح. لكن في بعض الأحيان نريد أن يتم تفسيرها. هذا هو الحال هنا، حيث تحتوي السلسلة [model.errors] على علامات HTML <li> و </li> المستخدمة لتحديد عنصر القائمة. في هذه الحالة، نستخدم مرشح [safe]، الذي يخبر Flask أن السلسلة المراد عرضها آمنة وبالتالي لا يجب عليه تنقية أي علامات HTML يجدها؛

دعونا نلاحظ العناصر الديناميكية التي يجب تعريفها في هذا الكود:

  • [model.error]: لعرض رسالة خطأ؛
  • [model.errors]: قائمة (بالمعنى HTML) برسائل الخطأ؛

32.5.2. جزء [v-banner.html]

يعرض جزء [v-banner.html] الشعار العلوي لجميع طرق العرض في تطبيق الويب:

Image

فيما يلي كود جزء [v-banner.html]:


<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
    <div class="row">
        <div class="col-md-4">
            <img src="{{ url_for('static', filename='images/logo.jpg') }}" alt="Cerisier en fleurs"/>
        </div>
        <div class="col-md-8">
            <h1>
                Calculez votre impôt
            </h1>
        </div>
    </div>
</div>

تعليقات

  • الأسطر 2–13: يتم تغليف الشعار في قسم Bootstrap Jumbotron [<div class="jumbotron">]. تعمل فئة Bootstrap هذه على تصميم المحتوى المعروض بطريقة معينة لجعله بارزًا؛
  • الأسطر 3–12: صف Bootstrap؛
  • الأسطر 4–6: يتم وضع صورة [img] في الأعمدة الأربعة الأولى من الصف؛
  • السطر 5: الصيغة:
{{ url_for('static', filename='images/logo.jpg') }}

تستخدم دالة [url_for] في Flask. هنا، قيمتها هي عنوان URL لملف [images/logo.jpg] الموجود في المجلد [static]؛

  • الأسطر 7–11: ستُستخدم الأعمدة الثمانية الأخرى في الصف (تذكر أن العدد الإجمالي هو 12) لعرض النص (السطر 9) بخط كبير الحجم (<h1>، الأسطر 8–10)؛

32.5.3. جزء [v-authentification.html]

يعرض جزء [v-authentification.html] نموذج المصادقة الخاص بتطبيق الويب:

Image

فيما يلي كود جزء [v-authentification.html]:


<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="/authentifier-utilisateur">
 
    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>
 
    <!-- bootstrap form -->
    <fieldset class="form-group">
        <!-- 1st line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
            <div class="col-md-4">
                <!-- text input field -->
                <input type="text" class="form-control" id="user" name="user"
                       placeholder="Nom d'utilisateur" value="{{ modèle.login }}" required>
            </div>
        </div>
        <!-- 2nd line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
            <!-- text input field -->
            <div class="col-md-4">
                <input type="password" class="form-control" id="password" name="password"
                       placeholder="Mot de passe" required>
            </div>
        </div>
        <!-- submit] button on a 3rd line -->
        <div class="form-group row">
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>
 
</form>

تعليقات

  • الأسطر 2–39: تحدد علامة <form> نموذج HTML. يتميز هذا النموذج عمومًا بالخصائص التالية:
    • يحدد حقول الإدخال (علامات <input> في السطرين 17 و27)؛
    • يحتوي على زر [submit] (السطر 34) الذي يرسل القيم المدخلة إلى عنوان URL المحدد في سمة [action] لعلامة [form] (السطر 2). يتم تحديد طريقة HTTP المستخدمة لطلب عنوان URL هذا في سمة [method] لعلامة [form] (السطر 2)؛
    • هنا، عندما ينقر المستخدم على زر [Submit] (السطر 34)، سيقوم المتصفح بإرسال (POST) (السطر 2) القيم التي تم إدخالها في النموذج إلى عنوان URL [/authenticate-user] (السطر 2)؛
    • القيم المرسلة هي القيم التي أدخلها المستخدم في حقول الإدخال في السطرين 17 و 27. سيتم إرسالها في نص طلب HTTP الذي يرسله المتصفح في النموذج [x-www-form-urlencoded]. تتوافق أسماء المعلمات [user, password] مع سمات [name] لحقول الإدخال في السطرين 17 و 27؛
  • الأسطر 5–7: قسم Bootstrap لعرض عنوان على خلفية زرقاء:
  • الأسطر 10-37: نموذج Bootstrap. سيتم بعد ذلك تصميم جميع عناصر النموذج بطريقة محددة؛ Image
  • الأسطر 12-20: تحدد الصف الأول من Bootstrap في النموذج:

Image

  • السطر 14 يحدد التسمية [1] عبر ثلاثة أعمدة. تربط السمة [for] لعلامة [label] التسمية بالسمة [id] لحقل الإدخال في السطر 17؛
  • الأسطر 15-19: وضع حقل الإدخال ضمن تخطيط مكون من أربعة أعمدة؛
  • الأسطر 17-18: تحدد علامة HTML [input] حقل إدخال. ولها عدة سمات:
    • [type='text']: هذا حقل إدخال نصي. يمكنك كتابة أي شيء فيه؛
    • [class='form-control']: نمط Bootstrap لحقل الإدخال؛
    • [id='user']: معرف حقل الإدخال. يستخدم هذا المعرف عمومًا بواسطة كود CSS و JavaScript؛
    • [name='user']: اسم حقل الإدخال. سيتم إرسال القيمة التي أدخلها المستخدم بواسطة المتصفح تحت هذا الاسم [user=xx]؛
    • [placeholder='prompt']: النص المعروض في حقل الإدخال عندما لا يكون المستخدم قد كتب أي شيء بعد؛

Image

  • (تابع)
    • [value='value']: سيتم عرض النص 'value' في حقل الإدخال فور ظهوره، قبل أن يقوم المستخدم بإدخال أي شيء آخر. تُستخدم هذه الآلية في حالة حدوث خطأ لعرض الإدخال الذي تسبب في الخطأ. هنا، ستكون هذه القيمة هي قيمة المتغير [model.login]؛
    • [required]: يتطلب من المستخدم إدخال قيمة حتى يمكن إرسال النموذج إلى الخادم:
  • الأسطر 21–30: كود مشابه لحقل إدخال كلمة المرور؛
  • السطر 27: [type='password'] ينشئ حقل إدخال نصي (يمكنك كتابة أي شيء) لكن الأحرف التي يتم إدخالها مخفية:

Image

  • الأسطر 32–36: سطر Bootstrap ثالث لزر [Submit]؛
  • السطر 34: نظرًا لوجود السمة [type="submit"]، يؤدي النقر على هذا الزر إلى قيام المتصفح بإرسال القيم المدخلة إلى الخادم، كما هو موضح سابقًا. تعرض السمة CSS [class="btn btn-primary"] زرًا أزرق اللون: Image

هناك أمر أخير يجب توضيحه. في السطر الثاني، تحدد السمة [action="/authentifier-utilisateur"] عنوان URL غير مكتمل (لا يبدأ بـ http://machine:port/chemin). في مثالنا، تكون جميع عناوين URL للتطبيق على شكل [http://machine:port/chemin/action/param1/param2/.. حيث [http://machine:port/chemin] هي جذر عناوين URL للخدمة. في [action="/authenticate-user"]، لدينا عنوان URL مطلق، أي عنوان يُقاس من جذر عناوين URL. وبالتالي، فإن عنوان URL الكامل لطلب POST هو [http://machine:port/chemin/authentifier-utilisateur وهذا هو ما سيستخدمه المتصفح.

لاحظ أن هذا الجزء يستخدم قالب [model.login].

32.5.4. الاختبارات المرئية

يمكننا اختبار طرق العرض جيدًا قبل دمجها في التطبيق. الهدف هنا هو اختبار مظهرها البصري. سنجمع جميع طرق العرض الاختبارية في مجلد [tests_views] الخاص بالمشروع:

Image

لاختبار العرض V [vue-authentification.html]، نحتاج إلى إنشاء نموذج البيانات M الذي سيعرضه. نقوم بذلك باستخدام البرنامج النصي [test_vue_authentification.py]:

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  user code
    modèle["login"] = "albert"
    #  error list
    modèle["error"] = True
    erreurs = ["erreur1", "erreur2"]
    #  build a HTML list of errors
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  page display
    return make_response(render_template("views/vue-authentification.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

تعليقات

  • الأسطر 1-3: نقوم بإنشاء تطبيق Flask الغرض الوحيد منه هو عرض طريقة العرض [authentication-view.html] (السطر 22)؛
  • السطر 7: يحتوي التطبيق على عنوان URL واحد فقط للخدمة؛
  • الأسطر 9–20: تحتوي طريقة عرض المصادقة على أجزاء ديناميكية يتحكم فيها كائن [model]. يُسمى هذا الكائن نموذج العرض. وفقًا لأحد التعريفين المقدمين لاختصار MVC، لدينا هنا حرف M في MVC ( ). عند تعريف طريقة العرض [authentication-view.html]، حددنا ثلاث قيم ديناميكية:
    • [model.error]: قيمة منطقية تشير إلى ما إذا كان يجب عرض رسالة خطأ أم لا؛
    • [model.errors]: قائمة HTML لرسائل الخطأ؛
    • [model.login]: تسجيل دخول المستخدم؛

لذلك، نحتاج إلى تعريف هذه القيم الديناميكية الثلاث.

  • الأسطر 9–20: نحدد العناصر الديناميكية الثلاثة لعرض المصادقة؛

لتشغيل الاختبار، نقوم بتشغيل البرنامج النصي [tests_views/test_vue_authentification.py] ونطلب عنوان URL [/localhost:5000/]:

نواصل هذه الاختبارات المرئية حتى نكون راضين عن النتيجة.

Image

32.5.5. حساب نموذج العرض

بمجرد تحديد المظهر المرئي للعرض، يمكننا المضي قدمًا في حساب نموذج العرض في ظل الظروف الواقعية. سيتم إنشاء نماذج العرض بواسطة فئات موجودة في المجلد [models_for_views]:

Image

ستقوم كل فئة تولد نموذج عرض بتنفيذ واجهة [InterfaceModelForView] التالية:


from abc import ABC, abstractmethod
 
from flask import Request
from werkzeug.local import LocalProxy
 
class InterfaceModelForView(ABC):
 
    @abstractmethod
    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        pass
  • الأسطر 8–10: الطريقة [get_model_for_view] مسؤولة عن إنتاج نموذج عرض مغلف في قاموس. للقيام بذلك، تتلقى المعلومات التالية:
    • [request, session, config] هي نفس المعلمات التي يستخدمها وحدة التحكم في الإجراءات. وبالتالي، يتم تمريرها أيضًا إلى النموذج؛
    • أنتجت وحدة التحكم نتيجة [result] يتم تمريرها أيضًا إلى النموذج. تحتوي هذه النتيجة على عنصر مهم [status] يشير إلى كيفية سير تنفيذ الإجراء الحالي. سيستخدم النموذج هذه المعلومات؛

لقد رأينا أنه في تكوين التطبيق [config]، تُستخدم رموز الحالة التي تعيدها وحدات التحكم لتحديد عرض HTML المراد عرضه:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussite
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],
    }

وبالتالي، فإن رموز الحالة [700، 201] (السطران 7 و9) هي التي تتسبب في عرض صفحة المصادقة. لفهم معنى هذه الرموز، يمكننا الرجوع إلى اختبارات [Postman] التي أجريت على تطبيق JSON:

  • [init-session-json-700]: 700 هو رمز الحالة الذي يتبع إجراء [init-session] الناجح: ثم يتم عرض نموذج المصادقة الفارغ؛
  • [authenticate-user-201]: 201 هو رمز الحالة الذي يتبع إجراء [authenticate-user] الفاشل (بيانات اعتماد غير معترف بها): ثم يتم عرض نموذج المصادقة حتى يمكن تصحيح بيانات الاعتماد؛

الآن بعد أن عرفنا متى يجب عرض نموذج المصادقة، يمكننا تعريف نموذجه في [ModelForAuthentificationView] (السطر 12):

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForAuthentificationView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  we encapsulate the paged data in the model
        modèle = {}
        #  application status
        état = résultat["état"]
        #  the model depends on the state
        if état == 700:
            #  case of empty form display
            modèle["login"] = ""
            #  no error to display
            modèle["error"] = False
        elif état == 201:
            #  false authentication
            #  the user initially entered is redisplayed
            modèle["login"] = request.form.get("user")
            #  there is an error to display
            modèle["error"] = True
            #  list HTML of error msg
            erreurs = ""
            for erreur in résultat["réponse"]:
                erreurs += f"<li>{erreur}</li>"
            modèle["erreurs"] = erreurs

        #  we render the model
        return modèle

تعليقات

  • السطر 8: يجب أن تُرجع طريقة [get_model_for_view] الخاصة بعرض المصادقة قاموسًا يحتوي على ثلاثة مفاتيح [error، errors، login]. يعتمد هذا الحساب على رمز الحالة الذي أرجعته وحدة التحكم في الإجراء؛
  • السطر 12: نسترد رمز الحالة الذي أعادته وحدة التحكم التي عالجت الإجراء الحالي؛
  • الأسطر 14–29: يعتمد النموذج على رمز الحالة هذا؛
  • الأسطر 15-18: الحالة التي يجب فيها عرض نموذج مصادقة فارغ؛
  • الأسطر 20-29: حالة فشل المصادقة: نعرض اسم المستخدم الذي أدخله المستخدم ونعرض رسالة خطأ. يمكن للمستخدم بعد ذلك محاولة المصادقة مرة أخرى؛
  • السطر 22: يمكن استرداد اسم المستخدم الذي أدخله المستخدم في البداية من طلب العميل؛
  • السطر 24: يُشار إلى وجود أخطاء يجب عرضها؛
  • الأسطر 26-29: في حالة وجود خطأ، تحتوي result[‘response’] على قائمة بالأخطاء؛

32.5.6. إنشاء استجابات HTML

لنعد إلى نموذج MVC لتطبيق HTML:

  • في 2 (2a، 2b): يقوم وحدة التحكم بتنفيذ إجراء؛ Image
  • في 3 (3a، 3b، 3c): يتم اختيار عرض وإرساله إلى العميل؛

في [3a]، يتم اختيار نوع الاستجابة (JSON، XML، HTML). لقد رأينا كيف يتم إنشاء استجابات JSON و XML، ولكن لم نرَ بعد استجابات HTML. يتم إنشاء هذه الاستجابات بواسطة فئة [HtmlResponse]:

Image

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


        ….
        # on construit la réponse à envoyer
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # on envoie la réponse
        return response, status_code

حيث، في السطر 3، config['responses'] هو القاموس التالي:


        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

وبالتالي، فإن فئة [HtmlResponse] هي التي تولد استجابة HTML. وفيما يلي شفرة البرمجة الخاصة بها:

#  dictionary of HTML responses according to the status contained in the result

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

from InterfaceResponse import InterfaceResponse

class HtmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  the HTML response depends on the status code returned by the controller
        état = résultat["état"]

        #  do I need to redirect?
        for redirection in config["redirections"]:
            #  conditions requiring redirection
            états = redirection["états"]
            if état in états:
                #  you need to redirect
                return redirect(f"/{redirection['to']}"), status.HTTP_302_FOUND

        #  a state corresponds to a view
        #  search for it in the list of views
        views_configs = config["views"]
        trouvé = False
        i = 0
        #  browse the list of views
        nb_views = len(views_configs)
        while not trouvé and i < nb_views:
            #  view n° i
            view_config = views_configs[i]
            #  states associated with view n° i
            états = view_config["états"]
            #  is the state you're looking for in the states associated with view n° i?
            if état in états:
                trouvé = True
            else:
                #  next view
                i += 1
        #  found?
        if not trouvé:
            #  if no view exists for the current state of the application
            #  render error view
            view_config = config["view-erreurs"]

        #  calculate the view model to be displayed
        model_for_view = view_config["model_for_view"]
        modèle = model_for_view.get_model_for_view(request, session, config, résultat)
        #  generate the HTML response code
        html = render_template(view_config["view_name"], modèle=modèle)
        #  build the HTTP response
        response = make_response(html)
        response.headers['Content-Type'] = 'text/html; charset=utf-8'
        #  we return the result
        return response, status_code
  • السطر 11: تتلقى طريقة [build_http_response] المسؤولة عن إنشاء استجابة HTML المعلمات التالية:
    • [request, session, dict]: هذه هي المعلمات التي يستخدمها وحدة التحكم لمعالجة الإجراء الحالي؛
    • [status_code، result] هما النتيجتان اللتان تنتجهما وحدة التحكم هذه نفسها؛
  • السطر 14: كما ذكرنا، يعتمد استجابة HTML للخادم على رمز الحالة الموجود في قاموس [result]؛
  • الأسطر 16–22: يتم التعامل مع عمليات إعادة التوجيه أولاً. في الوقت الحالي، سنتجاهل هذا الأمر حتى نواجه مثالاً على إعادة التوجيه. لاحظ أن عمليات إعادة التوجيه عادةً ما تكون حالة استخدام لخادم HTML. لا يحدث هذا مع خوادم JSON أو XML؛
  • الأسطر 24–41: نبحث بين طرق العرض عن تلك التي تحتوي قائمة [states] الخاصة بها على الحالة المطلوبة؛
  • الأسطر 42–46: إذا لم يتم العثور على عرض، فهذا خطأ غير متوقع. دعونا نلقي نظرة على مثال. في التشغيل العادي للتطبيق، لا ينبغي أن تفشل الإجراء [/delete-simulation] أبدًا. في الواقع، سنرى أن حذف المحاكاة هذا يتم باستخدام روابط تم إنشاؤها بواسطة الكود. هذه الروابط صالحة ولا يمكن أن تسبب خطأً. ومع ذلك، كما رأينا، يمكن للمستخدم كتابة عنوان URL [/delete-simulation/id] مباشرةً وبالتالي التسبب في حدوث خطأ. في هذه الحالة، يُرجع [SupprimerSimulationController] رمز حالة 601. ومع ذلك، لا يوجد رمز الحالة هذا في قائمة رموز الحالة التي تؤدي إلى عرض صفحة HTML. لذلك، سيتم عرض عرض الخطأ. يتم تعريفه على النحو التالي في التكوين:

        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
  • السطر 49: بمجرد معرفة العرض المراد عرضه، نسترد الفئة التي تولد القالب الخاص به. توجد هذه الفئة أيضًا في التكوين [config]؛
  • السطر 50: بمجرد العثور على هذه الفئة، نقوم بإنشاء نموذج العرض؛
  • السطر 52: بمجرد حساب النموذج M للطريقة V، يمكننا إنشاء كود HTML للطريقة؛
  • السطران 54–55: نقوم بإنشاء استجابة HTTP مع نص HTML؛
  • السطران 56-57: نُرجع استجابة HTTP مع رمز الحالة الخاص بها؛

32.5.7. الاختبارات [Postman]

سنقوم بتنفيذ الطلبات التي تُرجع الرموز [700، 201]، والتي تعرض شاشة المصادقة:

  • [init-session-html-700]: 700 هو رمز الحالة الذي يتبع إجراء [init-session] الناجح؛ ثم يتم عرض نموذج المصادقة الفارغ؛
  • [authenticate-user-201]: 201 هو رمز الحالة الذي يتبع إجراء [authenticate-user] الفاشل (بيانات اعتماد غير معترف بها): ثم يتم عرض نموذج المصادقة حتى يمكن تصحيحه؛

ما عليك سوى إعادة استخدامها والتحقق مما إذا كانت تعرض طريقة عرض المصادقة بشكل صحيح. فيما يلي مثالان:

الحالة 1: [init-session-html-700]، بداية جلسة HTML؛

Image

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

Image

  • في [5]، يتيح لك وضع [Preview] عرض صفحة HTML المستلمة؛
  • في [6]، نرى النموذج الفارغ المتوقع؛
  • في [7]، لم يتبع Postman الرابط المؤدي إلى صورة الصفحة؛
  • في [8]، يوفر وضع [Raw] الوصول إلى HTML المستلم؛

Image

  • في [3]، الرابط الذي فشل Postman في تحميله. عرض قيمة السمة [alt=alternative]، والتي تظهر عندما يتعذر تحميل الصورة. هنا، يبدو أن Postman لم يرغب في تحميلها. في ، يمكنك التحقق من ذلك عن طريق طلب عنوان URL [http://localhost:5000/static/images.logo.jpg] باستخدام Postman:

الحالة 2: [user-authentication-201]، خطأ في المصادقة

Image

الآن، دعونا نقوم بمصادقة غير صحيحة بعد تهيئة جلسة HTML بنجاح:

Image

أعلاه:

  • في [4,7]: يرسل الطلب السلسلة [user=bernard&password=thibault]؛

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

Image

  • في [4]، يتم عرض رسالة خطأ؛
  • في [3]، تم عرض المستخدم غير الصحيح مرة أخرى؛

32.5.8. الخلاصة

تمكنا من اختبار العرض [vue-authentification.html] دون كتابة العروض الأخرى. كان ذلك ممكنًا للأسباب التالية:

  • كتابة جميع وحدات التحكم؛
  • [Postman] يسمح لنا بإرسال الطلبات إلى الخادم دون الحاجة إلى جميع العروض. عند كتابة وحدات التحكم، يجب أن تكون مستعدًا للتعامل مع الطلبات التي لا تسمح بها أي عرض. يجب ألا تفترض مسبقًا أن "هذا الطلب مستحيل". يجب عليك التحقق؛

32.6. عرض حساب الضريبة

Image

32.6.1. نظرة عامة على العرض

عرض حساب الضريبة هو كما يلي:

Image

يتكون العرض من ثلاثة أجزاء:

  • 1: يتم إنشاء الشعار العلوي بواسطة الجزء [v-bandeau.html] الذي تم عرضه سابقًا؛
  • 2: نموذج حساب الضريبة الذي تم إنشاؤه بواسطة الجزء [v-calcul-impot.html]؛
  • 3: قائمة تحتوي على رابطين، تم إنشاؤها بواسطة الجزء [v-menu.html]؛

يتم إنشاء عرض حساب الضريبة بواسطة الكود التالي [vue-calcul-impot.html]:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <!-- the menu -->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- calculation form -->
        <div class="col-md-9">
            {% include "fragments/v-calcul-impot.html" %}
        </div>
    </div>
    <!-- success stories -->
 
    {% if modèle.success %}
    <!-- a success alert is displayed -->
    <div class="row">
        <div class="col-md-3">
 
        </div>
        <div class="col-md-9">
            <div class="alert alert-success" role="alert">
                {{modèle.impôt}}</br>
                {{modèle.décôte}}</br>
                {{modèle.réduction}}</br>
                {{modèle.surcôte}}</br>
                {{modèle.taux}}</br>
            </div>
        </div>
    </div>
    {% endif %}
 
    {% if modèle.error %}
    <!-- 9-column error list -->
    <div class="row">
        <div class="col-md-3">
 
        </div>
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs suivantes se sont produites :
                <ul>{{modèle.erreurs | safe}}</ul>
            </div>
        </div>
    </div>
    {% endif %}
</div>
</body>
</html>

تعليقات

  • نقوم بتعليق الميزات الجديدة التي لم يتم مواجهتها بعد فقط؛
  • السطر 16: إدراج الشعار العلوي للعرض في الصف الأول من Bootstrap الخاص بالعرض؛
  • السطر 21: إدراج القائمة، التي ستشغل ثلاثة أعمدة من الصف الثاني لـ Bootstrap في العرض (السطران 18 و20)؛
  • السطر 25: إدراج نموذج حساب الضريبة، الذي سيشغل تسعة أعمدة (السطر 24) من الصف الثاني لـ Bootstrap في العرض (السطر 18)؛
  • الأسطر 30–46: إذا نجح حساب الضريبة [model.success=True]، فسيتم عرض نتيجة حساب الضريبة في إطار أخضر (الأسطر 37–43). يوجد هذا المربع في الصف الثالث من Bootstrap في العرض (السطر 32) ويشغل تسعة أعمدة (السطر 36) على يمين ثلاثة أعمدة فارغة (الأسطر 33-35). وبالتالي، سيكون هذا المربع أسفل نموذج حساب الضريبة؛
  • الأسطر 48–61: إذا فشل حساب الضريبة [model.error=True]، فسيتم عرض رسالة خطأ في حاوية وردية (الأسطر 55–58). يوجد هذا الإطار في الصف الثالث من Bootstrap في العرض (السطر 50) ويشغل تسعة أعمدة (السطر 54) على يمين ثلاثة أعمدة فارغة (الأسطر 51–53). وبالتالي، سيكون هذا الإطار أيضًا أسفل نموذج حساب الضريبة؛

32.6.2. المقتطف [v-calcul-impot.html]

يعرض الجزء [v-calcul-impot.html] نموذج حساب الضريبة الخاص بالتطبيق الويب:

فيما يلي كود جزء [v-calcul-impot.html]:

Image


<!-- form HTML posted -->
<form method="post" action="/calculer-impot">
    <!-- 12-column message on blue background -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    <!-- form elements -->
    <fieldset class="form-group">
        <!-- first row of 9 columns -->
        <div class="row">
            <!-- 4-column wording -->
            <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
            <!-- 5-column radio buttons-->
            <div class="col-md-5">
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" {{modèle.checkedOui}}>
                    <label class="form-check-label" for="gridRadios1">
                        Oui
                    </label>
                </div>
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" {{modèle.checkedNon}}>
                    <label class="form-check-label" for="gridRadios2">
                        Non
                    </label>
                </div>
            </div>
        </div>
        <!-- second row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
            <!-- 5-column numerical entry field for number of children -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="{{modèle.enfants}}" required>
            </div>
        </div>
        <!-- third row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="salaire" class="col-md-4 col-form-label">Salaire annuel net imposable</label>
            <!-- 5-column numeric input field for wages -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel net imposable" aria-describedby="salaireHelp" value="{{modèle.salaire}}" required>
                <small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
            </div>
        </div>
        <!-- fourth row, [submit] button on 5 columns -->
        <div class="form-group row">
            <div class="col-md-5">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>
 
</form>

تعليقات

  • السطر 2: سيتم إرسال نموذج HTML (السمة method) إلى عنوان URL [/calculer-import] (السمة action). وستكون القيم المرسلة هي قيم حقول الإدخال:
  • قيمة زر الاختيار المحدد في النموذج:
      • [married=yes] إذا تم تحديد زر الاختيار [Yes] (الأسطر 17–22). [married] هي قيمة السمة [name] في السطر 18، و[yes] هي قيمة السمة [value] في السطر 18؛
      • [married=no] إذا تم تحديد زر الاختيار [No] (الأسطر 23–28). [married] هي قيمة السمة [name] في السطر 24، و[no] هي قيمة السمة [value] في السطر 24؛
    • قيمة حقل الإدخال الرقمي في السطر 37 في النموذج [children=xx]، حيث [children] هي قيمة السمة [name] في السطر 37، و[xx] هي القيمة التي أدخلها المستخدم عبر لوحة المفاتيح؛
    • قيمة حقل الإدخال الرقمي في السطر 46 في النموذج [salary=xx]، حيث [salary] هي قيمة السمة [name] في السطر 46، و[xx] هي القيمة التي أدخلها المستخدم عبر لوحة المفاتيح؛

وأخيرًا، ستكون القيمة المنشورة بالشكل [married=xx&children=yy&salary=zz].

  • (تابع)
    • سيتم إرسال القيم المدخلة عندما ينقر المستخدم على زر [submit] في السطر 53؛
  • الأسطر 16-30: زرّا الاختيار:

Image

يعد زرا الـ"راديو" جزءًا من نفس مجموعة زرا الـ"راديو" لأنهما يحملان نفس سمة [name] (السطران 18 و24). يضمن المتصفح أنه داخل مجموعة زرا الـ"راديو"، يتم تحديد زر واحد فقط في أي وقت. لذلك، يؤدي النقر على أحدهما إلى إلغاء تحديد الزر الذي كان محددًا سابقًا؛

  • هذه أزرار اختيار بسبب السمة [type="radio"] (السطران 18 و24)؛
  • عند عرض النموذج (قبل إدخال البيانات)، يجب تحديد أحد أزرار الاختيار: للقيام بذلك، ما عليك سوى إضافة السمة [checked='checked'] إلى علامة <input type="radio"> ذات الصلة. يتم تحقيق ذلك باستخدام المتغيرات الديناميكية:
    • [model.checkedYes] في السطر 18؛
    • [model->checkedNo] في السطر 24؛

ستكون هذه المتغيرات جزءًا من قالب العرض.

  • السطر 37: حقل إدخال رقمي [type="number"] بقيمة دنيا تبلغ 0 [min="0"]. في المتصفحات الحديثة، يعني هذا أن المستخدم لا يمكنه إدخال سوى رقم >=0. وفي هذه المتصفحات الحديثة نفسها، يمكن الإدخال باستخدام شريط تمرير يمكن النقر عليه لأعلى أو لأسفل. تشير السمة [step="1"] في السطر 37 إلى أن شريط التمرير سيعمل بزيادات قدرها 1. ونتيجة لذلك، لن يقبل شريط التمرير سوى القيم الصحيحة التي تتراوح من 0 إلى n بخطوات قدرها 1. بالنسبة للإدخال اليدوي، يعني هذا أن الأرقام التي تحتوي على أرقام عشرية لن يتم قبولها؛
  • السطر 37: في بعض طرق العرض، يجب ملء حقل الإدخال children مسبقًا بآخر إدخال تم إجراؤه في هذا الحقل. للقيام بذلك، نستخدم السمة [value]، التي تحدد القيمة التي سيتم عرضها في حقل الإدخال. ستكون هذه القيمة ديناميكية ويتم إنشاؤها بواسطة المتغير [model.children]؛ Image
  • السطر 37: تجبر السمة [required] المستخدم على إدخال البيانات حتى يتم التحقق من صحة النموذج؛
  • السطر 46: تنطبق نفس التفسيرات على حقل الراتب كما تنطبق على حقل الأطفال؛
  • السطر 53: يقوم زر [submit] بتشغيل طلب POST للقيم المدخلة إلى عنوان URL [/calculer-impot] (السطر 2)؛

Image

32.6.3. مقتطف [v-menu.html]

يعرض هذا الجزء قائمة على يسار نموذج حساب الضريبة:

Image

فيما يلي كود هذه القطعة:


<!-- bootstrap menu -->
<nav class="nav flex-column">
    <!-- display a list of links HTML -->
    {% for optionMenu in modèle.optionsMenu %}
      <a class="nav-link" href="{{optionMenu.url}}">{{optionMenu.text}}</a>
    {% endfor %}
</nav>

تعليقات

  • السطور 2–7: تحيط علامة HTML [nav] بجزء من مستند HTML يحتوي على روابط تنقل إلى مستندات أخرى؛
  • السطر 5: تقدم علامة HTML [a] رابط تنقل:
  • [optionMenu.url]: هو عنوان URL الذي يتم توجيه المستخدم إليه عند النقر على رابط [optionMenu.text]. ثم يقوم المتصفح بتنفيذ عملية [GET optionMenu.url]. سيكون [optionMenu.url] عنوان URL مطلقًا بالنسبة إلى جذر التطبيق [http://machine:port/path]. وبالتالي، في [1]، سننشئ الرابط:
<a href=’/lister-simulations’>Liste des simulations</a>
  • السطر 5: سيكون قالب [modèle.optionsMenu] الخاص بالجزء عبارة عن قائمة بالصيغة التالية:
[‘Liste des simulations’:’/liste-simulations’,
‘Fin de session’:’/fin-session’]
  • السطران 2 و7: فئات CSS [nav، flex-column، nav-link] هي فئات Bootstrap التي تحدد مظهر القائمة؛

32.6.4. الاختبار المرئي

نجمع هذه العناصر المختلفة في مجلد [Tests] وننشئ قالب اختبار للعرض [vue-calcul-impot.html]:

Image

سيكون نص البرنامج النصي للاختبار [test_vue_calcul_impot] كما يلي:

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  form
    modèle["checkedOui"] = ""
    modèle["checkedNon"] = 'checked="checked"'
    modèle["enfants"] = 2
    modèle["salaire"] = 300000
    #  message of success
    modèle["success"] = True
    modèle["impôt"] = "Montant de l'impôt : 1000 euros"
    modèle["décôte"] = "Décôte : 15 euros"
    modèle["réduction"] = "Réduction : 20 euros"
    modèle["surcôte"] = "Surcôte : 0 euros"
    modèle["taux"] = "Taux d'imposition : 14 %"
    #  error message
    modèle["error"] = True
    erreurs = ["erreur1", "erreur2"]
    #  build a HTML list of errors
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  menu
    modèle["optionsMenu"] = [
        {"text": 'Liste des simulations', "url": '/lister-simulations'},
        {"text": 'Fin de session', "url": '/fin-session'}]
    #  page display
    return make_response(render_template("views/vue-calcul-impot.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

تعليقات

  • الأسطر 9–34: تهيئة جميع الأجزاء الديناميكية من العرض [vue-calcul-impot.html] والأجزاء [v-calcul-impot.html] و[v-menu.html]؛
  • السطر 36: يتم عرض العرض [vue-calcul-impot.html]؛

عند تشغيل البرنامج النصي للاختبار [test_vue_calcul_impot]، نحصل على النتيجة التالية:

نعمل على هذا العرض حتى نكون راضين عن النتيجة المرئية. يمكننا بعد ذلك المضي قدمًا في دمج العرض في تطبيق الويب قيد التطوير حاليًا.

Image

32.6.5. حساب نموذج العرض

بمجرد تحديد المظهر البصري للعرض، يمكننا المضي قدمًا في حساب نموذج العرض في ظل ظروف واقعية. دعونا نراجع رموز الحالة التي تؤدي إلى هذا العرض. يمكن العثور عليها في ملف التكوين:


            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },

وبالتالي، فإن رموز الحالة [200، 300، 301، 800] هي التي تؤدي إلى عرض طريقة عرض حساب الضريبة. لفهم معنى هذه الرموز، يمكننا الرجوع إلى اختبارات [Postman] التي أجريت على تطبيق JSON:

  • [authenticate-user-200]: 200 هو رمز الحالة الذي يتبع إجراء [authenticate-user] الناجح؛ ثم يتم عرض نموذج حساب الضريبة الفارغ؛
  • [calculate-tax-300]: 300 هو رمز الحالة الذي يتم إرجاعه بعد نجاح عملية [calculate-tax]. ثم يتم عرض نموذج الحساب، مع إظهار البيانات التي تم إدخالها ومبلغ الضريبة. ويمكن للمستخدم بعد ذلك إجراء عملية حسابية أخرى؛
  • رمز الحالة [301] هو الرمز الذي يتم إرجاعه في حالة الحساب الضريبي غير الصحيح؛
  • سيتم مناقشة رمز الحالة [800] لاحقًا. لم نواجهه بعد؛

الآن بعد أن عرفنا متى يجب عرض نموذج حساب الضريبة، يمكننا تعريف نموذجه في فئة [ModelForCalculImpotView]:

Image

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForCalculImpotView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  encapsulate view data in model
        modèle = {}
        #  application status
        état = résultat["état"]
        #  the model depends on the state
        if état in [200, 800]:
            #  initial display of an empty form
            modèle["success"] = False
            modèle["error"] = False
            modèle["checkedNon"] = 'checked="checked"'
            modèle["checkedOui"] = ""
            modèle["enfants"] = ""
            modèle["salaire"] = ""
        elif état == 300:
            #  successful calculation - result display
            modèle["success"] = True
            modèle["error"] = False
            modèle["impôt"] = f"Montant de l'impôt : {résultat['réponse']['impôt']} euros"
            modèle["décôte"] = f'Décôte : {résultat["réponse"]["décôte"]} euros'
            modèle["réduction"] = f"Réduction : {résultat['réponse']['réduction']} euros"
            modèle["surcôte"] = f'Surcôte : {résultat["réponse"]["surcôte"]} euros'
            modèle["taux"] = f"Taux d'imposition :  {résultat['réponse']['taux'] * 100} %"
            #  form restored with values entered
            modèle["checkedOui"] = 'checked="checked"' if request.form.get("marié") == "oui" else ""
            modèle["checkedNon"] = 'checked="checked"' if request.form.get("marié") == "non" else ""
            modèle["enfants"] = request.form.get("enfants")
            modèle["salaire"] = request.form.get("salaire")
        elif état == 301:
            #  error encountered - form restored with values entered
            modèle["checkedOui"] = 'checked="checked"' if request.form.get("marié") == "oui" else ""
            modèle["checkedNon"] = 'checked="checked"' if request.form.get("marié") == "non" else ""
            modèle["enfants"] = request.form.get("enfants")
            modèle["salaire"] = request.form.get("salaire")
            #  error
            modèle["success"] = False
            modèle["error"] = True
            modèle["erreurs"] = ""
            for erreur in résultat['réponse']:
                modèle['erreurs'] += f"<li>{erreur}</li>"

        #  menu options
        modèle["optionsMenu"] = [
            {"text": 'Liste des simulations', "url": '/lister-simulations'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

تعليقات

  • السطر 12: يعتمد العرض المراد عرضه على رمز الحالة الذي يرجعه وحدة التحكم؛
  • الأسطر 14–21: عرض نموذج فارغ؛
  • الأسطر 22-35: حساب الضريبة بنجاح. يتم عرض القيم المدخلة ومبلغ الضريبة مرة أخرى؛
  • الأسطر 36–47: حالة فشل حساب الضريبة؛
  • الأسطر 49-52: حساب خياري القائمة؛

32.6.6. الاختبارات [Postman]

نقوم بتهيئة جلسة HTML باستخدام الطلب [init-session-html-700]، ثم المصادقة باستخدام الطلب [authenticate-user-200]. بعد ذلك، نستخدم الطلب [calculate-tax-300] التالي:

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

Image

Image

الآن دعونا نجرب الطلب التالي [calculate-tax-301]:

Image

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

الآن دعونا نجرب سيناريو غير متوقع: سيناريو تفتقد فيه المعلمات من طلب POST. هذا السيناريو غير ممكن في ظل التشغيل العادي للتطبيق. لكن يمكن لأي شخص "التلاعب" بطلب HTTP بالطريقة التي نتبعها الآن:

Image

Image

  • في [6]، قمنا بإلغاء تحديد المعلمة المرسلة [married]؛

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

Image

  • في [3]، رسالة الخطأ الصادرة عن الخادم؛

في هذا التطبيق، كان لدينا خيار. كان بإمكاننا تعيين رمز حالة لهذه الحالة الخطأ يعيد التوجيه إلى صفحة الأخطاء غير المتوقعة. في هذا التطبيق، اخترنا رمزي حالة لكل وحدة تحكم:

  • [xx0]: للنجاح؛
  • [xx1]: للفشل؛

بالنسبة لحالات الفشل، يمكننا استخدام رموز حالة مختلفة لتمكين معالجة أكثر دقة للأخطاء. على سبيل المثال، كان بإمكاننا استخدام:

  • [xx1]: للأخطاء التي سيتم عرضها على الصفحة التي تسببت في الخطأ؛
  • [xx2]: للأخطاء غير المتوقعة أثناء الاستخدام العادي للتطبيق؛

32.7. عرض قائمة المحاكاة

Image

32.7.1. نظرة عامة على العرض

عرض قائمة المحاكاة هو كما يلي:

Image

يتكون العرض الذي تم إنشاؤه بواسطة الكود [vue-liste-simulations.html] من ثلاثة أجزاء:

  • 1: يتم إنشاء الشعار العلوي بواسطة الجزء [v-banner.html] الذي تم عرضه سابقًا؛
  • 3: جدول المحاكاة الذي تم إنشاؤه بواسطة الجزء [v-simulation-list.html]؛
  • 2: قائمة تحتوي على رابطين، تم إنشاؤها بواسطة الجزء [v-menu.html] الذي تم عرضه سابقًا؛

يتم إنشاء عرض المحاكاة بواسطة الكود التالي [vue-liste-simulations.html]:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <!-- three-column menu-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
 
        </div>
        <!-- list of simulations on 9 columns-->
        <div class="col-md-9">
            {% include "fragments/v-liste-simulations.html" %}
        </div>
    </div>
</div>
</body>
</html>

تعليقات

  • السطر 16: إدراج شعار التطبيق [1]؛
  • السطر 21: إدراج القائمة [2]. سيتم عرضها في ثلاثة أعمدة أسفل الشعار؛
  • السطر 26: إدراج جدول المحاكاة [3]. سيتم عرضه في تسعة أعمدة أسفل الشعار وإلى يمين القائمة؛

لقد علقنا بالفعل على جزأين من الأجزاء الثلاثة لهذا العرض:

الجزء [v-liste-simulations.html] هو كما يلي:


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Votre liste de simulations est vide</h4>
</div>
{% endif %}
 
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
 
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    <!-- headers of the six table columns -->
    <thead>
    <tr>
        <th scope="col">#</th>
        <th scope="col">Marié</th>
        <th scope="col">Nombre d'enfants</th>
        <th scope="col">Salaire annuel</th>
        <th scope="col">Montant impôt</th>
        <th scope="col">Surcôte</th>
        <th scope="col">Décôte</th>
        <th scope="col">Réduction</th>
        <th scope="col">Taux</th>
        <th scope="col"></th>
    </tr>
    </thead>
    <!-- table body (data displayed) -->
    <tbody>
    <!-- display each simulation by browsing the simulation table -->
    {% for simulation in modèle.simulations %}
 
    <!-- display a table row with 6 columns - <tr> tag -->
    <!-- column 1: row header (simulation no.) - <th scope='row' tag -->
    <!-- column 2: parameter value [married] - <td> tag -->
    <!-- column 3: parameter value [children] - <td> tag -->
    <!-- column 4: parameter value [salary] - <td> tag -->
    <!-- column 5: [tax] parameter value - <td> tag -->
    <!-- column 6: parameter value [surcôte] - <td> tag -->
    <!-- column 7: parameter value [discount] - <td> tag -->
    <!-- column 8: parameter value [reduction] - <td> tag -->
    <!-- column 9: parameter value [rate] (of tax) - <td> tag -->
    <!-- column 10: link to delete simulation - <td> tag -->
    <tr>
        <th scope="row">{{simulation.id}}</th>
        <td>{{simulation.marié}}</td>
        <td>{{simulation.enfants}}</td>
        <td>{{simulation.salaire}}</td>
        <td>{{simulation.impôt}}</td>
        <td>{{simulation.surcôte}}</td>
        <td>{{simulation.décôte}}</td>
        <td>{{simulation.réduction}}</td>
        <td>{{simulation.taux}}</td>
        <td><a href="/supprimer-simulation/{{simulation.id}}">Supprimer</a></td>
    </tr>
    {% endfor %}
    </tr>
    </tbody>
</table>
{% endif %}

تعليقات

  • يتم إنشاء جدول HTML باستخدام علامة <table> (السطران 15 و62)؛
  • يتم تعريف عناوين أعمدة الجدول داخل علامة <thead> (رأس الجدول، السطور 17 و30). تحدد علامة <tr> (صف الجدول، السطران 18 و29) صفًا. الأسطر 19-28: تحدد علامة <th> (رأس الجدول) رأس عمود. يوجد عشرة منها. تشير [scope="col"] إلى أن الرأس ينطبق على العمود. تشير [scope="row"] إلى أن الرأس ينطبق على الصف؛
  • الأسطر 32-61: تحيط علامة <tbody> بالبيانات المعروضة في الجدول؛
  • الأسطر 47-58: تحدد علامة <tr> إطارًا لصف من الجدول؛
  • السطر 48: تحدد علامة <th scope=’row’> رأس الصف. يقوم المتصفح بتمييز هذا الرأس؛
  • الأسطر 49–57: تحدد كل علامة td (بيانات الجدول) عمودًا في الصف؛
  • السطر 34: توجد قائمة المحاكاة في النموذج [model.simulations]، وهي قائمة من القواميس؛
  • السطر 57: رابط لحذف المحاكاة. يستخدم عنوان URL رقم المحاكاة المعروض في الصف؛

32.7.2. اختبار مرئي

نقوم بإنشاء نصوص برمجية اختبارية للعرض [view-simulation-list.html]:

فيما يلي نص البرنامج النصي [test_simulation_list_view]:

Image

from flask import Flask, make_response, render_template

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  put the simulations in the format expected by the page
    modèle["simulations"] = [
        {
            "id": 7,
            "marié": "oui",
            "enfants": 2,
            "salaire": 60000,
            "impôt": 448,
            "décôte": 100,
            "réduction": 20,
            "surcôte": 0,
            "taux": 0.14
        },
        {
            "id": 19,
            "marié": "non",
            "enfants": 2,
            "salaire": 200000,
            "impôt": 25600,
            "décôte": 0,
            "réduction": 0,
            "surcôte": 8400,
            "taux": 0.45
        }
    ]
    #  menu
    modèle["optionsMenu"] = [
        {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
        {"text": 'Fin de session', "url": '/fin-session'}]
    #  page display
    return make_response(render_template("views/vue-liste-simulations.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

تعليقات

  • الأسطر 12–35: تمت إضافة محاكيتين إلى النموذج
  • الأسطر 37-39: جدول خيارات القائمة؛

دعونا نعرض هذا العرض عن طريق تشغيل هذا البرنامج النصي. نحصل على النتيجة التالية:

Image

نواصل العمل على هذا العرض حتى نكون راضين عن شكله. يمكننا بعد ذلك الانتقال إلى دمج العرض في تطبيق الويب الذي نقوم بتطويره حاليًا.

32.7.3. حساب نموذج العرض

بمجرد تحديد المظهر المرئي للعرض، يمكننا المضي قدمًا في حساب نموذج العرض في ظل الظروف الواقعية. دعونا نراجع رموز الحالة التي تؤدي إلى هذا العرض. يمكن العثور عليها في ملف التكوين:

Image


            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }

وبالتالي، فإن رموز الحالة [500، 600] هي التي تؤدي إلى عرض عرض المحاكاة. لفهم معنى هذه الرموز، يمكننا الرجوع إلى اختبارات [Postman] التي أجريت على تطبيق JSON:

  • [list-simulations-500]: 500 هو رمز الحالة الذي يظهر عقب نجاح إجراء [list-simulations]: حيث يتم بعد ذلك عرض قائمة عمليات المحاكاة التي أجراها المستخدم؛
  • [delete-simulation-600]: 600 هو رمز الحالة الذي يظهر بعد نجاح إجراء [delete-simulation]. ثم يتم عرض قائمة المحاكاة الجديدة التي تم الحصول عليها بعد هذا الحذف؛

الآن بعد أن عرفنا متى يجب عرض قائمة المحاكاة، يمكننا تعريف نموذجها في فئة [ModelForListeSimulationsView]:

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForListeSimulationsView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  we encapsulate the paged data in the model
        modèle = {}
        #  simulations are found in the response of the controller that executed the action
        #  as an array of TaxPayer dictionaries
        modèle["simulations"] = résultat["réponse"]
        #  menu
        modèle["optionsMenu"] = [
            {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

تعليقات

  • السطر 13: توجد المحاكاة المراد عرضها في [result["response"]
  • الأسطر 15–17: خيارات القائمة المراد عرضها؛

32.7.4. [Postman] الاختبارات

نقوم

  • نقوم بتهيئة جلسة HTML؛
  • المصادقة؛
  • نقوم بإجراء ثلاثة حسابات ضريبية؛

يتيح لنا الاختبار [lister-simulations-500] الحصول على رمز الحالة 500. وهو يتوافق مع طلب لعرض المحاكاة:

Image

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

Image

يعرض اختبار [delete-simulation-600] رمز الحالة 600. هنا، سنقوم بحذف المحاكاة رقم 2.

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

Image

Image

32.8. عرض الأخطاء غير المتوقعة

هنا، نشير إلى الخطأ غير المتوقع على أنه خطأ لم يكن من المفترض أن يحدث أثناء الاستخدام العادي لتطبيق الويب. على سبيل المثال، طلب حساب الضريبة دون المصادقة. لا شيء يمنع المستخدم من كتابة عنوان URL [/tax-calculation] مباشرة في متصفحه. علاوة على ذلك، كما رأينا، يمكنه إرسال طلب POST إلى عنوان URL [/tax-calculation] دون تضمين المعلمات المتوقعة. لقد رأينا أن تطبيق الويب الخاص بنا يعرف كيفية الاستجابة بشكل صحيح لهذا الطلب. سنسمي "الخطأ غير المتوقع" خطأً لا ينبغي أن يحدث داخل تطبيق HTML. إذا حدث ذلك، فمن المحتمل أن يكون هناك شخص ما يحاول "اختراق" التطبيق. لأغراض تعليمية، قررنا عرض صفحة خطأ لهذه الحالات. في الواقع، يمكننا إعادة عرض آخر صفحة تم إرسالها إلى العميل. للقيام بذلك، نحتاج ببساطة إلى تخزين آخر استجابة HTML تم إرسالها في الجلسة. في حالة حدوث خطأ غير متوقع، نقوم بإرجاع هذه الاستجابة. بهذه الطريقة، سيكون لدى المستخدم انطباع بأن الخادم لا يستجيب لأخطائه لأن الصفحة المعروضة لا تتغير.

32.8.1. نظرة عامة على العرض

Image

العرض الذي يعرض الأخطاء غير المتوقعة هو كما يلي:

Image

تتكون طريقة العرض التي تم إنشاؤها بواسطة الكود [vue-erreurs.html] من ثلاثة أجزاء:

  • 1: يتم إنشاء الشعار العلوي بواسطة الجزء [v-banner.html] الذي تم عرضه مسبقًا؛
  • 2: الأخطاء غير المتوقعة؛
  • 3: قائمة تحتوي على ثلاثة روابط، تم إنشاؤها بواسطة الجزء [v-menu.html] المعروض بالفعل؛

يتم إنشاء عرض الأخطاء غير المتوقعة بواسطة البرنامج النصي التالي [error-view.html]:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- 12-column banner -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-section line -->
    <div class="row">
        <!-- 3-column menu-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- 9-column error list -->
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs inattendues suivantes se sont produites :
                <ul>{{modèle.erreurs|safe}}</ul>
            </div>
        </div>
    </div>
</div>
</body>
</html>

التعليقات

  • السطر 16: إدراج شعار التطبيق [1]؛
  • السطر 21: إدراج القائمة [3]. سيتم عرضها في ثلاثة أعمدة أسفل الشعار؛
  • الأسطر 24-29: عرض منطقة الخطأ عبر تسعة أعمدة؛
  • السطر 25: سيتم عرض هذا في حاوية Bootstrap بخلفية وردية؛
  • السطر 26: نص تمهيدي؛
  • السطر 27: تحتوي علامة <ul> على قائمة نقطية. يتم توفير هذه القائمة النقطية بواسطة القالب [template.errors]؛

لقد قمنا بالفعل بالتعليق على جزأين من هذا العرض:

32.8.2. اختبار مرئي

نقوم بإنشاء نصوص اختبارية لعرض [vue-erreurs.html]:

Image

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  build a HTML list of errors
    content = ""
    for erreur in ["erreur1", "erreur2"]:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  menu options
    modèle["optionsMenu"] = [
        {"text": "Calcul de l'impôt", "url": '/calculer-impot'},
        {"text": 'Liste des simulations', "url": '/lister-simulations'},
        {"text": 'Fin de session', "url": '/fin-session'}]

    #  page display
    return make_response(render_template("views/vue-erreurs.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

تعليقات

  • الأسطر 11–15: إنشاء قائمة HTML بالأخطاء؛
  • الأسطر 17–20: مصفوفة خيارات القائمة؛

دعونا نقوم بتشغيل هذا البرنامج النصي. نحصل على النتيجة التالية:

نعمل على هذا العرض حتى نكون راضين عن النتيجة المرئية. يمكننا بعد ذلك الانتقال إلى دمج العرض في تطبيق الويب الذي نقوم بتطويره حاليًا.

Image

32.8.3. حساب نموذج العرض

Image

بمجرد تحديد المظهر البصري للعرض، يمكننا المضي قدمًا في حساب نموذج العرض في ظل الظروف الواقعية. دعونا نراجع رموز الحالة التي تؤدي إلى هذا العرض. يمكن العثور عليها في ملف التكوين:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /fin-session
                    400,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

هذه هي رموز الحالة التي لا تؤدي إلى عرض HTML في الأسطر 3–41، مما يتسبب في عرض صفحة الأخطاء غير المتوقعة.

يتم حساب نموذج العرض [view-errors.html] بواسطة فئة [ModelForErrorsView] التالية:

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForErreursView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  the model
        modèle = {}
        #  errors
        modèle["erreurs"] = ""
        for erreur in résultat['réponse']:
            modèle['erreurs'] += f"<li>{erreur}</li>"
        #  menu
        modèle["optionsMenu"] = [
            {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
            {"text": 'Liste des simulations', "url": '/lister-simulations'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

تعليقات

  • الأسطر 11-14: حساب النموذج [model.errors] المستخدم من قبل العرض [view-errors.html]؛
  • الأسطر 16-197: حساب النموذج [model.optionsMenu] المستخدم من قبل الجزء [v-menu.html]؛

32.8.4. الاختبارات [Postman]

نقوم بما يلي:

  • الإجراء [/init-session/html]؛
  • ثم الإجراء [/init-session/x]؛

ثم تكون استجابة HTML كما يلي:

Image

32.9. تنفيذ إجراءات قائمة التطبيق

سنناقش هنا تنفيذ إجراءات القائمة. دعونا نستعرض معنى الروابط التي صادفناها

عرض
الرابط
الهدف
الدور
حساب الضرائب
[قائمة المحاكاة]
[/قائمة-المحاكاة]
طلب قائمة المحاكاة
  
[نهاية الجلسة]
قائمة المحاكاة
[حساب الضريبة]
[/عرض-حساب-الضريبة]
عرض حساب الضريبة
  
[إنهاء الجلسة]
أخطاء غير متوقعة
[حساب الضريبة]
[/عرض-حساب-الضريبة]
عرض حساب الضريبة
  
[قائمة المحاكاة]
  
[إنهاء الجلسة]

من المهم ملاحظة أن النقر على رابط يؤدي إلى إرسال طلب GET إلى وجهة الرابط. تم تنفيذ الإجراءات [/lister-simulations، /end-session] باستخدام عملية GET، مما يسمح لنا باستخدامها كوجهات للروابط. عندما يتم تنفيذ الإجراء عبر طلب POST، لا يصبح استخدام الرابط ممكنًا ما لم يتم دمجه مع JavaScript.

32.9.1. الإجراء [/display-tax-calculation]

من الإجراءات المذكورة أعلاه، يبدو أن الإجراء [/display-tax-calculation] لم يتم تنفيذه بعد. هذه عملية تنقل بين عرضين: لا يوجد سبب يدعو خادم JSON أو XML إلى تنفيذها لأنهما لا يمتلكان مفهوم العرض. خادم HTML هو الذي يقدم هذا المفهوم.

لذلك، نحتاج إلى تنفيذ الإجراء [/display-tax-calculation]. سيسمح لنا ذلك بمراجعة إجراءات تنفيذ إجراء داخل الخادم.

أولاً، نحتاج إلى إضافة وحدة تحكم ثانوية جديدة. سنسميها [AfficherCalculImpotController]:

Image

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

    #  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
    


        #  authorized shares and their controllers
        "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()
        },

       #  HTML views and their models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                
            },
            {
                #  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()
            },
            {
            }
        ],
  • السطر 2: وحدة التحكم الجديدة؛
  • السطر 28: الإجراء الجديد ووحدة التحكم الخاصة به؛
  • السطر 51: ستُرجع وحدة التحكم الجديدة رمز الحالة 800. لا يمكن أن يحدث أي خطأ عند تبديل العروض. العرض المعروض هو عرض [vue-calcul-import.html] الذي درسناه وشرحناه واختبرناه؛

سيكون وحدة التحكم [AfficherCalculImpotController] كما يلي:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherCalculImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  change of view - just a status code to set
        return {"action": action, "état": 800, "réponse": ""}, status.HTTP_200_OK

تعليقات

  • السطر 6: مثل وحدات التحكم الثانوية الأخرى، تنفذ وحدة التحكم الجديدة واجهة [InterfaceController]؛
  • السطر 13: من السهل تنفيذ تغييرات العرض: ما عليك سوى إرجاع رمز الحالة المرتبط بعرض الهدف، وهو الرمز 800 كما هو موضح أعلاه؛

32.9.2. الإجراء [/end-session]

يعد الإجراء [/end-session] إجراءً خاصًا. فهو لا يؤدي مباشرةً إلى عرض بل إلى إعادة توجيه. تذكر أن عمليات إعادة التوجيه يتم تكوينها في ملف [config] على النحو التالي:


        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussi
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],

لا يوجد سوى إعادة توجيه واحدة في التطبيق:

  • عندما ترجع وحدة التحكم رمز الحالة [400] (السطر 5)، يجب إعادة توجيه العميل إلى عنوان URL [http://machine:port/chemin/init-session/html] (السطر 8)؛

رمز الحالة [400] هو الرمز الذي يتم إرجاعه بعد نجاح إجراء [/fin-session]. فلماذا، إذن، يجب إعادة توجيه العميل إلى عنوان URL [/init-session/html]؟ لأن رمز الإجراء [/fin-session] يزيل نوع الجلسة من جلسة الويب. لم نعد نعرف أننا في جلسة HTML. نحتاج إلى إعادة توجيهها. نقوم بذلك باستخدام الإجراء [/init-session/html].

تتم معالجة عمليات إعادة التوجيه HTML بواسطة فئة [HtmlResponse]:

   def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  the HTML response depends on the status code returned by the controller
        état = résultat["état"]

        #  do I need to redirect?
        for redirection in config["redirections"]:
            #  conditions requiring redirection
            états = redirection["états"]
            if état in états:
                #  you need to redirect
                return redirect(f"{redirection['to']}"), status.HTTP_302_FOUND

        #  a state corresponds to a view
        #  search for it in the list of views
 ..
  • تتعامل الأسطر 6–12 مع عمليات إعادة التوجيه؛
  • السطر 7: config['redirections'] هي قائمة عمليات إعادة التوجيه. كل عملية إعادة توجيه عبارة عن قاموس يحتوي على المفاتيح التالية:
    • [states]: الحالات التي يعيدها وحدة التحكم والتي تؤدي إلى إعادة التوجيه؛
    • [to]: عنوان URL لإعادة التوجيه؛
  • الأسطر 7–12: نكرر عبر قائمة عمليات إعادة التوجيه؛
  • السطر 9: لكل عملية إعادة توجيه، نسترد الحالات التي تؤدي إليها؛
  • السطر 10: إذا كانت الحالة المختبرة موجودة في هذه القائمة، فقم بإجراء إعادة التوجيه، السطر 12؛
  • السطر 12: لاحظ أن طريقة [build_http_response] يجب أن تُرجع توبول مكون من عنصرين:
    • [response]: استجابة HTTP المراد إرسالها. يتم إنشاؤها باستخدام الدالة [redirect]، التي يكون معلمتها هو عنوان URL لإعادة التوجيه؛
    • [status_code]: رمز حالة استجابة HTTP، وهنا الرمز [status.HTTP_302_FOUNDالذي يطلب من العميل إعادة التوجيه؛

دعونا نجري اختبار [Postman]. نقوم بما يلي:

  • نقوم بتهيئة جلسة HTML [init-session/html]؛
  • المصادقة [/authenticate-user]؛
  • إنهاء الجلسة [/end-session]؛

Image

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

Image

لقد حصلنا على صفحة المصادقة. وهذا بالضبط ما كنا نتوقعه. والآن، لنرى كيف تم الحصول عليها. لننتقل إلى وحدة التحكم في [Postman] (Ctrl-Alt-C):

Image

  • في [1]، الإجراء [/end-session]؛
  • في [2-3]، يُخبر رمز حالة HTTP 302 الذي أعاده الخادم العميل بأنه يقوم بإعادة التوجيه؛
  • في [4]، يتبع العميل [Postman] عملية إعادة التوجيه؛

32.10. اختبار تطبيق HTML في ظروف واقعية

تمت كتابة الكود واختبار كل إجراء باستخدام [Postman]. ما زلنا بحاجة إلى اختبار تدفق العرض في سيناريو واقعي. نحتاج إلى طريقة لتهيئة جلسة HTML. نعلم أننا بحاجة إلى إرسال طلب [/init-session/html] إلى الخادم. هذا ليس عنوان URL عمليًا للغاية. نفضل البدء بعنوان URL [/].

لقد كتبنا المسار التالي في البرنامج النصي الرئيسي [main]:

from flask import request, Flask, session, url_for, redirect


@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()
  • الأسطر 4–7: معالجة مسار [/]. ستكون نقطة الدخول لتطبيق الويب هي عنوان URL [/init-session/html] (السطر 10). أيضًا، في السطر 7، نقوم بإعادة توجيه العميل إلى عنوان URL هذا:
  • يتم استيراد الدالة [url_for] في السطر 1. ولها معلمتان هنا (السطر 7):
      • المعلمة الأولى هي اسم إحدى دوال التوجيه، وهي في هذه الحالة الدالة الموجودة في السطر 11. نلاحظ أن هذه الدالة تتوقع معلمة [type_responseوهي نوع الاستجابة (json، xml، html) التي يطلبها العميل؛
      • المعلمة الثانية تأخذ اسم المعلمة من السطر 11، [type_response]، وتعيّن لها قيمة. لو كانت هناك معلمات أخرى، لكنا كررنا العملية لكل منها؛
      • تُرجع الدالة عنوان URL المرتبط بالدالة المحددة بواسطة المعلمتين المقدمتين إليها. هنا، ستُرجع الدالة عنوان URL من السطر 10، حيث يتم استبدال المعلمة بقيمتها [/init-session/html]؛
    • تم استيراد الدالة [redirect] في السطر 1. وتتمثل مهمتها في إرسال رأس إعادة توجيه HTTP إلى العميل:
      • المعلمة الأولى هي عنوان URL الذي يجب إعادة توجيه العميل إليه؛
      • المعلمة الثانية هي رمز حالة استجابة HTTP المرسلة إلى العميل. الرمز [status.HTTP_302_FOUND] يتوافق مع إعادة توجيه HTTP؛

نحن جاهزون. لنلقِ نظرة الآن على بعض تسلسلات العرض.

في متصفحنا، نقوم بتمكين أدوات المطور (F12 في Chrome وFirefox وEdge) ونطلب عنوان URL لبدء التشغيل [http://localhost:5000/]. استجابة الخادم هي كما يلي:

Image

إذا نظرنا إلى حركة مرور الشبكة بين العميل والخادم:

Image

  • نرى أنه في [4، 5]، تلقى المتصفح طلب إعادة توجيه إلى عنوان URL [/init-session/html]؛

دعونا نملأ النموذج الذي تلقيناه؛

Image

ثم لنقم بإجراء بعض المحاكاة:

Image

Image

دعونا نطلب قائمة عمليات المحاكاة:

Image

لنحذف المحاكاة الأولى:

Image

إنهاء الجلسة:

Image

نشجع القراء على تجربة اختبارات أخرى.