Published by

Il y a 10 ans -

Temps de lecture 11 minutes

NoThunes, l’espace VIP

Projet NoThunes

Continuons le montage de notre projet OpenSource propulsé par Grails. Au dernier épisode, nous avions démarré le projet, façonné les styles CSS, mis en place la sécurité, mais aussi et surtout, rédigé les classes du modèle de données. Aujourd’hui nous allons leur donner vie en ajoutant des fonctionnalités aux utilisateurs membres.

Nous nous attarderons sur :

  • la gestion du profil utilisateur
  • la génération des écrans CRUD de nos données
  • leur adaptation à nos ‘règles métier’
  • la gestion des upload et download

Ces étapes peuvent paraître triviales à ceux qui ont déjà une expérience de Grails. Cependant, j’ai choisi de les traiter malgré tout. J’ai moi-même perdu trop de temps à chercher des exemples concrets et simples sur la toile pour oser faire l’impasse dessus. Donc, toi qui débute sur Grails, sois le bienvenu.

Sprint backlog détaillé

En tant que … je dois pouvoir … détail …
membre voir et mettre à jour mon profil utilisateur Un membre authentifié doit pouvoir modifier son profil, sauf son login et ses rôles.
membre créer/modifier/supprimer des groupes de musiques Un membre authentifié doit pouvoir créer des groupes de musique et lister/modifier/supprimer les groupes qu’il a créés.
membre créer/modifier/supprimer des albums liés à mes groupes Si un membre a créé un groupe de musique, il peut créer un album pour ce groupe.

Gestion du profil utilisateur

Confirmation des modifications de mot de passe

Lorsque nos membres pourront mettre à jour leur profil, ils pourront également modifier leur mot de passe. Pour commencer, il faut donc que nous fassions des adaptations pour ajouter la confirmation du mot de passe dans la gestion des comptes utilisateurs. En deux coups de cuillère à pot, cela donne :

  • Ajout d’un champ confirmPasswd dans la classe de domaine User, et d’un validateur associé :

grails-app/domain/fr/xebia/nothunes/security/User.groovy

class User {
   ...
   static transients = ['confirmPasswd']
   ...
   String confirmPasswd

   static constraints = {
      ...
      confirmPasswd(validator :{val, obj ->
         if (obj.properties['passwd'] != val) {
            return 'default.invalid.confirmPasswd.message'
         }
      })
   }
}

Le champ nouvellement créé est passé en transient pour éviter que sa valeur ne soit persistée en base. Notez que la valeur retournée par le validateur est un String correspondant au message d’erreur à afficher. Par conséquent, il faut ajouter ce message dans les fichiers messages.properties pour avoir l’affichage de l’erreur dans les GSP :

grails-app/i18n/messages.properties

default.invalid.confirmPasswd.message=Bad password confirmation
  • Modification du UserController :

Pour que le champ confirmPasswd soit correctement renseigné avant la validation, il faut aller modifier la classe UserController. Dans les méthodes save() et update(), le champ passwd est chiffré avant sauvegarde en base. Il faut appliquer le même traitement au champ confirmPasswd. De cette façon le validateur fait la comparaison entre les deux champs chiffrés. Si les versions chiffrées sont identiques alors la confirmation du mot de passe est valide, CQFD.

grails-app/controllers/UserController.groovy

...
// à chaque occurence du chiffrage du champ 'passwd' ...
person.passwd = authenticateService.encodePassword(params.passwd)

// on ajoute le chiffrage du champ 'confirmPasswd'
person.confirmPasswd = authenticateService.encodePassword(params.confirmPasswd)
...
  • Modification des formulaires de création et d’édition d’un User :

Pour l’instant les formulaires de création et d’édition des User ne contiennent pas de champ pour confirmPasswd donc le validateur renvoie toujours une erreur. Il suffit d’ajouter un champ texte, initialisé avec la valeur du mot de passe.

grails-app/views/user/create.gsp ET grails-app/views/user/edit.gsp

...

   
   
      
   


          

   
   
      
   

...

Création d’un contrôleur dédié à l’affichage et la mise à jour du profil

Maintenant que nous disposons d’une validation correcte pour la mise à jour des mots de passe, nous pouvons attaquer le cœur du sujet. Pour cela, on crée un nouveau contrôleur grâce à la commande :

grails create-controller fr.xebia.nothunes.profile.Profile

Cela génère une coquille vide nommée ProfileController. Le but de ce contrôleur est de permettre d’afficher le profil de l’utilisateur courant et de le mettre à jour. Par conséquent il faut créer les méthodes :

  • index : qu’on redirigera vers show. Dans le cas où le contrôleur est appelé sans méthode particulière, c’est index() qui sera lancé.
  • show : pour récupérer le compte de l’utilisateur courant en base et l’afficher.
  • edit : pour récupérer le compte de l’utilisateur courant en base et afficher le formulaire d’édition.
  • update : pour persister en base un envoi de formulaire d’édition. Si la sauvegarde se passe bien, à la fin on redirige vers show() avec un message de succès ; sinon on redirige vers edit() avec un message d’erreur.

Conjointement, il nous faut 2 vues :

  • profile/show.gsp : pour afficher le profil. Elle contient un bouton pour passer en édition.
  • profile/edit.gsp : pour éditer le profil. Elle contient un formulaire qui pointe sur la méthode update du contrôleur.

Afin de ne pas trop alourdir ce billet, je vous renvoie au GitHub pour le détail des vues et du contrôleur. Le code est très fortement inspiré de ce qui existe déjà dans le UserController et ses vues.

Pour être sûr que l’utilisateur est authentifié quand il accède au contrôleur, on ajoute dans les règles de sécurité la protection de /profile/* directement dans le bootstrap de l’application :

grails-app/conf/BootStrap.groovy

class BootStrap {

   def init = { servletContext ->
      ...
      def protectUserProfileManaging = new RequestMap(url: '/profile/*', configAttribute: 'ROLE_ADMIN,ROLE_USER').save()
   }
   ...
}

Une fois tout ceci fait, il suffit de rajouter le lien vers l’affichage du profil dans la GSP qui contient le menu pour les membres :

grails-app/views/menu/_user.gsp

...
   

Member manages ...

  • your profile
...

On redémarre notre application et le tour est joué !

edit_profil

Gestion des CRUD métiers : les classes Band et Album

Attaquons maintenant la gestion des CRUD de nos objets métiers. Premier de la liste : Band. Pour rappel, cette classe représente un groupe de musique géré par un utilisateur membre. Un membre doit pouvoir créer des groupes et modifier uniquement ceux qu’il a créé. Pour avoir un petit rappel sur le contenu de la classe Band, voir sur le GitHub. Grails nous offre un démarrage rapide en générant les vues et le contrôleur à partir de la classe de domaine avec la commande :

grails generate-all fr.xebia.nothunes.domain.Band

Le contrôleur et les vues générés sont génériques et doivent être adaptés pour nos besoins particuliers. Ces fonctionnalités doivent être réservées aux membres, donc on ajoute dans le bootstrap les règles de sécurité pour protéger le contrôleur :

grails-app/conf/BootStrap.groovy

class BootStrap {

   def init = { servletContext ->
      ...
      def protectUserProfileManaging = new RequestMap(url: '/band/*', configAttribute: 'ROLE_ADMIN,ROLE_USER').save()
   }
   ...
}

Ensuite il faut rajouter des contrôles métier pour permettre aux membres de ne lister, visualiser et modifier que les Band qu’ils ont créés. Pour cela, on va s’appuyer sur l’appartenance de la classe Band à un User (lien belongsTo). Pour commencer, la méthode list ne doit afficher que les groupes dont l’utilisateur est propriétaire :

grails-app/controllers/fr/xebia/nothunes/domain/BandController.groovy

...
def list = {
   params.max = Math.min(params.max ? params.int('max') : 10, 100)
   def owner = User.get(authenticateService.userDomain().id)
   [bandInstanceList: Band.findAllByOwner(owner, params), bandInstanceTotal: Band.count()]
}
...

Ensuite, lors d’une sauvegarde, on doit définir le propriétaire du Band avant la sauvegarde

grails-app/controllers/fr/xebia/nothunes/domain/BandController.groovy

...
def save = {
   def bandInstance = new Band(params)
    
   // on définit le propriétaire du Band
   bandInstance.owner = User.get(authenticateService.userDomain().id)
   ...
}
...

Enfin, à l’entrée des méthodes show, edit et update du BandController on teste la légitimité de l’utilisateur courant à agir sur le Band demandé de la façon suivante :

grails-app/controllers/fr/xebia/nothunes/domain/BandController.groovy

...
def show = {
   def bandInstance = Band.get(params.id)

   // on recupere le user courant
   def currentUser = User.get(authenticateService.userDomain().id)
    
   // s'il existe et qu'il est propriétaire du Band demandé, on autorise la suite
   if (bandInstance && currentUser && bandInstance.owner == currentUser) {
      [bandInstance: bandInstance]
   } else {
   // sinon on affiche un message et on le renvoie sur la liste de ses propres groupes
      flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'band.label', default: 'Band'), params.id])}"
      redirect(action: "list")
   }
}
...

Et, touche finale, on ajoute le lien vers la gestion des Band dans le menu utilisateur :

grails-app/views/menu/_user.gsp

...
   

Member manages ...

  • your profile
  • your bands
...

La technique est la même pour les Albums :

  • génération des vues et du contrôleur
  • protection par les règles de sécurité
  • ajout de contrôles métier pour filtrer les actions utilisateurs

Pour le détail du code tournant autour des Albums, faites un tour par le GitHub.

Upload d’une image logo pour les Band

Pour rendre notre gestion de Band un peu plus sexy, on y ajoute l’upload d’une image qui servira de logo. Rien de plus facile. Nous commençons par ajouter dans la configuration de l’application un chemin vers un répertoire de stockage :

grails-app/conf/Config.groovy

...
storage.image.directory='/tmp/nothunes_images/'
...

Pour pouvoir réutiliser la méthode d’upload de fichier, on crée un service grails dédié :

grails create-service fr.xebia.nothunes.upload.Upload

Puis on crée une méthode saveImage, qui s’occupe de vérifier le type mime du fichier soumis, et de le stocker dans le répertoire défini dans la Config de l’application.

class UploadService {
  
   def authorizedImageContentType = [ 'image/jpeg' :'jpg', 'image/gif' :'gif', 'image/png' :'png']
  
   def grailsApplication
  
   def saveImageFile(anImageFile, name) {
      if (!anImageFile.empty) {

         FileNameMap fileNameMap = URLConnection.getFileNameMap();
         def contentType = fileNameMap.getContentTypeFor(anImageFile.originalFilename)
      
         if ( authorizedImageContentType.keySet().contains(contentType) ) {
            File storageDir = new File(grailsApplication.config.storage.image.directory)
            if (!storageDir.exists()) {
               if (!storageDir.mkdir()) {
                  log.error 'Directory does not exist and cannot be created '+grailsApplication.config.storage.image.directory
                  return false
               }
            }

            // Pour un minimum de standardisation, on génère le nom du fichier à enregistré à partir du paramètre
            // 'name' et du type mime
            def targetFilename = name + '_logo.' + authorizedImageContentType[contentType]

            // sauvegarde du fichier
            anImageFile.transferTo( new File(grailsApplication.config.storage.image.directory + targetFilename) )
        
            return targetFilename
         } else {
            log.debug 'Someone tried to upload a non-image file : '+contentType
            return false
         }
      } else {
         return false
      }
   }
}

Ensuite on modifie nos GSP de création et d’édition pour ajouter un champ de type file et faire des formulaires multipart :

grails-app/views/band/create.gsp ET grails-app/views/band/edit.gsp



...
   
      
         
      
      

         
         
      
   
...

A la soumission du formulaire, on peut désormais récupérer le fichier et le passer à notre service de sauvegarde en appelant :

uploadService.saveImageFile(request.getFile('logoFile'), bandInstance.name)

Le paramètre passé à la méthode request.getFile() correspond à l’attribut name du champ input de notre formulaire. Encore une fois pour le code complet, voir le GitHub. Il va de soi que ce code n’est pas en l’état complet, et qu’il faut lui ajouter des fonctionnalités techniques. Par exemple, pour supprimer les fichiers lors d’un changement d’image, ou dans d’autres cas tordus. La plupart de ces cas sont traités dans le code du projet. Je vous laisse lire le détail de l’implémentation directement dans le code, cet article étant déjà assez (trop ?) verbeux. :-)

band_created

Contrôleur de téléchargement

Nos images sont maintenant dans le répertoire dédié au stockage, mais on ne peut pas les afficher en faisant un lien directement dans nos pages, puisqu’elles sont en dehors de l’arborescence du site. Pour pallier ce problème, on crée un nouveau contrôleur, dédié au téléchargement des images. Le but est de pouvoir insérer dans nos GSP des tag <img/> avec un attribut src="..." pointant sur notre contrôleur. Encore une fois la méthode est assez directe :

  • création d’un DlController :
grails create-controller fr.xebia.nothunes.Dl
  • création d’une méthode images pour servir l’image en fonction des paramètres de requête :
class DlController {
  
   def images = {
      String filename = params.id
      log.debug "dl image file : ${filename}"
    
      def file = new File(grailsApplication.config.storage.image.directory + filename)
      if (file.exists()) {
         response.setContentType("application/octet-stream")
         response.setHeader("Content-Disposition", "attachment; filename=${filename}")
         response.setContentLength(file.readBytes().size())
         response.getOutputStream() << file.readBytes()
      }
      response.flush()
   }
}

Nous pouvons maintenant afficher l'image en créant un lien vers ce contrôleur :

grails-app/views/band/show.gsp

...

...
album_created

Sprint review

Comme je l'avais annoncé dans l'intro, ce billet est certainement plus intéressant pour ceux qui débutent avec Grails. Nous avons pu voir une méthode de création d'un contrôleur de bout en bout, et une façon simple de stocker et servir des fichiers à l'extérieur de l'arborescence du site. Retenez que les vues et contrôleurs générés par Grails sont très ouverts, il faut donc bien se souvenir de systématiquement :

  • les protéger en rajoutant de filtres URL dans le bootstrap
  • ajouter les contrôles métiers si besoin dans le code du contrôleur
  • nettoyer les vues des informations techniques, superflues pour les utilisateurs

Mission accomplie : le sprint backlog est bouclé et notre projet est livrable à la fin du sprint, ce qui reste un point crucial. Au prochain épisode, on va s'occuper des classes Tracks et de la navigation pour les internautes, le tout en utilisant massivement AJAX.

Ressources

Published by

Publié par Aurélien Maury

Aurélien est passionné par les frameworks web haute productivité comme Grails, JRuby on Rails ou Play! framework. Il est également intéressé par tous les aspects de performance et d'optimisation (mais aussi par la phytothérapie, la PNL, la basse électrique, la philosophie et pleins d'autres sujets). Il est également formateur au sein de Xebia Training .

Commentaire

6 réponses pour " NoThunes, l’espace VIP "

  1. Published by , Il y a 10 ans

    Tu devrais mettre le field ‘confirmPasswd’ transient car ca sert a rien de le stocker dans la bd!

  2. Published by , Il y a 10 ans

    Très bonne remarque, en fait c’était déjà le cas dans les sources complètes, mais je l’avais passé sous silence dans les ‘…’. J’ai mis à jour l’article pour plus de clarté. Merci.

  3. Published by , Il y a 10 ans

    C’est drôles quelques jour avant la publication du premier article, j’avais moi aussi commencé une application du même genre, aussi en grails.
    Autre remarque, pourquoi limiter la vu des utilisateur aux groupes qu’ils ont crée ?
    Alors qu’il serait plus simple de laisser tout le monde travailler sur les même groupe,piste etc. Mon approche est plus participative.

  4. Published by , Il y a 10 ans

    J’ai encore une question.
    Pourquoi utiliser un controleur pour le download, pourquoi pas un service? A cause de la transactionalité ?

  5. Published by , Il y a 10 ans

    A la base je voyais les utilisateurs enregistrés comme des membres de différents groupes de musique. Donc, pour éviter que quelqu’un d’extérieur à un groupe puisse lui attribuer des chansons, j’ai cloisonner au niveau utilisateur.

    Par contre il aurait peut-être été intéressant de pouvoir rattacher d’autres utilisateurs à un même groupe de musique. J’y penserait peut-être pour la suite.

    Pour ce qui est du download, c’est simplement parce que, par convention, seuls les contrôleurs sont exposés dans l’appli. Les services sont des composants utilisés par un ou plusieurs contrôleurs. Il aurait été possible de mettre ce code dans un service et d’y faire appel depuis le contrôleur.

    Et si on a pas besoin de transaction dans ce service, qui ne fait aucun accès aux données, on peut rajouter dedans un :

    static transactional = false

  6. Published by , Il y a 10 ans

    je travaille sur une application en grails mais je rencontre une problème d’affichage de l’image téléchargé(l’icone d’image existe mais l’image n’affiche pas)
    merci

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.