Plugin MultiSelect dans le générateur de code

Introduction

Dans le générateur de code (voir l’article qui le décrit), les fichiers classiques de l’entité sont générés, mais il en manque une partie. Lorsqu’on défini une liaison n-n d’une entité vers une autre, on utilise un « objet de liaison ». Cet objet se décrit par :

  • un identifiant qui est clé unique de l’objet de liaison
  • l’identifiant de l’objet A
  • l’identifiant de l’objet B

Grâce à cet objet, un objet A est relié à n objets B et inversement, un objet B est relié à n objets A.

Exemple : entité « Album de bande dessinée » et entité « Auteur » :

  • un album de BD peut avoir plusieurs auteurs
  • un auteur peut avoir fait plusieurs albums de BD

 

L’objectif est de générer automatiquement du code pour avoir un multiselect dans la création et d’édition d’une entité A ou B.

 

Application avec les BD et les auteurs

L’objet « LienAuteurAlbum » est décrit comme ceci :

  • identifiant du lien
  • identifiant de l’album
  • identifiant de l’auteur

Remarque : l’identifiant du lien permet de supprimer facilement et sereinement un enregistrement avec une seule information, en plus des fonctionnalités offertes avec les autres champs.

Après moult réflexion, j’ai ajouté des features sur chaque entité de liaison :

Ces modifications sont faites dans la génération du helper et du modèle. Deux nouvelles vues ont étés faire pour l’appel du plugin multiselect, sous forme d’un fragment (une vue qui s’appelle depuis une vue principale). Une vue pour la création et une vue pour la modification.

Par contre, il faut modifier manuellement les controller et les vues des entités (exemple : Album) pour utiliser cet objet de liaison.

Appel du code généré pour la vue en édition ou en création d’album

Lorsque je crée un album, je veux avoir le multiselect des auteurs. Il faut alors ajouter dans la vue de création d’un album le code suivant :

<?php 

$data["multiselectOf"] = 'Auteur';
$this->load->view('lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php', $data);

?>

Le fichier « lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php » est généré et ne doit pas être modifié. Dans le paramètre « multiselectOf », il faut spéficier que je veux un multiselect de … « Auteur », afin d’avoir une liste d’auteurs à choisir.

Idem dans la vue d’édition d’un Album.

 

Modification du controller d’édition d’album

Dans le controller d’édition, lorsqu’on sauve les données saisies, il faut [1] supprimer les anciennes relations dans « LienAuteurAlbum » dans lesuqelles il y a l’album édité et [2] créer des nouveaux objets « LienAuteurAlbum » avec cet album et les auteurs cochés.

Première partie :

$lienAuteurAlbum_autidaut = $this->input->post('LienAuteurAlbum_autidaut');
LienAuteurAlbum_model::deleteAllLienAuteurAlbumsBy_xaaidalb($this->db, $model->albidalb);

ligne 1 : récupération des liens avec des identifiants d’auteurs que l’utilisateur a coché dans le multiselect

ligne 2 : suppression des anciens liens à partir de l’identifiant de l’album

 

Deuxième partie :

foreach($lienAuteurAlbum_autidaut as $autidaut){
            $auteurModel = new LienAuteurAlbum_model();
            $auteurModel->xaaidalb = $model->albidalb;
            $auteurModel->xaaidaut = $autidaut;
            $auteurModel->save($this->db);
}

Pour chaque auteur coché, on créer un nouveau lien avec :

  • album : celui que je suis en train d’éditer
  • auteur : un auteur coché

Remarque : Dans le controller de création d’un album, la première partie destinée à supprimer les anciens liens avec les auteurs n’est pas à faire.

 

 Sans oublier le javascript

Le plugin JS est à charger après les différents appels de jQuery et du JS du plugin multiselect. J’ai choisi de le mettre manuellement dans le JS de la vue de l’objet édité ou créé.

$(function(){
    $("#LienAuteurAlbum_autidaut").multiselect();
});

 

Résultat

Dans cet exemple, le champ « Genre » est un select simple (déjà disponible dans le générateur). Le champ « Liste des auteurs » fait appel au plugin multiselect.

 

Conclusion

Finalement, la génération de code a ses limites, mais beaucoup de choses sont pré-mâchées. Pour aider les futurs développeurs à intégrer le plugin, j’ai inséré le code à copier-coller dans la vue et dans le JS.

