35. Application exercise: version 15
35.1. Introduction
This version aims to resolve issues related to refreshing the application’s pages in the browser (F5). Let’s take an example. The user has just deleted the simulation with id=3:

After deletion, the URL in the browser is [/delete-simulation/3/…]. If the user refreshes the page (F5), the URL [1] is replayed. We therefore request the deletion of the simulation with id=3 again. The result is as follows:

Here is another example. The user has just calculated a tax:
- in [1], the URL [/calculate-tax] that was just queried with a POST request;
If the user refreshes the page (F5), they receive a warning message:

The page refresh operation re-executes the last request made by the browser, in this case a [POST /calculate-tax]. When asked to re-execute a POST, browsers display a warning similar to the one above. This warns that it is re-executing an action that has already been performed. Suppose this POST completed a purchase; it would be unfortunate to repeat it.
Additionally, we will limit the user’s ability to type URLs into their browser. Let’s take one of the previous views as an example:

The links provided by this page are:
- [List of simulations] associated with the URL [/lister-simulations];
- [End of session] associated with the URL [/end-session];
- [Submit] associated with the URL (not shown above) [/calculate-tax];
When the tax calculation view is displayed, we will only accept the actions [/list-simulations, /end-session, /calculate-tax]. If the user enters another action in their browser, an error will be thrown. We will perform this type of validation for all four views of the application.
We propose to solve the page refresh issue as follows:
- we will distinguish between two types of actions:
- ADS (Action Do Something) actions that modify the application’s state. ADS actions generally have parameters in the URL or the request body;
- ASV (Action Show View) actions that display a view without changing the application’s state. There will be as many ASV actions as there are views V. ASV actions have no parameters;
- Until now, ADS actions were executed and then completed by displaying a view V after preparing its model M. From now on, they will place the view’s model M into the session and instruct the browser to redirect to the ASV action responsible for displaying the view V;
- V views will only be displayed following an ASV action. They will retrieve their model from the session;
The advantage of this method is that the browser will display the ASV URL in its address bar. Refreshing the page will then re-execute the ASV action. This action does not modify the application’s state and uses a session model. Therefore, the same page will be re-displayed without any side effects. Finally, due to the redirects, the user will only see ASV action URLs in their browser and will feel as though they are navigating from page to page;
35.2. Implementation

The [impots/http-servers/10] directory is initially created by copying the [impots/http-servers/09] directory. It is then modified.
35.2.1. The new routes

