SoundBox avec ionic

Introduction

Après avoir réalisé une boite à sons avec Kivy, j’ai tenté de faire la même chose avec ionic. Les réflexes sont les mêmes, la méthode de travail est un peu différente.

 

Ajout du plugin Media

Pour faire jouer un son sur le mobile, il est nécessaire d’installer le plugin Cordova « Media ». Malheureusement, il ne fonctionne pas sous le navigateur du développeur. Il faudra faire le test sur le device mobile. Seule différence notable avec Kivy, qui permet de jouer un son presque de la même façon depuis le PC ou depuis le mobile.

 

Structuration du code

Même s’il n’y a pas grand chose, je tiens à avoir le code le plus structuré et le plus propre possible. L’application se découpe en 2 pages : la liste des catégories (page #1) et la liste des sons de la catégorie choisie (page #2).

Je me suis fortement inspiré du code de cette application : https://github.com/angular-app/angular-app/tree/master/client/src/app

Voici le contenu du répertoire www :

  • app : Répertoire de l’application
    • intro : Répertoire des fichiers de la vue « intro », page #1 de l’application
      • intro.js : Controller de la page « intro »
      • intro.tpl.html : Vue de la page « intro »
    • sounds : Répertoire de la page #2
      • sounds.js : Controller de la page des sons
      • sounds.tpl.html : Vue de la page des sons
    • app.js : Définition des services et du routage des pages
  • css : Répertoire vide pour les CSS spécifiques à l’application
  • js : Répertoire vide pour bibliothèques JS spécifiques à l’application
  • lib : Répertoire des plugins ajoutés pour l’application (géré par cordova)
  • mp3 : les sons

 

Détail du code

Il y a 6 fichiers importants pour que l’application fonctionne correctement.

index.html

<body ng-app="starter">
    <ion-nav-view></ion-nav-view>

    <script src="lib/media/Media.js"></script>
    <script src="lib/media/MediaError.js"></script>

    <script src="app/app.js"></script>

    <script src="app/intro/intro.js"></script>
    <script src="app/sounds/sounds.js"></script>
</body>

 

app.js

angular.module('soundBox.services', [])
.factory('CategoryService', function() {
    // Might use a resource here that returns a JSON array
    var categories = {
            "Animaux": ["Aigle", "Cigales"],
            "Divers": ["Claque", "Dentiste", "Klaxon", "PopDing", "Pouic", "Sieste bebe"],
            "Humain": ["Applaudissements", "Ramirez - alors ausweis papier svp - au trot", "Ramirez - hop hop hop hop", "Ramirez - jai dit ausweis"],
            "Objets": ["Canette Coca", "Casse assiette", "Cloche hotel", "Guitare", "Matrix_phone", "Telephone_Ancien", "Toy telephone", "Corne de brume", "Jouet de chien", "Sirene Alarme", "Telephone_x2"],
            "TV": ["Thames TV", "The Benny Hill Show"]
        };
    return {
        all: function() {
            return Object.keys(categories);
        },
        get: function(aCategory) {
            return categories[aCategory];
        }
    };
})
;

var app = angular.module('starter', ['ionic', 'ui.router', 'soundBox.services'])

.config(function($stateProvider, $urlRouterProvider) {

  $stateProvider.state('intro', {
    url: '/',
    templateUrl: 'app/intro/intro.tpl.html',
    controller: 'IntroCtrl'
  });

  $stateProvider.state('sounds', {
    url: '/sounds/:category',
    templateUrl: 'app/sounds/sounds.tpl.html',
    controller: 'SoundsCtrl'
  });

  $urlRouterProvider.otherwise('/');

});

Ce fichier aurait pu être découpé en 2 : le service et le routage.

 

intro.tpl.html

<ion-view title="Sound Box">
    <ion-nav-bar type="bar-positive"
        animation="nav-title-slide-ios7"
        back-button-type="button-icon button-clear"
        back-button-icon="ion-ios7-arrow-back">
        <ion-nav-back-button>
            <i></i> Back
        </ion-nav-back-button>
    </ion-nav-bar>

    <ion-content>
        <ion-list>
            <ion-item ng-repeat="item in items" href="#/sounds/{{item}}">
                {{item}}
                <i></i>
            </ion-item>
        </ion-list>
    </ion-content>
</ion-view>

 

intro.js

app.controller('IntroCtrl', function($scope, $location, $state, CategoryService) {
    $scope.items = CategoryService.all();
})
;

 

sounds.tpl.html

<ion-view title="Sound Box">
    <ion-nav-bar type="bar-positive"
        animation="nav-title-slide-ios7"
        back-button-type="button-icon button-clear"
        back-button-icon="ion-ios7-arrow-back">
        <ion-nav-back-button>
            <i></i> Back
        </ion-nav-back-button>
    </ion-nav-bar>

    <ion-content>
        <ion-list>
            <ion-item ng-repeat="item in items" ng-click="selectSound('{{item}}')">
                {{item}}
            </ion-item>
        </ion-list>
    </ion-content>
</ion-view>

 

sounds.js

app.controller('SoundsCtrl', function($scope, $state, $location, $stateParams, $ionicPlatform, CategoryService) {

    var sounds = CategoryService.get($stateParams.category);
    $scope.items = sounds;

    $scope.selectSound = function(sound) {
        var src = "/mp3/"+sound+".mp3";
        if(ionic.Platform.isAndroid()){
            src = "/android_asset/www" + src;
        }
        if($scope.media){
            $scope.media.stop();
        }
        $scope.media = new Media(src, function(){console.log("successfuly played "+ sound);}, function(e){console.log(e);});
        $scope.media.play();
        // ne pas faire d'autres interprétations du click
        return false;
    };

    $ionicPlatform.onHardwareBackButton(function(){
        if($scope.media){
            $scope.media.stop();
            $scope.media = null;
        }
    });
});

 

Conclusion / comparaison avec Kivy

Le code doit être rigoureusement écrit. Par exemple le tag « <ion-view> » doit être le seul dans la vue, sinon l’affichage est un peu cassé.

Selon le formalisme utilisé, les fichiers sont multiples mais courts et bien classés, ce qui est très agréable à maintenir.

Le design n’a pas été fait de manière aussi complète que l’application réalisée avec Kivy, mais j’imagine que les CSS et images seront faciles à manipuler, comme pour un site web.

Vivement d’autres applications pour mieux comparer les frameworks dans une application plus complète.

 

Téléchargement de l’APK

SoundBox-ionic-debug.apk

Snippet kv : IconTextCounter

Présentation

Pour aller plus loin avec le widget IconText cité dans un article de ce site, j’ai ajouté un indicateur avec un compteur, tel qu’on peut le voir dans des applications. Kivy ne proposant que des widgets basiques, voici une construction qui répond au problème.

 

Composition

Ce widget hérite de FloatLayout pour placer les éléments de manière relative, l’un au dessus de l’autre. Il contient :

  • un widget IconText
  • un widget Label, avec en image de fond le bleu avec des coins arrondis

 

Code kv

<IconTextCounter>:
	_icontext: _icontext_id
	_counter: _counter_id
	IconText:
		id: _icontext_id
		text: "dummy"
		icon: "images/ic_action_star.png"
		pos_hint: {'x':0, 'top':1}

	Label:
		canvas.before:
			Color:
				rgba: 1,1,1, 1
			Rectangle:
				pos: self.pos
				size: self.size
				source: "kvx_widgets/images/counter_bg.png"
		id: _counter_id
		text: "x"
		pos_hint: {'center_x':0.95, 'top':0.65}
		size_hint: (None, None)
		size: (self.texture_size[0]+sp(16), self.texture_size[1]+sp(8))
		font_size: sp(12)

Une astuce pour avoir un fond qui prend toute la taille du libellé : définir la taille à partir de la texture + un petit espace.

size: (self.texture_size[0]+sp(16), self.texture_size[1]+sp(8))

 

Code Python

class IconTextCounter(FloatLayout):
    counter = StringProperty("")
    counter_position = StringProperty("")
    counter_background = StringProperty("")

    text = StringProperty("")
    icon = StringProperty("")
    icon_size = NumericProperty(sp(80))
    font_size = NumericProperty(sp(18))
    text_color = VariableListProperty([0,0,0,1])
    forced_width = NumericProperty(sp(80))

    def __init__(self, **kwargs):
        super(IconTextCounter, self).__init__(**kwargs)
        self.bind(counter = IconTextCounter.set_counter,
                counter_position = IconTextCounter.set_counter_position,
                counter_background = IconTextCounter.set_counter_background,
                text = IconTextCounter.set_text,
                icon = IconTextCounter.set_icon,
                icon_size = IconTextCounter.set_icon_size,
                font_size = IconTextCounter.set_font_size,
                text_color = IconTextCounter.set_text_color,
                forced_width = IconTextCounter.set_forced_width
                )

    def set_counter(self, aValue):
        self._counter.text = aValue

    def set_counter_position(self, aPosition):
        delta = 0.15
        center_x = 0.5
        top = 0.80
        if aPosition == 'top-left':
            center_x -= delta
            top += delta
        elif aPosition == 'top-right':
            center_x += delta
            top += delta
        elif aPosition == 'bottom-left':
            center_x -= delta
            top -= delta
        elif aPosition == 'bottom-right':
            center_x += delta
            top -= delta

        self._counter.pos_hint = {'center_x':center_x, 'top':top}

    def set_counter_background(self, aSourceImage):
        self._counter.canvas.before.children[1].source = aSourceImage

    ## define all IconText methods
    def set_text(self, aText):
        self._icontext.text = aText

    def set_icon(self, aSourceImage):
        self._icontext.icon = aSourceImage

    def set_icon_size(self, aWidthHeight):
        self._icontext.icon_size = aWidthHeight

    def set_font_size(self, aFontSize):
        self._icontext.font_size = aFontSize

    def set_text_color(self, aColor):
        self._icontext.text_color = aColor

    def set_forced_width(self, aWidth):
        self._icontext.forced_width = aWidth

 

Ce widget est utilisé dans une grille :

picto = IconTextCounter()
picto.text = aCategory.catlblib
picto.icon = 'images/category_%s.png' % aCategory.catcdcode
picto.counter = "%s" % nb_pois
picto.counter_position = 'top-right'
self.grid_widget.add_widget( picto )

 

Snippet kv : Scrollable text

Présentation

Kivy permet d’implémenter un texte trop long et qui nécessite de scroller vers le bas, mais avec une composition de beaucoup de widgets pour l’intégrer à une page ainsi qu’une configuration assez difficile.

 

Composition

Ce widget hérite de la classe ScrollView qui permet le scrolling et contient un GridLayout, sur 3 lignes :

  • 1ère ligne : un label pour faire le padding-top
  • 2ème ligne : un autre GridLayout, sur 3 colonne :
    • 1ère colonne : un label pour faire le padding-left
    • 2ème colonne : le texte sous forme d’un Label
    • 3ème colonne : un label pour faire le padding-right
  • 3ème ligne un label pour faire le padding-bottom

 

Code kv

<ScrollableText>:
	_text_widget: _text_widget_id
	_padding_top: _padding_top_id
	_padding_left: _padding_left_id
	_padding_right: _padding_right_id
	_padding_bottom: _padding_bottom_id
	canvas.before:
		Color:
			rgba: self.background_color
		Rectangle:
			pos: self.pos
			size: self.size
	GridLayout:
		cols: 1
		height: _text_widget_id.height + _padding_top_id.height + _padding_bottom_id.height
		size_hint_y: None

		Label:
			id: _padding_top_id
			text: " "
			size_hint_y: None
			height: sp(4)

		GridLayout:
			rows: 1
			Label:
				id: _padding_left_id
				text: " "
				size_hint_x: None
				width: sp(8)
			Label:
				id: _text_widget_id
				text: "no text yet"
				font_size: '18sp'
				color: (0.1,0.1,0.1, 1)
				valign: 'top'
				# make it scrollable
				text_size: (self.width, None)
				size_hint_y: None
				size: (self.parent.width, self.texture_size[1] )
			Label:
				id: _padding_right_id
				text: " "
				size_hint_x: None
				width: sp(8)
		Label:
			id: _padding_bottom_id
			text: " "
			size_hint_y: None
			height: sp(4)

Astuce : la propriété « background_color » est directement utilisée dans le KV et non dans le python (comme la taille du texte par exemple).

 

Code Python

class ScrollableText(ScrollView):
    text = StringProperty("")
    background_color = VariableListProperty([1,1,1,0])
    text_color = VariableListProperty([0,0,0,1])
    padding = VariableListProperty([sp(8), sp(8), sp(8), sp(8)])
    font_size = NumericProperty(sp(18))

    def __init__(self, **kwargs):
        super(ScrollableText, self).__init__(**kwargs)
        self.bind(text = ScrollableText.set_text,
                text_color = ScrollableText.set_text_color,
                padding = ScrollableText.set_padding,
                font_size = ScrollableText.set_font_size
                )

    def set_text(self, aText):
        self._text_widget.text = aText

    def set_text_color(self, aColor):
        self._text_widget.color = aColor

    def set_padding(self, aPadding):
        """Top, Right, Bottom, Left
        """
        if isinstance(aPadding, (float,int,long,float)):
            self._padding_top.height = aPadding
            self._padding_right.width = aPadding
            self._padding_bottom.height = aPadding
            self._padding_left.width = aPadding
        if len(aPadding) == 4:
            self._padding_top.height = aPadding[0]
            self._padding_right.width = aPadding[1]
            self._padding_bottom.height = aPadding[2]
            self._padding_left.width = aPadding[3]
        elif len(aPadding) == 2:
            self._padding_top.height = aPadding[0]
            self._padding_right.width = aPadding[1]
            self._padding_bottom.height = aPadding[0]
            self._padding_left.width = aPadding[1]

    def set_font_size(self, aSize):
        self._text_widget.font_size = aSize

 

Kivi : surcharge du ScreenManager

Introduction

Le ScreenManager de Kivy permet de gérer les pages de l’application mobile comme des pages qui se chevauchent, scrollable horizontalement. L’interface Metro l’utilise dans Windows 8. Si on utilise le ScreenManager fourni par Kivy, il faut pas mal de code pour scroller d’un écran à un autre en faisant attention à l’écran précédent et à l’écran suivant. Avec une surcharge de cette classe, le code est réduit et l’utilisation est uniforme.

Code habituel

Habituellement, Kivy nous conseille de procéder comme ceci (1 écran « Welcome » et 3 écrans « Settings », « About », « Battery ») :

class Welcome(Screen):

    def do_enter(self):
        self.manager.transition = SlideTransition(direction="left")
        self.manager.current = SettingsApp.screenName
        dataforSettings = ....
        self.manager.current_screen.setItems( dataforSettings )

class WelcomeApp(App):
    def build(self):
        self.manager = ScreenManager()

        # ajout de l'instance de page d'accueil
        welcomeScreen = Welcome(name='Welcome')

        self.manager.add_widget(welcomeScreen)

        # ajout des vues pour les 3 écrans
        for app in [SettingsApp(), AboutApp(), BatteryApp()]:
            app.load_kv()
            self.manager.add_widget( app.build() )

        self.manager.transition = SlideTransition(direction="left")

        return self.manager

class Settings(Screen):
    def do_enter(self):
        ## passer a l'ecran suivant
        self.manager.transition = SlideTransition(direction="left")
        self.manager.current = screens.AboutApp.screenName
    def back(self):
        self.manager.transition = SlideTransition(direction="right")
        self.manager.current = 'Welcome'

class SettingsApp(App):
    screenName = "Settings"
    def build(self):
        return Settings(self.screenName)

Explication de Kivy : http://kivy.org/docs/api-kivy.uix.screenmanager.html

Il faut donc :

  • gérer les directions des transitions (« left » ou « right »)
  • donner les noms des écrans
  • utiliser ces noms d’écrans dans les autres écrans (genre plat de spaghetti)

 

Proposition d’amélioration

Puisque ces écrans sont séquentiels (1 -> 2 -> 3 et jamais 1 -> 3 -> 1 -> 2), on peut simplifier cette gestion dans le ScreenManager.

1. Surcharge du ScreenManager

Une nouvelle classe est définie et elle prendra en charge la gestion du bouton retour sous Android :

class CustomScreenManager(ScreenManager):
    def __init__(self, **kwargs):
        super(ScreenManager, self).__init__(**kwargs)
        self.allScreens = []
        self.screenIndex = 0
        Window.bind(on_keyboard=self.hook_keyboard)

    def hook_keyboard(self, window, key, *largs):
        if key == 27: # BACK
            return self.go_back()
        elif key in (282, 319): # SETTINGS
            print("SETTINGS")

    def add_screen(self, aScreen):
        self.allScreens.append(aScreen.name)
        self.add_widget(aScreen)

    def go_next(self):
        self.screenIndex = self.screenIndex + 1
        self.transition = SlideTransition(direction="left")
        self.current = self.allScreens[self.screenIndex]
        return self.current_screen

    def go_back(self):
        if self.screenIndex == 0:
            return False
        self.screenIndex = self.screenIndex - 1
        self.transition = SlideTransition(direction="right")
        self.current = self.allScreens[self.screenIndex]
        self.current_screen.postback()
        return True

Elle gère un tableau d’écrans et un index pour savoir quel écran est présenté. Pour le premier écran, le retour ne fait rien.

Méthodes :

  • go_back() : un écran l’appelle pour retourner en arrière. L’écran n’est pas obligé de savoir quel écran il doit appeler.
  • go_next() : un écran l’appelle pour faire apparaître l’écran suivant. L’écran n’est pas obligé de savoir quel écran il doit appeler.

 

2. Modifications dans l’utilisation

L’utilisation de cette fonctionnalité est alors un simplifiée et uniforme :

class Welcome(Screen):

    def do_enter(self):
        self.manager.go_next()
        reader = ClientDataReader()
        self.manager.current_screen.setItems( reader.getAllRecords() )

    def postback(self):
        pass

class WelcomeApp(App):

    def build(self):
        manager = CustomScreenManager()

        # ajout de l'instance de page d'accueil
        welcomeScreen = Welcome(name='Welcome')

        manager.add_screen(welcomeScreen)
        # ajout des vues pour les 3 écrans
        for app in [SettingsApp(), AboutApp(), BatteryApp()]:
            app.load_kv()
            self.manager.add_screen( app.build() )

        return self.manager

class Settings(Screen):
    def do_enter(self):
        ## passer a l'ecran suivant
        nextScreen = self.manager.go_next()

    def postback(self):
        pass

class SettingsApp(App):
    screenName = "Settings"
    def build(self):
        return Settings(self.screenName)

Dans le fichier « settings.kv », l’appel à l’écran précédent « root.back() » devient « root.manager.go_back() ».

3. Encore plus loin

Pour terminer complètement cette amélioration, il faut définir une classe CustomScreen (qui hérite de Screen) et qui défini les 4 méthodes :

  • pre_back() : avant de passer à l’écran précédent
  • post_back() : une fois que l’écran précédent est affiché
  • pre_next() : avant de passer à l’écran suivant
  • post_next() : une fois que l’écran suivant est affiché

Par défaut, ces 4 méthodes ne font rien et les classes des écrans (qui héritent de Screen) doivent hériter de CustomScreen.

On a alors un code bien plus simple :

class Settings(Screen):
    def do_enter(self):
        ## passer a l'ecran suivant
        nextScreen = self.manager.go_next()

class SettingsApp(App):
    screenName = "Settings"
    def build(self):
        return Settings(self.screenName)

Téléchargement : customscreen.py

Génération de PDF avec coloration de texte

Introduction

La bibliothèque  »FPDF » (de Olivier PLATHEY) qui permet la génération de PDF à partir de code PHP est surchargée par une autre bibliothèque « PDF_HTML » (de Radek HULAN), permettant d’écrire du code HTML formaté.

Tout l’objet de cette surcouche est de parser le HTML et d’écrire du code spécifique  au langage PDF. Sauf qu’elle est incomplète pour le formatage avec un couleur de texte ou une couleur de fond de texte… Lorsqu’on formate son texte avec CK-Editor (par exemple), le code enregistré est du HTML et la coloration se fait par des tags SPAN style= »color: … » ou style= »background-color: …. ».

Voici les modification effectuées pour permettre cette coloration, mais elle a ses limites (jusqu’à ce qu’un autre développeur apporte sa pierre à l’édifice).

 

Modifications opérées

  1. Prise en compte correcte l’attribut du SPAN (afin d’avoir l’attribut complet)
  2. Ajout d’une variable $startFillColor (qui indique qu’on commence à écrire avec un attribut background-color)
  3. Prise en compte du tag SPAN et changement des colorations selon l’attribut
  4. Lecture de la couleur avec le mode hexa : « #ff00ff » ou en mode rgb : « rgb(255, 0, 255) »

 

Limitations

  1. Pour le changement de couleur du texte, la bibliothèque PDF l’intègre totalement. Ceci nous assure que cet attribut dans le HTML sera toujours correctement respectée.
  2. Rien n’est disponible pour prendre en compte cette exigence. Afin de bien faire le background, un rectangle est dessiné juste avant d’écrire le texte. Si le texte  doit être sur plusieurs lignes, l’attribut ne fonctionnera pas car le rectangle ne sera pas bien calculé (et il faudrait un rectangle par ligne…). Il faut mettre cet attribut sur chaque mot ou sur chaque ligne.

 

Téléchargement

Afin de mettre à disposition le code, je le mets en téléchargement sur mon site, mais je vais le communiqué à l’auteur Radek HULAN.

Télécharger la bibliothèque PDF_HTML modifiée.

Appli Mobile Kivy : Compte à rebours

Introduction

Après avoir réalisé l’application qui présente en détail les thés, j’aimerai la compléter par une autre application qui fait un compte à rebours. Voici le détail de sa réalisation avec Kivy.

 

Design

Après mes premiers essais avec Kivy, j’ai constaté l’importance de la conception des fichiers KV avant même de coder en python. C’est un peu comme si on réfléchissait à la position des champs et boutons dans les écrans avant de concevoir la base de données… un peu déroutant.

Cette application sera en 2 écrans (widget « Screen ») :

  • Ecran #1 de saisie du nombre de minutes et secondes à décompter
  • Ecran #2 de présentation du décompte, avec principalement 2 boutons « Start » et « Stop »

 

Détail de l’écran #1

Le fichier KV se décompose en :

  • BoxLayout : layout principal
    • Label : « Simple Countdown »
    • GridLayout : en 2 colonnes, avec
      • TextInput : minutes à saisir
      • TextInput : secondes à saisie
      • Label : « Minutes »
      • Label : « Secondes »
    • Label pour le message d’erreur de formatage d’un des 2 champs de saisie
    • BoxLayout : pour avoir un « spacing » sur le widget contenu
      • Button : « Ready », qui valide les données et lance le 2ème écran

Résultat :

 

Détail de l’écran #2

Le fichier KV se décompose en :

  • BoxLayout : layout principal
    • Label : « Simple Countdown »
    • GridLayout : en 2 colonnes, avec
      • Label : décompte des minutes
      • Label : décompte des secondes
      • Label : « Minutes »
      • Label : « Secondes »
    • GridLayout : en 2 colonnes, avec
      • Button : « Start »
      • Button : « Stop »
    • BoxLayout :
      • Button : « Back », qui valide revient au premier écran

Résultat :

 

Conclusion

Difficultés :

  • Définir la bonne façon pour découper les écrans : vaut-t-il mieux utiliser un BoxLayout ou un GridLayout pour placer les objets en une colonne ?
  • Définir la bonne taille des objets : dans un GridLayout, il faut définir la hauteur de chaque case d’une ligne pour que la hauteur de la ligne soit prise en compte.
  • La taille de la police est différente entre mon Samsung S4 et le rendu sur mon PC. Des ajustements sont toujours à faire

La manipulation de l’objet « Clock » est plutôt bien documentée et cohérente. L’ajout de sons dans une application Kivy est très facile avec la classe SoundLoader (bien documentée aussi).

[ Télécharger l'APK ] [Télécharger les sources]

 

Relativité autour de nous

En passant

A quelle vitesse on se déplace ?

Rien à voir avec les sujets habituels de ce site, mais je tenais à faire partager ce point de vue : La vitesse de rotation de la terre autour du soleil.

La terre au tour du soleil

La terre tourne autour du soleil en 1 an, soit 365,25 x 24 x 60 x 60 secondes. Elle se situe à 149,6 . 106 km du soleil.

Sa vitesse de rotation est donc 2π x 149,6 . 106 / 365,25 x 24 x 60 x 60 = 29.8 km / s ; soit 29 800 m/s

Si on compare cette vitesse à celle de la lumière (qui est de 299 792 458 m/s), on a un ratio de 1 pour 10 000. C’est assez impressionnant finalement…

Et les autres planètes ? Laquelle est la plus rapide ?

  • Mercure : à 58 millions de km du Soleil, fait une révolution en 87,969 jours ==> 47 km/s
  • Vénus : à 108 millions de km du Soleil, fait une révolution en 224,7 jours ==> 35 km/s
  • Mars : à 227 millions de km du Soleil, fait une révolution en 687 jours ==> 24 km/s
  • Jupiter : à 778 millions de km du Soleil, fait une révolution en 399 jours ==> 13 km/s
  • Saturne : à 1 421 millions de km du Soleil, fait une révolution en 10 758 jours ==> 9.6 km/s
  • Uranus : à 2 883 millions de km du Soleil, fait une révolution en 30 800 jours ==> 6.8 km/s
  • Neptune : à 4 500 millions de km du Soleil, fait une révolution en 60 224 jours ==> 5.4 km/s

La terre autour de son axe

Le rayon de la terre est de 6378,137 km à l’équateur, ce qui fait un périmètre de 40 . 106 m.

La vitesse à laquelle est projeté une personne se trouvant à l’équateur est donc de 40 . 106 m / 24h (=86 400 secondes) ; soit 463 m/s, alors qu’aux pôles, la vitesse est nulle.

 

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é)