06 février 2010

Mise en relief des champs obligatoires

 

Mise en relief des champs obligatoires

Étant donnée que nos objets de domaine indique par les contraintes si les propriétés sont obligatoires ou facultatives, on voudrait le refléter dans l'écran de saisie. Dans nos normes on affiche ces champs avec une étoile rouge.

La façon la plus simple est de se construire un tablib qui vérifie l'état de la contrainte isNullable et d'outputter une classe correspondante, qui se trouve dans le CSS.

Ainsi, si on modifie les contraintes dans la définition de la classe, la vue le reflète automatiquement.

Le taglib:

 

class UtilJDTagLib {

def utilHTML = { attrs, body ->

out << "${attrs['thru']}"

}

def utilOblig = { attrs, body ->

if (attrs['bean'].constraints.get(attrs['field']).isNullable())

{ out << 'facultatif'}

else {out << 'obligatoire' }

}

}

Dans la vue, il s'agit de rajouter notre taglib dans la classe de l'élément, par exemple dans la view Create.gsp du projet, ici le label.

<g:form action="save" method="post" >

<div class="dialog">

<table>

<tbody>

<tr class="prop">

<td valign="top" class="name">

<label for="numero" class="<g:utilOblig bean="${projetInstance}" field="numero" />"><g:message code="projet.numero" default="Numero" />:</label>

</td>

<td valign="top" class="value ${hasErrors(bean: projetInstance, field: 'numero', 'errors')} ">

<g:textField name="numero" value="${fieldValue(bean: projetInstance, field: 'numero')}" />

</td>

</tr>

<tr class="prop">

<td valign="top" class="name">

<label for="nom" class="<g:utilOblig bean="${projetInstance}" field="nom" />"><g:message code="projet.nom" default="Nom" />:</label>

</td>

<td valign="top" class="value ${hasErrors(bean: projetInstance, field:'nom', 'errors')} ">

<g:textField name="nom" maxlength="80" value="${fieldValue(bean: projetInstance, 'nom')}" />

</td>

</tr>

Ce qui donne à l'écran:

 

oblig01.jpg

 

 

 

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