In both the [routes_with_csrftoken] and [routes_without_csrftoken] files, we need to create the four routes for the four ASV actions that display the four views. The other routes remain unchanged.
In [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()
|
Lines 1–23: We have created four routes for four ASV actions:
- [/display-authentication-view], line 8, displays the authentication view;
- [/display-tax-calculation-view], line 2, displays the tax calculation view;
- [/display-simulation-list-view], line 14, displays the simulation view;
- [/display-error-list-view], line 20, displays the unexpected errors view;
We do the same in the [routes_with_csrftoken] file for routes without a token:
| # 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. The new controllers

The [AuthenticationViewController] executes the ASV action [/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
|
We mentioned that ASV actions have no parameters and do not modify the application’s state. We simply display the desired view by setting a status code on line 14.
The [AfficherVueCalculImpotController] controller executes the ASV action [/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
|
The [AfficherVueListeSimulationsController] controller executes the ASV action [/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
|
The [AfficherVueListeErreursController] controller executes the ASV action [/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
|
Let’s summarize the status codes:
- code 1100 should display the authentication view;
- code 1400 should display the tax calculation view;
- code 1200 should display the simulation view;
- code 1300 should display the unexpected errors view;
35.2.3. The new MVC configuration
Because it had become too large, the MVC configuration has been split into four files:
- [controllers]: the list of C controllers for the MVC application;
- [ads_actions]: lists the ADS (Action Do Something) actions;
- [asv_actions]: lists the ASV (Action Show View) actions;
- [responses]: lists the application’s HTTP response classes;
Let’s start with the simplest one, the HTTP response file [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,
}
|
No surprises here.
The [controllers] file is also unsurprising. We simply added the new controllers for the ASV actions.
| 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,
}
|
The [asv_actions] configuration for ASV actions is as follows:
| 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,
}
|
- The [asv_actions] file contains the four new actions, whose functionality is summarized below:
- they have no parameters;
- they display a specific view whose template is in session;
- The [asv] list in lines 6–35 associates a view with each ASV action;
The [ads_actions] file contains the ADS actions:
| 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,
}
|
- lines 11–51: the list of ADS (Action Do Something) actions. All actions from previous versions are included. However, their behavior has changed:
- they do not display a view V. They only prepare the model M for this view V;
- they request the display of view V via a redirect to the ASV action associated with view V;
- not all ADS actions lead to a redirection to an ASV action: lines 12–18, the ADS action [/fin-session] leads to a redirection to the ADS action [/init-session/html]. To distinguish between ADS -> ADS and ADS -> ASV redirects, we can use the [model_for_view] template. This does not exist for ADS -> ADS redirects;
The [main/config] file, which contains all configurations, changes as follows:
| 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. The new models

The models will generate new information. Let’s take, for example, the authentication view model:
| 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
|
- The new feature is on line 17: each model will generate a list of possible actions when the view V, for which it is the model M, is displayed. To see these actions, we need to return to the view V. In the case of the authentication view:

- we see that the authentication view offers only one action, that of the [Validate] button. This action is [/authenticate-user];
We wrote:
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
In the view above, we see that if the user refreshes the view, action [1] [/display-authentication-view] will be replayed. It must therefore be authorized. We could have chosen not to authorize it, in which case the user would have encountered an error every time they reloaded the page. We determined that this was not desirable.
The possible actions are placed in the view model. We know that this model will be stored in the session.
We do this for each of the four views. The possible actions are then as follows:
Authentication view
modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
Tax calculation view
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-calcul-impot", "calculer-impot", "lister-simulations","fin-session"]
Simulation list view
# 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
Error list view
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-liste-erreurs", "afficher-calcul-impot", "lister-simulations", "fin-session"]
35.2.5. The new main controller

The [MainController] undergoes a few changes:
| # 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
|
- lines 39–44: the first checks are performed on the action. We’ll come back to this. The static method [MainController.check_action] returns a tuple of three elements:
- [error]: True if an error was detected, False otherwise;
- [result]: an error result if (error == True), None otherwise;
- [response_type1]: the type (json, xml, html, None) of the session, as found in the session;
- line 39: no check is performed if the action is the ASV action [display-error-list-view], which displays the list of errors. Indeed, if an error were found during this action, we would be redirected back to the [/display-error-list-view] action and enter an infinite loop of redirects;
- The static method [check_action] is as follows:
| @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
|
- line 2: the [check_action] method performs several checks on the validity of the current action;
- lines 6–26, 45–57: previous versions already performed these checks;
- lines 28–42: a new check is added. We verify whether the current action is possible given the application’s current state. If the current action is not possible, we generate a status code 151 (line 40), which ensures that the current action will be redirected to the unexpected errors view;
35.2.6. The new HTML response

The current changes apply only to HTML sessions. JSON or XML sessions are not affected. The [HtmlResponse] class is updated as follows:
| # 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
|
- lines 18–35: check if the status returned by the last executed action is that of an ASV action;
- lines 36–46: if so, the V view associated with the ASV action is displayed using the model found in the session associated with the [‘model’] key;
- lines 48-60: when we reach this point, we know that the status produced by the last executed action is that of an ADS action. It will then trigger a redirect. We search the configuration file for its definition;
- line 62: when we reach this point, we have the configuration for the redirect to be performed. There are two cases:
- it is a redirection to another ADS action. In this case, there is no view model to calculate;
- it is a redirect to an ASV action. In this case, a view model must be calculated (lines 67–68). This model is then added to the session (line 70);
- lines 72–76: the redirect URL is calculated;
- lines 78–79: the redirect response is sent to the client;
35.3. Tests
Perform the following tests using a browser:
- Use the application as usual. Verify that the only URLs displayed by the browser are ASV URLs [/display-view-view_name];
- refresh the pages (F5) and verify that the same page is displayed again. There are no side effects;
Additionally, use the client [impots/http-clients/09]. Since the changes made only affect HTML sessions, the clients [main, main2, main3, Test1HttpClientDaoWithSession, Test2HttpClientDaoWithSession] should continue to function.
Now let’s look at a case where an action is impossible. The following view is displayed:

Instead of [1], enter the URL [/delete-simulation/1]. The action [/delete-simulation] is not among the actions offered by the view, which are actions 1–4. It will therefore be rejected. The server response is as follows:
