Skip to content

30. Toepassingsopdracht: versie 12

In dit hoofdstuk gaan we een webapplicatie schrijven volgens de MVC-architectuur (Model-View-Controller). De applicatie kan haar resultaten in drie formaten weergeven: jSON, XML, HTML. Er is een aanzienlijke toename in complexiteit tussen wat we nu gaan doen en wat eerder is gedaan. We zullen de meeste concepten die we tot nu toe hebben behandeld hergebruiken en alle stappen die naar de uiteindelijke applicatie leiden in detail beschrijven.

30.1. Architectuur MVC

We gaan het zogenaamde MVC-architectuurmodel (Model – View – Controller) als volgt implementeren:

De verwerking van een verzoek van een klant verloopt als volgt:

  • 1 - verzoek

De aangevraagde URL’s zullen de vorm http://machine:port/action/param1/param2/… hebben. De [Contrôleur principal] zal een configuratiebestand gebruiken om het verzoek naar de juiste controller te ‘routeren’. Hiervoor gebruikt hij het veld [action] van de URL. De rest van de URL en [param1/param2/…] bestaat uit optionele parameters die naar de actie worden doorgestuurd. De C van MVC is hier de tekenreeks [Contrôleur principal, Contrôleur / Action]. Als geen enkele controller de gevraagde actie kan verwerken, zal de webserver antwoorden dat de gevraagde URL niet is gevonden.

  • 2 - verwerking
  • De gekozen actie [2a] kan gebruikmaken van de parameters parami die de [Contrôleur principal] aan haar heeft doorgegeven. Deze kunnen afkomstig zijn uit twee bronnen:
      • het pad [/param1/param2/…] van de URL,
      • van parameters die in de hoofdtekst van het verzoek van de klant zijn opgenomen;
    • bij de verwerking van het verzoek van de gebruiker kan de actie de laag [métier] [2b] nodig hebben. Zodra het verzoek van de client is verwerkt, kan dit verschillende reacties oproepen. Een klassiek voorbeeld is:
      • een foutmelding als het verzoek niet correct kon worden verwerkt;
      • anders een bevestigingsreactie;
    • de [Contrôleur / Action] stuurt zijn antwoord [2c] terug naar de hoofdcontroller, samen met een statuscode. Deze statuscodes geven op unieke wijze de status van de applicatie weer. Dit is ofwel een succescode, ofwel een foutcode;
  • 3 - antwoord
    • afhankelijk van of de client een antwoord jSON, XML of HTML heeft aangevraagd, zal [Contrôleur principal] het juiste antwoordtype [3a] instantiëren en deze vragen het antwoord naar de klant te verzenden. De [Contrôleur principal] zal zowel het antwoord als de statuscode doorgeven die zijn geleverd door de [Contrôleur / Action] die is uitgevoerd;
    • als het gewenste antwoord van het type jSON of XML is, zal het geselecteerde antwoord het antwoord van [Contrôleur / Action] dat aan hem is doorgegeven, opmaken en naar [3c] verzenden. De client die dit antwoord kan verwerken, kan een Python-consolescript zijn of een JavaScript-script dat is ondergebracht in een pagina met de naam HTML;
    • als het gewenste antwoord van het type HTML is, zal het geselecteerde antwoord [3b] een van de weergaven HTML of [Vuei] selecteren aan de hand van de statuscode die eraan is meegegeven. Dit is de V van MVC. Aan elke statuscode is één weergave gekoppeld. Deze weergave V geeft het antwoord weer van de [Contrôleur / Action] die is uitgevoerd. Deze weergave verwerkt de gegevens van dit antwoord met behulp van HTML, CSS en JavaScript. Deze gegevens noemen we het model van de weergave. Dit is de M van MVC. De client is in de meeste gevallen een browser;

Laten we nu het verband tussen webarchitectuur MVC en gelaagde architectuur verduidelijken. Afhankelijk van de definitie die men aan het model geeft, zijn deze twee concepten al dan niet met elkaar verbonden. Laten we een webapplicatie MVC met één laag nemen:

Image

Hierboven bevatten de [Contrôleur / Action]-lagen elk een deel van de lagen [métier] en [dao]. In de laag [web] is er weliswaar sprake van een MVC-architectuur, maar de applicatie als geheel heeft geen gelaagde architectuur. Hier is er slechts één laag, de weblaag, die alles doet.

Laten we nu eens kijken naar een meerlaagse webarchitectuur:

Image

De laag [web] kan worden geïmplementeerd zonder het model MVC te volgen. Er is dan wel sprake van een meerlaagse architectuur, maar de weblaag implementeert het model MVC niet.

In de .NET-omgeving kan de hierboven genoemde laag [web]bovenstaande laag worden geïmplementeerd met ASP.NET en MVC, waardoor we een gelaagde architectuur krijgen met een [web]-laag van het type MVC. Zodra dit is gebeurd, kan deze laag ASP.NET MVC worden vervangen door een klassieke laag ASP.NET (WebForms), terwijl de rest (bedrijfslogica, DAO, driver) ongewijzigd. We hebben dan een gelaagde architectuur met een laag [web] die niet langer van het type MVC is.

In MVC hebben we aangegeven dat het model M dat van de weergave V is, c.a.d. De verzameling gegevens die door de weergave V wordt weergegeven. Er wordt een andere definitie van het model M van MVC gegeven:

Image

Veel auteurs zijn van mening dat wat zich rechts van de laag [web] bevindt, het model M van MVC vormt. Om dubbelzinnigheden te voorkomen, kan men spreken van:

  • het domeinmodel wanneer men alles bedoelt wat zich rechts van de laag [web] bevindt;
  • het model van de weergave wanneer we verwijzen naar de gegevens die door een weergave V worden getoond;

Wanneer we hierna over het model spreken, bedoelen we altijd het weergavemodel.

30.2. Architectuur van de client/server-applicatie

De webapplicatie zal de volgende architectuur hebben:

  • in [1] zal de webserver twee soorten clients hebben:
    • in [2], een consoleclient die jSON en XML met de server uitwisselt;
    • in [3] een browser die HTML van de server ontvangt en weergeeft;
  • de webserver [1] behoudt de lagen [métier] en [dao] uit eerdere versies;
  • de webclient [2] zal worden aangepast om rekening te houden met de nieuwe URL-services van de webapplicatie;
  • de applicatie HTML die door de browser wordt weergegeven, moet volledig opnieuw worden geschreven;

We gaan de applicatie in verschillende fasen ontwikkelen:

  • we gaan de jSON-versie van de server ontwikkelen. We zullen de URL-services van de server een voor een testen met een Postman-client. Deze methode stelt ons in staat om het raamwerk van de webserver op te bouwen zonder ons zorgen te maken over de weergaven (=HTML) van de applicatie;
  • nadat we de jSON-server met Postman hebben getest, zullen we deze testen met een console-client;
  • daarna gaan we verder met de versie XML van de server. We hebben gezien dat de overgang van jSON naar XML eenvoudig was;
  • ten slotte gaan we over naar de serverversie HTML. We bouwen een architectuur MVC en definiëren de weer te geven weergaven. De applicatie HTML wordt zowel met de Postman-client als met een klassieke browser getest;

30.3. De structuur van de servercode

Image

  • in [1: de webserver in zijn geheel;
  • in [2]: we laten de mappen [static, templates, tests_views], die betrekking hebben op de serverversie HTML, voorlopig buiten beschouwing. Buiten deze map vinden we het hoofdscript [main] en de bijbehorende configuratie;
  • in [3] de controllers van de webserver. Dit zijn instanties van klassen;
 
  • in [4] wordt het antwoord HTTP van de server beheerd door klassen;
  • in [5] behouden we het logbestand van de vorige servers;

Wanneer we de versie HTML van de server bouwen, zullen er andere mappen bij komen:

 
  • in [6], de statische elementen van de applicatie HTML;
  • in [7], de sjablonen van de applicatie HTML, opgesplitst in weergaven [9] en weergavefragmenten [8];
  • in [9], de klassen die de modellen van de weergaven implementeren;

30.4. De URL-service van de applicatie

Om de webserver te bouwen, gaan we als volgt te werk:

  • op basis van de weergaven van de applicatie HTML gaan we de acties definiëren die de webserver moet uitvoeren. We gebruiken hier de daadwerkelijke weergaven, maar het zouden ook gewoon schetsen op papier kunnen zijn;
  • op basis van deze acties gaan we de URL-services van de applicatie HTML definiëren;
  • we gaan deze URL-services implementeren met een server die jSON levert. Zo kunnen we de basis van de webserver definiëren zonder ons zorgen te hoeven maken over de te leveren HTML-pagina's. We zullen deze URL-services testen met Postman;
  • vervolgens zullen we onze jSON-server testen met een consoleclient;
  • zodra de jSON-server is gevalideerd, gaan we verder met het schrijven van de HTML-applicatie;

Het eerste scherm is het authenticatiescherm:

Image

  • de actie die naar dit eerste scherm leidt, heet [init-session] [1];
  • Als je op de knop [Valider] klikt, wordt de actie [authentifier-utilisateur] geactiveerd met twee verzonden parameters [2-3];

Het scherm voor de belastingberekening:

Image

  • in [1], de actie [authentifier-utilisateur] die tot dit scherm heeft geleid;
  • in [2]: het klikken op de knop [Valider] activeert de uitvoering van de actie [calculer-impot] met drie doorgegeven parameters [2-5];
  • het klikken op de link [6] activeert de actie [lister-simulations] zonder parameters;
  • een klik op de link [7] activeert de actie [fin-session] zonder parameters;

Het derde scherm toont de simulaties die door de geauthenticeerde gebruiker zijn uitgevoerd:

Image

  • in [3], de actie [lister-simulations] die tot dit scherm heeft geleid;
  • in [2]: een klik op de link [Supprimer] activeert de actie [supprimer-simulation] met één parameter, namelijk het nummer van de simulatie die uit de lijst moet worden verwijderd;
  • een klik op de link [3] activeert de actie [afficher-calcul-impot] zonder parameters, waardoor de weergave van de belastingberekening opnieuw wordt getoond;
  • een klik op de link [4] activeert de actie [fin-session] zonder parameters;

Met deze eerste informatie kunnen we de verschillende URL-services van de server definiëren:

Actie
Rol
Uitvoeringscontext
/init-session
Wordt gebruikt om het type (json, xml, html) van de gewenste antwoorden vast te leggen
Verzoek GET
Kan op elk moment worden verzonden
/authentifier-utilisateur
Geeft een gebruiker al dan niet toestemming om in te loggen
Verzoek POST.
Het verzoek moet twee geposte parameters bevatten [user, password]
Kan alleen worden verzonden als het sessietype (json, xml, html) bekend is
/belasting-berekenen
Voert een simulatie van de belastingberekening uit
Verzoek POST.
Het verzoek moet drie POST-parameters bevatten: [marié, enfants, salaire]
Kan alleen worden verzonden als het sessietype (json, xml, html) bekend is en de gebruiker is geauthenticeerd
/simulaties-weergeven
Vraagt om de lijst met simulaties die sinds het begin van de sessie zijn uitgevoerd
Verzoek GET.
Kan alleen worden verzonden als het type van de sessie (json, xml, html) bekend is en de gebruiker is geauthenticeerd
/simulatie-verwijderen/nummer
Verwijdert een simulatie uit de lijst met simulaties
Verzoek GET.
Kan alleen worden verzonden als het type van de sessie (json, xml, html) bekend is en de gebruiker is geauthenticeerd
/belastingberekening-weergeven
Geeft de pagina HTML met de belastingberekening weer
Verzoek GET.
Kan alleen worden verzonden als het sessietype (json, xml, html) bekend is en de gebruiker is geauthenticeerd
/einde-sessie
Beëindigt de simulatiesessie.
Technisch gezien wordt de oude websessie verwijderd en wordt er een nieuwe sessie aangemaakt
Kan alleen worden verzonden als het type van de sessie (json, xml, html) bekend is en de gebruiker is geauthenticeerd

Deze verschillende URL-services zullen zowel voor de HTML-server als voor de jSON- of XML-servers worden gebruikt. Twee URL-instellingen worden uitsluitend voor deze laatste twee servers gebruikt: dit zijn de URL-instellingen uit de vorige versie van de client/webserver die we hier overnemen:

Actie
Rol
Uitvoeringscontext
/get-admindata
Levert de fiscale gegevens aan waarmee de belasting kan worden berekend
Verzoek GET.
Wordt alleen gebruikt als het sessietype json of xml is. De gebruiker moet geauthenticeerd zijn
/belasting-berekenen
Voert de belastingberekening uit voor een lijst met belastingplichtigen die is verzonden via jSON
Verzoek GET.
Wordt alleen gebruikt als het sessietype json of xml is. De gebruiker moet geauthenticeerd zijn

Alle controllers die aan deze acties zijn gekoppeld, gaan op dezelfde manier te werk:

  • ze controleren hun parameters. Deze zijn te vinden in het object:
    • [request.path] voor de parameters die aanwezig zijn in URL in de vorm [/action/param1/param2/…];
    • in het object [request.form] voor de parameters die in [x-www-form-urlencoded] in de hoofdtekst van het verzoek worden verzonden;
    • in het object [request.data] voor de parameters die in jSON in de hoofdtekst van het verzoek worden verzonden;
  • een controller lijkt op een functie of methode die de geldigheid van zijn parameters controleert. Voor de controller ligt het echter iets ingewikkelder:
    • de verwachte parameters kunnen ontbreken;
    • de parameters die door de controller worden opgehaald, zijn tekenreeksen. Als de verwachte parameter een getal is, moet de controller controleren of de tekenreeks van de parameter inderdaad die van een getal is;
    • zodra is gecontroleerd dat de verwachte parameters aanwezig en syntactisch correct zijn, moet worden gecontroleerd of ze geldig zijn in de huidige uitvoeringscontext. Deze context is aanwezig in de sessie. Het voorbeeld van authenticatie is een voorbeeld van een uitvoeringscontext. Bepaalde acties mogen pas worden verwerkt nadat de klant is geauthenticeerd. Meestal geeft een sleutel in de sessie aan of deze authenticatie heeft plaatsgevonden of niet;
    • zodra de voorgaande controles zijn uitgevoerd, kan de secundaire controller aan de slag. Dit controleren van de parameters is van groot belang. We kunnen niet accepteren dat een klant ons op elk willekeurig moment tijdens de levensduur van de applicatie zomaar alles stuurt. We moeten de volledige controle hebben over de levensduur ervan;
    • zodra zijn taak is voltooid, retourneert de secundaire controller een woordenboek met de sleutels [action, état, réponse] aan de primaire controller die hem heeft aangeroepen:
      • [action] is de actie die zojuist is uitgevoerd;
      • [état] is een driecijferig getal dat het resultaat van de verwerking van de actie aangeeft:
    • [x00] geeft aan dat de verwerking is geslaagd;
    • [x01] geeft aan dat de verwerking is mislukt;
  • [réponse] is het resultatenobject in de vorm {‘antwoord’:object}. De structuur van het object verschilt afhankelijk van de verwerkte actie;

We gaan nu de verschillende controllers doornemen, of, wat op hetzelfde neerkomt, de verschillende acties die deze controllers verwerken en die het ritme van de webapplicatie bepalen.

30.5. Serverconfiguratie

Image

De configuratie van de database [config_database] en die van de serverlagen [config_layers] zijn identiek aan die van eerdere versies. In het bestand [config] verschijnt nieuwe informatie:


def configure(config: dict) -> dict:
    import os

    # stap 1 ------

    # map van dit bestand
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # hoofdmap
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # afhankelijkheden
    absolute_dependencies = [
        # projectmappen
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Constanten, segmenten
        f"{root_dir}/impots/v05/entities",
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # scripts [config_database, config_layers]
        script_dir,
        # controllers
        f"{script_dir}/../controllers",
        # antwoorden HTTP
        f"{script_dir}/../responses",
        # weergavemodellen
        f"{script_dir}/../models_for_views",
    ]

    # het syspath instellen
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # afhankelijkheden van de webserver

    # de controllers
    from AfficherCalculImpotController import AfficherCalculImpotController
    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 MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    # de antwoorden HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # de sjablonen van de weergaven
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # stap 2 ------
    # configuratie van de applicatie
    config.update({
        # gebruikers die de applicatie mogen gebruiken
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

        # logbestand
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",

        # serverconfiguratie SMTP
        "adminMail": {
            # server SMTP
            "smtp-server""localhost",
            # serverpoort SMTP
            "smtp-port""25",
            # beheerder
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # onderwerp van de e-mail
            "subject""plantage du serveur de calcul d'impôts",
            # TLS op True als de server SMTP authenticatie vereist, anders op False
            "tls"False
        },

        # pauzetijd van de thread in seconden
        "sleep_time"0,

        # toegestane acties en hun controllers
        "controllers": {
            # initialisatie van een reken-sessie
            "init-session": InitSessionController(),
            # authenticatie van een gebruiker
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            # berekening van de belasting in de individuele modus
            "calculer-impot": CalculerImpotController(),
            # berekening van de belasting in batchmodus
            "calculer-impots": CalculerImpotsController(),
            # lijst met simulaties
            "lister-simulations": ListerSimulationsController(),
            # verwijdering van een simulatie
            "supprimer-simulation": SupprimerSimulationController(),
            # beëindiging van de berekeningssessie
            "fin-session": FinSessionController(),
            # weergave van het overzicht van de belastingberekening
            "afficher-calcul-impot": AfficherCalculImpotController(),
            # opvragen van gegevens bij de belastingdienst
            "get-admindata": GetAdminDataController(),
            # hoofdcontroller
            "main-controller": MainController()
        },

        # de verschillende soorten antwoorden (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        # de weergaven HTML en hun sjablonen zijn afhankelijk van de status die door de controller wordt geretourneerd
        "views"[
            {
                # authenticatieweergave
                "états": [
                    # /init-session geslaagd
                    700,
                    # /authentificatie-gebruiker mislukt
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # weergave van de belastingberekening
                "états"[
                    # /gebruikersauthenticatie geslaagd
                    200,
                    # /belasting-berekenen geslaagd
                    300,
                    # /belasting-berekenen mislukt
                    301,
                    # /belastingberekening-weergeven
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # overzicht van de lijst met simulaties
                "états"[
                    # /simulaties-weergeven
                    500,
                    # /simulatie-verwijderen
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        # overzicht van onverwachte fouten
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        # omleidingen
        "redirections"[
            {
                "états": [
                    400,  # /sessie succesvol beëindigd
                ],
                # omleiding naar
                "to""/init-session/html",
            }
        ],
    }
    )

    # stap 3 ------
    # databaseconfiguratie
    import config_database
    config["database"] = config_database.configure(config)

    # stap 4 ------
    # instantiëren van de applicatielagen
    import config_layers
    config['layers'] = config_layers.configure(config)

    # de configuratie wordt doorgegeven
    return config
  • tot regel 41 zien we de gebruikelijke zaken;
  • regels 43-66: vanaf regel 43 wordt het Python-pad van de server gedefinieerd. Vervolgens kunnen de afhankelijkheden van het project worden geïmporteerd:
    • regels 45-55: de lijst met controllers;
    • regels 57-60: de lijst met HTTP-responsen;
    • regels 62-66: de lijst met view-sjablonen;
  • regels 68-189: de configuratie van de applicatie met een reeks constanten;
    • regels 71-98: deze regels kennen we al uit eerdere versies;
    • regels 101-122: het woordenboek van de controllers:
      • de sleutels zijn de namen van de acties;
      • de waarden zijn een instantie van de controller die deze actie moet afhandelen. Van elke controller wordt slechts één exemplaar geïnstantieerd (singleton). Dezelfde instantie wordt door verschillende threads van de server uitgevoerd. Er moet dus rekening worden gehouden met gedeelde gegevens die elke controller mogelijk wil wijzigen;
    • regels 125-129: het woordenboek met de drie mogelijke antwoorden HTTP:
      • de sleutels zijn het type antwoord dat de klant wenst (jSON, xml, html);
      • de waarden zijn een instantie van het antwoord HTTP. Van elke antwoordgenerator wordt slechts één exemplaar geïnstantieerd (singleton). Dezelfde generator wordt door verschillende threads van de server uitgevoerd. Er moet dus rekening worden gehouden met gedeelde gegevens die elke generator mogelijk wil wijzigen;
    • regels 132-186: configuratie van de weergaven HTML. Voorlopig negeren we deze regels;
  • regels 191-202: deze regels zijn we al tegengekomen in eerdere versies;

30.6. Verloop van een clientverzoek binnen de server

Image

We volgen het traject van een clientverzoek dat op de server binnenkomt tot aan het antwoord HTTP dat wordt teruggestuurd. Het volgt het traject van de server MVC.

30.6.1. Het script [main]

Image

Het script [main] is in veel opzichten identiek aan dat van de vorige versies. We geven het niettemin volledig weer om op een goede basis te beginnen:


# er wordt gewacht op een mysql- of pgres-parameter
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})

# afhankelijkheden
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os

# er wordt een e-mail naar de beheerder verzonden
def send_adminmail(config: dict, message: str):
    # er wordt een e-mail verzonden naar de beheerder van de applicatie
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# het logbestand controleren
logger = None
erreur = False
message_erreur = None
try:
    # logboek
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # consoollogboek
    print(f"L'erreur suivante s'est produite : {exception}")
    # de fout wordt genoteerd
    erreur = True
    message_erreur = f"{exception}"
# logger opslaan in de configuratie
config['logger'] = logger
# foutafhandeling
if erreur:
    # e-mail naar de beheerder
    send_adminmail(config, message_erreur)
    # einde van de toepassing
    sys.exit(1)

# opstartlogboek
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

# ophalen van gegevens van de belastingdienst
erreur = False
try:
    # admindata wordt een alleen-lezen applicatie-gegevensveld
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    # succeslogboek
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # de fout wordt geregistreerd
    erreur = True
    # foutlogboek
    log = f"L'erreur suivante s'est produite : {ex}"
    # console
    print(log)
    # logbestand
    logger.write(f"{log}\n")
    # e-mail naar de beheerder
    send_adminmail(config, log)

# de hoofdthread heeft de logger niet meer nodig
logger.close()

# als er een fout is opgetreden, wordt het programma gestopt
if erreur:
    sys.exit(2)

# Flask-applicatie
app = Flask(__name__, template_folder="templates", static_folder="static")
# geheime sleutel van de sessie
app.secret_key = os.urandom(12).hex()

# de frontcontroller
def front_controller() -> tuple:
    # het verzoek wordt verwerkt
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    # omleiding naar /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# gebruiker-authenticeren
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# belasting-berekenen
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# simulaties weergeven
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# simulatie verwijderen
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# einde-sessie
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# belastingberekening weergeven
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

# alleen main
if __name__ == '__main__':
    # de server wordt gestart
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • regels 1-92: al deze regels zijn al eerder aan bod gekomen en uitgelegd;
  • regel 92: de server gaat een sessie beheren. We hebben dus een geheime sleutel nodig. Voor elke gebruiker zullen we twee gegevens in de sessie opslaan:
    • of de gebruiker zich correct heeft geauthenticeerd;
    • telkens wanneer hij een belastingberekening uitvoert, worden de resultaten van die berekening in een lijst geplaatst die we de lijst met simulaties van de gebruiker noemen. Deze lijst wordt in de sessie opgeslagen;
  • regels 100-151: de lijst met URL-services van de server. De bijbehorende functies fungeren als filter: alle URL die niet in deze lijst voorkomen, worden door de Flask-server afgewezen met de foutmelding [404 NOT FOUND]. Zodra deze filtering is doorlopen, wordt het verzoek systematisch doorgestuurd naar een ‘Front Controller’ die is geïmplementeerd door de functie [front_controller] in de regels 94-98, die we straks zullen bespreken;
  • regels 100-103: beheer van de route [/]. Het startpunt van de webapplicatie is de functie URL in regel 107. Daarom leiden we in regel 103 de client om naar deze functie URL:
  • de functie [url_for] wordt geïmporteerd op regel 18. Deze heeft hier twee parameters:
      • de eerste parameter is de naam van een van de routeringsfuncties, in dit geval die van regel 107. We zien dat deze functie een parameter [type_response] verwacht, wat het type (json, xml, html) is van het door de klant gewenste antwoord;
      • de tweede parameter neemt de naam van de parameter uit regel 107 over, [type_response], en kent er een waarde aan toe. Als er nog andere parameters zouden zijn, zouden we deze handeling voor elk van hen herhalen;
      • deze retourneert de URL die gekoppeld is aan de functie die wordt aangeduid door de twee parameters die eraan zijn doorgegeven. In dit geval levert dit de URL uit regel 106 op, waarbij de parameter wordt vervangen door de waarde [/init-session/html];
    • de functie [redirect] is geïmporteerd in regel 18. Deze heeft als taak een omleidingsheader HTTP naar de client te sturen:
      • de eerste parameter is de URL waarnaar de klant moet worden omgeleid;
      • de tweede parameter is de statuscode van het antwoord HTTP dat aan de klant wordt gegeven. De code [status.HTTP_302_FOUND] komt overeen met een omleiding HTTP;

De functie [front_controller] in de regels 94-98 voert de eerste verwerking van het verzoek van de klant uit:


# de frontcontroller
def front_controller() -> tuple:
    # het verzoek wordt verwerkt
    logger = None
    try:
        # loggen
        logger = Logger(config["logsFilename"])
        # het wordt opgeslagen in een configuratie die aan de thread is gekoppeld
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # het verzoek wordt gelogd
        logger.write(f"[ front_controller] requête : {request}\n")
        # de thread wordt onderbroken indien dit is gevraagd
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # de pauze is willekeurig, zodat sommige threads worden onderbroken en andere niet
            aléa = randint(01)
            if aléa == 1:
                # logboek vóór de pauze
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pauze
                time.sleep(sleep_time)
        # het verzoek wordt doorgestuurd naar de hoofdcontroller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # het naar de klant verzonden resultaat wordt gelogd
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # is er een fatale fout opgetreden?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # er wordt een e-mail verzonden naar de beheerder van de applicatie
            send_adminmail(config, log)
        # het gewenste type voor het antwoord wordt bepaald
        if session.get('typeResponse'is None:
            # het sessietype is nog niet vastgesteld – dit wordt jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # het te verzenden antwoord wordt samengesteld
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # het antwoord wordt verzonden
        return response, status_code
    except BaseException as erreur:
        # dit is een onverwachte fout – de fout wordt gelogd indien mogelijk
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # het antwoord aan de klant wordt voorbereid
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # we sturen een antwoord in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # het logbestand wordt gesloten als het geopend was
        if logger:
            logger.close()
  • regels 1-57: deze code is ons bekend. Dit was bijvoorbeeld de code van de functie met de naam [main] in het script [main] van de vorige versie. Er is slechts één ding op te merken, namelijk de controller die in de regels 25-26 wordt gebruikt:
  • regel 25: in de configuratie wordt de controllerinstantie opgehaald die gekoppeld is aan de naam [main-controller]. Het gaat om de volgende regels:

    # afhankelijkheden van de webserver
    # de controllers
    
    from MainController import MainController

     # toegestane acties en de bijbehorende controllers
        "controllers": {
            ,
            # hoofdcontroller
            "main-controller": MainController()
        },
  • (vervolg)
    • regel 10 hierboven: merk op dat we een klasse-instantie ophalen;
  • regel 26: de controller [MainController] wordt gevraagd het verzoek te verwerken;
  • regels 30-45: het antwoord van de controller [MainController] wordt naar de client verzonden. We komen later op deze regels terug;

De functie [front_controller] en vervolgens de klasse [MainController] hebben als taak het werk uit te voeren dat bij alle verzoeken hoort:

In het bovenstaande schema bevinden we ons nog steeds in fase 1 van de verwerking van het verzoek. De hoofdcontroller [MainController] zal stap 1 voortzetten.

30.6.2. De hoofdcontroller [MainController]

De hoofdcontroller [MainController] zet het werk voort dat is begonnen door de functie [front_controller]:

Alle controllers implementeren de volgende interface [InterfaceController] [2]:

Image


from abc import ABC, abstractmethod

from werkzeug.local import LocalProxy

class InterfaceController(ABC):

    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • De interface [InterfaceController] definieert alleen de enige methode [execute] op regel 8. Deze methode ontvangt drie parameters:
    • [request]: het verzoek van de klant;
    • [session]: de sessie van de klant;
    • [config]: de configuratie van de applicatie;

De methode [execute] retourneert een tuple met twee elementen:

  • het eerste element is het woordenboek met de resultaten in de vorm {‘actie’: actie, ‘status’: status, ‘antwoord’: resultaten};
  • het tweede element is de statuscode HTTP die aan de klant moet worden teruggestuurd;

De hoofdcontroller [MainController] [1] implementeert de interface [InterfaceController] als volgt:


# afhankelijkheden importeren

from flask_api import status
from werkzeug.local import LocalProxy

# controllers van de webapplicatie
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # we halen de elementen van het pad op
        params = request.path.split('/')
        action = params[1]

        # fouten
        erreur = False
        # het sessietype moet bekend zijn vóór bepaalde acties
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            # de fout wordt genoteerd
            résultat = {"action": action, "état"101,
                        "réponse"["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        # voor bepaalde acties moet men geauthenticeerd zijn
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
            # de fout wordt genoteerd
            résultat = {"action": action, "état"101,
                        "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        # zijn er fouten?
        if erreur:
            # er wordt een foutmelding teruggestuurd
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            # de bij de actie behorende controller wordt uitgevoerd
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

De controller [MainController] voert de eerste controles uit op de geldigheid van het verzoek.

  • regels 11-13: de controller begint met het ophalen van de door de client aangevraagde actie. Ter herinnering: de URL-serviceacties hebben de vorm [/action/param1/param2/…] en deze URL bevindt zich in [request.path];
  • regels 17-23: de actie [init-session] dient om het door de client gewenste antwoordtype (json, xml, html) te initialiseren. Deze informatie wordt in de sessie opgeslagen onder de sleutel [typeRéponse]. Als de actie dus niet [init-session] is, moet de sessie de sleutel [typeRéponse] bevatten, anders is het verzoek onjuist;
  • regels 21-22: de structuur van het resultaat dat door elke controller wordt geretourneerd, in dit geval een foutmelding:
    • [action]: is de naam van de huidige actie. Hierdoor kan de naam worden vastgelegd wanneer het resultaat van de aanvraag wordt gelogd;
    • [état]: is een statuscode van drie cijfers:
        • [x00] bij succes;
        • [x01] bij een mislukking;
  • [réponse]: is het antwoord op de aanvraag. De aard ervan is specifiek voor elke aanvraag;
  • regels 24-30: de actie [authentifier-utilisateur] dient om de gebruiker te authenticeren. Als dit lukt, wordt een sleutel [user=True] in de sessie van de gebruiker geplaatst. Bepaalde service-acties URL zijn alleen toegankelijk voor een geauthenticeerde gebruiker. Dit wordt hier gecontroleerd;
  • regel 26: alleen de acties [init-session] en [authentifier-utilisateur] kunnen worden uitgevoerd door een gebruiker die nog niet is geauthenticeerd;
  • regels 28-29: het te verzenden resultaat in geval van een fout;
  • regels 32-34: als een van de twee voorgaande fouten is opgetreden, wordt het foutbericht naar de client verzonden met de status HTTP 400 BAD REQUEST;
  • regels 35-39: als er geen fout is opgetreden, wordt de controle overgedragen aan de controller die verantwoordelijk is voor de verwerking van de lopende actie. De betreffende instantie wordt gevonden in de configuratie van de applicatie;

De klasse [MainController] zet het werk van de functie [front_controller] voort: samen bundelen ze alles wat bij de verwerking van verzoeken kan worden gefactoreerd, en wachten ze tot het laatste moment om het verzoek door te geven aan een specifieke controller. De verdeling van de code tussen de functie [front_controller] en de klasse [MainController] is volledig subjectief. Hier wilde ik de verworvenheden van de vorige versie behouden: de functie [front_controller] bestond al onder de naam [main]. In de praktijk zou men:

  • alles in de functie [front_controller] onderbrengen en de klasse [MainController] verwijderen;
  • alles in de klasse [MainController] onderbrengen en de functie [front_controller] verwijderen. Ik zou eerder voor deze oplossing kiezen, omdat dit het voordeel heeft dat de code van het hoofdscript [main] lichter wordt;

30.7. Actiespecifieke verwerking

Laten we terugkeren naar de architectuur MVC van de applicatie:

Image

We bevinden ons nog steeds in stap 1 hierboven. Als er geen fout is opgetreden, begint stap 2. Het verzoek is doorgestuurd naar de controller die specifiek is voor de actie waar het verzoek om vraagt. Laten we aannemen dat deze actie [/init-session] is, gedefinieerd door de route:


# sessie initialiseren
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

Deze actie is gekoppeld aan een controller in de configuratie [config]:


        # toegestane acties en de bijbehorende controllers
        "controllers": {
            # initialisatie van een reken-sessie
            "init-session": InitSessionController(),
            
        },

De controller [InitSessionController] (regel 4) neemt dus het heft in handen. De code ervan is als volgt:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action, type_response = request.path.split('/')

        # in eerste instantie geen fout
        erreur = False
        # controle van het type antwoord
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état"701,
                        "réponse"[f"paramètre [type={type_response}] invalide"]}
        # als er geen fout is
        if not erreur:
            # wordt het sessietype in de Flask-sessie opgeslagen
            session['typeResponse'] = type_response
            résultat = {"action": action, "état"700,
                        "réponse"[f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, status.HTTP_400_BAD_REQUEST
  • regel 6: net als de andere controllers implementeert de controller [InitSessionController] de interface [InterfaceController];
  • regel 10: de URL is van het type [/init-session/type_response]. We halen de actie [init-session] en het gewenste antwoordtype op;
  • regel 15: het gewenste antwoordtype kan alleen een van de typen zijn die in de antwoordconfiguratie voorkomen:

        # de verschillende responssoorten (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • als dat niet het geval is, wordt een foutmelding 701 opgesteld (regel 17);
  • regels 20-25: in het geval dat het gewenste antwoordtype geldig is;
  • regel 22: het gewenste antwoordtype wordt in de sessie opgeslagen. Dit moet namelijk worden onthouden voor de volgende verzoeken;
  • regels 23-24: er wordt een succesantwoord 700 voorbereid;
  • regel 25: het succesantwoord wordt teruggestuurd naar de aanroepende code;
  • regel 27: als er een fout is opgetreden, wordt het foutantwoord teruggestuurd naar de aanroepende code;

30.8. Opstellen van het antwoord HTTP van de server

Laten we terugkeren naar de architectuur MVC van de applicatie:

Image

We hebben zojuist stap 1 en 2 doorgenomen. We zijn drie statuscodes tegengekomen:

  • 700: /init-session is geslaagd;
  • 701: /init-session is mislukt;
  • 101: ongeldig verzoek, hetzij omdat de sessie niet is geïnitialiseerd, hetzij omdat de gebruiker niet is geauthenticeerd;

Laten we eens bekijken hoe het antwoord van de server in stap 3 hierboven naar de client wordt verzonden. Dit gebeurt in de functie [front_controller] van het script [main]:


# de frontcontroller
def front_controller() -> tuple:
    # het verzoek wordt verwerkt
    logger = None
    try:
        # logger
        logger = Logger(config["logsFilename"])
        # het wordt opgeslagen in een configuratie die aan de thread is gekoppeld
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # het verzoek wordt gelogd
        logger.write(f"[ front_controller] requête : {request}\n")
        # de thread wordt onderbroken indien dit is gevraagd
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # de pauze is willekeurig, zodat sommige threads worden onderbroken en andere niet
            aléa = randint(01)
            if aléa == 1:
                # logboek vóór de pauze
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pauze
                time.sleep(sleep_time)
        # het verzoek wordt doorgestuurd naar de hoofdcontroller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # het naar de klant verzonden resultaat wordt gelogd
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # is er een fatale fout opgetreden?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # er wordt een e-mail verzonden naar de beheerder van de applicatie
            send_adminmail(config, log)
        # het gewenste type voor het antwoord wordt bepaald
        if session.get('typeResponse'is None:
            # het sessietype is nog niet vastgesteld – dit wordt jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # het te verzenden antwoord wordt samengesteld
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # het antwoord wordt verzonden
        return response, status_code
    except BaseException as erreur:
        # dit is een onverwachte fout – de fout wordt gelogd indien mogelijk
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # het antwoord aan de klant wordt voorbereid
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # we sturen een antwoord in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # het logbestand wordt gesloten als het geopend was
        if logger:
            logger.close()
  • we zijn bij regel 26: de hoofdcontroller heeft zijn foutmelding teruggestuurd;
  • regels 27-29: ongeacht het antwoord van de hoofdcontroller (geslaagd of mislukt) wordt dit antwoord in het logbestand vastgelegd;
  • regels 30-33: net als in eerdere versies wordt, als de status HTTP gelijk is aan [500 INTERNAL SERVER ERROR], een e-mail naar de applicatiebeheerder gestuurd met het foutenlogboek;
  • regels 34-39: we sturen het antwoord HTTP en het door de controller geretourneerde resultaat wordt in de hoofdtekst van dit antwoord geplaatst. We moeten weten in welke vorm (json, xml, html) de klant dit antwoord wil ontvangen. We zoeken het gewenste antwoordtype in de sessie. Als het daar niet staat, stellen we dit type willekeurig in op jSON;
  • regels 40-43: het antwoord HTTP wordt samengesteld;

In het configuratiebestand is elk antwoordtype (json, xml, html) gekoppeld aan een klasse-instantie:


        # de verschillende soorten antwoorden (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

De antwoordklassen bevinden zich in de map [responses] in de serverstructuur:

Image

Elke responsklasse implementeert de volgende interface [InterfaceResponse]:


from abc import ABC, abstractmethod

from flask.wrappers import Response
from werkzeug.local import LocalProxy

class InterfaceResponse(ABC):

    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        pass
  • regels 8-11: de interface [InterfaceResponse] definieert één enkele methode [build_http_response] met de volgende parameters:
    • [request, session, config]: dit zijn de parameters die door de actiecontroller worden ontvangen;
    • [résultat, status_code]: dit zijn de resultaten die door de actiecontroller worden geproduceerd;

We zullen het antwoord jSON presenteren. Dit wordt gegenereerd door de volgende klasse [JsonResponse]:


import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # resultaten: het woordenboek met resultaten
        # status_code: de statuscode van het antwoord HTTP

        # het antwoord wordt teruggestuurd HTTP
        response = make_response(json.dumps(résultat, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

We kennen deze code, die we al vele malen zijn tegengekomen. Het is de code van de functie [json_response] uit de module [myutils].

30.9. Eerste tests

In de onderzochte code zijn we drie statuscodes tegengekomen:

  • 700: /init-session geslaagd;
  • 701: /init-session is mislukt;
  • 101: ongeldig verzoek, hetzij omdat de sessie niet is geïnitialiseerd, hetzij omdat de gebruiker niet is geauthenticeerd;

We gaan proberen deze te verkrijgen met een sessie jSON.

  • we starten de webserver, de SGBD, en de mailserver;
  • we starten een Postman-client;

Test 1

We tonen eerst een ongeldig verzoek omdat de sessie niet is geïnitialiseerd:

Image

  • [1-2]: de aanvraag [POST http://localhost:5000/authentifier-utilisateur] is een geldige route:

# gebruiker authenticeren
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

maar deze wordt alleen geaccepteerd als de sessie eerder is geïnitialiseerd met de actie [/init-session].

Laten we de aanvraag uitvoeren en het door de server verzonden resultaat bekijken:

Image

  • [1-2]: we hebben een antwoord jSON ontvangen. Wanneer het antwoordtype nog niet door de client is vastgesteld, gebruikt de server jSON om te antwoorden;
  • [3-5]: het woordenboek jSON van het antwoord;
    • [action]: de actie die is uitgevoerd;
    • [état]: de statuscode van het antwoord. Een code [x01] duidt op een fout;
    • [réponse]: is afgestemd op elke actie. Hier bevat deze een foutmelding;

Laten we nu een sessie starten met een onjuist antwoordtype:

Image

  • [1-2] is een correcte route:

# sessie initialiseren
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

Het verzoek komt dus terecht in de verwerkingstunnel van de server MVC. Het zal echter tijdens deze verwerking worden afgewezen omdat het aangevraagde sessietype onjuist is.

Het antwoord luidt als volgt:

Image

  • in [4], een foutcode [x01];
  • in [5], de uitleg van de fout;

Laten we nu een sessie jSON starten:

Image

Het antwoord is als volgt:

Image

Laten we nu een sessie XML starten. Het antwoord jSON wordt vervangen door een antwoord XML, gegenereerd door de volgende klasse [XmlResponse]:


import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # resultaten: het resultatenwoordenboek
        # status_code: de statuscode van het antwoord HTTP

        # resultaat: het woordenboek dat moet worden omgezet in een tekenreeks XML
        xml_string = xmltodict.unparse({"root": résultat})
        # het antwoord wordt weergegeven als HTTP
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

Dit is code die we kennen, namelijk die van de functie [xml_response] uit de gedeelde module [myutils].

We initialiseren een sessie XML:

Image

Het resultaat van de server is dan als volgt:

Image

We krijgen hetzelfde antwoord als bij jSON, maar dit keer is het antwoord verpakt in XML.

30.10. De actie [authentifier-utilisateur]

Met de actie [authentifier-utilisateur] kan een gebruiker die de belastingberekeningsapplicatie wil gebruiken, worden geauthenticeerd. De route ervan is als volgt gedefinieerd in het script [main]:


# gebruiker authenticeren
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De server verwacht twee verzonden parameters:

  • [user]: de gebruikers-ID;
  • [password]: zijn wachtwoord;

De lijst met geautoriseerde gebruikers wordt gedefinieerd in de configuratie [config]:


        # gebruikers die bevoegd zijn om de applicatie te gebruiken
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

Hier hebben we een lijst met één element.

De actie [authentifier-utilisateur] wordt verwerkt door de volgende controller [AuthentifierUtilisateurController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class AuthentifierUtilisateurController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action = request.path.split('/')

        # de parameters van de POST
        post_params = request.form
        # statuscode van het antwoord HTTP
        status_code = None
        # in eerste instantie geen fouten
        erreur = False
        erreurs = []
        # er is een POST met twee parameters nodig
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            # de parameters van POST worden opgehaald
            # parameter [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            # parameter [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            # fout?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        # fout?
        if not erreur:
            # de geldigheid van de combinatie (gebruikersnaam, wachtwoord) wordt gecontroleerd
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            # gevonden?
            if not trouvé:
                # de fout wordt genoteerd
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                # in de sessie wordt genoteerd dat de gebruiker is gevonden
                session["user"] = True
        # klaar
        if not erreur:
            # terug zonder fout
            résultat = {"action": action, "état"200"réponse"f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            # terug met fout
            return {"action": action, "état"201"réponse": erreurs}, status_code

  • regel 14: de parameters van POST worden opgehaald;
  • regel 19: de lijst met fouten die in het verzoek zijn aangetroffen;
  • regels 20-24: er wordt gecontroleerd of er inderdaad twee parameters zijn verzonden;
  • regels 27-31: er wordt gecontroleerd of de parameter [users] aanwezig is;
  • regels 32-36: er wordt gecontroleerd of de parameter [password] aanwezig is;
  • regels 38-39: als de verzonden parameters onjuist zijn, wordt een antwoord voorbereid met de parameters HTTP 400 BAD REQUEST;
  • regels 40-58: er wordt gecontroleerd of de inloggegevens [user, password] afkomstig zijn van een gebruiker die bevoegd is om de applicatie te gebruiken;
  • regels 51-55: als de gebruiker (gebruikersnaam, wachtwoord) geen toestemming heeft om de applicatie te gebruiken, wordt een antwoord opgesteld met de codes HTTP 401 UNAUTHORIZED;
  • regels 56-58: als hij bevoegd is, wordt met de sleutel [user] in de sessie genoteerd dat hij zich heeft geauthenticeerd;

Merk op dat als de gebruiker was geauthenticeerd met de inloggegevens [identifiants1] en de authenticatie met de inloggegevens [identifiants2] mislukt, hij niettemin geauthenticeerd blijft met de inloggegevens [identifiants1].

Laten we wat Postman-tests uitvoeren:

  • we starten de webserver, de SGBD en de mailserver;
  • met de Postman-client:
    • we starten een sessie met jSON;
    • vervolgens authenticeren we ons;

Hier volgen verschillende scenario's.

Geval 1: POST zonder verzonden parameters

Image

  • in [3-5] heeft POST geen inhoud;

Het resultaat van de aanvraag is als volgt:

Image

  • in [2] kregen we een antwoord HTTP 400 BAD REQUEST;
  • bij [5] kregen we een foutcode [201];

Geval 2: POST met onjuiste inloggegevens

Image

  • in [6] zijn de inloggegevens onjuist;

De server stuurt het volgende antwoord:

Image

  • in [2], het antwoord HTTP 401 UNAUTHORIZED;
  • in [5], het foutbericht;

Geval 2: POST met correcte inloggegevens

Image

  • in [6], de inloggegevens zijn correct;

Het antwoord van de server is als volgt:

  • in [2], een antwoord HTTP 200 OK; Image
  • in [5], het succesvolle antwoord;

30.11. De actie [calculer_impot]

Met de actie [calculer_impot] kan de belasting van een belastingplichtige worden berekend. Het traject ervan is als volgt gedefinieerd in het script [main]:


# belasting berekenen
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De server verwacht drie geposte parameters:

  • [marié]: ja / nee;
  • [enfants]: aantal kinderen van de belastingplichtige;
  • [salaire]: jaarsalaris van de belastingplichtige;

De controller [CalculerImpotController] verwerkt de actie [calculer_impot]:


import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # we halen de elementen van het pad op
        dummy, action = request.path.split('/')

        # in eerste instantie geen fout
        erreur = False
        erreurs = []
        # de parameters van POST
        post_params = request.form
        # er is een POST met drie parameters nodig
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        # de verzonden parameters worden geanalyseerd
        if not erreur:
            # parameter is gekoppeld
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                # is de parameter geldig?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            # parameter [enfants]
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                # is de parameter geldig?
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            # parameter salaris
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                # is de parameter geldig?
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        # fout?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état"301"réponse": erreurs}
            # het resultaat wordt weergegeven
            return résultat, status_code

        # belastingberekening
        # de laag [métier] en het woordenboek [adminData] worden opgehaald
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        # belastingberekening
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        # simulatienummer
        id_simulation = session.get('id_simulation'0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        # het resultaat wordt in de sessie opgeslagen in de vorm van een woordenboek met de naam TaxPayer
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        # het resultaat wordt toegevoegd aan de lijst met reeds uitgevoerde simulaties en deze lijst wordt in de sessie opgeslagen
        simulations = session.get("simulations"[])
        simulations.append(simulation)
        session["simulations"] = simulations
        # resultaat
        résultat = {"action": action, "état"300"réponse": simulation}
        status_code = status.HTTP_200_OK

        # het resultaat wordt weergegeven
        return résultat, status_code
  • regel 13: de naam van de lopende actie wordt opgehaald;
  • regel 17: de fouten worden in een lijst verzameld;
  • regel 19: de verzonden parameters worden opgehaald. Deze worden verzonden in de vorm [x-www-form-urlencoded] en daarom worden ze opgehaald in [request.form]. Als ze waren verzonden als jSON, zouden we ze hebben opgehaald als [request.data];
  • regels 21-24: er wordt gecontroleerd of er inderdaad drie parameters zijn verzonden;
  • regels 27-36: controle op de aanwezigheid en geldigheid van de verzonden parameter [marié];
  • regels 37-48: controle op de aanwezigheid en geldigheid van de verzonden parameter [enfants];
  • regels 49-60: controle op de aanwezigheid en geldigheid van de verzonden parameter [salaire];
  • regels 62-66: als er een fout is opgetreden, wordt een foutmelding 400 BAD REQUEST verzonden met een statuscode [301];
  • regels 69-71: als er geen fout is opgetreden, wordt de berekening van de belasting voorbereid. Hiervoor
    • regel 70: wordt een referentie opgehaald uit de laag [métier];
    • regel 71: worden de gegevens van de belastingdienst opgehaald uit de serverconfiguratie;
  • regels 72-74: de belasting van de belastingplichtige wordt berekend;
  • regels 75-77: we tellen het aantal belastingberekeningen dat de gebruiker heeft uitgevoerd;
    • regel 76: het nummer van de laatste berekening wordt uit de sessie opgehaald. Het resultaat van een berekening wordt hier [simulation] genoemd;
    • regel 77: het nummer van de laatste simulatie wordt verhoogd;
    • regel 78: dit nummer wordt weer in de sessie opgeslagen;
  • regels 79-84: om de door de gebruiker uitgevoerde berekeningen bij te houden, plaatsen we de lijst met simulaties die hij heeft uitgevoerd in zijn sessie;
  • regel 80: een simulatie wordt het woordenboek van een object TaxPayer, waarvan de eigenschap [id] de waarde van het simulatienummer krijgt;
  • regels 82-84: de huidige simulatie wordt toegevoegd aan de lijst met simulaties in de sessie;
  • regels 86-87: er wordt een succesvol antwoord HTTP voorbereid;
  • regel 90: het resultaat wordt teruggestuurd;

Laten we een paar tests uitvoeren: de webserver, de SGBD, de mailserver en een Postman-client worden gestart.

Geval 1: een belastingberekening uitvoeren terwijl de sessie niet is geïnitialiseerd

Image

Het antwoord is als volgt:

Image

Geval 2: een belastingberekening uitvoeren zonder geauthenticeerd te zijn

Eerst starten we een sessie jSON met [/init-session/json]. Vervolgens voeren we dezelfde aanvraag uit als eerder. Het antwoord is dan als volgt:

Image

Geval 3: een belastingberekening uitvoeren met ontbrekende parameters

We initialiseren een sessie jSON, authenticeren ons en voeren vervolgens de volgende aanvraag uit:

Image

  • in [5] ontbreekt de parameter [marié];

Het antwoord is als volgt:

Geval 4: een belastingberekening uitvoeren met onjuiste parameters

Image

Image

Het antwoord van de server luidt als volgt:

Image

Geval 4: een belastingberekening uitvoeren met de juiste parameters

Image

Het antwoord van de server is als volgt:

Image

30.12. De actie [lister-simulations]

Met de actie [lister-simulations] kan een gebruiker de lijst bekijken van de simulaties die hij sinds het begin van de sessie heeft uitgevoerd. Het pad ervan is als volgt gedefinieerd in het script [main]:


# simulaties weergeven
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De server verwacht geen parameters. De actie [lister-simulations] wordt verwerkt door de volgende controller [ListerSimulationsController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action = request.path.split('/')

        # de lijst met simulaties in de sessie wordt opgehaald
        simulations = session.get("simulations"[])
        # het resultaat wordt weergegeven
        return {"action": action, "état"500,
                "réponse": simulations}, status.HTTP_200_OK
  • regel 13: de lijst met simulaties wordt uit de sessie gehaald;
  • regels 15-16: er wordt een succesvol antwoord teruggestuurd;

Laten we de volgende Postman-test uitvoeren:

  • we starten een sessie jSON;
  • we authenticeren ons;
  • we voeren twee belastingberekeningen uit;
  • we vragen de lijst met simulaties op;

Het verzoek is als volgt:

  • in [3] zijn er geen parameters; Image

Het antwoord van de server is als volgt:

Image

  • in [4], de lijst met simulaties van de gebruiker;

30.13. De actie [supprimer-simulation]

Met de actie [supprimer-simulation] kan een gebruiker een van de simulaties uit zijn simulatielijst verwijderen. Het pad hiernaar is als volgt gedefinieerd in het script [main]:


# simulatie verwijderen
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De server verwacht één parameter: het nummer van de simulatie die moet worden verwijderd. De actie [supprimer-simulation] wordt verwerkt door de volgende controller [SupprimerSimulationController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action, numéro = request.path.split('/')

        # de parameter [numéro] is een positief geheel getal of nul, afhankelijk van de route
        numéro = int(numéro)
        # de simulatie met id=nummer moet voorkomen in de lijst met simulaties
        simulations = session.get("simulations"[])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            # er wordt een foutmelding weergegeven
            return {"action": action, "état"601"réponse"[msg_erreur]}, status.HTTP_400_BAD_REQUEST
        # verwijdering van de simulatie met id=nummer
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        # de simulaties worden weer aan de sessie toegevoegd
        session["simulations"] = simulations
        # het resultaat wordt weergegeven
        return {"action": action, "état"600"réponse": simulations}, status.HTTP_200_OK
  • regel 10: de twee elementen van het verzoekpad worden opgehaald. Ze worden opgehaald als tekenreeks;
  • regel 13: de parameter [numéro] wordt omgezet in een geheel getal. We weten dat dit mogelijk is vanwege de handtekening van de route,

@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])

Bovendien weten we dat het een geheel getal >=0 is. We kunnen namelijk geen URL of [/supprimer-simulation/-4] hebben. Deze worden door de Flask-server geweigerd;

  • regel 15: we halen de lijst met simulaties op uit de sessie;
  • regel 16: met de functie [filter] zoeken we de simulatie met id==nummer. We krijgen een object [filter] dat we omzetten naar het type [list];
  • regels 17-20: als het filter niets heeft opgeleverd, betekent dit dat de te verwijderen simulatie niet bestaat. Er wordt een foutmelding teruggestuurd die dit aangeeft;
  • regels 21-23: de door het filter teruggegeven simulatie wordt verwijderd;
  • regel 25: de nieuwe lijst met simulaties wordt weer in de sessie geplaatst;
  • regel 27: de nieuwe lijst met simulaties wordt in het antwoord teruggestuurd;

We voeren een succes- en een faaltest uit. We voeren simulaties uit en vragen vervolgens de lijst met simulaties op:

Image

  • de simulaties hebben hier de nummers 2 en 3;

We vragen om de simulatie met nummer 3 te verwijderen.

Image

Het antwoord is als volgt:

Laten we nu dezelfde handeling herhalen (het verwijderen van de simulatie met id=3). Het antwoord is dan als volgt:

Image

Image

30.14. De actie [fin-session]

Met de actie [fin-session] kan een gebruiker zijn simulatiesessie beëindigen. Het traject ervan is als volgt gedefinieerd in het script [main]:


# einde sessie
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De server verwacht geen parameters. De actie wordt verwerkt door de volgende controller [FinSessionController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action = request.path.split('/')

        # alle sleutels uit de huidige sessie worden verwijderd
        session.clear()
        # het resultaat wordt weergegeven
        return {"action": action, "état"400"réponse""session réinitialisée"}, status.HTTP_200_OK
  • regel 13: alle sleutels van de sessie worden verwijderd. Dit verwijdert:
    • [typeResponse]: het type van de antwoorden HTTP (json, xml, html);
    • [id_simulation]: het nummer van de laatst uitgevoerde simulatie;
    • [simulations]: de lijst met simulaties van de gebruiker;
    • [user]: de indicator dat de gebruiker is geauthenticeerd;
  • het antwoord wordt teruggestuurd;

Men kan zich afvragen hoe het antwoord HTTP uit regel 15 zal worden teruggestuurd, nu het antwoordtype niet meer in de sessie aanwezig is. Om dit te weten te komen, moeten we teruggaan naar de functie |front_controller| van het hoofdscript [main] en deze als volgt aanpassen:


…        
         # on not# het gewenste antwoordtype wordt genoteerd als deze informatie in de sessie aanwezig is
        type_response1 = session.get('typeResponse'None)
        # het verzoek wordt doorgestuurd naar de hoofdcontroller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # het naar de klant verzonden resultaat wordt gelogd
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # is er een fatale fout opgetreden?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # er wordt een e-mail verzonden naar de beheerder van de applicatie
            send_adminmail(config, log)
        # het gewenste type voor het antwoord wordt bepaald
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # het sessietype is nog niet vastgesteld – dit wordt jSON
            type_response = 'json'
        elif type_response2 is not None:
            # het type van het antwoord is bekend en staat in de sessie
            type_response = type_response2
        else:
            type_response=type_response1
        # het te verzenden antwoord wordt opgebouwd
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # het antwoord wordt verzonden
        return response, status_code
  • regel 3: het type van het antwoord dat momenteel in de sessie aanwezig is, wordt opgeslagen;
  • regel 6: de actie wordt uitgevoerd. Als het gaat om:
    • [fin-session], dan is de sleutel [typeResponse] niet meer in de sessie aanwezig;
    • [init-session], dan kan de waarde van de sleutel [typeResponse] in de sessie zijn gewijzigd;;
  • regels 14-20: het antwoord HTTP moet worden verzonden. We moeten weten in welke vorm:
    • regels 16-18: als het type van het antwoord niet is gedefinieerd door [type_response1] in regel 3, noch door [type_response2] in regel 15, dan was het type van het antwoord noch vóór, noch na de actie gedefinieerd. Dan gebruiken we jSON (regel 18);
    • regels 19-21: als [type_response2] bestaat, het type in de sessie na de actie, dan moet dit type worden gebruikt;
    • regels 22-23: anders is het [type_response1], het antwoordtype vóór de actie (dit is noodzakelijkerwijs [fin-session]), dat moet worden gebruikt;

30.15. De actie [get-admindata]

We gaan nu in op de twee URL die zijn gereserveerd voor de diensten jSON en XML:

Actie
Rol
Uitvoeringscontext
/get-admindata
Levert de fiscale gegevens aan waarmee de belasting kan worden berekend
Verzoek GET.
Wordt alleen gebruikt als het sessietype json of xml is. De gebruiker moet geauthenticeerd zijn
/belasting-berekenen
Voert de belastingberekening uit voor een lijst met belastingplichtigen die is verzonden in jSON
Verzoek GET.
Wordt alleen gebruikt als het sessietype json of xml is. De gebruiker moet geauthenticeerd zijn

URL en [/get-admindata] zijn in de routes van het hoofdscript [main] als volgt gedefinieerd:


# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

De route [/get-admindata] wordt verwerkt door de volgende controller [GetAdminDataController]:


# afhankelijkheden importeren

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action = request.path.split('/')
        # alleen json- en xml-sessies worden geaccepteerd
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # er wordt een foutmelding teruggestuurd
            return {
                       "action": action,
                       "état"1001,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            # er wordt een succesbericht teruggestuurd
            return {"action": action, "état"1000"réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • regels 13-21: er wordt gecontroleerd of het om een JSON- of XML-sessie gaat;
  • regel 24: het woordenboek met gegevens van de belastingdienst wordt weergegeven, dat bij het opstarten van de server in de configuratie was geplaatst:

    # admindata is een alleen-lezen gegeven met toepassingsbereik
    config["admindata"] = config["layers"]["dao"].get_admindata()

Laten we een Postman-client gebruiken en de URL [/get-admindata] opvragen, nadat we een sessie jSON hebben gestart en ons hebben geauthenticeerd:

Image

Het antwoord van de server is als volgt:

Image

30.16. De actie [calculer-impots]

De actie [calculer-impots] berekent de belasting voor een lijst met belastingplichtigen die in de hoofdtekst van het verzoek is opgenomen in de vorm van een tekenreeks jSON. We kennen deze actie al: in de vorige versie heette deze [calculate_tax_in_bulk_mode].

De route is als volgt:


# belastingberekening in batches
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    # de bij de actie behorende controller wordt uitgevoerd
    return front_controller()

Deze actie wordt verwerkt door de volgende controller [CalculerImpotsController]:


import json

from flask_api import status
from werkzeug.local import LocalProxy

from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # de elementen van het pad worden opgehaald
        dummy, action = request.path.split('/')

        # alleen json- en xml-sessies worden geaccepteerd
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # er wordt een foutmelding teruggestuurd
            return {
                       "action": action,
                       "état"1501,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        # de body van de POST-verzoek wordt opgehaald – er wordt een lijst met woordenboeken verwacht
        msg_erreur = None
        list_dict_taxpayers = None
        # de inhoud jSON van POST
        request_text = request.data
        try:
            # wordt omgezet in een lijst met woordenboeken
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            # we merken de fout op
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        # hebben we een niet-lege lijst?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            # we noteren de fout
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        # hebben we een lijst met woordenboeken?
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            # fout?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        # fout?
        if msg_erreur:
            # er wordt een foutmelding naar de klant gestuurd
            résultats = {"action": action, "état"1501"réponse"[msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        # de TaxPayers-codes worden één voor één gecontroleerd
        # in eerste instantie geen fouten
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            # er wordt een TaxPayer aangemaakt op basis van dict_taxpayer
            msg_erreur = None
            try:
                # de volgende bewerking verwijdert de gevallen waarin de parameters niet
                # van de eigenschappen van de klasse TaxPayer, evenals de gevallen waarin hun waarden
                # onjuist zijn
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            # bepaalde sleutels moeten in het woordenboek aanwezig zijn
            if not msg_erreur:
                # de sleutels [marié, enfants, salaire] moeten in het woordenboek voorkomen
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            # fouten?
            if msg_erreur:
                # de fout wordt vastgesteld in TaxPayer zelf
                dict_taxpayer['erreur'] = msg_erreur
                # we voegen TaxPayer toe aan de lijst met fouten
                list_erreurs.append(dict_taxpayer)

        # alle belastingplichtigen zijn verwerkt – zijn er fouten?
        if list_erreurs:
            # er wordt een foutmelding naar de klant verzonden
            résultats = {"action": action, "état"1501"réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        # geen fouten, we kunnen verdergaan
        # gegevens ophalen bij de belastingdienst
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            # de TaxPayer-bestanden worden één voor één verwerkt
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                # berekening van de belasting
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié']'enfants': dict_taxpayer['enfants'],
                     'salaris': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                # het resultaat wordt opgeslagen als een woordenboek
                list_taxpayers.append(taxpayer.asdict())
            # we voegen list_taxpayers toe aan de huidige simulaties en geven elke simulatie een nummer
            simulations = session.get("simulations"[])
            id_simulation = session.get("id_simulation"0)
            for simulation in list_taxpayers:
                # elke simulatie krijgt een nummer
                id_simulation += 1
                simulation['id'] = id_simulation
                # voegen we deze toe aan de huidige lijst met simulaties
                simulations.append(simulation)
            # we laden alles opnieuw in de sessie
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            # het antwoord wordt naar de klant verzonden
            return {"action": action, "état"1500"réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            # er wordt een foutmelding naar de klant verzonden
            return {"action": action, "état"1501"réponse"[f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • regels 16-24: er wordt gecontroleerd of we ons daadwerkelijk in een JSON- of XML-sessie bevinden
  • regels 26-120: deze code is ons in grote lijnen bekend. Het is de code van de functie |index_controller| uit versie 10 van de applicatie, die is aangepast om te voldoen aan de specificaties van de geïmplementeerde interface [InterfaceController];
  • regels 104-115: de code die is toegevoegd om rekening te houden met de nieuwe omgeving van deze controller. We hebben zojuist belastingberekeningen uitgevoerd. We moeten de resultaten opslaan in de lijst met simulaties die tijdens de sessie worden bijgehouden;
  • regel 105: we halen de lijst met simulaties in de sessie op;
  • regel 106: we halen het nummer van de laatst uitgevoerde simulatie op;
  • regels 107-112: we doorlopen de lijst met woordenboeken van de resultaten van de belastingberekening; aan elk daarvan wordt een simulatienummer [id] toegekend en elk woordenboek wordt toegevoegd aan de lijst met simulaties;
  • regels 113-115: de nieuwe lijst met simulaties en het nummer van de laatst uitgevoerde simulatie worden in de sessie opgeslagen;

We voeren de volgende Postman-test uit, nadat we een sessie jSON hebben geïnitialiseerd en ons hebben geauthenticeerd:

Image

Image

Het antwoord van de server is als volgt:

Image

Als we nu de lijst met simulaties opvragen:

Dan valt op dat in de resultatenlijst van [/calcul-impots] de belastingplichtigen geen attribuut [id] hebben, terwijl in de lijst met simulaties elke simulatie een identificatienummer heeft.

Image