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 (Модель – Вигляд – Контролер), наступним чином:
Обробка запиту клієнта відбуватиметься наступним чином:
- 1 — запит
Запитувані URL матимуть вигляд http://machine:port/action/param1/param2/… [Contrôleur principal] використовуватиме файл конфігурації для «маршрутизації» запиту до відповідного контролера. Для цього він використовуватиме поле [action] з URL. Решта URL та [param1/param2/…] складається з необов’язкових параметрів, які будуть передані до дії. Змінна C у MVC тут є рядком [Contrôleur principal, Contrôleur / Action]. Якщо жоден контролер не може обробити запитувану дію, веб-сервер відповість, що запитувана дія URL не знайдена.
- 2 — обробка
- обрана дія [2a] може використовувати параметри parami, які їй передала дія [Contrôleur principal]. Ці параметри можуть походити з двох джерел:
- з шляху [/param1/param2/…] функції URL,
- з параметрів, розміщених у тілі запиту клієнта;
- під час обробки запиту користувача дії може знадобитися рівень [métier] [2b]. Після обробки запиту клієнта ця дія може викликати різні відповіді. Типовим прикладом є:
- відповідь про помилку, якщо запит не вдалося обробити належним чином;
- у іншому випадку — підтверджувальний відповідь;
- [Contrôleur / Action] надішле свою відповідь [2c] головному контролеру разом із кодом стану. Ці коди стану однозначно відображатимуть стан, у якому перебуває додаток. Це буде або код успіху, або код помилки;
- 3 — відповідь
- залежно від того, чи клієнт запросив відповідь jSON, XML чи HTML, [Contrôleur principal] створить екземпляр відповідного типу відповіді [3a] і попросить його надіслати відповідь клієнту. [Contrôleur principal] передасть йому як відповідь, так і код стану, надані [Contrôleur / Action], який було виконано;
- якщо бажана відповідь має тип jSON або XML, вибрана відповідь відформатує відповідь від [Contrôleur / Action], яку їй передали, і надішле її [3c]. Клієнтом, здатним обробити цю відповідь, може бути консольний скрипт на Python або скрипт на JavaScript, розміщений на сторінці HTML;
- якщо бажана відповідь має тип HTML, то вибрана відповідь [3b] вибере один із переглядів HTML або [Vuei] за допомогою наданого їй коду стану. Це V з MVC. Кожному коду стану відповідає лише один вигляд. Цей вигляд V відобразить відповідь [Contrôleur / Action], який було виконано. Вона оформлює дані цієї відповіді за допомогою HTML, CSS та JavaScript. Ці дані називаються моделлю представлення. Це літера «M» у MVC. Клієнтом найчастіше є веб-браузер;
32.2. Дерево скриптів сервера HTML

- у [1] — статичні елементи сервера HTML;
- у [2-3] — види V сервера HTML. Фрагменти [2] — це елементи, що можуть повторно використовуватися у видах [3];
- у [4] — папку, яка буде використовуватися для статичного тестування представлень;
- у [5] — папка з шаблонами M для подання V, M з MVC;
32.3. Огляд візуалізацій
Веб-додаток HTML використовує чотири подання. Перше подання — це подання автентифікації:
- дією, що веде до цього першого виду, є дія [/init-session] [1];
- натискання на кнопку [Valider] запускає дію [/authentifier-utilisateur] з двома параметрами, що передаються методом POST [2-3];
Вигляд розрахунку податку:

- у [1] — дія [/authentifier-utilisateur], яка відкриває цей вигляд;
- у [2] натискання кнопки [Valider] запускає виконання дії [/calculer-impot] з трьома переданими параметрами [2-5];
- натискання на посилання [6] запускає дію [/lister-simulations] без параметрів;
- натискання на посилання [7] запускає дію [/fin-session] без параметрів;
Третій вигляд — це симуляції, виконані авторизованим користувачем:

- у [1] — дія [/lister-simulations], яка відкриває цей вигляд;
- у [2] клік на посилання [Supprimer] запускає дію [/supprimer-simulation] з одним параметром — номером симуляції, яку потрібно видалити зі списку;
- натискання на посилання [3] запускає дію [/afficher-calcul-impot] без параметрів, яка знову відображає вікно розрахунку податку;
- натискання на посилання [4] запускає дію [/fin-session] без параметрів;
Четвертий екран називатиметься екраном несподіваних помилок:
- у [1]: користувач самостійно ввів URL. Однак у цьому прикладі не було жодних симуляцій. Тому ми отримуємо повідомлення про помилку [2]. Це повідомлення нам відоме. Воно з’являлося у випадках jSON / XML. Цей тип помилки ми назвемо «несподіваною помилкою», оскільки вона не може виникнути під час звичайного використання програми. Вона може виникнути лише тоді, коли користувач самостійно вводить URL;
- у разі несподіваної помилки посилання [3-5] дозволяють повернутися до одного з трьох інших видів;
Нагадаємо про різні сервісні URL сервера jSON / XML:
Дія | Роль | Контекст виконання |
/init-session | Служить для визначення типу (json, xml, html) бажаних відповідей | Запит GET Може бути відправлений у будь-який час |
/authentifier-utilisateur | Надає або відмовляє користувачеві у вході | Запит POST. Запит повинен містити два параметри, надіслані методом POST [user, password] Може бути відправлений лише у тому випадку, якщо тип сеансу (json, xml, html) відомий |
/calculer-impot | Виконує симуляцію розрахунку податку | Запит POST. Запит повинен містити три параметри, що передаються методом POST: [marié, enfants, salaire] Може бути відправлений лише у разі, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/lister-simulations | Запит на перегляд списку симуляцій, виконаних з початку сеансу | Запит GET. Може бути відправлений лише за умови, що тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/supprimer-simulation/numéro | Видаляє симуляцію зі списку симуляцій | Запит GET. Може бути відправлений лише за умови, що тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/переглянути-розрахунок-податку | Відображає сторінку HTML з розрахунком податку | Запит GET. Може бути відправлений лише за умови, що тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/fin-session | Завершує сесію моделювання. | Технічно старий веб-сеанс видаляється, а створюється новий Може бути відправлений лише у тому випадку, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
Ці різні сервісні коди URL також використовуватимуться для сервера HTML.
32.4. Налаштування переглядів
Дія обробляється контролером. Цей контролер повертає кортеж (результат, status_code), де:
- [résultat] — це словник ключів [action, état, réponse];
- [status_code] — код статусу відповіді HTTP, яка буде надіслана клієнту;
У сесії HTML сторінка, що відображається після виконання дії, залежить від коду стану, який повертає контролер. Ця залежність реалізована в конфігурації [config] наступним чином:
# представлення HTML та їхні шаблони залежать від стану, що повертається контролером
"views": [
{
# сторінка автентифікації
"états": [
# /успішне-започаткування-сесії
700,
# /authentifier-utilisateur — невдача
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# сторінка розрахунку податку
"états": [
# /автентифікація-користувача успішно
200,
# /розрахунок-податку успішно
300,
# /розрахувати-податок невдача
301,
# /перегляд-розрахунку-податку
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# перегляд списку моделювань
"états": [
# /перелік-симуляцій
500,
# /видалити-моделювання
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# перегляд несподіваних помилок
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
# перенаправлення
"redirections": [
{
"états": [
400, # /успішне завершення сеансу
],
# перенаправлення на
"to": "/init-session/html",
}
],
}
- рядки 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. Вигляд автентифікації

32.5.1. Опис екрану
Екран автентифікації виглядає наступним чином:

Вигляд складається з двох елементів, які ми назвемо фрагментами:
- фрагмент [1] генерується фрагментом [v-bandeau.html];
- фрагмент [2] генерується фрагментом [v-authentification.html];
Сторінка автентифікації генерується наступною сторінкою [vue-authentification.html]:
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<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">
<!-- головний банер -->
{% include "fragments/v-bandeau.html" %}
<!-- двоколонковий макет -->
<div class="row">
<div class="col-md-9">
{% include "fragments/v-authentification.html" %}
</div>
</div>
<!-- у разі помилки — відображається повідомлення про помилку -->
{% 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: заголовок (head) документа HTML;
- рядок 6: тег <meta charset> вказує, що документ закодовано у форматі UTF-8;
- рядок 7: тег <meta name=’viewport’> визначає початкове відображення вікна перегляду: на всю ширину екрана, на якому воно відображається (width), у його початковому розмірі (initial-scale) без зміни розміру для пристосування до меншого розміру екрана (shrink-to-fit);
- рядок 9: тег <link rel=’stylesheet’> вказує на файл CSS, який визначає зовнішній вигляд сторінки. Тут ми використовуємо фреймворк CSS Bootstrap 4.4.1 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- рядок 10: тег <title> визначає заголовок сторінки:

- рядки 13–35: основний вміст веб-сторінки розміщений у тегах <body></body>;
- рядки 14–34: тег <div> окреслює розділ відображуваної сторінки. Атрибути [class], що використовуються у цьому вигляді, всі відносяться до фреймворку CSS Bootstrap. Тег <div class=’container’> (рядок 14) окреслює контейнер Bootstrap;
- рядок 26: вставляється фрагмент [v-bandeau.html]. Цей фрагмент формує верхній блок [1] сторінки. Ми опишемо його трохи пізніше;
- рядки 18–22: тег <div class=’row’> окреслює рядок Bootstrap. Ці рядки складаються з 12 стовпців;
- рядок 19: тег <div class=’col-md-9’> окреслює секцію з 9 стовпців;
- рядок 20: додається фрагмент [v-authentification.html], який відображає форму автентифікації [2] на цій сторінці. Ми опишемо її трохи пізніше;
- рядки 24–33: код HTML у цих рядках використовується лише в тому випадку, якщо значення [modèle.error] дорівнює True. Ми завжди будемо діяти таким чином: модель подання HTML буде інкапсульована в словник [modèle];
- рядки 24–33: автентифікація завершується невдало, якщо користувач вводить неправильні облікові дані. У цьому випадку вікно автентифікації відображається знову з повідомленням про помилку. Атрибут [modèle.error] вказує, чи слід відображати це повідомлення про помилку;
- рядки 27–30: окреслюють зону з рожевим фоном (class="alert alert-danger") (рядок 27);

- рядок 28: текст;
- рядок 29: тег HTML <ul> (неупорядкований список) відображає список з маркованими пунктами. Кожен елемент списку повинен мати синтаксис <li>елемент</li>. Тут відображається значення [modèle.erreurs]. Це значення фільтрується (за наявності символу |) фільтром [safe]. За замовчуванням, коли рядок символів має бути надісланий до браузера, Flask «нейтралізує» всі теги HTML, які можуть там міститися, щоб браузер їх не інтерпретував. Але іноді ми хочемо, щоб вони інтерпретувалися. Це буде той випадок, коли рядок [modèle.erreurs] міститиме теги HTML <li> та </li>, які слугують для обмеження елемента списку. У цьому випадку використовується фільтр [safe], який повідомляє Flask, що рядок, який потрібно відобразити, є безпечним (safe), і тому він не повинен нейтралізувати теги HTML, які він там знайде;
З цього коду слід запам’ятати такі динамічні елементи, які потрібно визначити:
- [modèle.error]: для відображення повідомлення про помилку;
- [modèle.erreurs]: список (у значенні HTML цього терміна) повідомлень про помилки;
32.5.2. Фрагмент [v-bandeau.html]
Фрагмент [v-bandeau.html] відображає верхню панель усіх сторінок веб-додатку:

Код фрагмента [v-bandeau.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] з Flask. Тут її значенням буде URL з файлу [images/logo.pg] у папці [static];
- рядки 7–11: інші 8 стовпців рядка (нагадаємо, що їх загалом 12) слугуватимуть для розміщення тексту (рядок 9) великими літерами (<h1>, рядки 8–10);
32.5.3. Фрагмент [v-authentification.html]
Фрагмент [v-authentification.html] відображає форму автентифікації веб-додатку:

Код фрагмента [v-authentification.html] такий:
<!-- форма HTML — значення надсилаються за допомогою дії [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur">
<!-- заголовок -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- форма Bootstrap -->
<fieldset class="form-group">
<!-- перший рядок -->
<div class="form-group row">
<!-- назва -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- поле для введення тексту -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="{{ modèle.login }}" required>
</div>
</div>
<!-- 2-й рядок -->
<div class="form-group row">
<!-- назва -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- поле введення тексту -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe" required>
</div>
</div>
<!-- кнопка типу [submit] у третьому рядку -->
<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);
- тут, коли користувач натисне кнопку [Valider] (рядок 34), браузер відправить (рядок 2) значення, введені у форму, до URL [/authentifier-utilisateur] (рядок 2);
- передані значення — це значення, введені користувачем у поля введення рядків 17 та 27. Вони будуть передані в тілі запиту HTTP, який браузер надішле у формі [x-www-forl-urlencoded]. Імена параметрів [user, password] відповідають атрибутам [name] полів введення даних у рядках 17 та 27;
- рядки 5–7: секція Bootstrap для відображення заголовка на синьому тлі:
- рядки 10–37: форма Bootstrap. Усі елементи форми будуть оформлені певним чином;
- рядки 12–20: визначають перший рядок Bootstrap у формі:
![]()
- рядок 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=’invite’]: текст, що відображається в полі введення, коли користувач ще нічого не ввів;
![]()
- (продовження)
- [value=’valeur’]: текст «значення» відображатиметься у полі введення, щойно воно з’явиться на екрані, тобто до того, як користувач введе будь-що інше. Цей механізм використовується у разі помилки для відображення значення, яке спричинило помилку. У цьому випадку цим значенням буде значення змінної [modèle.login];
- [required]: вимагає, щоб користувач ввів значення, щоб форму можна було надіслати на сервер:
- рядки 21–30: аналогічний код для введення пароля;
- рядок 27: [type=’password’] створює поле для введення тексту (можна вводити що завгодно), але введені символи приховані:
![]()
- рядки 32–36: третій рядок Bootstrap для кнопки [Valider];
- рядок 34: оскільки він має атрибут [type=submit], натискання на цю кнопку запускає відправку браузером на сервер введених значень, як було пояснено раніше. Атрибут CSS [class="btn btn-primary"] відображає синю кнопку:
Залишилося пояснити ще одну річ. У рядку 2 атрибут [action="/authentifier-utilisateur"] визначає неповний URL (він не починається з http://machine:port/chemin). У нашому прикладі всі URL у додатку мають вигляд [http://machine:port/chemin/action/param1/param2/..], де [http://machine:port/chemin] є коренем службових URL. У [action="/authentifier-utilisateur"] ми маємо абсолютний URL, тобто виміряний від кореня URL. Отже, повним URL для POST є [http://machine:port/chemin/authentifier-utilisateur], і саме його використовуватиме браузер.
Зазначимо, що цей фрагмент використовує шаблон [modèle.login].
32.5.4. Візуальні тести
Тестування візуалізацій можна проводити задовго до їх інтеграції в додаток. У цьому випадку йдеться про тестування їхнього візуального вигляду. Ми зберемо всі тестові візуалізації в папці [tests_views] цього проєкту:

Щоб протестувати вигляд V [vue-authentification.html], нам потрібно створити модель даних M, яку він буде відображати. Це робиться за допомогою скрипта [test_vue_authentification.py]:
from flask import Flask, render_template, make_response
# додаток Flask
app = Flask(__name__, template_folder="../templates", static_folder="../static")
# Головна URL
@app.route('/')
def index():
# дані сторінки інкапсулюються в шаблон
modèle = {}
# ідентифікатор користувача
modèle["login"] = "albert"
# список помилок
modèle["error"] = True
erreurs = ["erreur1", "erreur2"]
# складається список HTML помилок
content = ""
for erreur in erreurs:
content += f"<li>{erreur}</li>"
modèle["erreurs"] = content
# відображення сторінки
return make_response(render_template("views/vue-authentification.html", modèle=modèle))
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
Коментарі
- рядки 1–3: створюємо додаток Flask, єдиною метою якого є відображення подання [vue-authentification.html] (рядок 22);
- рядок 7: додаток має лише один сервіс URL;
- рядки 9–20: вигляд автентифікації має динамічні частини, що керуються об’єктом [modèle]. Цей об’єкт називають моделлю подання. Згідно з одним із двох визначень, наведених для абревіатури MVC, це є M у MVC. Під час визначення подання [vue-authentification.html] ми визначили три динамічні значення:
- [modèle.error]: логічне значення, що вказує, чи слід відображати повідомлення про помилку;
- [modèle.erreurs]: список HTML повідомлень про помилки;
- [modèle.login] — логін користувача;
Отже, нам потрібно визначити ці три динамічні значення.
- рядки 9–20: визначаємо три динамічні елементи вікна автентифікації;
Щоб провести тест, запускаємо скрипт [tests_views/test_vue_authentification.py] і запитуємо URL [/localhost:5000/]:
Продовжуємо ці візуальні тести, доки не будемо задоволені результатом.

32.5.5. Розрахунок моделі екрану
Після визначення візуального вигляду сторінки можна приступити до розрахунку моделі сторінки в реальних умовах. Моделі сторінок будуть згенеровані класами, зібраними в папці [models_for_views]:

Кожен клас, що генерує модель виду, відповідатиме такому інтерфейсу [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] — це ті самі параметри, що використовуються контролером дії. Отже, вони також передаються до моделі;
- контролер згенерував результат [résultat], який також передається до моделі. Цей результат містить важливий елемент [état], що вказує на те, як пройшло виконання поточної дії. Модель використовуватиме цю інформацію;
Ми бачили, що в конфігурації [config] додатка коди стану, що повертаються контролерами, використовуються для визначення подання HTML, яке слід відобразити:
# представлення HTML та їхні шаблони залежать від стану, який повертає контролер
"views": [
{
# вигляд автентифікації
"états": [
# /init-session успішно
700,
# /аутентифікація-користувача — невдача
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# сторінка розрахунку податку
"états": [
# /автентифікація-користувача успішно
200,
# /розрахунок-податку успішно
300,
# /розрахувати-податок невдача
301,
# /перегляд-розрахунку-податку
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# перегляд списку моделювань
"états": [
# /перелік-симуляцій
500,
# /видалити-моделювання
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# перегляд несподіваних помилок
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
# перенаправлення
"redirections": [
{
"états": [
400, # /успішне завершення сеансу
],
# перенаправлення на
"to": "/init-session/html",
}
],
}
Отже, саме коди стану [700, 201] (рядки 7 і 9) забезпечують відображення вікна автентифікації. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], виконаними для додатка jSON:
- [init-session-json-700]: 700 — це код стану після успішного виконання дії [init-session]: у цьому випадку відображається порожня форма автентифікації;
- [authentifier-utilisateur-201]: 201 — це код стану після невдалої дії [authentifier-utilisateur] (нерозпізнані облікові дані): у цьому випадку відображається форма автентифікації для внесення виправлень;
Тепер, коли ми знаємо, в яких випадках має відображатися форма автентифікації, можна обчислити її шаблон у [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:
# дані сторінки інкапсулюються в шаблон
modèle = {}
# стан додатка
état = résultat["état"]
# шаблон залежить від стану
if état == 700:
# у разі відображення порожньої форми
modèle["login"] = ""
# помилок для відображення немає
modèle["error"] = False
elif état == 201:
# помилка автентифікації
# повторно відображається спочатку введений користувач
modèle["login"] = request.form.get("user")
# є помилка, яку потрібно відобразити
modèle["error"] = True
# список HTML повідомлень про помилки
erreurs = ""
for erreur in résultat["réponse"]:
erreurs += f"<li>{erreur}</li>"
modèle["erreurs"] = erreurs
# повертається шаблон
return modèle
Коментарі
- рядок 8: метод [get_model_for_view] подання автентифікації повинен надати словник із трьома ключами [error, erreurs, login]. Цей розрахунок виконується на основі коду стану, повернутого контролером дії;
- рядок 12: отримується код стану, повернутий контролером, який обробив поточну дію;
- рядки 14–29: модель залежить від цього коду стану;
- рядки 15–18: випадок, коли потрібно відобразити порожню форму автентифікації;
- рядки 20–29: випадок помилкової автентифікації: відображається введений користувачем ідентифікатор та повідомлення про помилку. Користувач може тоді за допомогою клавіатури повторити спробу автентифікації;
- рядок 22: ідентифікатор, спочатку введений користувачем, можна знайти у запиті клієнта;
- рядок 24: повідомляється про наявність помилок, які потрібно відобразити;
- рядки 26–29: у разі помилки результат [‘réponse’] містить список помилок;
32.5.6. Генерація відповідей HTML
Повернемося до моделі MVC додатка HTML:
- у 2 (2a, 2b): контролер виконує дію;
- у 3 (3a, 3b, 3c): обирається представлення та надсилається клієнту;
У [3a] обирається тип відповіді (jSON, XML, HTML). Ми розглянули, як генеруються відповіді jSON та XML, але ще не розглянули відповіді HTML. Вони генеруються класом [HtmlResponse]:

Нагадаємо, як у головному скрипті [main] визначається тип відповіді, яку слід надати користувачеві:
….
# формується відповідь для відправлення
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# відправляємо відповідь
return response, status_code
де в рядку 3 config[‘responses’] — це такий словник:
# різні типи відповідей (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
Отже, саме клас [HtmlResponse] генерує відповідь HTML. Його код такий:
# словник відповідей HTML залежно від статусу, що міститься в результаті
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):
# відповідь HTML залежить від коду стану, повернутого контролером
état = résultat["état"]
# чи потрібно виконати перенаправлення?
for redirection in config["redirections"]:
# стани, що вимагають перенаправлення
états = redirection["états"]
if état in états:
# необхідно виконати перенаправлення
return redirect(f"/{redirection['to']}"), status.HTTP_302_FOUND
# кожному стану відповідає один вигляд
# шукаємо її у списку переглядів
views_configs = config["views"]
trouvé = False
i = 0
# проглядаємо список переглядів
nb_views = len(views_configs)
while not trouvé and i < nb_views:
# погляд № i
view_config = views_configs[i]
# стани, пов’язані з переглядом № i
états = view_config["états"]
# чи знаходиться шуканий звіт серед звітів, пов'язаних з переглядом № i
if état in états:
trouvé = True
else:
# наступний вигляд
i += 1
# знайдено?
if not trouvé:
# якщо для поточного стану додатка не існує жодного перегляду
# виводиться вікно з повідомленням про помилки
view_config = config["view-erreurs"]
# розраховується шаблон для відображення
model_for_view = view_config["model_for_view"]
modèle = model_for_view.get_model_for_view(request, session, config, résultat)
# генерується код HTML відповіді
html = render_template(view_config["view_name"], modèle=modèle)
# побудовано відповідь HTTP
response = make_response(html)
response.headers['Content-Type'] = 'text/html; charset=utf-8'
# повертається результат
return response, status_code
- рядок 11: метод [build_http_response], відповідальний за генерацію відповіді HTML, отримує такі параметри:
- [request, session, dict] — це параметри, які використовує контролер для обробки поточної дії;
- [status_code, résultat] — це два результати, що генеруються цим самим контролером;
- рядок 14: як ми вже зазначали, відповідь HTML від сервера залежить від коду стану, що міститься у словнику [résultat];
- рядки 16–22: спочатку обробляються перенаправлення. Поки що ми проігноруємо цей випадок, доки не натрапимо на приклад перенаправлення. Зазначимо, що перенаправлення, як правило, є типовим випадком використання сервера HTML. Цей випадок не зустрічається на серверах jSON та ouXML;
- рядки 24–41: серед переглядів шукаємо той, список якого [états] містить шуканий стан;
- рядки 42–46: якщо жодного перегляду не знайдено, це означає несподівану помилку. Розглянемо приклад. Під час нормальної роботи додатка дія [/supprimer-simulation] ніколи не повинна давати збій. Дійсно, ми побачимо, що видалення симуляцій відбувається на основі посилань, згенерованих кодом. Ці посилання є правильними і не можуть призвести до помилки. Однак, як ми вже бачили, користувач може безпосередньо ввести URL [/supprimer-simulation/id] і тим самим спричинити помилку. У цьому випадку контролер [SupprimerSimulationController] повертає код стану 601. Однак цей код стану відсутній у списку кодів стану, що призводять до відображення сторінки HTML. Тому буде відображено сторінку помилки. Вона визначена в конфігурації наступним чином:
# перегляд несподіваних помилок
"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]: після цього відображається порожня форма автентифікації;
- [authentifier-utilisateur-201]: 201 — це код стану після невдалої дії [authentifier-utilisateur] (нерозпізнані облікові дані): у цьому випадку відображається форма автентифікації для внесення виправлень;
Достатньо повторно використати їх і перевірити, чи правильно відображається сторінка аутентифікації. Тут наведено два випадки:
Випадок 1: [init-session-html-700], початок сеансу HTML;

Відповідь така:

- у [5] режим [Preview] дозволяє переглянути отриману сторінку HTML;
- у [6] ми бачимо саме ту порожню форму, яку й очікували;
- у [7] Postman не перейшов за посиланням на зображення на сторінці;
- у [8] режим [Raw] надає доступ до отриманого HTML;

- у [3] — посилання, яке Postman не завантажив. Він відобразив значення атрибута [alt=alternative], яке з’являється, коли зображення не вдається завантажити. У цьому випадку, скоріше за все, Postman просто не захотів завантажити зображення. Це можна перевірити, надіславши запит на URL та [http://localhost :5000/static/images.logo.jpg] за допомогою Postman:
Випадок 2: [authentifier-utilisateur-201], помилка автентифікації

Тепер спробуємо виконати неправильну автентифікацію після успішної ініціалізації сесії HTML:

Вище:
- у [4,7]: запит надсилає рядок [user=bernard&password=thibault];
Відповідь така:

- у форматі [4] відображається повідомлення про помилку;
- у [3] — знову відображено неправильного користувача;
32.5.8. Висновок
Нам вдалося протестувати представлення [vue-authentification.html], не написавши інших представлень. Це стало можливим завдяки тому, що:
- усі контролери вже написані;
- [Postman] дозволяє нам надсилати запити на сервер без необхідності наявності всіх подання. Під час написання контролерів слід бути готовим обробляти запити, які жодне подання не дозволяє. Ніколи не слід заздалегідь вважати, що «цей запит неможливий». Потрібно перевірити;
32.6. Вид для розрахунку податку

32.6.1. Опис подання
Вигляд для розрахунку податку виглядає так:

Вигляд складається з трьох частин:
- 1: верхній банер генерується фрагментом [v-bandeau.html], який вже було представлено;
- 2: форма розрахунку податку, що генерується фрагментом [v-calcul-impot.html];
- 3: меню з двома посиланнями, яке генерується фрагментом [v-menu.html];
Вікно розрахунку податку генерується за допомогою такого коду [vue-calcul-impot.html]:
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<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">
<!-- головний банер -->
{% include "fragments/v-bandeau.html" %}
<!-- двоколонковий макет -->
<div class="row">
<!-- меню -->
<div class="col-md-3">
{% include "fragments/v-menu.html" %}
</div>
<!-- форма розрахунку -->
<div class="col-md-9">
{% include "fragments/v-calcul-impot.html" %}
</div>
</div>
<!-- у разі успіху -->
{% if modèle.success %}
<!-- виводиться повідомлення про успішне виконання -->
<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 стовпцях -->
<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: якщо розрахунок податку завершився успішно ([modèle.success=True]), то результат розрахунку податку відображається у зеленому блоці (рядки 37–43). Ця рамка знаходиться у третьому рядку Bootstrap подання (рядок 32) і займає дев’ять стовпців (рядок 36) праворуч від трьох порожніх стовпців (рядки 33–35). Отже, ця рамка буде розташована під формою розрахунку податку;
- рядки 48–61: якщо розрахунок податку завершився невдало ([modèle.error=True]), то повідомлення про помилку відображається у рожевій рамці (рядки 55–58). Ця рамка знаходиться у третьому рядку Bootstrap цього вікна (рядок 50) і займає дев’ять стовпців (рядок 54) праворуч від трьох порожніх стовпців (рядки 51–53). Отже, ця рамка також буде розташована під формою розрахунку податку;
32.6.2. Фрагмент [v-calcul-impot.html]
Фрагмент [v-calcul-impot.html] відображає форму розрахунку податку у веб-додатку:
Код фрагмента [v-calcul-impot.html] такий:

<!-- форма HTML відправлено -->
<form method="post" action="/calculer-impot">
<!-- повідомлення у 12 стовпцях на синьому тлі -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- елементи форми -->
<fieldset class="form-group">
<!-- перший рядок на 9 стовпців -->
<div class="row">
<!-- напис у 4 стовпцях -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- перемикачі на 5 стовпців-->
<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>
<!-- другий рядок із 9 стовпців -->
<div class="form-group row">
<!-- напис у 4 стовпцях -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- поле для введення кількості дітей у 5 стовпців -->
<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>
<!-- третій рядок на 9 стовпців -->
<div class="form-group row">
<!-- напис у 4 стовпцях -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel net imposable</label>
<!-- поле для введення числового значення заробітної плати, що складається з 5 стовпців -->
<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>
<!-- четвертий рядок, кнопка [submit] на 5 стовпців -->
<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-impot] (атрибут [action]). Передані значення будуть відповідати значенням полів введення:
- значення позначеної кнопки-перемикача у вигляді:
- [marié=oui], якщо відмічено перемикач [Oui] (рядки 17–22). [marié] — це значення атрибута [name] у рядку 18, [oui] — значення атрибута [value] у рядку 18;
- [marié=non], якщо встановлено прапорець [Non] (рядки 23–28). [marié] — це значення атрибута [name] у рядку 24, [non] — значення атрибута [value] у рядку 24;
- значення поля введення чисел у рядку 37 у вигляді [enfants=xx], де [enfants] — це значення атрибута [name] у рядку 37, а [xx] — значення, введене користувачем за допомогою клавіатури;
- значення поля введення чисел у рядку 46 у вигляді [salaire=xx], де [salaire] — це значення атрибута [name] у рядку 46, а [xx] — значення, введене користувачем за допомогою клавіатури;
Зрештою, значення, що буде відправлено, матиме вигляд [marié=xx&enfants=yy&salaire=zz].
- (продовження)
- введені значення будуть відправлені, коли користувач натисне кнопку типу [submit] у рядку 53;
- рядки 16–30: дві кнопки-перемикачі:
![]()
Обидві кнопки-перемикачі входять до однієї групи, оскільки мають однаковий атрибут [name] (рядки 18, 24). Браузер гарантує, що в групі кнопок-перемикачів у будь-який момент часу може бути позначена лише одна. Отже, натискання на одну з них деактивує ту, що була позначена раніше;
- це перемикачі завдяки атрибуту [type="radio"] (рядки 18, 24);
- під час відображення форми (до введення даних) одна з радіо-кнопок має бути відмічена: для цього достатньо додати атрибут [checked=’checked’] до відповідного тегу <input type="radio">. Це реалізується за допомогою динамічних змінних:
- [modèle.checkedOui] у рядку 18;
- [modèle->checkedNon] у рядку 24;
Ці змінні стануть частиною шаблону подання.
- рядок 37: поле для введення чисел [type="number"] із мінімальним значенням 0 [min="0"]. У сучасних браузерах це означає, що користувач зможе ввести лише число >=0. У цих самих сучасних браузерах введення можна здійснювати за допомогою повзунка, який можна пересувати вгору або вниз. Атрибут [step="1"] у рядку 37 вказує, що повзунок буде змінювати значення з кроком у 1 одиницю. Це означає, що повзунок прийматиме лише цілі числа від 0 до n з кроком у 1. Для ручного введення це означає, що числа з десятковою крапкою не будуть прийняті;
- рядок 37: у деяких випадках поле введення дочірніх елементів має бути попередньо заповнене останнім значенням, введеним у це поле. Для цього використовується атрибут [value], який встановлює значення, що відображатиметься у полі введення. Це значення буде динамічним і генеруватиметься змінною [modèle.enfants];
- рядок 37: атрибут [required] змушує користувача ввести дані, щоб форма була затверджена;
- рядок 46: пояснення щодо введення даних про заробітну плату такі самі, як і для даних про дітей;
- рядок 53: кнопка типу [submit], яка запускає POST на основі значень, введених у URL та [/calculer-impot] (рядок 2);
![]()
32.6.3. Фрагмент [v-menu.html]
Цей фрагмент відображає меню ліворуч від форми розрахунку податку:

Код цього фрагмента такий:
<!-- меню Bootstrap -->
<nav class="nav flex-column">
<!-- відображення списку посилань 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/chemin] додатка. Таким чином, у [1] буде створено посилання:
- рядок 5: шаблон [modèle.optionsMenu] фрагмента буде списком у вигляді:
- рядки 2, 7: класи CSS та [nav, flex-column, nav-link] — це класи Bootstrap, які визначають зовнішній вигляд меню;
32.6.4. Візуальне тестування
Ми об’єднуємо ці різні елементи у папці [Tests] і створюємо тестовий шаблон для подання [vue-calcul-impot.html]:

Тестовий скрипт [test_vue_calcul_impot] матиме такий вигляд:
from flask import Flask, render_template, make_response
# додаток Flask
app = Flask(__name__, template_folder="../templates", static_folder="../static")
# Головна URL
@app.route('/')
def index():
# інкапсуляція даних сторінки в шаблон
modèle = {}
# форма
modèle["checkedOui"] = ""
modèle["checkedNon"] = 'checked="checked"'
modèle["enfants"] = 2
modèle["salaire"] = 300000
# повідомлення про успішне виконання
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 %"
# повідомлення про помилку
modèle["error"] = True
erreurs = ["erreur1", "erreur2"]
# створюється список HTML помилок
content = ""
for erreur in erreurs:
content += f"<li>{erreur}</li>"
modèle["erreurs"] = content
# меню
modèle["optionsMenu"] = [
{"text": 'Liste des simulations', "url": '/lister-simulations'},
{"text": 'Fin de session', "url": '/fin-session'}]
# відображення сторінки
return make_response(render_template("views/vue-calcul-impot.html", modèle=modèle))
# головне меню
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] отримуємо такий результат:
Працюємо над цим видом, доки візуальний результат не задовольнить нас. Після цього можна перейти до інтеграції виду у веб-додаток, що зараз розробляється.

32.6.5. Розрахунок моделі подання
Після визначення візуального вигляду подання можна приступити до розрахунку моделі подання в реальних умовах. Нагадаємо коди станів, які ведуть до цього подання. Їх можна знайти у файлі конфігурації:
{
# перегляд розрахунку податку
"états": [
# /автентифікація-користувача успішно
200,
# /розрахувати-податок успішно
300,
# /розрахувати-податок — невдача
301,
# /відобразити-розрахунок-податку
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
Отже, саме коди стану [200, 300, 301, 800] забезпечують відображення вікна розрахунку податку. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], виконаними на додатку jSON:
- [authentifier-utilisateur-200]: 200 — це код стану після успішного виконання дії [authentifier-utilisateur]: у цьому випадку відображається порожня форма розрахунку податку;
- [calculer-impot-300]: 300 — це код стану після успішного виконання дії [calculer-impot]. У цьому випадку відображається форма розрахунку з введеними даними та сумою податку. Користувач може виконати ще один розрахунок;
- код стану [301] — це код, отриманий у результаті помилкового розрахунку податку;
- код стану [800] буде представлено пізніше. Ми ще не стикалися з ним;
Тепер, коли ми знаємо, в яких випадках має відображатися форма розрахунку податку, можна розрахувати її шаблон у класі [ModelForCalculImpotView]:

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:
# інкапсуляція даних з подання в шаблон
modèle = {}
# стан додатка
état = résultat["état"]
# модель залежить від стану
if état in [200, 800]:
# початкове відображення порожньої форми
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:
# успішне виконання обчислення — відображення результату
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} %"
# форма відновлена із введеними значеннями
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:
# сталася помилка — форма відновлена з введеними значеннями
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")
# помилка
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>"
# опції меню
modèle["optionsMenu"] = [
{"text": 'Liste des simulations', "url": '/lister-simulations'},
{"text": 'Fin de session', "url": '/fin-session'}]
# повертаємо шаблон
return modèle
Коментарі
- рядок 12: вигляд, що відображається, залежить від коду стану, який повертає контролер;
- рядки 14–21: відображення порожньої форми;
- рядки 22–35: випадок успішного розрахунку податку. Знову відображаються введені значення, а також сума податку;
- рядки 36–47: випадок невдалого розрахунку податку;
- рядки 49–52: обчислення двох опцій меню;
32.6.6. Тести [Postman]
Ініціалізуємо сесію HTML за допомогою запиту [init-session-html-700], а потім авторизуємося за допомогою запиту [authentifier-utilisateur-200]. Далі використовуємо наступний запит [calculer-impot-300]:
Відповідь сервера така:


Тепер спробуємо виконати такий запит: [calculer-impot-301]:

Відповідь сервера така:
Тепер спробуємо розглянути несподіваний випадок, коли в запиті POST бракує параметрів. Такий випадок неможливий під час нормальної роботи додатка. Але будь-хто може «скласти» запит HTTP, як ми це робимо зараз:


- у [6] ми зняли галочку з параметра, що надсилається у [marié];
Відповідь сервера така:

- на [3] — повідомлення про помилку сервера;
У цьому додатку у нас був вибір. Ми могли присвоїти цьому випадку помилки код стану, який перенаправляє на сторінку несподіваних помилок. У цьому додатку ми вибрали для кожного контролера два коди стану:
- [xx0] — для успішного виконання;
- [xx1] — для невдачі;
У випадках невдачі можна диверсифікувати коди стану, щоб забезпечити більш детальне управління помилками. Наприклад, ми могли б мати:
- [xx1]: для помилок, які слід відобразити на сторінці, що спричинила помилку;
- [xx2]: для несподіваних помилок під час нормального використання додатка;
32.7. Вигляд списку симуляцій

32.7.1. Опис вікна
Вигляд, що відображає список симуляцій, є таким:

Вигляд, згенерований кодом [vue-liste-simulations.html], складається з трьох частин:
- 1: верхня панель генерується фрагментом [v-bandeau.html], який вже було представлено;
- 3: таблиця симуляцій, згенерована фрагментом [v-liste-simulations.html];
- 2: меню з двома посиланнями, згенероване фрагментом [v-menu.html], який вже було представлено;
Вигляд симуляцій генерується за допомогою такого коду [vue-liste-simulations.html]:
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<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">
<!-- головний банер -->
{% include "fragments/v-bandeau.html" %}
<!-- двоколонковий макет -->
<div class="row">
<!-- триколонне меню-->
<div class="col-md-3">
{% include "fragments/v-menu.html" %}
</div>
<!-- список симуляцій у 9 колонках-->
<div class="col-md-9">
{% include "fragments/v-liste-simulations.html" %}
</div>
</div>
</div>
</body>
</html>
Коментарі
- рядок 16: включення верхнього банера додатка [1];
- рядок 21: додавання меню [2]. Воно відображатиметься у трьох стовпцях під банером;
- рядок 26: включення таблиці моделювань [3]. Вона відображатиметься у дев’яти стовпцях під банером та праворуч від меню;
Ми вже прокоментували два з трьох фрагментів цього виду:
- [v-bandeau.html]: у розділі «Посилання»;
- [v-menu.html]: у параграфі з посиланням;
Фрагмент [v-liste-simulations.html] виглядає так:
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- повідомлення на синьому тлі -->
<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 %}
<!-- повідомлення на синьому тлі -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- таблиця моделювань -->
<table class="table table-sm table-hover table-striped">
<!-- заголовки шести стовпців таблиці -->
<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>
<!-- тіло таблиці (відображені дані) -->
<tbody>
<!-- кожна симуляція відображається під час перегляду таблиці симуляцій -->
{% for simulation in modèle.simulations %}
<!-- відображення рядка таблиці з 6 стовпцями — тег <tr> -->
<!-- стовпець 1: заголовок рядка (№ симуляції) — тег <th scope='row' -->
<!-- стовпець 2: значення параметра [marié] — тег <td> -->
<!-- стовпець 3: значення параметра [enfants] — тег <td> -->
<!-- стовпець 4: значення параметра [salaire] — тег <td> -->
<!-- стовпець 5: значення параметра [impôt] (податку) — тег <td> -->
<!-- стовпець 6: значення параметра [surcôte] — тег <td> -->
<!-- стовпець 7: значення параметра [décôte] — тег <td> -->
<!-- стовпець 8: значення параметра [réduction] — тег <td> -->
<!-- стовпець 9: значення параметра [taux] (податку) — тег <td> -->
<!-- стовпець 10: посилання на скасування моделювання — тег <td> -->
<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> (table head, рядки 17, 30). Тег <tr> (table row, рядки 18 та 29) окреслює рядок. У рядках 19–28 тег <th> (table header) визначає заголовок стовпця. Отже, їх десять. [scope="col"] вказує, що заголовок стосується стовпця. [scope="row"] вказує, що заголовок стосується рядка;
- рядки 32–61: тег <tbody> обрамлює дані, що відображаються в таблиці;
- рядки 47–58: тег <tr> обрамлює рядок таблиці;
- рядок 48: тег <th scope=’row’> визначає заголовок рядка. Браузер виділяє цей заголовок;
- рядки 49–57: кожен тег <td> (дані таблиці) визначає стовпець рядка;
- рядок 34: список симуляцій міститься у шаблоні [modèle.simulations], який є списком словників;
- рядок 57: посилання для видалення симуляції. Модель URL використовує номер симуляції, що відображається в рядку;
32.7.2. Візуальне тестування
Створюємо тестовий скрипт для подання [vue-liste-simulations.html]:
Скрипт [test_vue_liste_simulations] має такий вигляд:

from flask import Flask, make_response, render_template
# додаток на Flask
app = Flask(__name__, template_folder="../templates", static_folder="../static")
# Головна URL
@app.route('/')
def index():
# дані сторінки інкапсулюються в шаблон
modèle = {}
# перетворюємо результати моделювання у формат, який очікує сторінка
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
}
]
# меню
modèle["optionsMenu"] = [
{"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
{"text": 'Fin de session', "url": '/fin-session'}]
# відображення сторінки
return make_response(render_template("views/vue-liste-simulations.html", modèle=modèle))
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
Коментарі
- рядки 12–35: у модель додаємо дві симуляції
- рядки 37–39: таблиця опцій меню;
Відобразимо цей вигляд, виконавши цей скрипт. Отримуємо такий результат:

Працюємо над цим видом, доки отриманий візуальний результат нас не задовольнить. Потім можна перейти до інтеграції виду у веб-додаток, який зараз пишемо.
32.7.3. Розрахунок моделі подання
Після визначення візуального вигляду вікна можна приступити до розрахунку моделі вікна в реальних умовах. Нагадаємо коди станів, які ведуть до цього вікна. Їх можна знайти у файлі конфігурації:

{
# перегляд списку симуляцій
"états": [
# /перелік-симуляцій
500,
# /видалити-симуляцію
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
Отже, саме коди стану [500, 600] забезпечують відображення виду симуляцій. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], виконаними на додатку jSON:
- [lister-simulations-500]: 500 — це код стану після успішного виконання дії [lister-simulations]: у цьому випадку відображається список симуляцій, виконаних користувачем;
- [supprimer-simulation-600]: 600 — це код стану після успішного виконання дії [supprimer-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:
# дані сторінки інкапсулюються в шаблон
modèle = {}
# симуляції знаходяться у відповіді контролера, який виконав дію
# у вигляді масиву словників TaxPayer
modèle["simulations"] = résultat["réponse"]
# меню
modèle["optionsMenu"] = [
{"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
{"text": 'Fin de session', "url": '/fin-session'}]
# повертається шаблон
return modèle
Коментарі
- рядок 13: симуляції, які потрібно відобразити, знаходяться в [результат["réponse"]];
- рядки 15–17: параметри меню, які потрібно відобразити;
32.7.4. Тести [Postman]
Ініціалізується
- ініціалізується сесія HTML;
- проводиться аутентифікація;
- виконуємо три розрахунки податку;
Тест [lister-simulations-500] дозволяє отримати код стану 500. Він відповідає запиту на перегляд моделювань:

Відповідь сервера така:

Тест [supprimer-simulation-600] дозволяє отримати код стану 600. Тут ми видалимо симуляцію № 2.
У відповідь отримано список симуляцій, у якому на одну симуляцію менше:


32.8. Перегляд несподіваних помилок
Тут несподіваною помилкою називається помилка, яка не мала б статися за умов нормального використання веб-додатку. Наприклад, запит на розрахунок податку без проходження автентифікації. Ніщо не заважає користувачеві безпосередньо ввести URL [/calcul-impot] у свій браузер. Крім того, як ми вже бачили, він може надіслати запит «POST» на «URL [/calcul-impot]», не надіславши очікуваних параметрів. Ми переконалися, що наш веб-додаток вміє правильно відповідати на такий запит. Несподіваною помилкою ми назвемо таку, яка не повинна виникати в рамках роботи додатка HTML. Якщо вона трапляється, це, ймовірно, означає, що хтось намагається «зламати» додаток. З навчальною метою ми вирішили відображати сторінку помилок у таких випадках. Насправді можна було б повторно відобразити останню сторінку, надіслану клієнту. Для цього достатньо зберегти в сесії останню надіслану відповідь HTML. У разі несподіваної помилки ми повертаємо цю відповідь. Таким чином, у користувача складеться враження, що сервер не реагує на його помилки, оскільки відображена сторінка не змінюється.
32.8.1. Огляд подання

Вигляд, що відображає несподівані помилки, є таким:

Вигляд, згенерований кодом [vue-erreurs.html], складається з трьох частин:
- 1: верхній банер генерується фрагментом [v-bandeau.html], який вже було представлено;
- 2: несподівана помилка або помилки;
- 3: меню з трьома посиланнями, згенероване фрагментом [v-menu.html], який вже було наведено;
Вигляд несподіваних помилок генерується наступним скриптом [vue-erreurs.html]:
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<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 колонок -->
{% include "fragments/v-bandeau.html" %}
<!-- двосекційний рядок -->
<div class="row">
<!-- меню на 3 колонки-->
<div class="col-md-3">
{% include "fragments/v-menu.html" %}
</div>
<!-- список помилок на 9 колонок -->
<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> обрамлює маркований список. Цей маркований список надається шаблоном [modèle.erreurs];
Ми вже прокоментували два фрагменти цього виду:
- [v-bandeau.html]: у абзаці-посиланні;
- [v-menu.html]: у абзаці-посиланні;
32.8.2. Візуальне тестування
Ми створюємо тестовий скрипт для шаблону [vue-erreurs.html]:

from flask import Flask, render_template, make_response
# додаток Flask
app = Flask(__name__, template_folder="../templates", static_folder="../static")
# Головна URL
@app.route('/')
def index():
# інкапсуляція даних сторінки в шаблон
modèle = {}
# створюємо список HTML помилок
content = ""
for erreur in ["erreur1", "erreur2"]:
content += f"<li>{erreur}</li>"
modèle["erreurs"] = content
# опції меню
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'}]
# відображення сторінки
return make_response(render_template("views/vue-erreurs.html", modèle=modèle))
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
Коментарі
- рядки 11–15: формування списку помилок HTML;
- рядки 17–20: масив опцій меню;
Запустимо цей скрипт. Отримаємо такий результат:
Працюємо над цим видом, доки отриманий візуальний результат нас не задовольнить. Після цього можна перейти до інтеграції цього виду у веб-додаток, який ми зараз розробляємо.

32.8.3. Обчислення моделі подання

Після визначення візуального вигляду подання можна приступити до розрахунку моделі подання в реальних умовах. Нагадаємо коди станів, які ведуть до цього подання. Їх можна знайти у файлі конфігурації:
# види HTML та їхні шаблони залежать від стану, що повертається контролером
"views": [
{
# вигляд автентифікації
"états": [
# /успішне ініціювання сеансу
700,
# /завершення-сесії
400,
# /аутентифікація-користувача — невдача
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# сторінка розрахунку податку
"états": [
# /автентифікація-користувача успішно
200,
# /розрахунок-податку успішно
300,
# /розрахувати-податок невдача
301,
# /перегляд-розрахунку-податку
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# перегляд списку моделювань
"états": [
# /перелік-симуляцій
500,
# /видалити-моделювання
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# перегляд несподіваних помилок
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
Саме коди станів, які не приводять до вигляду HTML у рядках 3–41, забезпечують відображення вигляду несподіваних помилок.
Розрахунок моделі подання [vue-erreurs.html] здійснюється наступним класом [ModelForErreursView]:
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:
# модель
modèle = {}
# помилки
modèle["erreurs"] = ""
for erreur in résultat['réponse']:
modèle['erreurs'] += f"<li>{erreur}</li>"
# меню
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'}]
# створення моделі
return modèle
Коментарі
- рядки 11–14: обчислення шаблону [modèle.erreurs], що використовується поданням [vue-erreurs.html];
- рядки 16–197: обчислення шаблону [modèle.optionsMenu], що використовується фрагментом [v-menu.html];
32.8.4. Тести [Postman]
Виконується:
- дію [/init-session/html];
- потім дію [/init-session/x];
Відповідь HTML має такий вигляд:

32.9. Реалізація дій меню додатка
Тут ми розглянемо реалізацію дій меню. Нагадаємо значення посилань, з якими ми вже стикалися
Вигляд | Посилання | Ціль | Роль |
Розрахунок податку | [Liste des simulations] | [/lister-simulations] | Запросити список моделювань |
[Fin de session] | [/fin-session] | ||
Перелік моделювань | [Calcul de l’impôt] | [/afficher-calcul-impot] | Переглянути розрахунок податку |
[Fin de session] | [/fin-session] | ||
Несподівані помилки | [Calcul de l’impôt] | [/afficher-calcul-impot] | Відобразити вікно розрахунку податку |
[Liste des simulations] | [/lister-simulations] | ||
[Fin de session] | [/fin-session] |
Слід нагадати, що клік на посилання викликає GET до цілі посилання. Дії [/lister-simulations, /fin-session] були реалізовані за допомогою операції GET, що дозволяє нам використовувати їх як цілі посилань. Коли дія виконується за допомогою POST, використання посилання стає неможливим, якщо тільки не пов’язати його з JavaScript.
32.9.1. Дія [/afficher-calcul-impot]
З наведених вище дій випливає, що дія [/afficher-calcul-impot] ще не реалізована. Це операція переходу між двома видами: сервери jSON або XML не мають підстав її реалізовувати, оскільки в них відсутнє поняття «вид». Саме сервер HTML вводить це поняття.
Отже, нам потрібно реалізувати дію [/afficher-calcul-impot]. Це дасть нам змогу переглянути порядок реалізації дії на сервері.
Насамперед нам потрібно додати новий вторинний контролер. Ми назвемо його [AfficherCalculImpotController]:

Цей контролер потрібно додати до файлу конфігурації [config]:
# контролери
from AfficherCalculImpotController import AfficherCalculImpotController
from AuthentifierUtilisateurController import AuthentifierUtilisateurController
from CalculerImpotController import CalculerImpotController
from CalculerImpotsController import CalculerImpotsController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
…
# дозволені дії та їхні контролери
"controllers": {
# ініціалізація обчислювальної сесії
"init-session": InitSessionController(),
# аутентифікація користувача
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# розрахунок податку в індивідуальному режимі
"calculer-impot": CalculerImpotController(),
# розрахунок податку в пакетному режимі
"calculer-impots": CalculerImpotsController(),
# перелік моделювань
"lister-simulations": ListerSimulationsController(),
# видалення моделювання
"supprimer-simulation": SupprimerSimulationController(),
# завершення сеансу розрахунку
"fin-session": FinSessionController(),
# відображення вікна розрахунку податку
"afficher-calcul-impot": AfficherCalculImpotController(),
# отримання даних від податкової служби
"get-admindata": GetAdminDataController(),
# головний контролер
"main-controller": MainController()
},
…
# види HTML та їхні шаблони залежать від стану, який повертає контролер
"views": [
{
# вікно автентифікації
…
},
{
# вигляд розрахунку податку
"états": [
# /автентифікація-користувача успішно
200,
# /розрахунок-податку успішно
300,
# /розрахувати-податок — невдача
301,
# /перегляд-розрахунку-податку
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{…
}
],
- рядок 2: новий контролер;
- рядок 28: нова дія та її контролер;
- рядок 51: новий контролер поверне код стану 800. При зміні подання помилки бути не може. Відображається подання [vue-calcul-impot.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):
# отримано елементи шляху
dummy, action = request.path.split('/')
# зміна перегляду — потрібно лише встановити код стану
return {"action": action, "état": 800, "réponse": ""}, status.HTTP_200_OK
Коментарі
- рядок 6: як і інші вторинні контролери, новий контролер реалізує інтерфейс [InterfaceController];
- рядок 13: зміни перегляду легко реалізувати: достатньо повернути код стану, пов’язаний із цільовим переглядом, у даному випадку код 800, як було зазначено вище;
32.9.2. Дія [/fin-session]
Дія [/fin-session] є особливою. Вона не веде безпосередньо до подання, а до перенаправлення. Нагадаємо, що перенаправлення налаштовуються у конфігурації [config] наступним чином:
# перенаправлення
"redirections": [
{
"états": [
400, # /успішне завершення сеансу
],
# перенаправлення на
"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):
# відповідь HTML залежить від коду стану, повернутого контролером
état = résultat["état"]
# чи потрібно здійснити перенаправлення?
for redirection in config["redirections"]:
# стани, що вимагають перенаправлення
états = redirection["états"]
if état in états:
# необхідно виконати перенаправлення
return redirect(f"{redirection['to']}"), status.HTTP_302_FOUND
# кожному стану відповідає один вигляд
# її шукають у списку переглядів
..
- рядки 6–12 обробляють перенаправлення;
- рядок 7: config[‘redirections’] — це список перенаправлень. Кожне перенаправлення є словником із такими ключами:
- [états]: статуси, що повертаються контролером і призводять до перенаправлення;
- [to]: адреса перенаправлення;
- рядки 7–12: пробігаємо список перенаправлень;
- рядок 9: для кожного перенаправлення отримуємо стани, що до нього призводять;
- рядок 10: якщо перевірений стан є в цьому списку, то виконується перенаправлення, рядок 12;
- рядок 12: нагадаємо, що метод [build_http_response] повинен повертати кортеж із двох елементів:
- [response]: відповідь HTTP, яку потрібно сформувати. Вона будується за допомогою функції [redirect], параметром якої є адреса перенаправлення;
- [status_code]: код статусу відповіді HTTP, у даному випадку код [status.HTTP_302_FOUND], який вказує клієнту, що він повинен перенаправитися;
Давайте проведемо тест [Postman]. Ми:
- ініціюємо сесію HTML [init-session/html];
- проводимо аутентифікацію [/authentifier-utilisateur];
- завершуємо сесію [/fin-session];

Відповідь сервера така:

Ми отримали сторінку автентифікації. Це саме те, чого ми чекали. Тепер давайте подивимося, як її було отримано. Перейдемо до консолі [Postman] (Ctrl-Alt-C):

- у [1] — дія [/fin-session];
- на [2-3], код статусу 302 HTTP, повернутий сервером, повідомляє клієнту, що він перенаправляє його;
- у [4] клієнт [Postman] виконує перенаправлення;
32.10. Тестування додатка HTML в реальних умовах
Код написано, і кожну дію перевірено за допомогою [Postman]. Залишилося перевірити послідовність переходу між вікнами в реальних умовах. Нам потрібен спосіб ініціалізувати сесію HTML. Ми знаємо, що потрібно надіслати на сервер запит [/init-session/html]. Це не дуже зручно. Ми б воліли розпочати з URL [/].
У головному скрипті [main] ми написали такий маршрут:
from flask import request, Flask, session, url_for, redirect
…
…
@app.route('/', methods=['GET'])
def index() -> tuple:
# перенаправлення на /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
…
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
- рядки 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/]. Відповідь сервера така:

Якщо розглянути мережевий обмін даними, що відбувся між клієнтом і сервером:

- бачимо, що в [4, 5] браузер отримав запит на перенаправлення на URL [/init-session/html];
Заповнимо отриману форму;

Потім проведемо кілька симуляцій:


Запросимо список моделювань:

Видалімо перше моделювання:

Завершимо сесію:

Читачеві пропонується провести інші тести.