Ok

En poursuivant votre navigation sur ce site, vous acceptez l'utilisation de cookies. Ces derniers assurent le bon fonctionnement de nos services. En savoir plus.

06 février 2010

Franciser les messages

Franciser les messages

Par défaut, l'ensemble des libellés et messages apparaissent en anglais. Pour franciser l'interface il faut installer le plugin d'internationalisation

Grails install-plugin i18n-templates

Cette commande installe une nouvelle commande

grails generate-i18n-messages

qui permet de créer les messages pour les divers objets de domaine de l'applications.

Cependant, le script permet de créer les entrés, mais avec un texte anglais. Il faut donc remplacer le script par une version HQ française. Ce script se trouve dans le répertoire D:Documents and SettingsCF2098.grails1.1.1projectsNOM DU PROJETpluginsi18n-templates-1.1.0.1scripts. Il suffit de le remplacer par la version HQ.

Par exemple pour deux classes simple, exécuter le script produit la sortie suivante:

# Emp messages par Hydro-Québec, Jean Desbiens

emp.create=Créer Emp

emp.edit=Editer Emp

emp.list=Liste des Emps

emp.new=Nouveau Emp

emp.show=Afficher Emp

emp.created=Emp {0} créé

emp.updated=Emp {0} mis à jour

emp.deleted=Emp {0} supprimer

emp.not.found=Emp introuvable avec identifiant {0}

emp.not.deleted=Emp non supprimé avec identifiant {0}

emp.optimistic.locking.failure=Un autre utilisateur a mis à jour Emp pendant que vous le modifiez

emp.id=Id

emp.nom=Nom

emp.nom.nullable.error=La propriété [Nom] de l'objet [Emp] ne peut être nulle

# Book messages par Hydro-Québec, Jean Desbiens

book.create=Créer Book

book.edit=Editer Book

book.list=Liste des Books

book.new=Nouveau Book

book.show=Afficher Book

book.created=Book {0} créé

book.updated=Book {0} mis à jour

book.deleted=Book {0} supprimer

book.not.found=Book introuvable avec identifiant {0}

book.not.deleted=Book non supprimé avec identifiant {0}

book.optimistic.locking.failure=Un autre utilisateur a mis à jour Book pendant que vous le modifiez

book.id=Id

book.titre=Titre

book.titre.nullable.error=La propriété [Titre] de l'objet [Book] ne peut être nulle

Il suffit de coller ce code dans le bundle de ressources FR.

Modifier les templates afin d'avoir la normes Hydro-Québec

Modifier les templates afin d'avoir la normes Hydro-Québec

Pour modifier les templates, il faut télécharger le plugin i18n-templates. On trouvera ensuite les templates dans /src/templates/scaffolding. Dans ce répertoire, on retrouvera quatre templates représentant les vues principales (show, edit, create, list), un template servant à générer les contrôleurs de domaine, ainsi qu'un script qui sert à générer les champs de saisi. Tous les templates des vues principales se réfèrent à un layout par défaut situé dans le fichier main.gsp.

Pour obtenir le standard HQ, copier les fichiers personnalisés par l'entreprise dans ce répertoire.

02 février 2010

Tests unitaires d'une application grails simple

Tests unitaires

Afin de m'assurer dans le futur que les objets d'affaires remplissent bien leur contrat j'ai rédigé des tests pour les fonctionnalités attendue. Le gros de notre logique d'affaires est actuellement sous forme de contraintes. Les tests vont donc tourner autour de ces contraintes.

 

Les tests pour la classe Suivi sont assez simple car il s'agit d'une table simple avec peu de données.

 

class SuiviTests extends GroovyTestCase {

 

 

    void testUnSuiviMinimal() { 

            def p = new Projet(nom:"Un projet",dateProposition:new Date())

      def s = new Suivi(notes:"Test", projet:p)

      s.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+s.errors,s.hasErrors()

            s.notes = ""

            s.validate()

      assertTrue "Il devrait  y avoir erreur: "+s.errors,s.hasErrors()

    } 

}

 

Celui pour la classe de marché est aussi simple:

 

class MarcheTests extends GroovyTestCase {

 

