Python et Webpy : Partie 2

by david

Vous pouvez récupérer le code de la partie 1 ici , le sql et la conf pour Apache ici (n’oubliez pas de modifier vos hosts).

Dans cette partie, on va créer le formulaire de connexion&compagnie.

Avant de commencer, il faut rajouter dans conf.py (ainsi que conf.template.py) pour site/ et files/ ceci :

BD_TYPE = « mysql »
BD_HOTE = « localhost »
BD_NOM = « quisaura »
BD_USER = « quisaura »
BD_PASS = « 123456″

Comme on veut faire la connexion,
Il faut commencer par créer le modèle Utilisateur.
Mais avant, on va créer la classe Mère DbTable. Pourquoi ?
Parce que nos modèles auront beaucoup de méthodes communes : insert(),save(),delete(),etc.

Donc on crée le fichier utils/bd.py :

#-*- coding:utf-8 -*-
import web
from conf import BD_TYPE, BD_HOTE, BD_NOM, BD_USER, BD_PASS
class DbConnection:
    «  »" Gère une connexion à la base de donées.
        La connexion n’est ouverte qu’une seule fois pour toutes les requêtes d’une même page
    «  »"
    _db = None

    @staticmethod
    def getDb():
        «  »" Retourne _db s’il vaut pas None, sinon on créer la connexion «  »"
        if DbConnection._db == None:
            DbConnection._db = web.database(host=BD_HOTE,dbn=BD_TYPE, user=BD_USER, pw=BD_PASS, db=BD_NOM)
        return DbConnection._db

class DbTable(DbConnection):
        «  »" La classe mère, qui va fournir tout ce qui faut à nos modèles, y compris le saucisson «  »"

        tableName = «  » #nom de la table
        columns = ()#les colonnes de la table
        primaryKey = «  » # primaryKey de la table
        debug = False

        def __init__(self, row):
            self._row = row#c’est un dico représentant une ligne de la table=> nomColonne:valeur

        def __getattr__(self, name):
            if (self._row.has_key(name)):
                return self._row[name]
            raise AttributeError, name

        def getRow(self):
            return self._row

        @classmethod
        def _query(cls, query, vars={}):
            rowIter = cls.getDb().query(query, vars=vars, _test=cls.debug)
            if (cls.debug):
                print (« SQL:  » + rowIter)
                return []

            if (isinstance(rowIter,long)):return rowIter
            results = [cls(row) for row in rowIter]
            return results

        @classmethod
        def getRowById(cls, id):
            if (cls.primaryKey):
                return cls._getRow(where=cls.primaryKey + ‘=’ + str(id))
            else:
                return None

        @classmethod
        def _getRow(cls, columns=[], where= », order= », vars={}):
            «  »" Construit une requête SELECT et renvoie la première ligne
            columns: la liste éventuelle des colonnes que l’on veut prendre
            where: clause WHERE de la requête SQL
            order: clause ORDER BY de la requête SQL
            vars: dictionnaire d’argument à remplacer dans la requête »" »

            results = cls._getRows(columns, where, order, ’0,1′, vars)
            return results[0] if len(results) else None

        @classmethod
        def _getRows(cls, columns=[], where= », order= », limit= », vars={}):
            «  »"Construit une requête SELECT et renvoie le résultat
            columns: la liste éventuelle des colonnes que l’on veut prendre
            where: clause WHERE de la requête SQL
            order: clause ORDER BY de la requête SQL
            limit clause LIMIT de la requête SQL
            vars: dictionnaire d’argument à remplacer dans la requête »" »

            if columns :
                query = « SELECT  » + « , ».join(columns)
            else:
                query = « SELECT  » + « , ».join(cls.columns)

            query +=  » FROM ` » + cls.tableName + « ` »
            if (where):
                query +=  » WHERE  » + where

            if (order):
                query +=  » ORDER BY  » + order

            if (limit):
                query +=  » LIMIT     » + limit

            return cls._query(query, vars)

        @classmethod
        def save(cls, values):
            «  »" Crée un nouvel objet dans la table
            values: Dictionaire (nom_colonne: valeur)
            «  »"
            columns = ‘,’.join(['%s' %(i) for i in values.keys()])
            val = ‘,’.join(['$%s' %(i) for i in columns.split(',')])
            query = « INSERT INTO `%s` (%s) VALUES(%s) » % (cls.tableName,columns,val)
            cls._query(query,vars=values)
           
        @classmethod
        def delete(cls,values):
            «  »"supprime une ligne dans la table »" »
            condition =  » AND « .join(['%s=$%s' % (i,i) for i in values.keys()])

            return cls.getDb().delete(« `%s` » %cls.tableName,where=condition,vars=values,_test=cls.debug)

        @classmethod
        def update(cls, values, id):
            «  »" Met à jour un objet existant dans la table
            values: Dictionaire (nom_colonne: valeur)
            «  »"
            conditions = ‘,’.join(["%s=$%s"%(i,i) for i in values.keys()])
            query = « UPDATE `%s` SET %s WHERE %s=%s » % (cls.tableName,conditions,cls.primaryKey,str(id))
            return cls._query(query, vars=values)

 

 

Bon, j’explique pas le code de DbTable, puisque c’est pas le but de ce billet, puis il y a assez de commentaires je pense.

Maintenant, on crée notre premier modèle : models/utilisateur.py :

#-*- coding:utf-8 -*-
from utils.bd import *
from hashlib import sha1

class Utilisateur(DbTable):
    «  »"Represente un Utilisateur (table Utilisateur) «  »"

    tableName = ‘utilisateur’
    columns = (‘idUser’,'login’,'password’,'bloque’)
    primaryKey = ‘idUser’

    @classmethod
    def identification(cls, login, mdp):
        «  »" tente de logguer l’utilisateur »" »
        mdp = sha1(mdp).hexdigest()
        utilisateur = cls._getRow( where=’`login` = $login AND `password` = $mdp’,
            vars={‘login’:login, ‘mdp’:mdp} )
        return utilisateur

 

Pour créer un modèle, rien de plus simple :
- on fait hériter la classe de DbTable
- on indique dans tableName, le nom de la table
- on indique dans columns, les colonnes
- on indique dans primaryKey, la clé primaire
- et voilà !

Maintenant que notre modèle Utilisateur est créé , on va pouvoir faire notre première Action.
Mais en fait non, puisque l’on va avant créer la classe BaseAction dans utils/coeur.py .
Elle va être la classe mère de toutes nos actions. Pourquoi ?
Parce qu’à chaque fois que l’on change de pages(donc d’actions) on a besoin d’informations récurrentes , comme la session de l’utilisateur,un objet représentant l’utilisateur, l’objet qui va retourner notre template,etc.

Donc dans le fichier utils/coeur.py , on rajoute :

import web
from conf import CHEMIN_VUES
class BaseAction(object):
    def __init__(self):
        self.session =  web.ctx.environ['beaker.session']
        self.render = web.template.render(CHEMIN_VUES,globals={‘_’: _})# _ : pour l’i18n

        if self.session.has_key(‘user’):
            from models.utilisateur import Utilisateur
            self.session['user'] = Utilisateur.getRowById(self.session['user'].idUser)

 

Voilà, donc chaque action pour accéder à la session depuis self.session, à l’utilisateur depuis self.session['user'], et au template depuis self.render.

Maintenant, on peut réellement commencer à créer nos actions.

Place à l’indexAction, qui retourne la page d’accueil, une fois qu’on est connecté .
Fichier site/controleurs/IndexControleur.py :

# -*- coding:utf-8 -*-
from conf import *
from utils.coeur import *
from utils.decorateurs import *

class IndexAction(BaseAction):
        «  »"action pour afficher la page d’accueil »" »
        __metaclass__ = MetaClass

        url = ‘/’

        @estConnecte
        def GET(self):
            utilisateur = self.session['user']
            return self.render.main(utilisateur=utilisateur)

Créer une action est simple :
- on lui définit la métaclasse MetaClass
- on lui indique l’url à partir de laquelle elle est accessible (ca peut être aussi un tuple).
- et on crée la méthode, selon le type de la requête HTTP : si c’est du GET alors on crée la méthode GET(), si c’est du POST=>POST(), OPTIONS=>OPTIONS(),etc.
- on retourne la vue via self.render
- et voilà !

Ici l’action est très simple : si l’utilisateur est connecté, on retourne la vue main.html.

Maintenant comme vous l’avez constaté, il nous reste à créer le décorateur estConnecte.
Donc on crée le fichier utils/decorateurs.py et on rajoute :

#-*- coding:utf-8 -*-
import web

def estConnecte(maMethode):
    «  »"Indique si l’utilisateur est loggué ou pas »" »
    def parametres(*params):
        if params[0].session.has_key(‘user‘):
            if params[0].session['user'] != None:
                return maMethode(*params)
        return web.redirect(‘/login‘)
    return parametres

C’est un décorateur simple (cf mon article dessus).
Si l’utilisateur n’a pas la clé user dans sa session, alors il n’est pas connecté, et donc on le redirige sur /login.

Retour à IndexAction.
Dans la méthode GET() , on retourne self.render.main(),cad main.html.
Donc voici le fichier site/vues/main.html :

$def with (utilisateur)
<p>$_(« tu_es_connecte »)</p>
<p><a href= »/logout »>$_(« se_deconnecter »)</a></p>

Comme on a passé à main.html un paramètre, il faut aussi le déclarer dans le template.

Pour finir ce billet, on va créer le LoginControleur, qui va permettre à l’utilisateur de se dé/connecter.
Fichier site/controleurs/LoginControleur.py :

#-*- coding:utf-8 -*-
from utils.coeur import *
from utils.decorateurs import *
from models.utilisateur import Utilisateur
import web

class LoginAction(BaseAction):
    «  »"action responsable pour le login »" »
    __metaclass__ = MetaClass

    url = ‘/login

    def GET(self):
        «  »"page de login seul »" »
        return self.render.index()

    def POST(self):
        «  »"si c’est du post, on est pas loggué où on travaille à la poste «  »"
        login,mdp =  web.input()['login'], web.input()['password']

        utilisateur = Utilisateur.identification(login, mdp)
        if utilisateur :
            self.session['user'] = utilisateur
            return web.redirect(‘/’)
        return self.render.index()

class LogoutAction(BaseAction):
    «  »"action pour se délogguer »" »
    __metaclass__ = MetaClass

    url = ‘/logout

    @estConnecte
    def GET(self):
        «  »"on se déco »" »
        self.session.delete()
        return web.redirect(‘/’)

Je commence par LogoutAction.
Sa méthode GET() ne fait rien de plus que de supprimer la session de l’utilisateur, puis de le rediriger à la racine du site.

Et place à LoginAction.
Quand on arrive sur /login, alors la méthode GET() est appelée. Elle ne sert qu’à retourner la vue index (site/vues/index.html), que voici(c’est un simple formulaire, donc pas de commentaire) :

<form id= »connexion_form » method= »POST » action = « /login »>
    <fieldset>
        <legend>$_(« connexion »)</legend>
        <label for= »login »>$_(« login »)<input type= »text » name= »login » /></label>
        <label for= »password »>$_(« password »)<input type= »password » name= »password » /></label>
        <input type= »submit » value= »$_(‘ok’) » />
    </fieldset>
</form>

Ensuite, quand on valide ce formulaire, c’est donc la méthode POST() de LoginAction qui est appelée.
On récupère les valeurs du formulaire via web.input()["name_de_l_input"].
Ensuite via la méthode de classe identification() du modèle Utilisateur, on  tente de logguer l’utilisateur, et selon le résultat, on redirige sur IndexAction ou pas.

Et voilà, on peut se connecter, se deconnecter, fin du billet !

Dans la partie 3, on verra comment uploader un fichier.

Pour récupérer les sources de la partie 2 c’est ici, le sql et la conf pour Apache ici (n’oubliez pas de modifier vos hosts).

 

PS1 : je cherche un stage d’au moins à 3 mois à partir de juillet dans le dévelopmment Python (ou C)  /Administration Linux, plus d’infos ici.

PS2 : depuis quelques mois, je développe une plateforme de partage de contenus multimédia en python (webpy) : http://prod.qui-saura.fr (vous pouvez vous inscrire, le design est assez moche)
C’est plutôt bien avancé, mais je recherche un « associé »,  quelqu’un qui serait intéressé à continuer ce projet avec moi, donc s’il y a des intéressés faites-moi signe.

Leave a Reply