Rien n’est généré pour le controller, il faudra lire cet article si vous ne savez pas comment faire (mais je trouve que ce n’est pas trop difficile).

 

Application Back-office & Mobile : CodeIgniter & Kivy

Introduction

Après quelques essais avec Kivy pour faire une application mobile qui indique des données d’infusion sur les thés, je me lance pour faire une application en back-office pour gérer les données. L’application mobile récupère les données en JSON et stocke dans une base SQLite.

Voici les outils utilisés de chaque coté :

Back-office Appli Mobile
Générateur de code Écriture du code « à la main »
PHP, CodeIgniter Python, Kivy
MySQL SQLite3

 

Principe général :

 

Application Back-office

Avec le générateur de code (plus d’infos sur ce site), l’application est créée, déployée et les données ajoutées, le tout en 1 heure sur « free.fr ». Le plus long a été de copier / coller les données entre ma source (le site du palais des thés) et ma nouvelle application.

Le controller fournissant le flux JSON est lui aussi généré :

Les écrans n’ont pas été retravaillés suite à la génération de code, mais c’est suffisant pour tout faire fonctionner sur l’environnement de « free.fr » : http://jc.specs.free.fr/TeaTime/ (un identifiant et mot de passe sont nécessaires…). Le nouveau thème Ubuntu (en gris et orange a été choisi).

Écrans du back-office

Catégorie de thés :

Thés :

Application mobile

Avec Kivy, je disposais déjà d’une application qui lit un JSON sous forme de fichier. Il m’a suffit de lui permettre de lire des fichiers JSON par Internet, de remplir une base de données SQLite et de la remplir.

Voici les étapes de réalisation :

  1. Lire dans un processus parallèle le JSON. Au lieu d’attendre que l’URL réponde, les widgets de l’écran de l’appli mobile sont disponibles.
  2. Stocker le JSON dans une base SQLite embarquée avec l’application. Lorsque c’est fait, un widget de l’application est modifié : un label devient « Fichier chargé ».
  3. Lorsque l’utilisateur entre dans les écrans de l’appli mobile, il faut lire la base de données SQLite et valoriser les objets qui seront utilisés par Kivy.

Kyvi n’est d’aucune utilité pour réaliser ces opérations de synchronisation ; Python prend le dessus : gestion des processus asynchrones, lecture d’une URL, transformation de JSON en objet, accès à une base de données SQLite.

 

Écrans de l’appli mobile

   

Le fichier APK de l’application mobile est téléchargeable sur http://jc.specs.free.fr/TeaTime/

 

Design avec Kivy

Introduction

Kivy ne propose pas encore d’interface pour designer les interfaces (dans le genre Glade). Voici mes essais de graphisme dans les écrans d’une application.

 

Résultats obtenus

Voici d’abord les résultats que j’ai obtenu avec le paramétrage de kivy (un maximum en kv afin de rester cohérent avec la description des pages).

1. Changement de police

Dans ce cas, il suffit de mettre le fichier TTF dans le répertoire des sources de l’application et d’y faire appel dans le KV :

<Welcome>:
    logLabel_id: log

    BoxLayout:
        orientation: 'vertical'
        Label:
            text: "TeaTime"
            font_size: '60sp'
            font_name: "font/segoe_ui_light.ttf"
...

Résultat :

Lors du build de l’application (avec buildozer),  il ne faut pas oublier de l’ajouter dans le package (dans les extensions à packager, il faut ajouter « ttf »).

 

2. Lignes et bordures, sans images

J’ai testé l’ajout de bordures en langage KV, sans utiliser d’images pour le moment. Cela me garantie que les images ne seront pas déformées selon la résolution du média et ça me semble plus proche du CSS pour le HTML.

A partir de cet exemple, avec les lignes en haut, dans le futur Ubuntu Phone :

Lien : http://www.ubuntu.com/phone

