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]:
# display-tax-calculation-view
@app.route('/display-tax-calculation-view/<string:csrf_token>', methods=['GET'])
def display_tax_calculation_view(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-authentication-view
@app.route('/display-authentication-view/<string:csrf_token>', methods=['GET'])
def display_authentication_view(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-simulation-list-view
@app.route('/display-simulation-list-view/<string:csrf_token>', methods=['GET'])
def display_simulation_list_view(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-error-list-view
@app.route('/display-error-list-view/<string:csrf_token>', methods=['GET'])
def display_error_list_view(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:
# ASV routes -------------------------
# display-tax-calculation-view
@app.route('/display-tax-calculation-view', methods=['GET'])
def display_tax_calculation_view() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-authentication-view
@app.route('/display-authentication-view', methods=['GET'])
def display_authentication_view() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-simulation-list-view
@app.route('/display-simulation-list-view', methods=['GET'])
def display_simulation_list_view() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-error-list-view
@app.route('/display-error-list-view', methods=['GET'])
def display_error_list_view() -> 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 ShowAuthenticationViewController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# retrieve the path elements
params = request.path.split('/')
action = params[1]
# change view - just set a status code
return {"action": action, "status": 1100, "response": ""}, 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 DisplayTaxCalculationViewController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# retrieve the path elements
params = request.path.split('/')
action = params[1]
# change view - just set the status code
return {"action": action, "status": 1400, "response": ""}, 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 DisplaySimulationListViewController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# retrieve the path elements
params = request.path.split('/')
action = params[1]
# change view - just set a status code
return {"action": action, "status": 1200, "response": ""}, 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 DisplayErrorListViewController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# retrieve the path elements
params = request.path.split('/')
action = params[1]
# change view - just set a status code
return {"action": action, "status": 1300, "response": ""}, 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:
# MVC application configuration
# HTTP responses
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 the HTTP response dictionary
return {
# HTTP responses
"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:
# MVC application configuration
# the main controller
from MainController import MainController
# ADS action controllers
from UserAuthController import UserAuthController
from CalculateTaxController import CalculateTaxController
from CalculateTaxesController import CalculateTaxesController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
from InitSessionController import InitSessionController
from ListSimulationsController import ListSimulationsController
from DeleteSimulationController import DeleteSimulationController
from DisplayTaxCalculationController import DisplayTaxCalculationController
# ASV action controllers
from DisplayTaxCalculationViewController import DisplayTaxCalculationViewController
from DisplayAuthenticationViewController import DisplayAuthenticationViewController
from DisplayErrorListViewController import DisplayErrorListViewController
from DisplaySimulationListViewController import DisplaySimulationListViewController
# Authorized actions and their controllers
controllers = {
# initialization of a calculation session
"init-session": InitSessionController(),
# user authentication
"authenticate-user": AuthenticateUserController(),
# link to tax calculation view
"display-tax-calculation": DisplayTaxCalculationController(),
# calculate tax in individual mode
"calculate-tax": CalculateTaxController(),
# tax calculation in batch mode
"calculate-taxes": CalculateTaxesController(),
# list of simulations
"list-simulations": ListSimulationsController(),
# delete a simulation
"delete-simulation": DeleteSimulationController(),
# end of calculation session
"end-session": EndSessionController(),
# retrieve data from the tax administration
"get-admindata": GetAdminDataController(),
# main controller
"main-controller": MainController(),
# display the authentication view
"display-authentication-view": DisplayAuthenticationViewController(),
# display the tax calculation view
"display-tax-calculation-view": DisplayTaxCalculationViewController(),
# display the simulation view
"display-simulation-list-view": DisplaySimulationListViewController(),
# Display the errors view
"display-error-list-view": DisplayErrorListViewController()
}
# Return the controller configuration
return {
# controllers
"controllers": controllers,
}
The [asv_actions] configuration for ASV actions is as follows:
def configure(config: dict) -> dict:
# MVC application configuration
# HTML views and their models depend on the state returned by the controller
# ASV actions (Action Show view)
asv = [
{
# Authentication view
"states": [
1100, # /display-authentication-view
],
"view_name": "views/authentication-view.html",
},
{
# tax calculation view
"statements": [
1400, # /display-tax-calculation-view
],
"view_name": "views/tax-calculation-view.html",
},
{
# simulation list view
"statements": [
1200, # /display-simulation-list-view
],
"view_name": "views/simulation-list-view.html",
},
{
# error list view
"statuses": [
1300, # /display-error-list-view
],
"view_name": "views/error-list.html",
},
]
# return the ASV configuration
return {
# views and templates
"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:
# MVC application configuration
# view models
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForTaxCalculationView import ModelForTaxCalculationView
from ModelForErrorView import ModelForErrorView
from ModelForSimulationListView import ModelForSimulationListView
# ADS (Action Do Something) actions
ads = [
{
"statuses": [
400, # /successful session end
],
# redirect to ADS action
"to": "/init-session/html",
},
{
"status": [
700, # /init-session - success
201, # /authenticate-user failure
],
# redirect to ASV action
"to": "/display-authentication-view",
# template for the following view
"model_for_view": ModelForAuthentificationView()
},
{
"statuses": [
200, # /authenticate-user success
300, # /calculate-tax success
301, # /calculate-tax failure
800, # /display-tax-calculation link
],
# redirect to ASV action
"to": "/display-tax-calculation-view",
# template for the following view
"model_for_view": ModelForCalculImpotView()
},
{
"status": [
500, # /list-successful-simulations
600, # /delete-simulation success
],
# redirect to ASV action
"to": "/display-simulation-list-view",
# model for the following view
"model_for_view": ModelForListeSimulationsView()
},
]
# unexpected errors view
view_errors = {
# Redirect to ASV action
"to": "/display-error-list-view",
# model for the following view
"model_for_view": ModelForErrorsView()
}
# Return the MVC configuration
return {
# ADS actions
"ads": ads,
# view for unexpected errors
"error_view": error_view,
}
- 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 settings
import parameters
config['parameters'] = parameters.configure(config)
# database configuration
import database
config["database"] = database.configure(config)
# instantiate the application layers
import layers
config['layers'] = layers.configure(config)
# MVC configuration for the [web] layer
config['mvc'] = {}
# [web] layer controller configuration
import controllers
config['mvc'].update(controllers.configure(config))
# ASV actions (Action Show View)
import asv_actions
config['mvc'].update(asv_actions.configure(config))
# ADS (Action Do Something) actions
import ads_actions
config['mvc'].update(ads_actions.configure(config))
# HTTP response configuration
import responses
config['mvc'].update(responses.configure(config))
# 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 ModelForAuthenticationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, result: dict) -> dict:
# encapsulate the page data in a model
model = {}
…
# CSRF token
model['csrf_token'] = super().get_csrftoken(config)
# Possible actions from the view
model['possible_actions'] = ["display-authentication-view", "authenticate-user"]
# return the model
return model
- 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:
# possible actions from the view
model['possible_actions'] = ["display-authentication-view", "authenticate-user"]
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
model['possible_actions'] = ["display-authentication-view", "authenticate-user"]
Tax calculation view
# Available actions from the view
model['possible_actions'] = ["display-tax-calculation-view", "calculate-tax", "list-simulations", "end-session"]
Simulation list view
# Possible actions from the view
possible_actions = ["display-simulation-list-view", "display-tax-calculation", "end-session"]
if len(model['simulations']) != 0:
possible_actions.append("delete-simulation")
model['possible_actions'] = possible_actions
Error list view
# possible actions from the view
model['possible_actions'] = ["display-error-list-view", "display-tax-calculation", "list-simulations", "end-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 email to the application administrator
config_mail = config['parameters']['adminMail']
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# Main controller of the application
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# Processing the request
type_response1 = None
logger = None
try:
# retrieve the path elements
params = request.path.split('/')
# the action is the first element
action = params[1]
# logger
logger = Logger(config['parameters']['logsFilename'])
…
# The /display-error-list-view action is special
# no checks are performed, otherwise there is a risk of entering an infinite loop of redirects
error = False
if action != "display-error-list-view":
# if error, [result] is the result to send to the client
(error, result, response_type1) = MainController.check_action(params, session, config)
# if no error - the action is executed
if not error:
# execute the controller associated with the action
controller = config['mvc']['controllers'][action]
result, status_code = controller.execute(request, session, config)
except BaseException as exception:
# (unexpected) exceptions
result = {"action": action, "status": 131, "response": [f"{exception}"]}
error = True
finally:
pass
# runtime error
if error:
# invalid request
status_code = status.HTTP_400_BAD_REQUEST
if config['parameters']['with_csrftoken']:
# add the csrf_token to the result
result['csrf_token'] = generate_csrf()
….
# 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 error, result, response_type1
- 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 error and no result initially
error = False
result = None
# The session type must be known before certain ADS actions
response_type1 = session.get('response_type')
if type_response1 is None and action != "init-session":
# log the error
result = {"action": action, "status": 101,
"response": ["No session in progress. Start with action [init-session]"]}
error = True
# For certain ADS actions, you must be authenticated
user = session.get('user')
if user is None and action is not in ["init-session",
"authenticate-user",
"display-authentication-view"]:
# Log the error
result = {"action": action, "status": 101,
"response": [f"action [{action}] requested by unauthenticated user"]}
error = True
# From a view, only certain actions are possible
if not error and action != "init-session":
# no actions are possible at this time
possible_actions = None
# retrieve the model for the future view from the session if it exists
model = session.get('model')
# if a model was found, retrieve its possible actions
if model:
possible_actions = model.get('possible_actions')
# if we have a list of possible actions, check if the current action is among them
if possible_actions and action not in possible_actions:
# log the error
result = {"action": action, "status": 151,
"response": [f"Incorrect action [{action}] in the current environment"]}
error = True
# error?
if not error 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 raised if the csrf_token is invalid
validate_csrf(csrf_token)
except ValidationError as exception:
# invalid CSRF token
result = {"action": action, "status": 121, "response": [f"{exception}"]}
# Log the error
error = True
# result of the method
return error, result, response_type1
- 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,
result: dict) -> (Response, int):
# The HTML response depends on the status code returned by the controller
status = result["status"]
# check if the status was generated by an ASV action
# in which case, a view must be displayed
asv_configs = config['mvc']["asv"]
found = False
i = 0
# iterate through the list of views
nb_views = len(asv_configs)
while not found and i < nb_views:
# view #i
asv_config = asv_configs[i]
# states associated with view #i
states = asv_config["states"]
# Is the state being searched for among the states associated with view #i
if state in states:
found = True
else:
# next view
i += 1
# found?
if found:
# this is an ASV action - we need to display a view whose model is already in session
# generate the HTML code for the response
html = render_template(asv_config["view_name"], template=session['template'])
# we construct the HTTP response
response = make_response(html)
response.headers['Content-Type'] = 'text/html; charset=utf-8'
# Return the result
return response, status_code
# Not found - this is an ADS action status code
# This will be followed by a redirect
redirected = False
for ads in config['mvc']['ads']:
# states requiring a redirect
states = ads["states"]
if status in statuses:
# there is a redirect
redirected = True
break
# Redirect dictionary for unexpected errors
if not redirected:
ads = config['mvc']['error_view']
# Is this a redirect to an ASD or ASV action?
# if there is a template, then it is a redirect to an ASV action
# we must then calculate the model for the V view that will be displayed by the ASV action
model_for_view = ads.get("model_for_view")
if model_for_view:
# Calculate the model for the next view
model = model_for_view.get_model_for_view(request, session, config, result)
# the model is set in the session for the next view
session['model'] = model
# now we need to generate the redirect URL, including the CSRF token if required
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:
