Flask

_images/flask.png

Flask est un framework web beaucoup plus évolué que Bottle.

Il reste compact mais comporte beaucoup plus de possibilité de bottle (a priori on ré-invente moins la roue)

Il prend en charge nativement les templates Jinja2

Il possède de plus un ensemble de plugin qui permet rapidement de lui rajouter ce qui lui manque

Installation

Rien de plus simple avec pip

pip install flask

Premier exemple

Commençons par un simple “hello word”...

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Pour tester ce code, il suffit juste de lancer la commande suivante. Et d’ouvrir le navigateur sur l’URL qui sera affichée dans la console.

$ python app.py
* Running on http://127.0.0.1:5000/

Pour l’instant si on compare Flask et Bottle cela ne fait pas beaucoup de différence. Et heureusement puisqu’ils respectent tous les deux le PEP de python sur les framework web.

Debugger

Flask possède un debugger intégré très performant puisqu’il permet de lancer via un naviguateur web une console python (accessible via chaque ligne du résultat du debugger)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    # une erreur ici
    a = 1/0
    return "Hello World!"

if __name__ == "__main__":
    app.run(debug=True)
_images/flask_debug.png

On peut aussi activer le debug via l’utilisation une variable globale

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
# Flask configuration
DEBUG = True

app = Flask(__name__)
app.config.from_object(__name__)

@app.route("/")
def hello():
    # une erreur ici
    a = 1/0
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Note

On peut utiliser n’importe quelle variable en majuscule de l’objet utilisé dans from_object. On peut ainsi passer le path d’un fichier log ou celui d’une base de donnée On récupère les variables via le code

app.config['NAME_OF_VARIABLE']

Logger

Flask possède de façon native un handler de logger ... on peut donc facilement rajouter des loggers

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging

from flask import Flask
# Flask configuration
DEBUG = True
SERVER_NAME = '0.0.0.0:5001'
LEVEL = logging.INFO

app = Flask(__name__)
app.config.from_object(__name__)

@app.route('/')
def hello():
    app.logger.warning('A warning occurred (%d apples)', 42)
    app.logger.error('An error occurred')
    app.logger.info('Info')
    return "Hello World!"

if __name__ == '__main__':
    handler = logging.StreamHandler()
    handler.setLevel(app.config['LEVEL'])
    app.logger.addHandler(handler)
    app.run()

Warning

si on passe le DEBUG a False on ne voit que les errors et les warnings plus les infos

Ajout de fonction pre et post

Comme pour bottle on peut décorer les appels ... mais Flask possède des décorateurs pré-défini

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging

from flask import Flask
# Flask configuration
DEBUG = True
SERVER_NAME = '0.0.0.0:5001'
LEVEL = logging.INFO

app = Flask(__name__)
app.config.from_object(__name__)

@app.before_request
def before_request():
    app.logger.warning('before call')

@app.teardown_request
def teardown_request(exception):
    app.logger.warning('after call')

@app.route('/')
def hello():
    app.logger.warning('A warning occurred (%d apples)', 42)
    app.logger.error('An error occurred')
    app.logger.info('Info')
    return "Hello World!"

if __name__ == '__main__':
    handler = logging.StreamHandler()
    handler.setLevel(app.config['LEVEL'])
    app.logger.addHandler(handler)
    app.run()

Filtrage des formulaires et des réponses

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'GET':
        # afficher le formulaire
    else:
        # traiter les données reçues
        # afficher : "Merci de m'avoir laissé un message !"

Les sessions

Les sessions sont donc un moyen simplifié et plus sécurisé de retenir des informations pour vos visiteurs. Les sessions utilisent deux choses distinctes :

  • Un cookie placé chez le client, pour identifier sa session.
  • Des données stockées sur le serveur, reliées à cette même session.

Pour fonctionner, nous aurons besoin d’importer l’objet session :

from flask import session

Cet objet s’utilise ensuite de la même manière qu’un dictionnaire Python. L’avantage est qu’on peut lire et écrire dedans, on ne passe donc plus par les objets requête et réponse. Voilà l’équivalent du code avec les cookies transposé dans une version utilisant session à la place :

@app.route('/')
def index():
    if 'pseudo' in session:
        return "C'est un plaisir de se revoir, {pseudo} !".format(pseudo=session['pseudo'])
    else:
        session['pseudo'] = 'Luc'
        return "Bonjour, c'est votre première visite ?"

Le défaut des sessions est qu’on ne peut pas choisir de période d’expiration différente pour chaque variable qu’on stocke dans la session.

Par défaut, la session sera effacée quand le visiteur fermera son navigateur. Si on veut qu’elle dure plus longtemps, il faut placer session.permanent = True après sa création. Elle vivra alors un mois.

Pour changer cette durée, il faut modifier la configuration de l’objet app :

app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # la session dure une heure

On peut stocker n’importe quoi dans session : entiers, chaînes, listes, objets, dictionnaires, etc.

Cependant, dans le cas des listes et des dictionnaires (et de tout type mutable plus généralement), il faut notifier la session lorsqu’on les modifie, en positionnant à True l’attribut modified :

session['mon_panier'].append('appareil photo') # mon_panier est une liste
session.modified = True

Pour finir, lorsqu’on désire supprimer une variable de la session, on utilise la méthode pop() de la session :

session.pop('pseudo', None)

Note

Le deuxième paramètre (None) est là uniquement pour éviter une erreur s’il n’y a pas de variable ‘pseudo’ dans la session.

Les templates

#! /usr/bin/python
# -*- coding:utf-8 -*-

from flask import Flask
app = Flask(__name__)

@app.route('/')
def accueil():
    mots = ["bonjour", "à", "toi,", "visiteur."]
    return render_template('accueil.html', titre="Bienvenue !", mots=mots)

if __name__ == '__main__':
    app.run(debug=True)

Note

penser dans les template a ne pas utiliser d’adresse en dur mais d’utiliser url_for()

<link href="{{ url_for('static', filename='mon_style.css') }}" rel="stylesheet" type="text/css" />

Création d’un site statique

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from flask import Flask
import flask

# Flask configuration
DEBUG = True
PATH_HTML = 'C:/Users/f.aoustin/Google Drive/mydoc/build/html'
PATH_INDEX = 'index.html'

app = Flask(__name__)
app.config.from_object(__name__)


@app.route("/")
def index():
    return flask.send_from_directory(app.config['PATH_HTML'],app.config['PATH_INDEX'])

@app.route("/<path:path>")
def static_web(path):
    return flask.send_from_directory(app.config['PATH_HTML'],path)

if __name__ == "__main__":
    app.run()

Plugins

flask-login

Généralement lors de la création d’une applicatino web une des premiere fonctionnalité est la gestion de l’accès à cette dernière.

Flask possède un plugin qui gère cela pour nous: flask-login

pip install flask
pip install flask-login

on va par la suite créer cette arborescence

dir
|__ myapp.py
|__ myapp.cfg
|__ templates
|         |__ login.html
|__ statics
        |__ css
        |__ fonts
        |__ js
        |   |__ myjs.js
        |__ less
        |__ scss
        |__ admin.html
        |__ protected.html

On utilisera les modules font-awesomes et nos modules propres de design (cf autre billet: design web)

admin.html

<html>
<head>
<link href="css/font-awesome.min.css" rel="stylesheet">
<link href="css/mycss.css" rel="stylesheet">
<link href="css/mytheme.css" rel="stylesheet">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="js/myjs.js"></script>

</head>
test admin
<a href="/logout">logout</a>
<a href="/">index</a>
</body>
</html>

protected.html

<html>
<head>
<link href="css/font-awesome.min.css" rel="stylesheet">
<link href="css/mycss.css" rel="stylesheet">
<link href="css/mytheme.css" rel="stylesheet">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="js/myjs.js"></script>

</head>
test index
<a href="/logout">logout</a>
<a href="/admin">admin</a>
</body>
</html>

myjs.js

$().ready(function() {
    $('.tdauto').css({
       width: ($(window).width() - 250)/2 + 'px'
      });

    $("input[type=checkbox]").click(function () {
        $(this).toggleClass("checked");
        $.ajax({
            type: "POST",
            url: "http://192.168.1.18:5001/update",
            dataType: "xml",
            data: "id=" + $(this).attr("id"),
            async: true,
            global: false,
            success: function(xml) {
                /* todo */
            },
            beforeSend: function() {
                /* todo */
            },
            complete: function() {
                /* todo */
            }
        });
    });

    // hide block expand
    $("div.list>input.checked").each(function() {
            $("div.expand>span[id='"+$(this).parent().attr("view")+"']").append(" <strong class='list-selected'>"+$(this).next().text()+"</strong>");
            }
        );

    $("input[type=radio]").click(function () {
        $('input:radio[name='+$(this).attr("name")+']').removeClass("checked");
        $(this).toggleClass("checked");
        /*$('input:radio[name='+$(this).attr("name")+']').toggleClass("checked");*/
        $.ajax({
            type: "POST",
            url: "http://192.168.1.18:5001/update",
            dataType: "xml",
            data: "id=" + $(this).attr("id")+ "&val=" + $(this).val(),
            async: true,
            global: false,
            success: function(xml) {
                /* todo */
            },
            beforeSend: function() {
                /* todo */
            },
            complete: function() {
                /* todo */
            }
        });
        /* manage list */
        if ( $( this ).hasClass( "list" ) ) {
            $(this).parent().css("display","none");
            $("div.expand>span[id='"+$(this).parent().attr("view")+"']").toggleClass("expand");
            $("div.expand>span[id='"+$(this).parent().attr("view")+"']>strong").remove();
            $("div.expand>span[id='"+$(this).parent().attr("view")+"']").append(" <strong class='list-selected'>"+$(this).next().text()+"</strong>");
        };

    });

    $('input[type=text].online').on('input', function() {
        $.ajax({
            type: "POST",
            url: "http://192.168.1.18:5001/update",
            dataType: "xml",
            data: "id=" + $(this).attr("id") + "&val=" + $(this).val(),
            async: true,
            global: false,
            success: function(xml) {
                /* todo */
            },
            beforeSend: function() {
                /* todo */
            },
            complete: function() {
                /* todo */
            }
        });
    });

    $('input.validform').keypress(function(event){
        if(event.keyCode == 13){
            $(this).parents("form")[0].submit();
        }
    });

    // hide block expand
    $("div.expand>span").each(function() {
                $("[view='"+$(this).attr("id")+"']").css("display","none");
            }
        );

    $("div.expand>span").click(function () {
        $(this).toggleClass("expand");
         if ( $( this ).hasClass( "expand" ) ) {
                $("[view='"+$(this).attr("id")+"']").css("display","inline-block");
            } else {
                $("[view='"+$(this).attr("id")+"']").css("display","none");
            }
    });

    $('input[type=range]').on('input change', function() {
        $.ajax({
            type: "POST",
            url: "http://192.168.1.18:5001/update",
            dataType: "xml",
            data: "id=" + $(this).attr("id") + "&val=" + $(this).val(),
            async: true,
            global: false,
            success: function(xml) {
                /* todo */
            },
            beforeSend: function() {
                /* todo */
            },
            complete: function() {
                /* todo */
            }
        });
    });

    $("button").click(function () {
        $.ajax({
            type: "POST",
            url: "http://192.168.1.18:5001/update",
            dataType: "xml",
            data: "id=" + $(this).attr("id"),
            async: true,
            global: false,
            success: function(xml) {
                /* todo */
            },
            beforeSend: function() {
                /* todo */
            },
            complete: function() {
                /* todo */
            }
        });
    });
});

login.html

<html>
    <head>
        <link href="css/font-awesome.min.css" rel="stylesheet">
        <link href="css/mycss.css" rel="stylesheet">
        <link href="css/mytheme.css" rel="stylesheet">
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script type="text/javascript" src="js/myjs.js"></script>
    </head>
    <body>
        <table class="center">
            <tr/>
            <tr>
                <td class="tdauto"></td>
                <td>
                    <form id="myformlogin" action="login" method="post">
                        <div class="login">
                            <div class="input-prepend">
                                <span class="add-on"><i class="fa fa-user"></i></span>
                                <input id="username" name="username" type="text" placeholder="username" class="online"/>
                            </div>
                            <div class="input-prepend">
                                <span class="add-on"><i class="fa fa-wrench"></i></span>
                                <input id="password" name="password" class="validform" type="password" placeholder="password"/>
                            </div>
                            {% for warning in warnings %}
                                <div class="warning" ><i class="fa fa-2x fa-warning"></i> <span>{{ warning }}</span></div>
                            {% endfor %}
                            {% for info in infos %}
                                <div class="info" ><i class="fa fa-2x fa-info-circle"></i> <span>{{ info }}</span></div>
                            {% endfor %}
                        </div>
                    </form>
                </td>
                <td/>
            </tr>
            <tr/>
        </table>
    </body>
</html>

myapp.cfg

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging

DEBUG = True
PATH_STATICS = './statics'
SECRET_KEY = 'secret_key'
PORT = 5001
HOST = '0.0.0.0'
LEVEL = logging.DEBUG
USERS = [
         {  'id'       : 1,
            'username' : 'admin',
            'password' : 'keyadmin',
            'groups'   : ['admin','user']
         },
         {  'id'       : 2,
            'username' : 'test',
            'password' : 'keytest',
            'groups'   : ['user']
         }
        ]

myapp.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging
from flask import Flask
import flask

from flask.ext.login import LoginManager
from flask.ext.login import login_user , logout_user , current_user , login_required
from werkzeug.security import generate_password_hash, check_password_hash



app = Flask(__name__)
app.config.from_pyfile('myapp.cfg')

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

class User():
    """ class of User, for login"""

    def __init__(self, username):
        self.username = username

    def check_password(self, password):
        if self.username in [i['username']  for i in app.config['USERS']]:
            if [ i['password'] for i in app.config['USERS'] if i['username'] == self.username][0] == password:
                return True
        return False

    def in_groups(self, group):
        if group in [ i['groups'] for i in app.config['USERS'] if i['username']  == self.username][0]:
            return True
        return False

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return [ i['id'] for i in app.config['USERS'] if i['username']  == self.username][0]

    def __repr__(self):
        return '<User %r>' % (self.username)

class checkGroup(object):
    def __init__(self, group):
        self.group = group

    def __call__(self, fn):
        def wrapped_f(*args, **kwargs):
            if current_user.in_groups('admin') :
                return fn(*args, **kwargs)
            return unauthorized()
        return wrapped_f

@app.route('/')
@login_required
def index():
    app.logger.debug('load protected.html by %s ' % current_user.username)
    return flask.send_from_directory(app.config['PATH_STATICS'],'protected.html')

@app.route('/admin')
@login_required
@checkGroup('admin')
def admin():
    app.logger.debug('load admin.html by %s ' % current_user.username)
    return flask.send_from_directory(app.config['PATH_STATICS'],'admin.html')

@app.route('/login',methods=['GET','POST'])
def login():
    app.logger.debug('login method: %s' % flask.request.method)
    if flask.request.method == 'GET':
        return flask.render_template('login.html', warnings=[], infos=[])
    username = flask.request.form['username']
    app.logger.debug(username)
    password = flask.request.form['password']
    registered_user = User(username)
    if registered_user.check_password(password):
        login_user(registered_user, remember = True)
        app.logger.debug('Logged in successfully')
        return  flask.redirect(flask.request.url_root)
    return flask.render_template('login.html', warnings=['username and/or password are wrong',], infos=[])
    #return flask.abort(401) # 401 Unauthorized

@app.route('/logout')
def logout():
    logout_user()
    return flask.redirect(flask.url_for('login'))

@login_manager.unauthorized_handler
def unauthorized():
    return "non autoriser bouh !!!"

@login_manager.user_loader
def load_user(id):
    app.logger.debug('id %s' % id)
    return User([i['username']  for i in app.config['USERS'] if i['id']== id][0])

@app.route("/<path:path>")
def static_web(path):
    app.logger.debug('load static %s' % path)
    return flask.send_from_directory(app.config['PATH_STATICS'],path)

if __name__ == "__main__":
    handler = logging.StreamHandler()
    handler.setLevel(app.config['LEVEL'])
    app.logger.addHandler(handler)
    app.run(host = app.config['HOST'],
        port = app.config['PORT'])

# TODO: https://www.openshift.com/blogs/use-flask-login-to-add-user-authentication-to-your-python-application
# TODO: https://github.com/shekhargulati/flask-login-openshift-quickstart/blob/master/wsgi/todoapp.py

on peut au lieu d’enregistrer le mot de passe en dure dans un fichier le hasher puis par la suite contrôler que le hash sauvegardé correspond au password fournit

4 >>from werkzeug.security import generate_password_hash, check_password_hash
5 >>hash = generate_password_hash('test')
6 >>print(hash)
pbkdf2:sha1:1000$FaKJFp6q$eee385f5f75cf3e7da560b51580c0c9b565e0cb5
7 >>print check_password_hash(hash,'test')
True
8 >>print check_password_hash(hash,'test2')
False
9 >>

Flask-WeasyPrint

Il est parfois utile d’avoir un outil simple pour générer du pdf à la voler.

Le plugin Flask-WeasyPrint est fait pour vous

pip install flask
pip install Flask-WeasyPrint

on va par la suite créer cette arborescence

dir
|__ myapp.py
|__ myapp.cfg
|__ templates
|         |__ hello.html
|__ statics
        |__ css
        |     |__ styles.css
        |__ img
             |__ hello.png

myapp.cfg

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

DEBUG = True
PATH_STATICS = './statics'
SECRET_KEY = 'secret_key'
PORT = 5001
HOST = '0.0.0.0'

myapp.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, render_template, url_for, send_from_directory
from flask_weasyprint import HTML, render_pdf

app = Flask(__name__)
app.config.from_pyfile('myapp.cfg')

@app.route('/hello/', defaults={'name': 'World'})
@app.route('/hello/<name>/')
def hello_html(name):
    return render_template('hello.html', name=name)

@app.route('/hello_<name>.pdf')
def hello_pdf(name):
    # Make a PDF from another view
    return render_pdf(url_for('hello_html', name=name))
    #return render_pdf(HTML(string=html)) other possibilite generate HTML

@app.route("/<path:path>")
def static_web(path):
    return send_from_directory(app.config['PATH_STATICS'],path)

if __name__ == "__main__":
    app.run(host = app.config['HOST'],
        port = app.config['PORT'])

hello.html

<!doctype html>
<title>Hello</title>
<link rel=stylesheet href="/css/style.css}" />

<p>Hello, {{ name }}!</p>
<img src="/img/hello.png"/>
<nav><a href="{{ url_for('hello_pdf', name=name) }}">Get as PDF</a></nav>

style.css

body { font: 2em Fontin, serif }
nav { font-size: .7em }

@page { size: A5; margin: 1cm }
@media print {
    nav { display: none }
}

on peut tester sur http://127.0.0.1:5001/hello

Flask-Mail

On peut avoir besoin dans une application d’envoyer des mails

pip install flask
pip install Flask-Mail

on va par la suite créer cette arborescence

dir
|__ myapp.py
|__ myapp.cfg
|__ templates
|         |__ mail.html
|__ statics
        |__ img
             |__ hello.png

myapp.cfg

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging

DEBUG = True
PATH_STATICS = './statics'
SECRET_KEY = 'secret_key'
PORT = 5001
HOST = '0.0.0.0'
MAIL_SERVER = 'srvxxxx1'

myapp.py

from flask import Flask, send_from_directory
from flask_mail import Mail, Message

mail = Mail()
app = Flask(__name__)
app.config.from_pyfile('myapp.cfg')
mail.init_app(app)


@app.route("/mail")
def send_mail():
    msg = Message("Hello",
                  sender="informatiquepz@myprop-group.com",
                  recipients=["f.aoustin@myprop-group.com"])
    msg.body = "testing"
    with app.open_resource("%s/img/hello.png" % app.config['PATH_STATICS']) as fp:
        msg.attach("hello.png", "image/png", fp.read())
    #msg.html = "<b>testing</b>"
    mail.send(msg)
    return send_from_directory(app.config['PATH_STATICS'],'mail.html')

if __name__ == "__main__":
    app.run(host = app.config['HOST'],
        port = app.config['PORT'])

mail.html

<html>
send mail
</html>

on peut tester sur http://127.0.0.1:5001/mail

Flask-WTF

Cette extension permet de gérer simplement des formulaires.

pip install flask
pip install Flask-WTF

Nous allons contruire une application possédant un formulaire de contact avec 4 champs:

  • nom
  • mail
  • sujet
  • message

Tout les champs sont obligatoires et nous validerons le format du mail

Notre application aura l’arborescence suivante

dir
|__ myapp.py
|__ myapp.cfg
|__ templates
|         |__ contact.html
|         |__ home.html
|         |__ layout.html
|__ statics
        |__ css
             |__ main.css

contact.html

{% extends "layout.html" %}
{% block content %}
  <h2>Contact</h2>

    {% for message in get_flashed_messages() %}
        <div class="flash">{{ message }}</div>
    {% endfor %}

  <form action="{{ url_for('contact') }}" method=post>
    {{ form.hidden_tag() }}

    {{ form.name.label }}
    {{ form.name }}

    {% for message in form.name.errors %}
        <div class="flash">{{ message }}</div>
    {% endfor %}

    {{ form.email.label }}
    {{ form.email }}

    {% for message in form.email.errors %}
        <div class="flash">{{ message }}</div>
    {% endfor %}

    {{ form.subject.label }}
    {{ form.subject }}

    {% for message in form.subject.errors %}
        <div class="flash">{{ message }}</div>
    {% endfor %}

    {{ form.message.label }}
    {{ form.message }}

    {% for message in form.message.errors %}
        <div class="flash">{{ message }}</div>
    {% endfor %}

    {{ form.submit }}
  </form>
{% endblock %}

layout.html

{% extends "layout.html" %}
{% block content %}
home
{% endblock %}

home.html

<!DOCTYPE html>
<html>
  <head>
    <title>Flask</title>
    <strong><link rel="stylesheet" href="css/main.css"></strong>
  </head>
  <body>
    <header>
      <div class="container">
        <h1 class="logo">Flask App</h1>
        <nav>
          <ul class="menu">
            <li><a href="{{ url_for('home') }}">Home</a></li>
            <li><a href="{{ url_for('contact') }}">Contact</a></li>
          </ul>
        </nav>
      </div>
    </header>
    <div class="container">
      {% block content %}
      {% endblock %}
    </div>
  </body>
</html>

Note

on utilise içi les templates avec des notions d’extends

myapp.cfg

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging

DEBUG = True
PATH_STATICS = './statics'
SECRET_KEY = 'secret_key'
PORT = 5001
HOST = '0.0.0.0'
MAIL_SERVER = 'srvxxxx1'

myapp.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging
from flask import Flask
import flask

from flask_wtf import Form
from wtforms import TextField, TextAreaField, SubmitField, validators


app = Flask(__name__)
app.config.from_pyfile('myapp.cfg')


    class RegistrationForm(Form):
        username = TextField('Username', [validators.Length(min=4, max=25)])
        email = TextField('Email Address', [validators.Length(min=6, max=35)])
        name = TextField("Name",  [validators.Required("Please enter your name.")])
        subject = TextField("Subject",  [validators.Required("Please enter a subject.")])
        message = TextAreaField("Message",  [validators.Required("Please enter a message.")])
        datefield = DateField('datefield')
        datetimefield = DateTimeField('datetimefield')
        decimalfield = DecimalField('decimalfield')
        filefield = FileField('filefield')
        floatfield = FloatField('floatfield')
        integerfield = IntegerField('integerfield')
        radiofield = RadioField('radiofield', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
        selectfield = SelectField('selectfield', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
        password = PasswordField('New Password', [
            validators.Required(),
            validators.EqualTo('confirm', message='Passwords must match')
        ])
        confirm = PasswordField('Repeat Password')
        accept_tos = BooleanField('I accept the TOS', [validators.Required()])
        submit = SubmitField("Send")

@app.route('/home')
def home():
    app.logger.debug(flask.render_template('home.html'))
    return flask.render_template('home.html')

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    if flask.request.method == 'POST':
        if form.validate() == False:
            app.logger.error('All fields are required.')
            flask.flash('All fields are required.')
            return flask.render_template('contact.html', form=form)
        else:
            app.logger.debug("name    : %s" % form.name.data)
            app.logger.debug("email   : %s" % form.email.data)
            app.logger.debug("subject : %s" % form.subject.data)
            app.logger.debug("message : %s" % form.message.data)
            return 'Form posted.'
    elif flask.request.method == 'GET':
        return flask.render_template('contact.html', form=form)

@app.route("/<path:path>")
def static_web(path):
    app.logger.debug('load static %s' % path)
    return flask.send_from_directory(app.config['PATH_STATICS'],path)

if __name__ == "__main__":
    handler = logging.StreamHandler()
    handler.setLevel(app.config['LEVEL'])
    app.logger.addHandler(handler)
    app.run(host = app.config['HOST'],
        port = app.config['PORT'])

On peut tester sur http://127.0.0.1:5001/contact

Il est important de noter que nous gérons les erreurs champs par champs au niveau du contact.html mais aussi au niveau gloabl grâce à la fonction flash.

On peut aussi facilement rajouter un form de gestion du login

from wtforms import TextField, TextAreaField, SubmitField, validators, PasswordField, BooleanField

class RegistrationForm(Form):
    username = TextField('Username', [validators.Length(min=4, max=25)])
    email = TextField('Email Address', [validators.Length(min=6, max=35)])
    password = PasswordField('New Password', [
        validators.Required(),
        validators.EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')
    accept_tos = BooleanField('I accept the TOS', [validators.Required()])

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm(flask.request.form)
    if flask.request.method == 'POST' and form.validate():
        flash('Thanks for registering')
        return redirect(url_for('login'))
    return flask.render_template('register.html', form=form)

Et pour register.html afin de simplifier son écriture on peut créer une fonction de template

register.html

{% from "_formhelpers.html" import render_field %}
{% extends "layout.html" %}
{% block content %}
<form method=post action="/register">
  {% for field in form %}
    {% if field.widget.__class__.__name__ in ('TextInput','PasswordInput','CheckboxInput','TextArea', 'ListWidget', 'Select') %}
        {{ render_field(field, class='wtf') }}
    {% endif %}
  {% endfor %}
  {{ form.submit }}
</form>
{% endblock %}

_formhelpers.html

{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}

Concernant les éléments qu’on peut uiliser via le module wtform il y a une excellente doc https://wtforms.readthedocs.org/en/latest

Il existe plusieurs validateurs:

  • validators.DataRequired(message=None)
  • validators.Email(message=None)
  • validators.EqualTo(fieldname, message=None)
  • validators.InputRequired(message=None)
  • validators.IPAddress(ipv4=True, ipv6=False, message=None)
  • validators.Length(min=-1, max=-1, message=None)
  • validators.MacAddress(message=None)
  • validators.NumberRange(min=None, max=None, message=None)
  • validators.Optional(strip_whitespace=True)
  • validators.Regexp(regex, flags=0, message=None)
  • validators.URL(require_tld=True, message=None)
  • validators.UUID(message=None)
  • validators.AnyOf(values, message=None, values_formatter=None)
  • validators.NoneOf(values, message=None, values_formatter=None)

Il est possible de créer facilement des nouveaux validateurs

def my_length_check(form, field):
    if len(field.data) > 50:
        raise ValidationError('Field must be less than 50 characters')

class MyForm(Form):
    name = StringField('Name', [InputRequired(), my_length_check])