Skip to content

36. Application Exercise: Version 16

Image

36.1. Introduction

The URLs in our application currently have the form [/action/param1/param2/…]. We would like to be able to prefix these URLs. For example, with the prefix [/do], we would have URLs of the form [/do/action/param1/param2/…].

36.2. The new route configuration

Image

  • In [2], the route calculation will be modified;
  • in [1], the [config] file is modified to reflect this change;

The [config] configuration becomes the following:


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 (Action Show View) actions
    import asv_actions
    config['mvc'].update(asv_actions.configure(config))

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

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

    # configure routes
    import routes
    routes.configure(config)

    # return the configuration
    return config
  • lines 37–39: the [routes] module (line 38) handles configuring the routes (line 39);

The routes file without a CSRF token [configs/routes_without_csrftoken] evolves as follows:


# dependencies

from flask import redirect, request, session, url_for
from flask_api import status

# application configuration
config = {}

# the front controller
def front_controller() -> tuple:
    # forward the request to the main controller
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

# Application root
def index() -> tuple:
    # Redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
def init_session(type_response: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# authenticate-user
def authenticate_user() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# calculate-tax
def calculate_tax() -> tuple:
    # execute the controller associated with the action
    return front_controller()


The file has been stripped of its routes. Only the functions associated with them remain.

The routes file with the CSRF token [configs/routes_with_csrftoken] suffers the same fate:


# dependencies

from flask import redirect, request, session, url_for
from flask_api import status
from flask_wtf.csrf import generate_csrf

# configuration
config = {}

# the front controller
def front_controller() -> tuple:
    # forward the request to the main controller
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

# application root
def index() -> tuple:
    # Redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)

# init-session
def init_session(type_response: str, csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# authenticate-user
def authenticate_user(csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()



# init-session-without-csrftoken for JSON and XML clients
def init_session_without_csrftoken(type_response: str) -> tuple:
    # Redirect to /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()),
                    status.HTTP_302_FOUND)

Routes are calculated by the following [configs/routes] module:


from flask import Flask

def configure(config: dict):
    # configuring routes

    # Flask application
    app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
    config['app'] = app

    # import web application routes
    if config['parameters']['with_csrftoken']:
        import routes_with_csrftoken as routes
    else:
        import routes_without_csrftoken as routes

    # inject the configuration into the routes
    routes.config = config

    # the application's URL prefix
    prefix_url = config["parameters"]["prefix_url"]

    # CSRF token
    with_csrftoken = config["parameters"]['with_csrftoken']
    if with_csrftoken:
        csrftoken_param = f"/<string:csrf_token>"
    else:
        csrftoken_param = ""

    # Flask application routes
    # Application root
    app.add_url_rule(f'{prefix_url}/', methods=['GET'],
                     view_func=routes.index)

    # init-session
    app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
                     view_func=routes.init_session)

    # init-session-without-csrftoken
    if with_csrftoken:
        app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken/<string:type_response>',
                         methods=['GET'],
                         view_func=routes.init_session_without_csrftoken)

    # authenticate-user
    app.add_url_rule(f'{prefix_url}/authenticate-user{csrftoken_param}', methods=['POST'],
                     view_func=routes.authenticate_user)

    # calculate-tax
    app.add_url_rule(f'{prefix_url}/calculate-tax{csrftoken_param}', methods=['POST'],
                     view_func=routes.calculate_tax)

    # calculate-tax-in-batches
    app.add_url_rule(f'{prefix_url}/calculate-taxes{csrftoken_param}', methods=['POST'],
                     view_func=routes.calculate_taxes)

    # list-simulations
    app.add_url_rule(f'{prefix_url}/list-simulations{csrftoken_param}', methods=['GET'],
                     view_func=routes.list_simulations)

    # delete-simulation
    app.add_url_rule(f'{prefix_url}/delete-simulation/<int:number>{csrftoken_param}', methods=['GET'],
                     view_func=routes.delete-simulation)

    # end-session
    app.add_url_rule(f'{prefix_url}/end-session{csrftoken_param}', methods=['GET'],
                     view_func=routes.end_session)

    # display-tax-calculation
    app.add_url_rule(f'{prefix_url}/display-tax-calculation{csrftoken_param}', methods=['GET'],
                     view_func=routes.display_tax_calculation)

    # get-admindata
    app.add_url_rule(f'{prefix_url}/get-admindata{csrftoken_param}', methods=['GET'],
                     view_func=routes.get_admindata)

    # display-tax-calculation-view
    app.add_url_rule(f'{prefix_url}/display-tax-calculation-view{csrftoken_param}', methods=['GET'],
                     view_func=routes.display_tax_calculation_view)

    # display-authentication-view
    app.add_url_rule(f'{prefix_url}/display-authentication-view{csrftoken_param}', methods=['GET'],
                     view_func=routes.display_authentication_view)

    # display-simulation-list-view
    app.add_url_rule(f'{prefix_url}/display-simulation-list-view{csrftoken_param}', methods=['GET'],
                     view_func=routes.display_simulation_list_view)

    # display-error-list-view
    app.add_url_rule(f'{prefix_url}/display-error-list-view{csrftoken_param}', methods=['GET'],
                     view_func=routes.display_error_list_view)

    # special case
    if with_csrftoken:
        # init-session-without-csrftoken for JSON and XML clients
        app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken', methods=['GET'],
                         view_func=routes.init_session_without_csrftoken)
  • lines 6–8: the Flask application is created and added to the configuration;
  • lines 10–14: The route file appropriate for the situation is imported. Note that the imported file actually contains no routes; it contains only the functions associated with them;
  • line 17: the functions associated with the routes need to know the application’s configuration;
  • line 20: We specify the URL prefix. This can be empty;
  • lines 22–27: Routes with a CSRF token have an additional parameter compared to those without one. To handle this difference, we use the [csrftoken_param] variable:
    • it contains an empty string if there is no CSRF token in the routes;
    • it contains the string [/<string:csrf_token>] if there is a CSRF token;
  • lines 29–96: Each route in the application is associated with a function from the routes file imported in lines 10–14;

36.3. The new controllers

Image

In the previous version, all controllers obtained the currently processed action as follows:


    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        params = request.path.split('/')
        action = params[1]

This code no longer works if there is a prefix, for example [/do/lister-simulations]. In this case, the action on line 3 would be [do] and would therefore be incorrect.

We modify this code as follows:


    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        prefix_url = config["parameters"]["prefix_url"]
        params = request.path[len(prefix_url):].split('/')
        action = params[1]

We do this in all controllers.

36.4. The new models

Image

In the previous version, each model class generated a model as follows:


def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, result: dict) -> dict:
        # encapsulate the page data in the model
        model = {}
        # application state
        state = result["state"]
        # the model depends on the state
        

        # CSRF token
        model['csrf_token'] = super().get_csrftoken(config)

        # possible actions from the view
        model['possible_actions'] = []

        # return the model
        return model

Now the model will have an additional key, the URL prefix:


def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, result: dict) -> dict:
        # encapsulate the page data in the model
        model = {}
        # Application status
        state = result["state"]
        # the model depends on the state
        

        # CSRF token
        model['csrf_token'] = super().get_csrftoken(config)

        # possible actions from the view
        model['possible_actions'] = []
    
        # URL prefix
        model["prefix_url"] = config["parameters"]["prefix_url"]

        # return the model
        return model

36.5. The new fragments

Image

All fragments containing URLs must be modified.

The [v-authentication] fragment


<!-- HTML form - we submit its values with the [authenticate-user] action -->
<form method="post" action="{{model.prefix_url}}/authenticate-user{{model.csrf_token}}">

    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Please log in</h4>
    </div>


</form>

The [v-calcul-impot] fragment


<!-- HTML form submitted -->
<form method="post" action="{{template.prefix_url}}/calculate-tax{{template.csrf_token}}">
    <!-- 12-column message on a blue background -->
    

</form>

The [v-liste-simulations] fragment




{% if model.simulations is defined and model.simulations.length!=0 %}


<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    
    <tr>
        <th scope="row">{{simulation.id}}</th>
        <td>{{simulation.married}}</td>
        <td>{{simulation.children}}</td>
        <td>{{simulation.salary}}</td>
        <td>{{simulation.tax}}</td>
        <td>{{simulation.surcharge}}</td>
        <td>{{simulation.discount}}</td>
        <td>{{simulation.reduction}}</td>
        <td>{{simulation.rate}}</td>
        <td><a href="{{modèle.prefix_url}}/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Delete</a></td>
    </tr>
    {% endfor %}
    </tr>
    </tbody>
</table>
{% endif %}

The [v-menu] fragment


<!-- Bootstrap menu -->
<nav class="nav flex-column">
    <!-- Displaying a list of HTML links -->
    {% for optionMenu in model.optionsMenu %}
    
    {% endfor %}
</nav>

36.6. The new HTML response

Image

In the previous version, the HTML response code ended with a redirect:


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"]

        

        # 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

This code now becomes the following:


def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            result: dict) -> (Response, int):
        

        # now we need to generate the redirect URL, making sure to include the CSRF token if required
        if config['parameters']['with_csrftoken']:
            csrf_token = f"/{generate_csrf()}"
        else:
            csrf_token = ""

        # Redirect response
        return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND

36.7. Tests

Image

Let's add the following prefix to the [parameters] configuration file:



        # csrf token
        "with_csrftoken": True,
        # supported databases MySQL (mysql), PostgreSQL (pgres)
        "databases": ["mysql", "pgres"],
        # application URL prefix
        # set the string to empty if no prefix is desired, or use /prefix otherwise
        "prefix_url": "/do",

Let’s launch the application and then request the URL [http://localhost:5000/do]; the response is as follows:

Image

  • in [1], the URL prefix;
  • in [2], the CSRF token;

The application can also be tested using the console tests from [http-clients/09]:

Image

In the configuration files [1] and [2], the URL prefix must be included in the server URL:


        "server": {
            # "urlServer": "http://127.0.0.1:5000",
            "urlServer": "http://127.0.0.1:5000/do",
            "user": {
                "login": "admin",
                "password": "admin"
            },
            "url_services": {
                
            }
  • Line 3: The server URL now includes the URL prefix;

With this change, all console tests should work.