Skip to content

35. 应用练习:第15版

35.1. 简介

本版本旨在解决与在浏览器中刷新应用程序页面(F5)相关的问题。让我们举个例子。用户刚刚删除了 id=3 的模拟:

Image

删除后,浏览器中的 URL 为 [/delete-simulation/3/…]. 如果用户刷新页面(F5),URL [1] 会被重新发送。因此,我们再次请求删除 id=3 的模拟。结果如下:

Image

再看另一个例子。用户刚刚计算了一笔税款:

  • [1] 中,URL [/calculate-tax] 刚刚通过 POST 请求被查询; Image

如果用户刷新页面(F5),将收到一条警告信息:

Image

页面刷新操作会重新执行浏览器发出的最后一次请求,本例中即 [POST /calculate-tax]。当被要求重新执行 POST 请求时,浏览器会显示类似上文的警告。该警告提示正在重新执行已完成的操作。假设此 POST 请求完成了购买操作,重复执行将造成不必要的后果。

此外,我们将限制用户在浏览器中输入 URL 的能力。让我们以之前的视图之一为例:

Image

该页面提供的链接包括:

  • [模拟列表],对应 URL [/lister-simulations]
  • [结束会话],对应 URL [/end-session]
  • [提交],对应 URL [/calculate-tax](上文未显示);

当显示税费计算视图时,我们仅接受 [/list-simulations、/end-session、/calculate-tax] 这三个操作。如果用户在浏览器中输入其他操作,系统将抛出错误。我们将对应用程序的全部四个视图执行此类验证。

我们建议按以下方式解决页面刷新问题:

  • 我们将区分两种类型的操作:
    • ADS(执行操作)操作:会修改应用程序状态。此类操作通常在 URL 或请求正文中包含参数;
    • ASV(Action Show View)操作,用于显示视图而不改变应用程序的状态。ASV操作的数量与视图V的数量相同。ASV操作不包含参数;
  • 此前,ADS 操作在执行后,会先准备视图 V 的模型 M,然后通过显示该视图 V 来完成操作。从现在起,它们将把视图的模型 M 放入会话中,并指示浏览器重定向至负责显示视图 V 的 ASV 操作;
  • 视图 V 仅会在 ASV 操作之后显示。它们将从会话中检索其模型;

此方法的优势在于,浏览器地址栏将显示 ASV 的 URL。 刷新页面将重新执行 ASV 操作。该操作不会修改应用程序的状态,且使用会话中的模型( )。因此,同一页面将无任何副作用地重新显示。最后,由于重定向机制,用户在浏览器中仅会看到 ASV 操作的 URL,从而产生在页面间流畅导航的体验;

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] 中:

#  afficher-vue-calcul-impot
@app.route('/afficher-vue-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_vue_calcul_impot(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-authentication
@app.route('/afficher-vue-authentification/<string:csrf_token>', methods=['GET'])
def afficher_vue_authentification(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-list-simulations
@app.route('/afficher-vue-liste-simulations/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_simulations(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-error_list
@app.route('/afficher-vue-liste-erreurs/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_erreurs(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

第 1–23 行:我们为四个 ASV 操作创建了四个路由:

  • [/display-authentication-view],第 8 行,显示身份验证视图;
  • [/display-tax-calculation-view],第 2 行,显示税费计算视图;
  • [/display-simulation-list-view],第 14 行,显示模拟视图;
  • [/display-error-list-view],第 20 行,显示意外错误视图;

对于没有令牌的路由,我们在 [routes_with_csrftoken] 文件中也做了同样的处理:

#  routes ASV -------------------------
#  afficher-vue-calcul-impot
@app.route('/afficher-vue-calcul-impot', methods=['GET'])
def afficher_vue_calcul_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-authentication
@app.route('/afficher-vue-authentification', methods=['GET'])
def afficher_vue_authentification() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-list-simulations
@app.route('/afficher-vue-liste-simulations', methods=['GET'])
def afficher_vue_liste_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-view-error_list
@app.route('/afficher-vue-liste-erreurs', methods=['GET'])
def afficher_vue_liste_erreurs() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

35.2.2. 新的控制器

Image

[AuthenticationViewController] 执行 ASV 操作 [/display-authentication-view]

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):
        #  path elements are retrieved
        params = request.path.split('/')
        action = params[1]

        #  change of view - just a status code to set
        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):
        #  path elements are retrieved
        params = request.path.split('/')
        action = params[1]

        #  view change - just a status code to set
        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):
        #  path elements are retrieved
        params = request.path.split('/')
        action = params[1]

        #  view change - just a status code to set
        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):
        #  path elements are retrieved
        params = request.path.split('/')
        action = params[1]

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