    void testUnMarcheMinimal() { 

      def m = new Marche(nom:"Test")

      m.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",m.hasErrors()

            m.nom = ""

            m.validate()

      assertTrue "Il devrait  y avoir erreur: "+m.errors,m.hasErrors()

    } 

   

      void testNomUnique() {      

      def m = new Marche(nom:"Test")

            m.save()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",m.hasErrors()

 

      def m2 = new Marche(nom:"Test")

 

      m2.validate()

      assertTrue "Il devrait y avoir une erreur avec l'unicité du nom: ",m2.hasErrors()

    } 

 

      void testNomLongueur() {    

      def m1 = new Marche(nom:"T000000000T000000000T0000")

            m1.validate()

            assertFalse "Une longeur du nom de 25 char devrait être ok: ",m1.hasErrors()

            m1.nom="T000000000T000000000T000001"

            m1.validate()

      assertTrue "Il devrait y avoir une erreur sur la longeur du nom: "+m1.nom,m1.hasErrors()

    }

}

 

Celui pour la classe Personne est plus intéressant en particulier pour les tests sur le CIP:

 

class PersonneTests extends GroovyTestCase {

 

    void testUnPersonneMinimal() {      

      def p = new Personne(nom:"TestNom",prenom:"TestPrenom",cIP:"XX9999")

      p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",p.hasErrors()

    } 

 

      void testCIPUnique() {      

      def p = new Personne(nom:"TestNom",prenom:"TestPrenom",cIP:"XX9999")

            p.save()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",p.hasErrors()

 

      def p2 = new Personne(nom:"TestNomAutre",prenom:"TestPrenomAutre",cIP:"XX9999")

      p2.validate()

      assertTrue "Il devrait y avoir une erreur avec l'unicité du nom: ",p2.hasErrors()

    } 

     

      void testCIPValide() {      

      def p = new Personne(nom:"TestNom",prenom:"TestPrenom",cIP:"XX9999")

            p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",p.hasErrors()

            p.cIP = "aa0000"

            p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs avec: " + p.cIP,p.hasErrors()

            p.cIP = "ZZ9999"

            p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs avec: " + p.cIP,p.hasErrors()

      }

 

      void testCIPInValide() {    

      def p = new Personne(nom:"TestNom",prenom:"TestPrenom",cIP:"XX9999")

            p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: ",p.hasErrors()

            p.cIP = "aa000" // trop court

            p.validate()

      assertTrue "Il devrait y avoir erreurs avec cip court: " + p.cIP,p.hasErrors()

            p.cIP = "aa00000" // trop long

            p.validate()

      assertTrue "Il devrait y avoir erreurs avec cip long: " + p.cIP,p.hasErrors()

            p.cIP = "123456" // juste des chiffres

            p.validate()

      assertTrue "Il devrait y avoir erreurs avec juste des chiffres: " + p.cIP,p.hasErrors()

            p.cIP = "abcdef" // juste des lettres

            p.validate()

      assertTrue "Il devrait y avoir erreurs avec juste des lettres: " + p.cIP,p.hasErrors()

            p.cIP = "99ABCD" // inversion du pattern

            p.validate()

      assertTrue "Il devrait y avoir erreurs avec un autre pattern: " + p.cIP,p.hasErrors()

           

      }

}

 

Finalement pour la classe Projet, le cœur du système:

 

class ProjetTests extends GroovyTestCase {

 

    void testUnProjetMinimal() { 

      def p = new Projet(nom:"Test",dateProposition:new Date())

      p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p.errors,p.hasErrors()

    } 

   

      void testNumeroValide() {   

      def p1 = new Projet(nom:"Test",dateProposition:new Date(),numero:1000)

      p1.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p1.errors,p1.hasErrors()

 

      def p2 = new Projet(nom:"Test",dateProposition:new Date(),numero:32767)

      p2.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p2.errors,p2.hasErrors()

 

      def p3 = new Projet(nom:"Test",dateProposition:new Date(),numero:9999)

      p3.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p3.errors,p3.hasErrors()

     

    } 

 