<Categories>:
    logLabel: logLabel_id
    containerListView: list_id
    canvas.before:
        Color:
            # 34 30 28
            rgba: 0.1333, 0.11764, 0.1098, 1
        Rectangle:
            pos: self.pos
            size: self.size
    BoxLayout:
        id: categoriesLayout
        orientation: 'vertical'

        Label:
            text: "TeaTime >"
            font_size: '34sp'
            size_hint_y: None
            height: '60sp'
            halign: 'left'
            text_size: self.size
            padding: '-10sp', '-8sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (1, 1, 1, 0.90)

        Label:
            text: " " # empty line
            size_hint_y: None
            padding: '10sp', '10sp'
            height: '16sp'
            canvas.before:
                Color:
                    # 28 25 23
                    rgba: 0,0,0, 0.33
                Rectangle:
                    pos: self.pos
                    size: self.size

        Label:
            text: " " # empty line
            size_hint_y: None
            padding: '0sp', '0sp'
            height: '2sp'
            canvas.before:
                Color:
                    # 89 86 84
                    rgba: 1,1,1, 0.33
                Rectangle:
                    pos: self.pos
                    size: self.size

        Label:
            text: "Catégories de thés"
            font_size: '32sp'
            size_hint_y: None
            height: '100sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (1, 1, 1, 0.95)

...

Résultat (correspondant aux premières lignes, avant la liste avec le picto de feuille) :

 

 

3. Design avec les images

Cette fois, je reprend un screenshot d’un iPhone pour l’intégrer dans Kivy…

Share albums

source : http://dribbble.com/shots/1231195-Share-albums

<Thes>:
    logLabel: logLabel_id
    containerListView: list_id
    nomcategorieLabel: nomcategorieLabel_id
    canvas.before:
        Color:
            # 247 244 240
            rgba: 0.96862, 0.95686, 0.94117, 1
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        id: categoriesLayout
        orientation: 'vertical'

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            id: nomcategorieLabel_id
            text: "TeaTime > XXX"
            font_size: '16sp'
            size_hint_y: None
            height: '25sp'
            halign: 'left'
            text_size: self.size
            padding: '-10sp', '0sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (0, 0, 0, 0.90)

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            text: "Liste des thés"
            font_size: '32sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (0, 0, 0, 0.90)
            size_hint_y: None
            height: '60sp'

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    source: "images/liner.png"
                    pos: self.pos
                    size: self.size
            text: " "
            size_hint_y: None
            height: '4sp'

        Label:
            canvas.before:
                Color:
                    rgba: 0.96862, 0.95686, 0.94117, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            text: " "
            size_hint_y: None
            height: '6sp'
...

Résultat :

 

Conclusions

  • Le design avec les images est quand même plus rapide et facile lorsqu’on dispose d’une maquette.
  • Il faut savoir que les images sont étirées lorsqu’elles sont en image de fond. Je n’ai pas encore trouvé la manière simple de dire à Kivy l’équivalent du « background-repeat » en CSS
  • Il y a une astuce qui n’est pas si évidente (et pas forcément bien expliquée) pour faire des boutons designés à partir d’une image. Kivy prend les bords de l’image pour reconstruire un bouton, en associant les propriétés « border » et « background_normal ».
  • L’objet « ListItemButton » reste pour moi un peu difficile à manipuler. Cet objet est un bouton dans la liste des items (la liste des thés dans le dernier screenshot ci-dessus). Il y a un layer de couleur qui est appliqué sur l’image de fond et je n’arrive pas à le supprimer… J’ai contourné le problème avec un autre objet en dessous qui a l’image de fond.

Test unitaires dans le générateur de code

Après avoir intégré avec succès les tests unitaires dans CodeIgniter avec Toast, j’ai ajouté un template de test unitaire dont l’objectif est de s’assurer que la couche modèle fonctionne. Ces tests sont indépendants de la BDD métier car ils utilisent une base SQLite3.

Pour ce faire, Toast permet de créer un controller dédié à cette tâche.

J’ai choisi une entité qui s’appelle « Groupe » et qui a 3 propriétés « grpidgrp = Identifiant », « grplblib = Libellé », « grpidsoc = Clé étrangère vers l’entité Société ». Voici en détail les tests réalisés (automatiquement générés avec le code de l’application, donc).
(Tout ce code est généré, rien n’a été retouché)

/*
 * Created by generator
 *
 */
require_once(APPPATH . '/controllers/test/Toast.php');

class GroupeTest extends Toast {

	function __construct(){
		parent::__construct();
		$this->load->database('test');

		$this->load->model('Groupe_model');

	}

