Analyse, conception et implémentation d’un système de commentaires – partie 2

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!

Leave a Reply

Your email address will not be published. Required fields are marked *