      void testNumeroInValide() { 

      def p = new Projet(nom:"Test",dateProposition:new Date(),numero:-1000)

      p.validate()

      assertTrue "Il devrait y avoir une erreur avec -1000: "+p.errors,p.hasErrors()

 

      def p2 = new Projet(nom:"Test",dateProposition:new Date(),numero:999)

      p2.validate()

      assertTrue "Il devrait y avoir une erreur avec 999: "+p2.errors,p2.hasErrors()

    } 

 

      void testNumeroUnique() {   

      def p1 = new Projet(nom:"Test",dateProposition:new Date(),numero:1000)

            p1.save()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p1.errors,p1.hasErrors()

 

      def p2 = new Projet(nom:"Test",dateProposition:new Date(),numero:1000)

      p2.validate()

      assertTrue "Il devrait y avoir une erreur avec l'unicité du numéro: "+p2.errors,p2.hasErrors()

    } 

 

      void testNomUnique() {      

      def p1 = new Projet(nom:"Test",dateProposition:new Date())

            p1.save()

      assertFalse "Il ne devrait pas y avoir d'erreurs: "+p1.errors,p1.hasErrors()

 

      def p2 = new Projet(nom:"Test",dateProposition:new Date(),numero:1000)

      p2.validate()

      assertTrue "Il devrait y avoir une erreur avec l'unicité du nom: "+p2.errors,p2.hasErrors()

    } 

 

      void testNomLongueur() {    

      def p1 = new Projet(nom:"T000000000T000000000T000000000T000000000T000000000T000000000T000000000T000000000",dateProposition:new Date())

            p1.validate()

            assertFalse "Une longeur du nom de 80 char devrait être ok: "+p1.errors,p1.hasErrors()

            p1.nom="T000000000T000000000T000000000T000000000T000000000T000000000T000000000T0000000001"

            p1.validate()

      assertTrue "Il devrait y avoir une erreur sur la longeur du nom: "+p1.errors,p1.hasErrors()  

    }

 

      void testBudgetValide() {   

      def p = new Projet(nom:"Test",dateProposition:new Date(),budget:1f)

      p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs avec budget de 1: "+p.errors,p.hasErrors()

            p.budget = 100000f

      p.validate()

      assertFalse "Il ne devrait pas y avoir d'erreurs avec budget de 100k: "+p.errors,p.hasErrors()

    } 

 

      void testBudgetInValide() { 

      def p = new Projet(nom:"Test",dateProposition:new Date(),budget:-1f)

      p.validate()

      assertTrue "Il devrait y avoir une erreur avec un budget négatif de -1: "+p.errors,p.hasErrors()

    }

 

      void testBudgetParDefaut() {      

      def p = new Projet(nom:"Test",dateProposition:new Date())

      assertEquals "j'attendais 20000",20000,p.budget

    }

}

 

En rédigeant ces tests et en les exécutants j'ai trouvé 3 bugs dans mon code. Le premier lors du test sur la contraintes sur la longueur du nom de projet, j'utilisais la contrainte maxLength au lieu de maxSize. Le second bug avait trait au regular expression que j'utilisais pour le CIP. J'avais "[a-zA-Z]2[0-9]6" au lieu de  "[a-zA-Z]{2}[0-9]{6}". Finalement il y avait une autre erreur dont je ne me souviens plus.

 

Les tests ont aussi été utile lorsque j'ai déployé l'application sur une autre machine. Quand j'ai fait Grails run-app, ça n'a pas compilé! Alors j'ai fais une Grails test-app et j'ai tout de suite vue quel test foirrait et la correction à pris 10 secondes. L'erreur était liée au fait que j'ai développé avec Grails 1.1 et que l'autre machine était sur 1.0.4.

 

Les tests produisent des rapports, dont voici un exemple.

grailsUnit1.jpg

 

 

 

 

 

 

 

Clickez pour une version plus grande

 

Et le détail :

grailsUni2.jpg

14 janvier 2010

Créer une application Grails de base

