Dans la première partie de l’article, j’ai détaillé l’analyse et la conception que j’ai effectuées pour créer le système de blog de ce site.
Dans cette seconde partie, je vais passer à phase d’implémentation des fonctionnalités.
NOTE : Je code actuellement beaucoup en C (systèmes embarqués) et C#.Net (programmes PC). Il se pourrait que vous remarquiez que mon code Python n’adhère pas tout à fait aux standards du langage, et qu’il pourrait même ressemble à du code C#. Nul n’est parfait !
Les données
Les données associées aux commentaires vont être stockées dans une table de la base de données du site. Cette table s’appelle comments. Avec Web2Py, les tables sont définies dans un fichier modèle : db.py:
db.define_table('comments',
Field('articleId', type='reference articles'),
Field('userId', type='reference auth_user'),
Field('name', type='string'),
Field('mail', type='string'),
Field('url', type='string'),
Field('publish_date', type='datetime'),
Field('ip', type='string'),
Field('validated', type='boolean'),
Field('comment_text', type='text')
)
Cette table contient donc les champs suivants:
- id : identifiant du commentaire. Ce champs est ajouté automatiquement par Web2Py
- articleId : identifiant de l’article associé (référence vers la table articles)
- userId : identifiant de l’utilisateur si il est identifié, ou NULL (référence vers la table auth_user)
- name : nom de l’utilisateur
- mail : email de l’utilisateur
- url : url de l’utilisateur
- publish_date : date de publication du commentaire
- ip : adresse IP du client
- validated : flag (booléen) indiquant si le commentaire à été approuvé par un adminstrateur
- comment_text : le text du commentaire
Un module pour encapsuler les accès à la base de données
Lorsque j’ai implémenté la partie blog du site, j’avais créé un module Web2Py contenant des classes me permettant d’encapsuler les accès à la base de données via des classes (notament avec une classe Articles). Cela me permet de ne coder ces accès à la DB qu’une seule fois et de les réutiliser dans n’importe quel contrôleur du site.
Vu que les commentaires font partie intégrante des articles, c’est dans ce même module que j’ai ajouté deux classes.
La classe Comment, tout d’abord, est une classe ‘entité’ qui représente un commentaire. Cette classe permet d’initialiser ses champs avec les valeurs de la base de données, de les sauvegarder dans la base de données, ou de supprimer un commentaire. Le code est assez simple:
class Comment:
def __init__(self):
self.id = None
self.userId = None
self.articleId = None
self.name = None
self.url = None
self.date = None
self.ip = None
self.validated = None
self.content = None
self.mail = None
def loadFromDb(self, dbData = None, id = None):
if dbData == None:
if id != None:
dbData = current.db.comments(id)
else:
raise Exception('Argument error')
self.id = dbData.id
self.userId = dbData.userId
if self.userId != None:
user = current.db.auth_user(self.userId)
self.name= user.first_name + user.last_name
self.mail=user.email
else:
self.name = dbData.name
self.mail = dbData.mail
self.articleId = dbData.articleId
self.name = dbData.name
self.url = dbData.url
self.date = dbData.publish_date
self.ip = dbData.ip
self.validated = dbData.validated
self.content = dbData.comment_text
def saveToDb(self):
if self.id == None:
current.db.comments.insert(
userId = self.userId,
articleId = self.articleId,
name = self.name,
url = self.url,
publish_date = self.date,
ip = self.ip,
validated = self.validated,
comment_text = self.content,
mail = self.mail)
else:
current.db.comments.update(
current.db.comments.id==self.id,
userId = selfuserId,
articleId = self.articleId,
name = self.name,
url = self.url,
publish_date = self.date,
ip = self.ip,
validated = self.validated,
comment_text = self.content,
mail = self.mail)
current.db.commit()
def deleteFromDb(self):
current.db(current.db.comments.id==self.id).delete()
current.db.commit()
Grâce à cette classe, les contrôleurs de mon application ne devront pas se soucier des accès en base de données, il utiliserons directement des instances de la classe Comment.
La deuxième classe est la classe CommentsCollection, qui se charge de créer une liste de commentaires à partir de la base de données sur base de deux paramètres:
- L’identifiant de l’article associés aux commentaires recherchés
- La valeur du flag validated
class CommentsCollection:
def __init__(self):
pass
def getComments(self, articleId, validated_only = True):
queryArticleId = current.db.comments.articleId==articleId
queryValidated = (current.db.comments.validated == True) if validated_only == True else None
comments = current.db(queryArticleId & queryValidated).select(current.db.comments.ALL, orderby=~current.db.comments.publish_date)
collection = []
if comments != None:
for item in comments:
comment = Comment()
comment.loadFromDb(dbData=item)
collection.append(comment)
return collection
Un contrôleur pour gérer les commentaires
J’ai déjà un contrôleur nommé ‘blog‘, qui gère actuellement les articles du blog : lister les articles, rechercher un article bien précis, éditer les articles,…
Vu que les commentaires font partie intégrantes des articles, c’est ce contrôleur qui va se charger d’ajouter les commentaires aux articles, de les rechercher dans la base de données,…
La fonction qui nous intéresse dans ce contrôleur, c’est la fonction ‘readArtcle()‘. Elle se charge d’aller rechercher en base de données l’article souhaité et fournit ces données à la vue pour l’affiche. C’est cette fonction qui a été appelée pour générer la page sur laquelle vous vous trouvez actuellement!
Cette fonction ressemble à ceci:
def readArticle():
articleId = request.vars.articleId #le numéro d'article est passé en argument dans l'URL : http://codingfield.com/blog/readArticle?articleId=999
article = Article()
article.loadFromDb(articleId)
return dict(article=article)
Dans l’état actuel, cette fonction instancie donc un objet de type Article, et appelle sa fonction loadFromDb(). La classe article est une classe entité qui encapsule les accès DB pour charger un article depuis la DB et l’exploiter sous forme d’un simple objet Python (tout comme le fait la classe Comment détaillée dans le chapitre précédent). Ainsi, l’objet article est instancié, ses champs sont initialisés avec les valeurs trouvé en DB, et il est fournit à la vue pour être affiché.
Poster un commentaire
Nous souhaitons dans un premier temps ajouter un formulaire en plus du contenu de l’article. Ce formulaire permettra aux visiteurs de poster un nouveau commentaire pour l’article qu’il est en train de lire.
La création de formulaire est très simple avec Web2Py:
userId = auth.user.id
mail = auth.user.email
name = auth.user.first_name + ' ' + auth.user.last_name
form = FORM(
TABLE(
TR('Nom/pseudo: ', INPUT(_name='name', value=name, _readonly=ON if auth.user!=None else False)),
TR('Site web: ', INPUT(_name='url', value='')),
TR('Adresse email: ', INPUT(_name='mail', value=mail, _readonly=ON if auth.user!=None else False)),
TR('Commentaire: ', TEXTAREA(_name='content', value='')),
TR(INPUT(_type='submit'))
))
Une fois affiché, ce formulaire ressemblera à ceci:
Les champs name et email seront initialisés avec les données du profile de l’utilisateur si il est identifié sur le site. Et dans ce cas, ces champs ne seront pas modifiables. Cela permet de pré-remplire le formulaire avec les données des utilisateurs identifiés, pour qu’ils ne doivent pas tout ré-écrire à chaque commentaire.
Ensuite, nous devons traiter les données du formulaire une fois que l’utilisateur clique sur le bouton Valider:
if form.process().accepted:
comment = Comment()
comment.articleId = articleId
comment.userId = userId
comment.name = form.vars.name
comment.mail = form.vars.mail
comment.url = form.vars.url
comment.content = form.vars.content
comment.date = datetime.datetime.now()
comment.validated = True
comment.ip = request.client
comment.saveToDb()
Très simplement, un objet de type Comment (voir le chapitre précédent) est instancié, et ses champs sont initialisés avec les valeurs du formulaire. Actuellement, je n’ai pas encore implémenté la fonctionnalité permettant à l’administrateur de valider les commentaires avant de les poster sur le site, ils seront donc validés par défaut. Finalement, la fonction saveToDb() est appelée. J’ai décris cette fonction dans le chapitre précédent, elle permet de sauvegarder l’objet dans la base de données. Remarquez bien que l’identifiant de l’article qui est affiché actuellement est bien sauvegardé dans le commentaire. C’est de cette façon que la relation entre les articles et les commentaires peut être établie.
N’oublions pas de passer le formulaire à la vue :
return dict(article=article, commentForm = form)
Afficher les commentaires
Maintenant que la DB contient des commentaires, nous pouvons les charger en même temps que l’article afin de pouvoir les afficher. En fait, les commentaires sont chargés dans la fonction loadFromDb() de la classe Article. En effet, les articles sont inclus dans les articles. C’est donc à la classe Article de s’occuper de faire le lien entre les articles et les commentaires. Pour ce faire, j’ai ajouté un champs à cette classe (self.comments). Il s’agit d’un tableau d’objet Comment. Dans la fonction loadFromDb de la classe Article, ce tableau se voit assigner des commentaires:
def classe Article
...
def loadFromDb(self):
...
#Initialisation des champs avec les valeurs obtenues en DB
...
commentsCollection = CommentsCollection()
self.comments = commentsCollection.getComments(self.id) # self.id est l'identifiant de l'article en DB
...
Les classes Comment et CommentsCollection ont été expliquées dans le chapitre précédent.
Le contrôleur, au final
def readArticle():
articleId = request.vars.articleId
name = ''
mail = ''
userId = None
if auth.user != None:
userId = auth.user.id
mail = auth.user.email
name = auth.user.first_name + ' ' + auth.user.last_name
form = FORM(
TABLE(
TR('Nom/pseudo: ', INPUT(_name='name', value=name, _readonly=ON if auth.user!=None else False)),
TR('Site web: ', INPUT(_name='url', value='')),
TR('Adresse email: ', INPUT(_name='mail', value=mail, _readonly=ON if auth.user!=None else False)),
TR('Commentaire: ', TEXTAREA(_name='content', value='')),
TR(INPUT(_type='submit'))
))
if form.process().accepted:
comment = Comment()
comment.articleId = articleId
comment.userId = userId
comment.name = form.vars.name
comment.mail = form.vars.mail
comment.url = form.vars.url
comment.content = form.vars.content
comment.date = datetime.datetime.now()
comment.validated = True
comment.ip = request.client
comment.saveToDb()
articleId = request.vars.articleId
article = Article()
article.loadFromDb(articleId)
return dict(article=article, commentForm = form)
Comme vous pouvez le constater, tout est (presque) parfaitement encapsulé et découplé :
- Le contrôleur blog charge une collection d’objets de type Article, sans savoir de quoi est constitué un article, et sans devoir se soucier des commentaires
- La classe Article a la responsabilité d’aller rechercher les données d’un article en base de données, et délègue la gestion des commentaires à la classe Comments.
- La classe Comments se charge de créer des objets correspondants aux commentaires sans avoir aucune connaissances des détails de la classe Article, elle ne connait que la valeur de l’identifiant de l’article correspondant aux commentaires qu’elle doit retrouver.
- La classe Comments n’est donc pas dépendante de la classe Article. Rien n’empêcherait d’utiliser cette classe pour commenter des images ou des produits d’un magasin. Le seul changement à effectué se situerait au niveau de l’identifiant, qui représenterait une image ou un produit au lieu d’un article. Mais la logique resterait la même. En Orienté-Objet pure, il aurait sans doute été conseillé de passer par une interface pour découpler complètement la classe Comments de la classe Article. Mais dans ce cas-ci, ce serait certainement un peu too-much, et je ne sais pas comment implémenter des interfaces dans la base de données…
Une vue pour afficher le tout
Voilà, le plus dur est fait: les données peuvent être stockées et chargées depuis la base de données, et l’intelligence nécessaire à été codée pour que les commentaires soient bien liés à leur article. Il ne reste plus qu’à afficher tout cela sur une jolie page web. Avec Web2Py, cela se fait en créant une vue portant le même nom que la fonction du contrôleur. Dans ce cas, j’ai donc créé un fichier readArticle.html dans le répertoire views/blog de mon application:
{{extend 'layout.html'}}
...
{{=P(XML(article.content))}}
{{=HR()}}
{{if (article.comments == None) or (len(article.comments) == 0):}}
{{=P(T('No comments yet'))}}
{{else:}}
{{for comment in article.comments:}}
{{=H3(comment.name)}}
{{=I(comment.date.strftime('%B %d, %Y %H:%M:%S'))}}
{{=BR()}}
{{=XML(comment.content)}}
{{pass}}
{{pass}}
{{=H2(T('Post a new comment'))}}
{{=commentForm}}
Cette vue commence par afficher le contenu de l’article. L’article est l’objet article, instance de la classe Article, qui a été fournit par le contrôleur blog, dans sa fonction readArticle()
Ensuite, si l’article contient des articles, ils seront tous affichés.
Et enfin, le formulaire permettant de poster un nouveau commentaire est affiché.
De nouveau, nous pouvons admirer la beauté du découplage et du pattern MVC proposé par Web2py. En effet, la vue ne fait qu’afficher des données, et ne contient aucune intellligence : il n’y a aucune requête SQL, aucun traitement pour retrouver les commentaires de l’article,… Tout cela a été fait par le contrôleur blog.
Conclusions de la partie 2
Dans cette partie, nous avons donc passé en revue les différentes étapes de l’implémentation de notre projet. Nous sommes partis de ‘spécifications fonctionnelles’, telles qu’un chef de projet auraient pu écrire. Dans ces spécifications, les fontionnalités ont été décrites avec plus ou moins de détails, mais sans donner aucun détail d’implémentation : il n’a jamais été question de dire qu’il fallait créer une classe Comments et que les commentaires seraient stockés sous forme de tableau.
Dans la phase de conception, nous avons réfléchit à comment nous allions architecturer le projet : les champs de la base de données ont été définis, et on a déjà réfléchis à comment les différents éléments stratégique du code allaient se goupiller pour permettre au projet d’arriver à ses fins.
Mais ce n’est quand dans cette phase d’implémentation que nous avons réellement réfléchit au code en lui-même. L’implémentation fournit bien une solution technique à un problème fonctionnel : le code permet de remplire les fonctions qui ont été spécifiée.
Certes, nous n’avons pas encore implémenté toutes les fonctions, et notament toutes les fonctions d’administration. Mais le principal est fait, la structure du code est là, et rajouter les autre fonctionnalités ne devrait pas poser de problème!