	/**
	 * OPTIONAL; Anything in this function will be run before each test
	 * Good for doing cleanup: resetting sessions, renewing objects, etc.
	 */
	function _pre() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	/**
	 * OPTIONAL; Anything in this function will be run after each test
	 * I use it for setting $this->message = $this->My_model->getError();
	 */
	function _post() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	public function test_insert(){
		$this->message = "Tested methods: save, getGroupe, delete";
		// création d'un enregistrement
		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);
		// $groupe_insert->grpidgrp est maintenant affecté

		$groupe_select = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		$this->_assert_equals($groupe_select->grpidgrp, $groupe_insert->grpidgrp);
		Groupe_model::delete($this->db, $groupe_select->grpidgrp);
	}

	public function test_update(){
		$this->message = "Tested methods: save, update, getGroupe, delete";
		$groupe_insert = new Groupe_model();

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test1_0';
		$groupe_insert->grpidsoc = 90;
		$groupe_insert->update($this->db);

		$groupe_update = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		if(!$this->_assert_equals($groupe_insert->grpidgrp, $groupe_update->grpidgrp)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grplblib, $groupe_update->grplblib)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grpidsoc, $groupe_update->grpidsoc)) {
			return false;
		}

		Groupe_model::delete($this->db, $groupe_insert->grpidgrp);
	}

	public function test_count(){
		$this->message = "Tested methods: getCountGroupes, save, getGroupe, delete";

		// comptage pour vérification : avant
		$countGroupesAvant = Groupe_model::getCountGroupes($this->db);

		// création d'un enregistrement
		$groupe = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe->grplblib = 'test_0';
		$groupe->grpidsoc = 0;
		$groupe->save($this->db);

		// comptage pour vérification : après insertion
		$countGroupesApres = Groupe_model::getCountGroupes($this->db);

		// verification d'ajout d'un enregistrement
		$this->_assert_equals($countGroupesAvant +1, $countGroupesApres);

		// recupération de l'objet par son  grpidgrp
		$groupe = Groupe_model::getGroupe($this->db, $groupe->grpidgrp);

		// suppression de l'enregistrement
		Groupe_model::delete($this->db, $groupe->grpidgrp);

		// comptage pour vérification : après suppression
		$countGroupesFinal = Groupe_model::getCountGroupes($this->db);
		$this->_assert_equals($countGroupesAvant, $countGroupesFinal);

	}

	function test_list(){
		$this->message = "Tested methods: save, getAllGroupes, delete";

		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		$groupes = Groupe_model::getAllGroupes($this->db);
		if( ! $this->_assert_not_empty($groupes) ) {
			return FALSE;
		}
		$found = 0;
		foreach ($groupes as $groupe) {
			if($groupe->grpidgrp == $groupe_insert->grpidgrp &&
					$this->_assert_equals($groupe->grplblib, $groupe_insert->grplblib ) &&
					$this->_assert_equals($groupe->grpidsoc, $groupe_insert->grpidsoc )
				){
				$found++;
			}
		}
		if( $found == 1 ){
			Groupe_model::delete($this->db, $groupe->grpidgrp);
			return TRUE;
		}else{
			return FALSE;
		}
	}

}

 

Il faut récupérer le script SQLite3 généré et l’intégrer dans la BDD de TU :

/*
 * Lancer la commande suivante pour insérer les données
 * cat cretab_groupe.sqlite | sqlite3 test_database.sdb
 */

CREATE TABLE expgrp (
	grpidgrp integer NOT NULL PRIMARY KEY AUTOINCREMENT ,
	grplblib varchar(255) NOT NULL ,
	grpidsoc integer NOT NULL
);

 

Dans un navigateur, voici le résultat sur l’URL du controller de TU :

A ce stade, aucune ligne de code n’a dû être écrite par un développeur. Il pourra commencer son application avec des bases saines. J’ose le terme de « Framework Métier », permettant de poser les briques du système à réaliser.

 

SQLite avec CodeIgniter

Introduction

Voici un descriptif des étapes à faire pour connecter son application CodeIgniter v2 à une base de données SQLite3. Cet article est à lier à celui sur les tests unitaires dans CodeIgniter avec Toast. L’intérêt est qu’une base de données de test est à disposition pour ne faire que des tests et laisser la base cible (de dev par exemple) en place, sans rien y changer.

Attention : CodeIgniter version 2.1.0 ne fonctionne pas avec SQLite3. Il faut passer à la version 2.1.3

 

Étapes

Installation des packages :