Après avoir monté une première démo avec Grails (voir les vidéos http://agile.blogspirit.com/archive/2009/03/14/demonstration-de-grails.html) j'ai voulu refaire le tour du jardin en couvrant la base de nos besoins en développement rapide. Je suis parti avec un modèle simple où il y avait la plupart des types de données et de relations, bien que ce ne soit pas exhaustif (par exemple il n'y a pas d'héritage). Je me suis inspiré d'un système que nous avons réalisé récemment (IP-Suivi de projets).

 

J'ai donc esquissé le modèle objet du système

 

UML01.jpg

 

En fait, pour être honnête, la figure précédente est ce que j'ai réalisé. Le modèle initial ressemblait à ceci:

 

UML02.jpg

 

J'ai aussi esquissé la navigation de l'application:

 

NAV01.jpg

Les classes du domaine

La première étape consistait à créer les classes du domaine. Il y a donc 4 entités : Projet, Suivi, Marché et Personne. Elles ont été générée par grails et j'ai complété le code (partout dans le texte j'ai mis en gras ce que j'ai dû écrire).

 

class Projet {

      Integer     numero

      String      nom

      Date        dateProposition

      String      description

      Boolean     hauteVisibilite

      String      urlProjet

      Marche      marche

      Float       budget = 20000

      Date        dateCreated

      Date        lastUpdated

 

      static hasMany = [suivis:Suivi, participants:Personne]

     

    static constraints = {

            numero(nullable:true, min:1000, unique:true )

            nom(blank:false,unique:true,maxSize:80)

            description(nullable:true )

            dateProposition()

            marche(nullable:true )

            budget(scale:2, min:0f, max:100000f,nullable:true )

            hauteVisibilite()

            urlProjet(url:true,nullable:true )

    }

 

      String toString() {return nom + " (" + numero + ")"}

}

 

 

class Suivi {

 

      String notes

      Date dateCreated

      Date lastUpdated

 

      static belongsTo = [projet:Projet]

 

    static constraints = {

            notes(blank:false)

    }

     

      String toString() {return notes + " @ " + lastUpdated}

}

 

class Marche {

      String nom

 

      static hasMany = [projets:Projet]

 

    static constraints = {

            nom(blank:false,unique:true,maxlength:25)

    }

 

      String toString() {return nom}

}

 

class Personne {

      String      nom

      String      prenom

      String      cIP

      String      adresse

      String      ville

 

      static hasMany = [projets:Projet]

      static belongsTo = Projet

 

    static constraints = {

            nom(blank:false)

            prenom(blank:false)

            cIP(matches:"[a-zA-Z]{2}[0-9]{4}",unique:true)

            adresse(nullable:true)

            ville(nullable:true)

    }

 

      String toString() {return prenom + " " + nom}

}

 

Ce code nous donnes des classes qui persistent en base de données.

Les controleurs et le scaffolding

Pour pouvoir les manipuler, j'ai utilisé la génération dynamique des écrans web en mettant une ligne de code dans les contrôleurs générés par Grails.

 

class ProjetController {

 

    def scaffold = Projet

}

 

Juste avec ces 50 lignes de codes, j'ai une application web qui me permet de valider avec mon client le modèle de données, incluant les validations de bases. Le look de l'application est très Grails et la navigation n'est pas nécessairement celle que l'on désire, mais j'ai une application fonctionnelle.

Feuille de style

J'ai ensuite remplacé la feuille de style par défaut, par une qui correspond plus à nos normes graphiques et à notre esthétique. J'ai aussi modifié la page d'index de l'application pour coller à nos gabarits.

 

 

 

Donc avec 50 lignes de code et un fichier CSS, j'ai des écrans de liste comme celui-ci:

 

 scr01.jpg

Cet écran supporte le tri des colonnes. Par exemple en cliquant sur la colonne CIP…

 

 scr02.jpg

Dans les pages des liste la pagination s'ajoute automatiquement lorsque le nombre de ligne passe un certain seuil (par défaut à 10).

 

 scr03.jpg

Les écrans de détails, en lecture seul, ressemble à ceci:

 

 scr04.jpg

Notez qu'il y a une relation entre Personne et Projets, et que par conséquent il est possible d'accéder directement au projets de la personne.

 

En mode édition, l'écran de détail est le suivant:

 

 scr05.jpg

Notez que la validation est fonctionnelle. On va montrer plus loin comment afficher un message plus significatif pour l'utilisateur.