Skip to content

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

35.1. Вступ

Ця версія призначена для вирішення проблем, пов’язаних з оновленням сторінок додатка в браузері (F5). Розглянемо приклад. Користувач щойно видалив симуляцію з id=3:

Image

Після видалення URL у браузері відображається [/supprimer-simulation/3/…]. Якщо користувач оновить сторінку (F5), то URL та [1] відтворюються знову. Отже, знову надсилається запит на видалення симуляції з id=3. Результат буде таким:

Image

Ось ще один приклад. Користувач щойно розрахував податок:

  • у [1], URL та [/calculer-impot], які щойно були запитані за допомогою POST;

Якщо користувач оновить сторінку (F5), він отримає попереджувальне повідомлення:

Image

Операція оновлення сторінки повторно виконує останній запит, здійснений браузером, у даному випадку — [POST /calculer-impot]. Коли користувач намагається повторити запит POST, браузери видають попередження, аналогічне наведеному вище. Воно попереджає, що браузер повторює дію, яка вже була виконана. Припустимо, що цей запит POST здійснив покупку; було б небажано повторювати цю операцію.

Крім того, ми обмежимо можливість користувача вводити URL у свій браузер. Візьмемо, наприклад, один із попередніх екранів:

Image

Посилання, що пропонуються на цій сторінці:

  • [Liste des simulations], пов’язане з URL та [/lister-simulations];
  • [Fin de session], пов’язане з URL та [/fin-session];
  • [Valider] пов'язаний з URL (його немає на малюнку вище) та [/calculer-impot];

Коли з’явиться вікно розрахунку податку, ми прийматимемо лише дії [/lister-simulations, /fin-session, /calculer-impot]. Якщо користувач введе в браузері іншу дію, з’явиться повідомлення про помилку. Таку перевірку ми будемо проводити для всіх чотирьох вікон додатка.

Ми пропонуємо вирішити проблему оновлення сторінок таким чином:

  • ми розрізнятимемо два типи дій:
    • дії ADS (Action Do Something), що змінюють стан додатка. Дії ADS зазвичай мають параметри в URL або в тілі запиту;
    • дії ASV (Action Show View), що відображають представлення без зміни стану додатка. Кількість дій ASV дорівнюватиме кількості представлень V. Дії ASV не мають жодних параметрів;
  • дотепер дії ADS виконувалися, а потім завершувалися відображенням подання V після підготовки його моделі M. Відтепер вони додаватимуть шаблон M цього виду до сесії та вимагатимуть від браузера перенаправлення на дію ASV, яка відповідає за відображення виду V;
  • види V будуть відображатися лише після виконання дії ASV. Вони братимуть свою модель із сесії;

Перевага цього методу полягає в тому, що браузер відображатиме URL ASV у своєму адресному рядку. Оновлення сторінки призведе до повторного виконання дії ASV. Ця дія не змінює стан додатка та використовує шаблон із сесії. Отже, та сама сторінка відобразиться знову без небажаних наслідків. Зрештою, через перенаправлення користувач бачитиме у своєму браузері лише дії URL та ASV і матиме враження, ніби переходить від сторінки до сторінки;

35.2. Реалізація

Image

Папка [impots/http-servers/10] спочатку створюється шляхом копіювання папки [impots/http-servers/09]. Потім вона модифікується.

35.2.1. Нові маршрути

Image

У обох файлах [routes_with_csrftoken] та [routes_without_csrftoken] нам потрібно створити чотири маршрути для чотирьох дій ASV, які відображають чотири види. Решта маршрутів залишаються без змін.

У [routes_with_csrftoken]:


# відображення-вигляду-розрахунку-податку
@app.route('/afficher-vue-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_vue_calcul_impot(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# відображення-вигляду-аутентифікації
@app.route('/afficher-vue-authentification/<string:csrf_token>', methods=['GET'])
def afficher_vue_authentification(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# відобразити-вигляд-списку-симуляцій
@app.route('/afficher-vue-liste-simulations/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_simulations(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з дією
    return front_controller()

# відобразити-перелік-помилок
@app.route('/afficher-vue-liste-erreurs/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_erreurs(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

У рядках 1–23 ми створили чотири маршрути для чотирьох дій ASV:

  • [/afficher-vue-authentification], рядок 8, відображає вікно автентифікації;
  • [/afficher-vue-calcul-impot], рядок 2, відображає вікно розрахунку податку;
  • [/afficher-vue-liste-simulations], рядок 14, відображає вікно моделювання;
  • [/afficher-vue-liste-erreurs], рядок 20, відображає вікно несподіваних помилок;

Те саме робимо у файлі [routes_with_csrftoken] для маршрутів без жетонів:


# маршрути ASV -------------------------
# відобразити-вигляд-розрахунку-податку
@app.route('/afficher-vue-calcul-impot', methods=['GET'])
def afficher_vue_calcul_impot() -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# відобразити-вигляд-аутентифікації
@app.route('/afficher-vue-authentification', methods=['GET'])
def afficher_vue_authentification() -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# відобразити-вигляд-списку-симуляцій
@app.route('/afficher-vue-liste-simulations', methods=['GET'])
def afficher_vue_liste_simulations() -> tuple:
    # виконується контролер, пов'язаний з дією
    return front_controller()

# відобразити-перелік-помилок
@app.route('/afficher-vue-liste-erreurs', methods=['GET'])
def afficher_vue_liste_erreurs() -> tuple:
    # виконується контролер, пов'язаний з дією
    return front_controller()

35.2.2. Нові контролери

Image

Контролер [AfficherVueAuthentificationController] виконує дію ASV [/afficher-vue-authentification]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherVueAuthentificationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # отримуються елементи шляху
        params = request.path.split('/')
        action = params[1]

        # зміна подання — потрібно лише встановити код стану
        return {"action": action, "état"1100"réponse"""}, status.HTTP_200_OK

Як уже зазначалося, дії ASV не мають жодних параметрів і не змінюють стан додатка. Ми просто відображаємо потрібний вигляд, встановлюючи у рядку 14 код стану.

Контролер [AfficherVueCalculImpotController] виконує дію ASV [/afficher-vue-calcul-impot]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherVueCalculImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # отримуємо елементи шляху
        params = request.path.split('/')
        action = params[1]

        # зміна подання — потрібно лише встановити код стану
        return {"action": action, "état"1400"réponse"""}, status.HTTP_200_OK

Контролер [AfficherVueListeSimulationsController] виконує дію ASV [/afficher-vue-liste-simulations]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherVueListeSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # отримуємо елементи шляху
        params = request.path.split('/')
        action = params[1]

        # зміна подання — потрібно лише встановити код стану
        return {"action": action, "état"1200"réponse"""}, status.HTTP_200_OK

Контролер [AfficherVueListeErreursController] виконує дію ASV [/afficher-vue-liste-erreurs]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherVueListeErreursController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # отримуємо елементи шляху
        params = request.path.split('/')
        action = params[1]

        # зміна подання — потрібно лише встановити код стану
        return {"action": action, "état"1300"réponse"""}, status.HTTP_200_OK

Підсумуємо коди стану:

  • код 1100 повинен відображати вікно автентифікації;
  • код 1400 повинен відображати вікно розрахунку податку;
  • код 1200 повинен відображати вікно моделювання;
  • код 1300 повинен відображати вікно несподіваних помилок;

35.2.3. Нова конфігурація MVC

Image

Оскільки конфігурація MVC стала занадто великою, її було розділено на чотири файли:

  • [controllers]: список контролерів C додатка MVC;
  • [ads_actions]: містить список дій ADS (Action Do Something);
  • [asv_actions]: перелік дій ASV (Action Show View);
  • [responses]: перелік класів відповідей HTTP додатка;

Почнемо з найпростішого — файлу відповідей HTTP [responses]:


def configure(config: dict) -> dict:
    # налаштування додатка MVC

    # відповіді HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # різні типи відповідей (json, xml, html)
    responses = {
        "json": JsonResponse(),
        "html": HtmlResponse(),
        "xml": XmlResponse()
    }

    # створюємо словник відповідей HTTP
    return {
        # відповіді HTTP
        "responses": responses,
    }

Нічого дивного.

Файл контролерів [controllers] також не містить нічого несподіваного. До нього просто додано нові контролери дій з файлу ASV.


def configure(config: dict) -> dict:
    # конфігурація додатка MVC

    # головний контролер
    from MainController import MainController

    # контролери дій ADS
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from SupprimerSimulationController import SupprimerSimulationController
    from AfficherCalculImpotController import AfficherCalculImpotController

    # контролери дій ASV
    from AfficherVueCalculImpotController import AfficherVueCalculImpotController
    from AfficherVueAuthentificationController import AfficherVueAuthentificationController
    from AfficherVueListeErreursController import AfficherVueListeErreursController
    from AfficherVueListeSimulationsController import AfficherVueListeSimulationsController

    # дозволені дії та їхні контролери
    controllers = {
        # ініціалізація сеансу обчислення
        "init-session": InitSessionController(),
        # аутентифікація користувача
        "authentifier-utilisateur": AuthentifierUtilisateurController(),
        # посилання на перегляд розрахунку податку
        "afficher-calcul-impot": AfficherCalculImpotController(),
        # розрахунок податку в індивідуальному режимі
        "calculer-impot": CalculerImpotController(),
        # розрахунок податку в пакетному режимі
        "calculer-impots": CalculerImpotsController(),
        # перелік моделювань
        "lister-simulations": ListerSimulationsController(),
        # видалення моделювання
        "supprimer-simulation": SupprimerSimulationController(),
        # завершення сеансу розрахунку
        "fin-session": FinSessionController(),
        # отримання даних від податкової служби
        "get-admindata": GetAdminDataController(),
        # головний контролер
        "main-controller": MainController(),
        # відображення вікна автентифікації
        "afficher-vue-authentification": AfficherVueAuthentificationController(),
        # відображення екрану розрахунку податку
        "afficher-vue-calcul-impot": AfficherVueCalculImpotController(),
        # відображення екрану моделювання
        "afficher-vue-liste-simulations": AfficherVueListeSimulationsController(),
        # відображення екрану помилок
        "afficher-vue-liste-erreurs": AfficherVueListeErreursController()
    }

    # виведення налаштувань контролерів
    return {
        # контролери
        "controllers": controllers,
    }

Конфігурація [asv_actions] для дій ASV є такою:


def configure(config: dict) -> dict:
    # конфігурація додатка MVC

    # представлення HTML та їхні шаблони залежать від стану, що повертається контролером
    # дії ASV (дія «Показати вигляд»)
    asv = [
        {
            # вигляд автентифікації
            "états": [
                1100,  # /відобразити-вікно-аутентифікації
            ],
            "view_name""views/vue-authentification.html",
        },
        {
            # вигляд розрахунку податку
            "états"[
                1400,  # /відобразити-вигляд-розрахунку-податку
            ],
            "view_name""views/vue-calcul-impot.html",
        },
        {
            # перегляд списку моделювань
            "états"[
                1200,  # /перегляд-списку-моделювань
            ],
            "view_name""views/vue-liste-simulations.html",
        },
        {
            # перегляд списку помилок
            "états"[
                1300,  # /відобразити-перелік-помилок
            ],
            "view_name""views/vue-erreurs.html",
        },
    ]

    # виводиться конфігурація ASV
    return {
        # види та шаблони
        "asv": asv,
    }
  • файл [asv_actions] об’єднує чотири нові дії, принцип роботи яких нагадаємо:
    • вони не мають жодних параметрів;
    • вони відображають конкретний вигляд, шаблон якого знаходиться в сесії;
  • список [asv] у рядках 6–35 пов’язує вигляд із кожною дією ASV;

Файл [ads_actions] містить дії ADS:


def configure(config: dict) -> dict:
    # конфігурація додатка MVC

    # шаблони переглядів
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # дії ADS (Action Do Something)
    ads = [
        {
            "états": [
                400,  # /успішне завершення сеансу
            ],
            # перенаправлення на дію ADS
            "to""/init-session/html",
        },
        {
            "états"[
                700,  # /init-session — успішно
                201,  # /автентифікація-користувача — невдача
            ],
            # перенаправлення на дію ASV
            "to""/afficher-vue-authentification",
            # шаблон наступного виду
            "model_for_view": ModelForAuthentificationView()
        },
        {
            "états"[
                200,  # /аутентифікація-користувача успішно
                300,  # /розрахувати-податок успішно
                301,  # /розрахувати-податок невдача
                800,  # /показати-розрахунок-податку посилання
            ],
            # перенаправлення на дію ASV
            "to""/afficher-vue-calcul-impot",
            # шаблон наступного виду
            "model_for_view": ModelForCalculImpotView()
        },
        {
            "états"[
                500,  # /перелік-симуляцій успішно
                600,  # /видалити-симуляцію успішно
            ],
            # перенаправлення до дії ASV
            "to""/afficher-vue-liste-simulations",
            # шаблон наступного перегляду
            "model_for_view": ModelForListeSimulationsView()
        },
    ]

    # вигляд несподіваних помилок
    view_erreurs = {
        # перенаправлення на дію ASV
        "to""/afficher-vue-liste-erreurs",
        # шаблон наступного виду
        "model_for_view": ModelForErreursView()
    }

    # виконується конфігурація MVC
    return {
        # дії ADS
        "ads": ads,
        # перегляд несподіваних помилок
        "view_erreurs": view_erreurs,
    }
  • рядки 11–51: список дій ADS (Action Do Something). Тут містяться всі дії з попередніх версій. Однак їхнє функціонування змінилося:
    • вони не відображають вигляд V. Вони лише готують модель M цього вигляду V;
    • вони ініціюють відображення подання V шляхом перенаправлення на дію ASV, пов’язану з поданням V;
  • не всі дії ADS призводять до перенаправлення на дію ASV: у рядках 12–18 дії ADS та [/fin-session] призводять до перенаправлення на дію ADS та [/init-session/html]. Щоб розрізнити перенаправлення ADS -> ADS та ADS -> ASV, можна скористатися шаблоном [model_for_view]. Такий шаблон не існує для перенаправлень ADS → ADS;

Файл [main/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))

    # виконується конфігурація
    return config

35.2.4. Нові шаблони

Image

Шаблони генеруватимуть нову інформацію. Візьмемо, наприклад, шаблон екрану автентифікації:


from flask import Request
from werkzeug.local import LocalProxy

from AbstractBaseModelForView import AbstractBaseModelForView

class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        # інкапсулюємо дані сторінки в шаблон
        modèle = {}
        

        # токен CSRF
        modèle['csrf_token'] = super().get_csrftoken(config)

        # можливі дії з рівня подання
        modèle['actions_possibles'] = ["afficher-vue-authentification""authentifier-utilisateur"]

        # повертаємо шаблон
        return modèle
  • Нововведення міститься у рядку 17: кожна модель генеруватиме список можливих дій, коли буде відображатися вікно V, для якого вона є моделлю M. Щоб дізнатися про ці дії, потрібно повернутися до вікна V. У випадку вікна автентифікації:

Image

  • ми бачимо, що вікно автентифікації пропонує лише одну дію — ту, що відповідає кнопці [Valider]. Ця дія — [/authentifier-utilisateur];

Ми написали:


        # можливі дії з рівня подання
        modèle['actions_possibles'] = ["afficher-vue-authentification""authentifier-utilisateur"]

На наведеному вище екрані видно, що якщо користувач оновить сторінку, дія [1] [/afficher-vue-authentification] буде виконана повторно. Тому її потрібно дозволити. Можна було б не дозволяти її, але в такому разі користувач отримував би помилку щоразу, коли перезавантажував би сторінку. Ми вирішили, що це небажано.

Можливі дії розміщуються в шаблоні подання. Ми знаємо, що цей шаблон буде збережено в сесії.

Ми робимо це для кожного з чотирьох представлень. Отже, можливі дії такі:

Вигляд автентифікації


modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]

Вигляд розрахунку податку


# можливі дії з рівня перегляду
modèle['actions_possibles'] = ["afficher-vue-calcul-impot""calculer-impot""lister-simulations","fin-session"]

Вигляд списку моделювань


# можливі дії з цього перегляду
actions_possibles = ["afficher-vue-liste-simulations""afficher-calcul-impot""fin-session"]
if len(modèle['simulations']) != 0:
  actions_possibles.append("supprimer-simulation")
modèle['actions_possibles'] = actions_possibles

Перегляд списку помилок


# можливі дії з цього перегляду
modèle['actions_possibles'] = ["afficher-vue-liste-erreurs""afficher-calcul-impot""lister-simulations", "fin-session"]

35.2.5. Новий головний контролер

Image

Контролер [MainController] зазнає деяких змін:


# імпорт залежностей
import threading
import time
from random import randint

from flask_api import status
from flask_wtf.csrf import generate_csrf, validate_csrf
from werkzeug.local import LocalProxy
from wtforms import ValidationError

from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail

def send_adminmail(config: dict, message: str):
    # надсилання електронного листа адміністратору додатка
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# головний контролер додатка
class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # запит обробляється
        type_response1 = None
        logger = None
        try:
            # отримуємо елементи шляху
            params = request.path.split('/')

            # дія є першим елементом
            action = params[1]

            # запис у журнал
            logger = Logger(config['parameters']['logsFilename'])

            

            # дія /afficher-vue-liste-erreurs є особливою
            # не проводиться жодної перевірки, інакше існує ризик потрапити в нескінченний цикл перенаправлень
            erreur = False
            if action != "afficher-vue-liste-erreurs":
                # у разі помилки [результат] — це результат, який слід надіслати клієнту
                (erreur, résultat, type_response1) = MainController.check_action(params, session, config)

            # якщо помилки немає — дія виконується
            if not erreur:
                # виконується контролер, пов'язаний з цією дією
                controller = config['mvc']['controllers'][action]
                résultat, status_code = controller.execute(request, session, config)

        except BaseException as exception:
            # (несподівані) винятки
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            erreur = True

        finally:
            pass

        # помилка виконання
        if erreur:
            # неправильний запит
            status_code = status.HTTP_400_BAD_REQUEST

        if config['parameters']['with_csrftoken']:
            # додається csrf_token до результату
            résultat['csrf_token'] = generate_csrf()

        ….

        # відправляється відповідь HTTP
        return response, status_code

    @staticmethod
    def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
        

        # результат методу
        return erreur, résultat, type_response1
  • рядки 39–44: виконуються перші перевірки дії. Ми повернемося до цього пізніше. Статичний метод [MainController.check_action] повертає трійку елементів:
    • [erreur]: True, якщо виявлено помилку, інакше — False;
    • [résultat]: результат помилки, якщо (помилка == True), інакше — None;
    • [type_response1]: тип (json, xml, html, None) сесії, який було знайдено в сесії;
  • рядок 39: перевірка не виконується, якщо дія — це дія ASV [afficher-vue-liste-erreurs], яка відображає список помилок. Адже якщо під час виконання цієї дії було б виявлено помилку, відбулося б перенаправлення назад до дії [/afficher-vue-liste-erreurs], що призвело б до нескінченного циклу перенаправлень;
  • Статичний метод [check_action] виглядає наступним чином:

    @staticmethod
    def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
        # отримуємо поточну дію
        action = params[1]

        # на початку немає ні помилок, ні результату
        erreur = False
        résultat = None

        # тип сеансу має бути відомий перед виконанням певних дій ADS
        type_response1 = session.get('typeResponse')
        if type_response1 is None and action != "init-session":
            # помічено помилку
            résultat = {"action": action, "état"101,
                        "réponse"["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True

        # для виконання певних дій ADS необхідно пройти автентифікацію
        user = session.get('user')
        if user is None and action not in ["init-session",
                                           "authentifier-utilisateur",
                                           "afficher-vue-authentification"]:
            # відзначається помилка
            résultat = {"action": action, "état"101,
                        "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True

        # у цьому вікні доступні лише певні дії
        if not erreur and action != "init-session":
            # наразі дії неможливі
            actions_possibles = None
            # отримуємо шаблон майбутнього вікна в сесії, якщо він існує
            modèle = session.get('modèle')
            # якщо модель знайдено, отримуємо її можливі дії
            if modèle:
                actions_possibles = modèle.get('actions_possibles')
            # якщо є список можливих дій, перевіряємо, чи поточна дія входить до нього
            if actions_possibles and action not in actions_possibles:
                # фіксуємо помилку
                résultat = {"action": action, "état"151,
                            "réponse"[f"action [{action}] incorrecte dans l'environnement actuel"]}
                erreur = True


        # помилка?
        if not erreur and config['parameters']['with_csrftoken']:
            # перевіряємо дійсність токена CSRF
            # csrf_token є останнім елементом шляху
            csrf_token = params.pop()
            try:
                # якщо csrf_token є недійсним, буде згенеровано виняток
                validate_csrf(csrf_token)
            except ValidationError as exception:
                # недійсний токен CSRF
                résultat = {"action": action, "état"121"réponse"[f"{exception}"]}
                # помилка зафіксована
                erreur = True

        # результат виклику методу
        return erreur, résultat, type_response1
  • рядок 2: метод [check_action] виконує кілька перевірок щодо правильності поточної дії;
  • рядки 6–26, 45–57: попередні версії вже виконували ці перевірки;
  • рядки 28–42: додається нова перевірка. Перевіряється, чи можлива поточна дія в поточному стані додатка. Якщо поточна дія неможлива, генерується стан 151 (рядок 40), що гарантує перенаправлення поточної дії на сторінку несподіваних помилок;

35.2.6. Нова відповідь HTML

Image

Поточні зміни стосуються лише сесій HTML. Сесії jSON або XML не зазнають змін. Клас [HtmlResponse] змінюється наступним чином:


# залежності

from flask import make_response, redirect, render_template
from flask.wrappers import Response
from flask_api import status
from flask_wtf.csrf import generate_csrf
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"]

        # перевіряється, чи стан був згенерований дією ASV
        # у цьому випадку потрібно відобразити представлення
        asv_configs = config['mvc']["asv"]
        trouvé = False
        i = 0
        # проглядається список переглядів
        nb_views = len(asv_configs)
        while not trouvé and i < nb_views:
            # погляд № i
            asv_config = asv_configs[i]
            # стани, пов’язані з переглядом № i
            états = asv_config["états"]
            # чи знаходиться шуканий звіт серед звітів, пов'язаних з переглядом № i
            if état in états:
                trouvé = True
            else:
                # наступний перегляд
                i += 1

        # знайдено?
        if trouvé:
            # це дія ASV — потрібно відобразити перегляд, шаблон якого вже є в сесії
            # генерується код відповіді HTML
            html = render_template(asv_config["view_name"], modèle=session['modèle'])
            # будується відповідь HTTP
            response = make_response(html)
            response.headers['Content-Type'] = 'text/html; charset=utf-8'
            # повертається результат
            return response, status_code

        # не знайдено — це код стану дії ADS
        # за цим стане перенаправлення
        redirected = False
        for ads in config['mvc']['ads']:
            # стани, що вимагають перенаправлення
            états = ads["états"]
            if état in états:
                # відбувається перенаправлення
                redirected = True
                break
        # словник перенаправлення на випадок несподіваних помилок
        if not redirected:
            ads = config['mvc']['view_erreurs']

        # чи це перенаправлення на дію ASD або ASV?
        # якщо є шаблон, то це перенаправлення на дію ASV
        # тоді потрібно обчислити шаблон подання V, яке буде відображено дією ASV
        model_for_view = ads.get("model_for_view")
        if model_for_view:
            # розрахунок шаблону наступного виду
            modèle = model_for_view.get_model_for_view(request, session, config, résultat)
            # шаблон завантажується в сесію для наступного виду
            session['modèle'] = modèle

            # тепер потрібно згенерувати 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
  • рядки 18–35: перевіряється, чи стан, створений останньою виконаною дією, відповідає дії ASV;
  • рядки 36–46: якщо так, то відображається представлення V, пов’язане з дією ASV, використовуючи як шаблон той, що знайдено в сесії, пов’язаній із ключем [‘modèle’];
  • рядки 48–60: коли доходимо до цього місця, ми знаємо, що стан, створений останньою виконаною дією, відповідає дії ADS. Тоді це спричинить перенаправлення. У файлі конфігурації шукаємо визначення цього перенаправлення;
  • рядок 62: коли ми доходимо до цього місця, маємо конфігурацію перенаправлення, яке потрібно виконати. Є два випадки:
    • це перенаправлення на іншу дію ADS. У цьому випадку обчислювати шаблон подання не потрібно;
    • це перенаправлення на дію ASV. У цьому випадку потрібно обчислити шаблон подання (рядки 67–68). Потім цей шаблон заноситься в сесію (рядок 70);
  • рядки 72–76: обчислюється URL перенаправлення;
  • рядки 78–79: відправляється відповідь про перенаправлення клієнту;

35.3. Тестування

Виконайте наступні тести за допомогою браузера:

  • користуйтеся додатком у звичайному режимі. Переконайтеся, що браузер відображає лише такі URL: URL, ASV, [/afficher-vue-nom_de_la_vue];
  • оновіть сторінки (F5) і переконайтеся, що знову відображається та сама сторінка. Жодних побічних ефектів немає;

Крім того, скористайтеся клієнтом [impots/http-clients/09]. Оскільки внесені зміни стосуються лише сесій HTML, клієнти [main, main2, main3, Test1HttpClientDaoWithSession, Test2HttpClientDaoWithSession] повинні продовжувати працювати.

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

Image

Замість [1] вводимо URL [/supprimer-simulation/1]. Дія [/supprimer-simulation] не входить до переліку дій, запропонованих у вікні (дії 1–4). Тому вона буде відхилена. Відповідь сервера така:

Image