让我们总结一下状态码:

  • 代码 1100 应显示身份验证视图;
  • 代码 1400 应显示税费计算视图;
  • 代码 1200 应显示模拟视图;
  • 代码 1300 应显示意外错误视图;

35.2.3. 新的 MVC 配置

由于规模过大,MVC 配置已拆分为四个文件:

  • [controllers]:MVC 应用程序的 C 控制器列表;
  • [ads_actions]:列出 ADS(Action Do Something)操作;
  • [asv_actions]:列出 ASV(Action Show View)操作;
  • [responses]:列出了应用程序的 HTTP 响应类;

让我们从最简单的开始,即 HTTP 响应文件 [responses]

def configure(config: dict) -> dict:
    #  application configuration MVC

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

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

    #  return response dictionary HTTP
    return {
        #  answers HTTP
        "responses": responses,
    }

这没什么意外。

[controllers] 文件的内容也毫不意外。我们只是为 ASV 操作添加了新的控制器。

def configure(config: dict) -> dict:
    #  application configuration MVC

    #  the main controller
    from MainController import MainController

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

    #  action controllers ASV
    from AfficherVueCalculImpotController import AfficherVueCalculImpotController
    from AfficherVueAuthentificationController import AfficherVueAuthentificationController
    from AfficherVueListeErreursController import AfficherVueListeErreursController
    from AfficherVueListeSimulationsController import AfficherVueListeSimulationsController

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

    #  make the controller configuration
    return {
        #  controllers
        "controllers": controllers,
    }

ASV 操作的 [asv_actions] 配置如下:

def configure(config: dict) -> dict:
    #  application configuration MVC

    #  HTML views and their models depend on the state rendered by the controller
    #  actions ASV (Action Show view)
    asv = [
        {
            #  authentication view
            "états": [
                1100,  #  /show-view-authentication
            ],
            "view_name": "views/vue-authentification.html",
        },
        {
            #  tax calculation
            "états": [
                1400,  #  /display-view-tax-calculation
            ],
            "view_name": "views/vue-calcul-impot.html",
        },
        {
            #  view of simulation list
            "états": [
                1200,  #  /display-view-list-simulations
            ],
            "view_name": "views/vue-liste-simulations.html",
        },
        {
            #  view of error list
            "états": [
                1300,  #  /display-view-error-list
            ],
            "view_name": "views/vue-erreurs.html",
        },
    ]

    #  return the ASV configuration
    return {
        #  views and models
        "asv": asv,
    }
  • [asv_actions] 文件包含四个新操作,其功能总结如下:
    • 它们不带参数;
    • 它们会显示一个特定视图,其模板位于会话中;
  • 第 6–35 行中的 [asv] 列表将每个 ASV 操作与一个视图相关联;

[ads_actions] 文件包含 ADS 操作:

def configure(config: dict) -> dict:
    #  application configuration MVC

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

    #  actions ADS (Action Do Something)
    ads = [
        {
            "états": [
                400,  #  /end-session success
            ],
            #  redirection to action ADS
            "to": "/init-session/html",
        },
        {
            "états": [
                700,  #  /init-session - success
                201,  #  /authentifier-user failure
            ],
            #  redirection to action ASV
            "to": "/afficher-vue-authentification",
            #  model of the following view
            "model_for_view": ModelForAuthentificationView()
        },
        {
            "états": [
                200,  #  /authentifier-user success
                300,  #  /calculate-tax-success
                301,  #  /calculate-tax failure
                800,  #  /display-tax-calculation link
            ],
            #  redirection to action ASV
            "to": "/afficher-vue-calcul-impot",
            #  model of the following view
            "model_for_view": ModelForCalculImpotView()
        },
        {
            "états": [
                500,  #  /lister-simulations success
                600,  #  /suppress-simulation success
            ],
            #  redirection to action ASV
            "to": "/afficher-vue-liste-simulations",
            #  model of the following view
            "model_for_view": ModelForListeSimulationsView()
        },
    ]

    #  view of unexpected errors
    view_erreurs = {
        #  redirection to action ASV
        "to": "/afficher-vue-liste-erreurs",
        #  model of the following view
        "model_for_view": ModelForErreursView()
    }

    #  return the MVC configuration
    return {
        #  shares ADS
        "ads": ads,
        #  the sight of unexpected errors
        "view_erreurs": view_erreurs,
    }
  • 第 11–51 行:ADS(Action Do Something)操作的列表。包含所有旧版本中的操作。不过,它们的行为已发生变化:
    • 它们不再直接显示视图 V,而是仅为该视图 V 准备模型 M;
    • 它们通过重定向至与视图 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 configuration
    import syspath
    config['syspath'] = syspath.configure(config)

    #  application setup
    import parameters
    config['parameters'] = parameters.configure(config)

    #  database configuration
    import database
    config["database"] = database.configure(config)

    #  instantiation of application layers
    import layers
    config['layers'] = layers.configure(config)

    #  configuration MVC of the [web] layer
    config['mvc'] = {}

    #  web] layer controller configuration
    import controllers
    config['mvc'].update(controllers.configure(config))

    #  actions ASV (Action Show View)
    import asv_actions
    config['mvc'].update(asv_actions.configure(config))

    #  actions ADS (Action Do Something)
    import ads_actions
    config['mvc'].update(ads_actions.configure(config))

    #  response configuration HTTP
    import responses
    config['mvc'].update(responses.configure(config))

    #  we return the configuration
    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:
        #  we encapsulate the paged data in the model
        modèle = {}
        

        #  csrf token
        modèle['csrf_token'] = super().get_csrftoken(config)

        #  possible actions from the view
        modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]

        #  we render the model
        return modèle
  • 新功能位于第 17 行:当显示视图 V(该视图对应的模型即为 M)时,每个模型都会生成一个可能操作的列表。要查看这些操作,我们需要返回视图 V。以身份验证视图为例:

Image

  • 我们可以看到,认证视图仅提供一个操作,即 [Validate] 按钮的操作。该操作为 [/authenticate-user]

我们编写了:


        # actions possibles à partir de la vue
        modèle['actions_possibles'] = ["afficher-vue-authentification""authentifier-utilisateur"]

在上方的视图中,我们可以看到,如果用户刷新视图,操作 [1] [/display-authentication-view] 将被重新执行。因此,该操作必须经过授权。我们本可以选择不对其进行授权,但在这种情况下,用户每次刷新页面时都会遇到错误。我们认为这种情况是不理想的。

可能的操作被放置在视图模型中。我们知道该模型将被存储在会话中。

我们对四个视图中的每一个都进行此操作。此时,可能的操作如下:

身份验证视图


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

税费计算视图


# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-calcul-impot""calculer-impot""lister-simulations","fin-session"]

模拟列表视图


# possible actions from the view
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

错误列表视图


# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-liste-erreurs""afficher-calcul-impot""lister-simulations", "fin-session"]

35.2.5. 新的主控制器

Image

[MainController] 进行了若干改动:

#  import dependencies
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):
    #  send an e-mail to the application administrator
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

#  main application controller
class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  we process the request
        type_response1 = None
        logger = None
        try:
            #  path elements are retrieved
            params = request.path.split('/')

            #  action is the 1st element
            action = params[1]

            #  logger
            logger = Logger(config['parameters']['logsFilename'])

            

            #  the /display-view-error-list action is special
            #  we don't check anything, otherwise we risk entering an infinite loop of redirects
            erreur = False
            if action != "afficher-vue-liste-erreurs":
                #  if error, (result] is the result to be sent to the customer
                (erreur, résultat, type_response1) = MainController.check_action(params, session, config)

            #  if no error - action executed
            if not erreur:
                #  execute the controller associated with the action
                controller = config['mvc']['controllers'][action]
                résultat, status_code = controller.execute(request, session, config)

        except BaseException as exception:
            #  (unexpected) exceptions
            résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
            erreur = True

        finally:
            pass

        #  runtime error
        if erreur:
            #  erroneous request
            status_code = status.HTTP_400_BAD_REQUEST

        if config['parameters']['with_csrftoken']:
            #  add the csrf_token to the result
            résultat['csrf_token'] = generate_csrf()

        .

        #  we send the HTTP response
        return response, status_code

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

        #  result of the method
        return erreur, résultat, type_response1
  • 第 39–44 行:对操作进行首次检查。我们稍后会再讨论这一点。静态方法 [MainController.check_action] 返回一个包含三个元素的元组:
    • [error]:若检测到错误则为 True,否则为 False;
    • [result]:若 (error == True) 则为错误结果,否则为 None;
    • [response_type1]:会话的类型(json、xml、html 或 None),取自会话对象;
  • 第 39 行:若操作为 ASV 操作 [display-error-list-view](该操作用于显示错误列表)则不进行任何检查。事实上,若在此操作过程中发现错误,系统将重定向回 [/display-error-list-view] 操作,从而陷入无限重定向循环;
  • 静态方法 [check_action] 如下:
    @staticmethod
    def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
        #  retrieve the current action
        action = params[1]

        #  no errors and no results to start with
        erreur = False
        résultat = None

        #  session type must be known before certain actions ADS
        type_response1 = session.get('typeResponse')
        if type_response1 is None and action != "init-session":
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True

        #  some ADS actions require authentication
        user = session.get('user')
        if user is None and action not in ["init-session",
                                           "authentifier-utilisateur",
                                           "afficher-vue-authentification"]:
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True

        #  from a view, only certain actions are possible
        if not erreur and action != "init-session":
            #  for the moment no action possible
            actions_possibles = None
            #  retrieve the model of the future session view, if it exists
            modèle = session.get('modèle')
            #  if a model has been found, its possible actions are retrieved
            if modèle:
                actions_possibles = modèle.get('actions_possibles')
            #  if you have a list of possible actions, check that the current action is one of them
            if actions_possibles and action not in actions_possibles:
                #  we note the error
                résultat = {"action": action, "état": 151,
                            "réponse": [f"action [{action}] incorrecte dans l'environnement actuel"]}
                erreur = True


        #  mistake?
        if not erreur and config['parameters']['with_csrftoken']:
            #  check the validity of the csrf token
            #  the csrf_token is the last element of the path
            csrf_token = params.pop()
            try:
                #  an exception will be thrown if csrf_token is invalid
                validate_csrf(csrf_token)
            except ValidationError as exception:
                #  csrf token invalid
                résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
                #  we note the error
                erreur = True

        #  result of the method
        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] 类更新如下:

#  dependencies

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):
        #  the HTML response depends on the status code returned by the controller
        état = résultat["état"]

        #  find out if the state was produced by a ASV action
        #  in which case a
        asv_configs = config['mvc']["asv"]
        trouvé = False
        i = 0
        #  browse the list of views
        nb_views = len(asv_configs)
        while not trouvé and i < nb_views:
            #  view n° i
            asv_config = asv_configs[i]
            #  states associated with view n° i
            états = asv_config["états"]
            #  is the state you're looking for in the states associated with view n° i?
            if état in états:
                trouvé = True
            else:
                #  next view
                i += 1

        #  found?
        if trouvé:
            #  this is a ASV action - you need to display a view whose model is already in session
            #  generate the HTML response code
            html = render_template(asv_config["view_name"], modèle=session['modèle'])
            #  build the HTTP response
            response = make_response(html)
            response.headers['Content-Type'] = 'text/html; charset=utf-8'
            #  we return the result
            return response, status_code

        #  not found - this is an action status code ADS
        #  this will be followed by a redirection
        redirected = False
        for ads in config['mvc']['ads']:
            #  conditions requiring redirection
            états = ads["états"]
            if état in états:
                #  redirection
                redirected = True
                break
        #  redirection dictionary for unexpected errors
        if not redirected:
            ads = config['mvc']['view_erreurs']

        #  is it a redirect to a ASD or ASV action?
        #  if there's a template, then it's a redirection to a ASV action
        #  then calculate the model of view V to be displayed by action ASV
        model_for_view = ads.get("model_for_view")
        if model_for_view:
            #  calculation of the next view model
            modèle = model_for_view.get_model_for_view(request, session, config, résultat)
            #  the model is sessioned for the following view
            session['modèle'] = modèle

            #  now it's time to generate the URL redirection, not forgetting the CSRF token if requested
        if config['parameters']['with_csrftoken']:
            csrf_token = f"/{generate_csrf()}"
        else:
            csrf_token = ""

        #  redirect response
        return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
  • 第 18–35 行:检查上次执行的操作返回的状态是否为 ASV 操作的状态;
  • 第 36–46 行:如果是,则使用会话中[‘model’] 键关联的模型,显示与该 ASV 操作关联的 V 视图;
  • 第 48–60 行:到达此处时,我们已知最后执行的操作返回的状态码属于 ADS 操作。随后将触发重定向。我们会在配置文件中查找其定义;
  • 第 62 行:到达此处时,我们已获取待执行的重定向配置。有两种情况:
    • 重定向至另一个 ADS 操作。在此情况下,无需计算视图模型;
    • 重定向至 ASV 操作。此时必须计算视图模型(第 67–68 行)。随后将该模型添加到会话中(第 70 行);
  • 第 72–76 行:计算重定向 URL;
  • 第 78–79 行:将重定向响应发送给客户端;

35.3. 测试

使用浏览器执行以下测试:

  • 如常使用应用程序。验证浏览器显示的唯一 URL 是否为 ASV URL [/display-view-view_name]
  • 刷新页面(F5),并验证是否再次显示同一页面。不存在任何副作用;

此外,请使用客户端 [impots/http-clients/09]。由于所做的更改仅影响 HTML 会话,因此客户端 [main, main2, main3, Test1HttpClientDaoWithSession, Test2HttpClientDaoWithSession] 应能继续正常运行。

现在我们来看一个操作无法执行的情况。显示如下视图:

Image

请不要输入 [1],而是输入 URL [/delete-simulation/1]。操作 [/delete-simulation] 不在视图提供的操作(即操作 1–4)之列,因此将被拒绝。服务器响应如下:

Image