sudo apt-get install sqlite3 php5-sqlite php5-curl

Vérification des extensions PHP:

ls /etc/php5/apache2/conf.d

Il doit y a voir « pdo.ini » et « sqlite3.ini »

 

Définir des tables dans la base de données :

cat script.sql | sqlite3 test_database.sqlite

Le fichier « script.sql » contient les lignes suivantes :

CREATE TABLE expsoc (
    socidsoc integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    soclblib varchar(255) NOT NULL
);

 

Autoriser l’accès au fichier « test_database.sqlite » :

chmod o+rw test_database.sqlite

 

Ajouter un fichier de langue (vide) dans /application/language/french/db_lang.php

 

Configurer la base de données dans CodeIgniter :

require_once(APPPATH . '/controllers/test/Toast.php');

class SocieteTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
		$this->load->model('Societe_model');

		$config['hostname'] = "sqlite:".APPPATH."/database/test_database.sdb";
		$config['username'] = "";
		$config['password'] = "";
		$config['database'] = "";
		$config['dbdriver'] = "pdo";
		$config['dbprefix'] = "";
		$config['pconnect'] = TRUE;
		$config['db_debug'] = TRUE;
		$config['cache_on'] = FALSE;
		$config['cachedir'] = "";
		$config['char_set'] = "utf8";
		$config['dbcollat'] = "utf8_general_ci";
		$config['swap_pre'] = '';
		$config['autoinit'] = TRUE;
		$config['stricton'] = FALSE;

		$this->load->database($config);

	}
	...
}

 

Un test basique

Voici une méthode du controller de test (dont le début est ci-dessus) pour valider que les accès à la base de données sont toujours bons :

    public function test_count(){

        // comptage pour vérification : avant
        $countSocietesAvant = Societe_model::getCountSocietes($this->db);

        // création d'un enregistrement
        $societe = new Societe_model();
        $societe->socidsoc = 1;
        $societe->soclblib = "Ma société";
        $societe->save($this->db);

        // comptage pour vérification : après insertion
        $countSocietesApres = Societe_model::getCountSocietes($this->db);

        // verification d'ajout d'un enregistrement
        $this->_assert_equals($countSocietesAvant +1, $countSocietesApres);

        // recupération de la societe socidsoc=1
        $societe = Societe_model::getSociete($this->db, 1);

        // suppression de l'enregistrement
        Societe_model::delete($this->db, $societe->socidsoc);

        // comptage pour vérification : après suppression
        $countSocietesFinal = Societe_model::getCountSocietes($this->db);

        $this->_assert_equals($countSocietesAvant, $countSocietesFinal);

    }

On y fait :

  1. Décompte des enregistrements dans la base
  2. Ajout d’un enregistrement
  3. Décompte des enregistrements dans la base
  4. Vérification de l’ajout : [nb avant] + 1 == [nb après]
  5. Récupération de l’enregistrement ajouté
  6. Suppression de l’enregistrement
  7. Décompte des enregistrements dans la base
  8. Vérification de la suppression : [nb avant] == [nb à la fin]

 

Lancement du test

L’URL suivante permet de tester unitairement certains accès à la base :

http://localhost/monprojet/index.php/test/societetest/count

 

Voici le résultat :

 

Reste à faire

Il reste à intégrer ce genre de tests unitaires dans le générateur de code afin de disposer de ces tests dès que l’application est prête :

  • Un nouveau template pour le controller de test (1 controller de test par entité)
  • Nouveau template pour le SQL à passer dans SQLite3 (1 fichier sqlite par entité)

 

Test unitaires avec CodeIgniter

Introduction

Les tests unitaires, tout le monde connait. En Java, junit fait l’unanimité (en plus, il est intégré à Eclipse ce qui en fait une arme redoutable). En PHP, avec le framework CodeIgniter, beaucoup de choses sont proposées, mais sans tutoriel pour montrer comment ça fonctionne concrètement.

Voici ma méthode, avec PHPUnit, Toast et CodeIgniter v2. Bien sûr, ça n’a pas fonctionné du premier coup (ce serait trop facile). Les prérequis sont :

 

Il faut commencer par revoir l’installation de Toast

