36. Практичне завдання: версія 16

36.1. Вступ
URL у нашому додатку наразі мають вигляд [/action/param1/param2/…]. Ми хотіли б мати можливість додавати префікс до цих URL. Наприклад, із префіксом [/do] ми отримаємо URL у форматі [/do/action/param1/param2/…].
36.2. Нова конфігурація маршрутів

- на [2], розрахунок маршрутів буде змінено;
- у [1] файл [config] змінюється, щоб відобразити цю зміну;
Конфігурація [config] набуває такого вигляду:
def configure(config: dict) -> dict:
# конфігурація syspath
import syspath
config['syspath'] = syspath.configure(config)
# параметри додатка
import parameters
config['parameters'] = parameters.configure(config)
# конфігурація бази даних
import database
config["database"] = database.configure(config)
# інстанціювання рівнів додатка
import layers
config['layers'] = layers.configure(config)
# налаштування MVC шару [web]
config['mvc'] = {}
# конфігурація контролерів шару [web]
import controllers
config['mvc'].update(controllers.configure(config))
# дії ASV (Action Show View)
import asv_actions
config['mvc'].update(asv_actions.configure(config))
# дії ADS (Action Do Something)
import ads_actions
config['mvc'].update(ads_actions.configure(config))
# налаштування відповідей HTTP
import responses
config['mvc'].update(responses.configure(config))
# налаштування маршрутів
import routes
routes.configure(config)
# застосовуємо налаштування
return config
- рядки 37–39: модуль [routes] (рядок 38) відповідає за конфігурацію маршрутів (рядок 39);
Файл маршрутів без токенів CSRF [configs/routes_without_csrftoken] змінюється наступним чином:
# залежності
from flask import redirect, request, session, url_for
from flask_api import status
# конфігурація додатка
config = {}
# фронт-контролер
def front_controller() -> tuple:
# запит перенаправляється до головного контролера
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
# кореневий каталог додатка
def index() -> tuple:
# перенаправлення на /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
# init-session
def init_session(type_response: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# аутентифікація користувача
def authentifier_utilisateur() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# розрахувати-податок
def calculer_impot() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
…
З файлу видалено маршрути. Залишилися лише функції, пов’язані з ними.
Файли маршрутів із маркерами CSRF та [configs/routes_with_csrftoken] зазнають тієї самої долі:
# залежності
from flask import redirect, request, session, url_for
from flask_api import status
from flask_wtf.csrf import generate_csrf
# конфігурація
config = {}
# фронт-контролер
def front_controller() -> tuple:
# запит перенаправляється до головного контролера
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
# кореневий каталог додатка
def index() -> tuple:
# перенаправлення на /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session
def init_session(type_response: str, csrf_token: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# аутентифікація користувача
def authentifier_utilisateur(csrf_token: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
…
# init-session-without-csrftoken для клієнтів json та xml
def init_session_without_csrftoken(type_response: str) -> tuple:
# перенаправлення на /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()),
status.HTTP_302_FOUND)
Маршрути обчислюються наступним модулем [configs/routes]:
from flask import Flask
def configure(config: dict):
# налаштування маршрутів
# додаток Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
config['app'] = app
# імпорт маршрутів веб-додатку
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# вбудовуємо конфігурацію в маршрути
routes.config = config
# префікс URL додатка
prefix_url = config["parameters"]["prefix_url"]
# токен CSRF
with_csrftoken = config["parameters"]['with_csrftoken']
if with_csrftoken:
csrftoken_param = f"/<string:csrf_token>"
else:
csrftoken_param = ""
# маршрути додатка Flask
# кореневий каталог додатка
app.add_url_rule(f'{prefix_url}/', methods=['GET'],
view_func=routes.index)
# init-session
app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
view_func=routes.init_session)
# init-session-without-csrftoken
if with_csrftoken:
app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken/<string:type_response>',
methods=['GET'],
view_func=routes.init_session_without_csrftoken)
# аутентифікація-користувача
app.add_url_rule(f'{prefix_url}/authentifier-utilisateur{csrftoken_param}', methods=['POST'],
view_func=routes.authentifier_utilisateur)
# розрахунок-податку
app.add_url_rule(f'{prefix_url}/calculer-impot{csrftoken_param}', methods=['POST'],
view_func=routes.calculer_impot)
# пакетний розрахунок податку
app.add_url_rule(f'{prefix_url}/calculer-impots{csrftoken_param}', methods=['POST'],
view_func=routes.calculer_impots)
# перелік-симуляцій
app.add_url_rule(f'{prefix_url}/lister-simulations{csrftoken_param}', methods=['GET'],
view_func=routes.lister_simulations)
# видалити-симуляцію
app.add_url_rule(f'{prefix_url}/supprimer-simulation/<int:numero>{csrftoken_param}', methods=['GET'],
view_func=routes.supprimer_simulation)
# завершення сесії
app.add_url_rule(f'{prefix_url}/fin-session{csrftoken_param}', methods=['GET'],
view_func=routes.fin_session)
# перегляд розрахунку податку
app.add_url_rule(f'{prefix_url}/afficher-calcul-impot{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_calcul_impot)
# отримати-дані-адміністратора
app.add_url_rule(f'{prefix_url}/get-admindata{csrftoken_param}', methods=['GET'],
view_func=routes.get_admindata)
# перегляд-розрахунку-податків
app.add_url_rule(f'{prefix_url}/afficher-vue-calcul-impot{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_calcul_impot)
# перегляд-аутентифікації
app.add_url_rule(f'{prefix_url}/afficher-vue-authentification{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_authentification)
# перегляд-переліку-симуляцій
app.add_url_rule(f'{prefix_url}/afficher-vue-liste-simulations{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_liste_simulations)
# показати-перелік-помилок
app.add_url_rule(f'{prefix_url}/afficher-vue-liste-erreurs{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_liste_erreurs)
# особливий випадок
if with_csrftoken:
# ініціалізація сесії без csrftoken для клієнтів json та xml
app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken', methods=['GET'],
view_func=routes.init_session_without_csrftoken)
- рядки 6–8: створюється додаток Flask і додається до конфігурації;
- рядки 10–14: імпортується файл маршрутів, що відповідає даній ситуації. Слід пам’ятати, що імпортований файл насправді не містить маршрутів. Він містить лише функції, пов’язані з ними;
- рядок 17: функції, пов’язані з маршрутами, потребують інформації про конфігурацію додатка;
- рядок 20: звертаємо увагу на префікс URL. Він може бути порожнім;
- рядки 22–27: маршрути з токеном CSRF мають додатковий параметр порівняно з тими, що його не мають. Для обробки цієї відмінності використовується змінна [csrftoken_param]:
- вона містить порожній рядок, якщо в маршрутах немає маркера CSRF;
- вона містить рядок [/<string:csrf_token>], якщо є маркер CSRF;
- рядки 29–96: кожен маршрут додатка пов’язаний із функцією з файлу маршрутів, імпортованого в рядках 10–14;
36.3. Нові контролери

У попередній версії всі контролери отримували дію, що обробляється, таким чином:
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримання елементів шляху
params = request.path.split('/')
action = params[1]
Цей код більше не працює, якщо є префікс, наприклад [/do/lister-simulations]. У цьому випадку дія, рядок 3, буде [do] і, отже, буде неправильною.
Цей код перетворюється наступним чином:
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримання елементів шляху
prefix_url = config["parameters"]["prefix_url"]
params = request.path[len(prefix_url):].split('/')
action = params[1]
Це робимо у всіх контролерах.
36.4. Нові шаблони

У попередній версії кожен клас шаблону генерував шаблон таким чином:
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# інкапсуляція даних сторінки в шаблон
modèle = {}
# стан додатка
état = résultat["état"]
# шаблон залежить від стану
…
# токен CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# можливі дії з рівня подання
modèle['actions_possibles'] = […]
# виводимо шаблон
return modèle
Відтепер шаблон матиме додатковий ключ — префікс URL:
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# інкапсулюємо дані сторінки в модель
modèle = {}
# стан додатка
état = résultat["état"]
# модель залежить від стану
…
# токен CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# можливі дії з рівня подання
modèle['actions_possibles'] = […]
# префікс URL
modèle["prefix_url"] = config["parameters"]["prefix_url"]
# повертаємо шаблон
return modèle
36.5. Нові фрагменти

Усі фрагменти, що містять URL, потрібно змінити.
Фрагмент [v-authentification]
<!-- форма HTML — відправляємо її значення за допомогою дії [authentifier-utilisateur] -->
<form method="post" action="{{modèle.prefix_url}}/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- заголовок -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
Фрагмент [v-calcul-impot]
<!-- форма HTML, відправлена -->
<form method="post" action="{{modèle.prefix_url}}/calculer-impot{{modèle.csrf_token}}">
<!-- повідомлення у 12 стовпчиках на синьому тлі -->
…
</form>
Фрагмент [v-liste-simulations]
…
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
…
<!-- таблиця моделювань -->
<table class="table table-sm table-hover table-striped">
…
<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="{{modèle.prefix_url}}/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
</tr>
{% endfor %}
</tr>
</tbody>
</table>
{% endif %}
Фрагмент [v-menu]
<!-- меню Bootstrap -->
<nav class="nav flex-column">
<!-- відображення списку посилань HTML -->
{% for optionMenu in modèle.optionsMenu %}
<a class="nav-link" href="{{modèle.prefix_url}}{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
{% endfor %}
</nav>
36.6. Нова відповідь HTML

У попередній версії код відповіді HTML закінчувався перенаправленням:
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"]
…
# тепер потрібно згенерувати URL перенаправлення, не забуваючи про токен CSRF, якщо його вимагають
if config['parameters']['with_csrftoken']:
csrf_token = f"/{generate_csrf()}"
else:
csrf_token = ""
# відповідь на перенаправлення
return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
Тепер цей код виглядає так:
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
…
# тепер потрібно згенерувати URL для перенаправлення, не забуваючи про токен CSRF, якщо його вимагають
if config['parameters']['with_csrftoken']:
csrf_token = f"/{generate_csrf()}"
else:
csrf_token = ""
# відповідь перенаправлення
return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
36.7. Тестування

Додамо такий префікс у файл конфігурації [parameters]:
…
# токен CSRF
"with_csrftoken": True,
# керовані бази даних MySQL (mysql), PostgreSQL (pgres)
"databases": ["mysql", "pgres"],
# префікс URL додатка
# встановити порожній рядок, якщо префікс не потрібен, або /префікс в іншому випадку
"prefix_url": "/do",
…
Запустимо додаток, а потім надішлімо запит URL [http://localhost:5000/do], відповідь буде такою:

- у [1] — префікс URL;
- у [2] — токен CSRF;
Додаток також можна протестувати за допомогою консольних тестів [http-clients/09]:

У файлах конфігурації [1] та [2] префікс URL має бути включений до URL сервера:
"server": {
# «urlServer»: «http://127.0.0.1:5000»,
"urlServer": "http://127.0.0.1:5000/do",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
…
}
- рядок 3: URL сервера тепер містить префікс URL;
Після внесення цієї зміни всі тести консолі повинні працювати.