Juste un petit mot sur Toast : il est basé sur les classes de test unitaires du framework de Codeigniter (qui s’appuie sur PHPUnit pour les tests unitaires) ; il est composé de quelques fichiers seulement (facile à maintenir et à customiser) ; il permet de se connecter à une base données indépendantes, juste pour les tests. C’est donc un bon candidat pour faire ces tests unitaires.

  • Les fichiers Toast sont dans « monProjet/application/controllers/test/ » pour les contrôleurs (qui feront les tests) et dans « monProjet/application/views/test/ » pour le rendu graphique.
  • Fichier « Toast.php » : la classe doit être déclarée pour utiliser CodeIgniter v2 et il faut recâbler les accès au nom de la classe de test :
abstract class Toast extends CI_Controller {
  ...
  function __construct(){
		parent::__construct();
		$this->load->library('unit_test');
		$this->modelname = get_class($this); //$name;
		$this->modelname_short = get_class($this); //basename($name, '.php');
		$this->messages = array();
	}
  ...
}
  • Idem avec le fichier « Toast_all.php » :
class Toast_all extends CI_Controller {
  ...
  function __construct(){ 
      parent::__construct(); 
  }
  ...
}

 

Le premier test unitaire basique peut commencer

  • Créez un nouveau fichier « SimpleTest.php » :

Il faut préfixer les méthodes de test par « test_ » et seul la fin du nom de la méthode sera présentée dans le rendu graphique.

require_once(APPPATH . '/controllers/test/Toast.php');

class SimpleTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
	}

	public function test_basic(){
		$my_var = 2 + 2;
		$this->_assert_equals($my_var, 4);
	}

	function test_false(){
		// Test code goes here
		$my_var = true;
		$this->_assert_false($my_var);
	}
}

Ce code est celui de Toast. Je pense faire une version plus correcte, mais ce sera pour plus tard…

 

Voir le résultat du test

Le test unitaire est vu comme un contrôleur du projet. Il suffit d’y accéder par son URL, dans son navigateur favori :

http://localhost/monProjet/index.php/test/simpletest

Le résultat est une page web qui présente les tests réalisés :

On peut cliquer sur le nom de la classe ou sur le nom de la méthode pour limiter l’affichage. Toast propose aussi un lancement de tous les tests disponibles.

Par la suite, on peut imaginer automatiser le lancement de ces tests avec un « wget » pour lancer le test suivi d’un « grep FAILED » pour filtrer le résultat. Je me demande si un rendu XML ou JSON peut être réalisé…

 

La suite

Pour clôturer ce sujet, je ferai un VRAI test unitaire dans mon projet, maintenant qu’on sait comment ça fonctionne. Si vous avez des idées de customisation du rendu graphique, postez-les !

J’intègrerai aussi certainement des tests unitaires dans mon générateur de code afin de pondre aussi des tests tout prêts.

 

i18n dans le générateur de CRUD

Après l’article sur une amélioration de l’internationalisation dans CodeIgniter, j’ai intégré la gestion des traductions dans le générateur de CRUD pour CodeIgniter. Un fichier de plus est généré (un fichier de langue) par entité et tous les libellés sont maintenant appelés par cette interface. Les vues n’ont plus aucun libellé.

En conclusion, mon dernier template « Bootstrap » contient cette évolution, mais je ne pense pas le faire pour les autres templates, sauf si un nouveau projet sur un template voit le jour.

Voici le fichier de langue qui sera parsé par le générateur :

%[kind : lang]
%[file : messages_%%(self.obName.lower())%%_lang.php]
%[path : language/french]
<?php
/**
 * Message file for entity %%(self.obName)%%
 *
 * Please don't forget to load this file:
 *  Solution A : Use "/application/config/autoload.php"
 *               Add this line:
 *               $autoload['language'] = array(..., 'messages_%%(self.obName.lower())%%', ...);
 *
 *  Solution B : Load this message file anywhere you want.
 *  
 */

$lang['%%(self.obName.lower())%%.message.confirm.deleted'] = "%%(self.displayName)%% supprimé";
$lang['%%(self.obName.lower())%%.message.confirm.added'] = "%%(self.displayName)%% créé avec succès";
$lang['%%(self.obName.lower())%%.message.confirm.modified'] = "%%(self.displayName)%% mis à jour avec succès";

$lang['%%(self.obName.lower())%%.form.create.title'] = "Ajouter un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.edit.title'] = "Editer un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.list.title'] = "Liste des %%(self.displayName.lower())%%s";

$lang['%%(self.obName.lower())%%.menu.item'] = "%%(self.displayName)%%";

%%allAttributesCode = ""
for field in self.fields:
    attributeCode = """$lang['%(objectObName)s.form.%(dbName)s.label'] = "%(obName)s";
$lang['%(objectObName)s.form.%(dbName)s.description'] = "%(desc)s";  
""" % {    'dbName': field.dbName,
        'obName': field.obName,
        'objectObName':self.obName.lower(),
        'desc' : field.description
    }
    allAttributesCode += attributeCode
    
RETURN = allAttributesCode
%%

?>

 

i18n avec arguments : extension pour CodeIgniter

Dans CodeIgniter, il est possible de réaliser un fichier de traduction et d’y faire appel dans les vues.

Des articles sur le web donnent des pistes : [Google]

Par contre, pour ajouter des arguments à la chaine de texte à traduire, ça se corse. Comme en java, j’aimerai avoir le texte « Vous avez 5 messages dont 2 non lus ». La chaine de base serait « Vous avez {arg1} messages dont {arg2} non lus » et les 2 arguments seraient « 5″ et « 2″.

Voici comment procéder avec CodeIgniter v2.1.0 :

  1. Créez un fichier « /application/core/MY_Lang.php » :
    <?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
    
    class MY_Lang extends CI_Lang{
    
        public function __construct() {
            parent::__construct();
        }
    
        /**
         * Fetch a single line of text from the language array. Takes variable number
         * of arguments and supports wildcards in the form of '%1', '%2', etc.
         * Overloaded function.
         *
         * @access public
         * @return mixed false if not found or the language string
         */
        public function line(){
            //get the arguments passed to the function
            $args = func_get_args();
    
            //count the number of arguments
            $c = count($args);
    
            //if one or more arguments, perform the necessary processing
            if ($c) {
                //first argument should be the actual language line key
                //so remove it from the array (pop from front)
                $line = array_shift($args);
    
                //check to make sure the key is valid and load the line
                $line = ($line == '' OR ! isset($this->language[$line])) ? $line : $this->language[$line];
    
                //if the line exists and more function arguments remain
                //perform wildcard replacements
                if ($line && $args) {
                    $i = 1;
                    foreach ($args as $arg)
                    {
                        $line = preg_replace('/\%'.$i.'/', $arg, $line);
                        $i++;
                    }
                }
            } else {
                //if no arguments given, no language line available
                $line = false;
            }
    
            return $line;
        }
    
    }
    
    ?>
  2. Utilisez vos arguments dans le fichier de traduction de cette façon :
    <?php
    $lang['list.evenements.title'] = "Liste des evènements pour %1";
    ?>
  3. Passez par la nouvelle classe pour traduire vos libellés avec les arguments :
    <?= $this->lang->line('list.evenements.title', $month.'/'.$year) ?>

Dernière modification : si la traduction n’est pas retrouvée, afficher le premier paramètre.

Intégration du GanttCalendar

Suite à l’article sur le GanttCalendar, j’ai intégré ce plugin jquery dans une application web en PHP : « ExoPlanet ».

Intégration dans une application

Le plugin est tout à fait fonctionnel. Les évènements sont présentés sur 1 mois, le Gantt au dessus et la liste des évènements sous forme de tableau en dessous.

Le plugin a été intégré en quelques heures ; le paramétrage a été le plus long.

Template Design & Deploy

J’ai commencé à faire une page HTML basique à partir de Bootstrap avec d’autres plugins JS/CSS pour agrémenter les pages. Bootstrap a l’avantage de proposer des pages simples, respectant des anciens principes d’ergonomie du web (qu’on qualifie de « 1.0″) : page blanche, texte écrit en noir, boutons simples, menu basique.

Voici les plugins qui sont ajoutés :

  • Font-Awesome : pour avoir des pictos standards et en de multiples tailles
  • elusive-iconfont : une autre bibliothèque de pictos, avec les mêmes propriétés
  • Colorpicker : pour saisir une valeur pour le type « couleur »
  • Datepicker : pour saisir une date avec un calendrier

Il me reste à trouver un plugin pour le fileupload et peut-être pour le select.

Il faut aussi que je trouve une autre façon de présenter les tables de manière plus précise. Je trouve que la présentation par défaut est trop claire ; on confond la table avec le fond de la page.