Validations optionnelles en rails

Les validations

En rails les validations sont un concept particulièrement intéressant et pratique. Effectuées au niveau du modèle elles constituent le dernier rempart avant la modification de la base de donnée.

Cependant, suivant le contexte de l'application les vérifications ne sont pas forcément les même, certaines parties d'une application peuvent requérirent plus d'informations que d'autres, ce qui nous oblige à valider uniquement le nombre de champs minimal par défaut.

Prenons un cas concret : on a un modèle Contact. On veut que notre contact puisse laisser un avis sur le site mais aussi prendre un rendez vous.
Afin de dispatcher le rendez vous on a besoin de connaitre le code postal de ce contact.
Toutefois ce serait décourageant de demander le code postal d'une personne souhaitant uniquement laisser un avis.

Dans ce cas les seuls validateurs par défaut seront le nom et le prénom :

 # Dans le modèle
validates_presence_of :firstname
validates_presence_of :lastname

Coté vue :

 # Dans la vue
<%= error_messages_for :contact %>

Comment faire alors pour valider le code postal, si et seulement si nous somme en présence d'une demande de rendez vous ?

La mauvaise idée serait de penser faire le test dans notre contrôleur :

 # Dans le contrôleur
@contact.errors.add(:zip_code, "n'est pas valide.") unless params[:contact][:zip_code] =~ /^\d{5,6}$/
return render :action => 'form' unless @contact.save

Tout d'abord, ce code ne fonctionne pas (du moins en 1.2.6) : il est toujours possible d'insérer un enregistrement sans, ou avec un mauvais, code postal.
En effet la méthode save écrase le hash d'errors (tout comme la méthode valid?).
La méthode save ne teste donc pas si son modèle contient déjà des erreurs ou non. Ce qui signifie que si nos champs lastname et firstname sont remplis, le hash d'erreurs sera vide et l'enregistrement correct.

Pour avoir un code qui fonctionne voilà le genre d'insanités qu'il faudrait écrire :

 # Dans le contrôleur
if params[:contact][:zip_code] =~ /^\d{5,6}$/
      return render :action => 'form' unless @contact.save
else
      @contact.valid?
      @contact.errors.add(:zip_code, "n'est pas valide.")      
      return render :action => 'form'
end

Le fait d'insérer l'erreur après le valid? garantit à l'utilisateur de voir toutes les erreurs. Quoi de pire que de découvrir les champs requis au fur et à mesure de la saisie, après plusieurs soumissions.

Toutefois imagines la qualité du code au cas ou il y aurait plusieurs champs optionnels à vérifier. Quid aussi du côté DRY si on se retrouve à avoir le cas pour plusieurs formulaires ? Dupliquer ce morceau de code serait vraiment une mauvaise idée.

La bonne solution :validate ... :if

La meilleure idée c'est toujours de mettre nos validations dans le modèle :

 # Dans le modèle
def validate_zip_code
    errors.add(:zip_code, "n'est pas valide.") unless zip_code =~ /^\d{5,6}$/
end 

À ce niveau on pourrait choisir d'overrider validate dans notre contrôleur comme ceci :

 # Dans le contrôleur
def submit
    @contact = Contact.new(params[:contact])
  
    def @contact.validate
      validate_zip_code
    end

    ...
end

En effet, rien ne nous empêche d'ajouter des méthodes à notre objet au sein même de l'action, mais ça reste une mauvaise idée.
La notion d'ajout de méthodes dans un objet “à chaud” est un concept à manipuler avec précaution, qui peut s'avérer un casse tête à débugger sur du code volumineux.
De plus, dans le cas d'autre formulaires il serait dommage d'avoir à surcharger systématiquement la méthode validate.

Nous allons donc (quasiment) tout réaliser dans le modèle. Pour cela on rajoute un champ à notre modèle : optional_validations ...

 # Dans le modèle
  def optional_validations
    @optional_validations ||= []
  end

... ainsi qu'un validateur standard, doté du symbole :if :

 # Dans le modèle
validates_format_of :zip_code, :with => /^\d{5,6}$/, :if => Proc.new {|c| c.optional_validations.include?(:zip_code)}

Nous venons de mettre en place un getter qui renvoie notre propriété ou un tableau vide. Le validateur prend en paramètre une regex qui teste que le code postal contient bien 5 ou 6 chiffres au maximum.
Proc prend en argument un bloc de code. La variable c correspond à l'instance de contact courante.
La validation du code postal ne s'effectuera donc que si le symbole :zip_code est présent dans les validations optionnelles.

Il ne nous reste donc plus qu'à modifier légèrement notre contrôleur :

 # Dans le contrôleur
@contact.optional_validations << :zip_code

Le fait que notre getter renvoie un tableau vide permet d'éviter de faire un push sur nil dans le cas du premier ajout de symbole.

Voilà, nous avons maintenant à disposition un système de validations optionnelles souple et évolutif, qui impacte nos contrôleurs au minimum.

Qt + WebKit = espoir pour Lark

J’avais de grands espoirs en Lark et puis avec la peine que j’ai eu à développer en Javascript avec XulRunner et les difficultés rencontrées pour la version Windows m’ont quelque peu découragé. Mais j’ai eu l’idée il y a quelques temps de changer complètement ma façon de voir les choses.

J’ai écrit un petit article il y a quelques semaines sur QtJambi et JRuby avec un petit teaser sur WebKit. Vous imaginez le truc : Un runtime ala Prism ou Adobe AIR totalement développé en Ruby ! Mais bon voilà pour l’instant je dois attendre.

En effet WebKit ne sera intégré qu’à Qt 4.4 et la beta qui est sortie hier pour C++ n’est pas encore disponible pour QtJambi (ils l’annoncent pour la fin du mois). Alors en attendant j’ai essayé de créer une petite application Qt en C++ (c’est la première fois que je touche à ce langage) qui intègre des scripts bash de démarrage de d’arrêt de mongrel et une petite fenêtre avec WebKit. Et ça marche !

En une 30aine de lignes, j’ai réussi à créer un petit web browser, à lancer Mongrel au démarrage de l’application et à le stopper quand l’application se quitte ! Un nouveau Lark réalisable facilement sous Mac et Linux (un peu plus difficilement sous Windows mais pas trop non plus). Je vous conseille de lire l’article et surtout de voir les vidéos de démo des possibilités de WebKit avec Qt. On pourrait presque parler de Silverlight-killer.

Bien sûr je n’aime pas du tout le C++ et je préfèrerais pouvoir utiliser Ruby, mais je vais prendre mon mal en patience et attendre quelques jours (pas plus j’espère). Le renouveau de Lark est en bonne voie et je vous tiendrait au courant avec des bouts de code et des articles sur JRuby, Qt et QtJambi.

Thin, le buzz qui se justifie

C'est quoi Thin ?

Depuis maintenant 3 mois seulement un nouveau gem est apparu comme webserver. Sa logique est simple. Prendre tout ce qu'il y a de meilleur partout, de compiler tout çà et d'en faire un meilleur produit. Ce gem, c'est Thin. Il se dit "Yet Another WebServer". Je pense que le titre est en référence à YARV. Mais je n'en ai aucune preuve.

Depuis la version 0.7.0 de thin, le buzz a pris de l'ampleur sur les bienfaits de ce WebServer. Étant un peu aventurier, j'ai décidé de tenter l'aventure. J'ai migré mes 3 applications de mongrel_cluster vers Thin. Voici donc comment faire cette migration car il n'y a rien de plus simple.

Installer Thin

Il faut déjà installer le gem thin

#gem install thin

Le meilleur moyen pour lancer les commandes de lancement de Thin, il faut allez dans le répertoire de son application Rails. Dans ce répertoire, faire :

$thin config -C config/thin.yml

Avec cette commande thin va préparer un fichier de configuration complet il ne vous restera plus qu'à le modifier. J'ai mis ce fichier dans config/thin.yml pour être homogène avec mongrel_cluster. Il n'y a aucune obligation de nomage ou d'emplacement. Le fichier de base est du type suivant :

---
pid: tmp/pids/thin.pid
log: log/thin.log
port: 3000
max_conns: 1024
timeout: 30
environment: development
max_persistent_conns: 512
chdir: /var/rails/typo-5-0-stable
address: 0.0.0.0

Il ressemble ainsi très fortement au fichier de mongrel_cluster.yml. Il ne reste plus qu'a le modifier pour qu'il fasse ce que l'on souhaite. En général, on modifie le port et l'environnement. Ensuite, on peux définir le nombre d'instance de thin dédiés à cette application. Pour cela on a juste à rajouter la clé servers: et donner le nombre d'instance. Mon fichier est ainsi devenu :

---
pid: tmp/pids/thin.pid
log: log/thin.log
port: 44000
max_conns: 1024
timeout: 30
environment: production
max_persistent_conns: 512
chdir: /var/rails/typo-5-0-stable
address: 0.0.0.0
servers: 2

Enfin pour démarrer ou arreter ces instances, rien de plus simple.

démarrage :

$ thin start -C config/thin.yml

arrêt :

$ thin stop -C config/thin.yml

Et maintenant l'intérêt ?

Intérêt de thin

Au niveau de la mémoire, il n'y a pas vraiment de différence entre mongrel et thin. Ils utilisent a peu près la même mémoire d'après ce que j'ai pu constater. Par contre, au niveau de la vitesse de réponse, thin est un peu plus rapide. Ainsi voici les résultats d'un benchmark rapide que j'ai réalisé sur ma dédibox.

Pour mongrel

hello:~/$ ab -n 1000 -c 10 http://dev.shingara.fr/
This is ApacheBench, Version 2.0.40-dev  apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking dev.shingara.fr (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests


Server Software:        nginx/0.6.26
Server Hostname:        dev.shingara.fr
Server Port:            80

Document Path:          /
Document Length:        4640 bytes

Concurrency Level:      10
Time taken for tests:   81.920747 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      4995000 bytes
HTML transferred:       4640000 bytes
Requests per second:    12.21 [#/sec] (mean)
Time per request:       819.207 [ms] (mean)
Time per request:       81.921 [ms] (mean, across all concurrent requests)
Transfer rate:          59.53 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       36   38   1.5     38      76
Processing:    94  778 726.4    644    4108
Waiting:       92  774 723.3    641    4106
Total:        131  816 726.4    681    4146

Percentage of the requests served within a certain time (ms)
  50%    681
  66%   1273
  75%   1344
  80%   1382
  90%   1669
  95%   1924
  98%   2634
  99%   3011  
 100%   4146 (longest request)

Pour thin

hello:~/$ ab -n 1000 -c 10 http://dev.shingara.fr/
This is ApacheBench, Version 2.0.40-dev  apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking dev.shingara.fr (be patient)
Completed 100 requests                                                                                                                                 
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests


Server Software:        nginx/0.6.26
Server Hostname:        dev.shingara.fr
Server Port:            80

Document Path:          /
Document Length:        4640 bytes

Concurrency Level:      10
Time taken for tests:   67.245564 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      4986008 bytes
HTML transferred:       4646330 bytes
Requests per second:    14.87 [#/sec] (mean)
Time per request:       672.456 [ms] (mean)
Time per request:       67.246 [ms] (mean, across all concurrent requests)
Transfer rate:          72.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       36   44 133.9     38    3035
Processing:    93  621 462.3    518    1946
Waiting:       91  548 389.6    473    1665
Total:        132  666 483.9    561    4278

Percentage of the requests served within a certain time (ms)
  50%    561
  66%    878
  75%   1017
  80%   1098
  90%   1309
  95%   1580
  98%   1703
  99%   1795
 100%   4278 (longest request)

Ca veux dire quoi ?

Il y a une petite avance pour thin qui permet de faire 14,87 requêtes par seconde contre 12,21 requêtes pour mongrel. Tout cela avec 10 instances simultanés et un total de 1000 requêtes. Mais après tout thin n'a que 3 mois. Il est encore jeune et n'a pas de version 1.0. Ça devrait pouvoir encore s'améliorer.

Coté parano ?

Je n'ai pas encore pu tester la stabilité de thin. En effet, on peux se demander à juste titre si il est vraiment stable. Pour cela il faudra attendre.

Brand New Logo !

La première étape de la refonte complète de Boldr est terminée. Je recherchais une nouvelle identité visuelle et je voulais pour ça un logo avec des couleurs plus claires et une nouvelle police de style qui fasse plus différente par rapport à celle d’Ubuntu. Et voici le résultat :

J’aime plutôt bien, on dirait un truc de super héros du genre Superman ou les 4 Fantastiques. Ma première tentative en Janvier dernier donnait déjà le ton et j’ai bien envie de dire que j’ai transformé l’essai.

Maintenant que le logo est fait, ça va être beaucoup plus reposant pour moi car je ne voulais pas commencer le design avant d’en avoir trouver un. Je vais donc pouvoir commencer à passer les premiers coups de pinceaux mais comme pour le logo je veux que tout soit impeccable donc il va falloir encore attendre quelques semaines (ou pas) avant de découvrir le nouveau Boldr avec des nouvelles rubriques et des nouvelles choses (ou pas).

Le design actuel date de fin Aout et ça se voit je ne l’aime plus du tout. Mais bon je ne suis plus comme il y a 2 ou 3 ans à refaire les designs de mes sites tous les mois (surement parce que depuis j’ai du boulot ^^).

N’hésitez pas à me dire ce que vous en pensez. Pour la première fois j’ai fait quelque chose sous Illustrator donc ok c’est une bulle avec un dégradé radial, une lettre et une ombre portée mais bon c’est déjà ça. En tout cas alors que j’avais jamais vraiment aimé Illustrator en terme d’interface, la CS3 me comble assez.

Gtalk chatback

Google vient de publier un service complmentaire Gtalk, le chatback. Cela consiste en une iframe copier sur votre site, et permet n’importe qui de chatter avec vous sur un simple clic.
Dommage que a utilise flash et que l’on doit ouvrir un navigateur web pour rpondre, mais cela reste trs pratique.

via zorglub.

Le projet MacRuby

MacRuby est un projet d’Apple (initié par Laurent Sansonetti) qui a pour but de faire tourner un Ruby 1.9 au dessus d’Objective C, c’est à dire en utilisant le runtime et le ramasse-miettes d’Objective C. Toutes les classes Ruby sont par exemple des classes ObjC, tous les objets Ruby sont des objets ObjC. Le but est également de résoudre certains problèmes de RubyCocoa . MacRuby est encore au stade expérimental et nécessite Mac OS X > 10.5.2 sur processeur Intel.

Le projet MacRuby

MacRuby est un projet d’Apple (initié par Laurent Sansonetti) qui a pour but de faire tourner un Ruby 1.9 au dessus d’Objective C, c’est à dire en utilisant le runtime et le ramasse-miettes d’Objective C. Toutes les classes Ruby sont par exemple des classes ObjC, tous les objets Ruby sont des objets ObjC. Le but est également de résoudre certains problèmes de RubyCocoa . MacRuby est encore au stade expérimental et nécessite Mac OS X > 10.5.2 sur processeur Intel.

Il y a un VISTA chez moi…

Pour être exact, il y a le portable, sous Vista, de ma copine qui trône à côté de mon MacBook, mon MacBook Pro, mon Mac Mini, mon iPhone, mon iPod Touch, … Et bien je dois avouer que Windows est resté aussi amusant que dans mes souvenirs :

ilovewindows.jpg

iPhone : attendre, encore et toujours …

Comme pour tous les nouveaux produits high tech bling bling qui sortent j'essaie d'être un peu raisonnable et de ne pas me jeter dessus dans un excès de geekerie.

Je préfère que ce soit les premiers qui essuient les pots cassés : problèmes software, matériel défectueux (même si ça arrive sur des produits rodés), etc ...

En ce moment j'ai toutes les bonnes raison d'attendre pour l'iPhone :

  • D'une part on évoque de plus en plus un iPhone 2, qui en plus d'intégrer la 3G (et même la 3.5G) se verrait munie d'une caméra embarquée. Il semble de plus en plus probable qu'Apple mette en place une version mobile d'iChat afin de faciliter la visio conférence. Cette fonctionnalité serait disponible sur les iphone et les touch. Il y aussi de fortes chances que l'on voit apparaitre du GPS. Le frein majeur à toutes ces évolutions étant l'autonomie de la batterie, un gros travail devra être fait à ce niveau.
  • Ensuite quelques bruits de couloir évoquent une baisse de celui-ci d'environ 100$. Même si ce n'est pas dans les habitudes d'Apple de casser les prix on est en droit d'y croire puisque que pour un grand nombre, le prix élevé reste le principal obstacle.
  • Je suis curieux de voir un peu le potentiel du SDK afin de pouvoir répondre à la fameuse question : jailbreakera, jailbreakera pas ? Si chaque application doit être validée par Apple afin de pouvoir être installée inutile de dire que ce serait une énorme déception.
  • Me concernant ce qui m'a toujours le plus rebuté c'est ce forfait data anormalement cher, facturé par orange. Ici ce que j'attend avec impatience c'est l'arrivée de free sur le marché de la 3G. Si free parvient à obtenir cette licence il disposera alors d'une force de frappe conséquente dans le domaine de la téléphonie (et autres applications) mobiles puisqu'il détient aussi une licence WiMAX.

Etant donné que Free a souvent mis des grands coups de pied dans la fourmilière on peut rêver de plusieurs choses :

  1. De la téléphonie 100% IP sur son mobile, et donc des coûts largement réduits, voire de l'illimité.
  2. La gratuité des appels depuis sa freebox vers les mobiles (d'ailleurs Alice commence à offrir des heures de communications).
  3. Une seule facture englobant sa box triple and play et son forfait mobile (cependant ne révons pas trop, le forfait ne serait sûrement plus de 29,99€)

Il est amusant de constater que la stratégie de Free est à l'opposé de celle de SFR : se servir de son implantation dans les foyers pour conquérir le marché de la mobilité.
Nul doute que les mois à venir nous réservent encore de belles suprises ...

quand la vue modifie son layout

Cas d'utilisation

Dans l'administration de Typo, en fonction du controller appelé, voir de l'action, il faut modifier la partie indiquant le sous-menu. On a donc 3 possibilités pour réaliser cette partie dynamique.

  1. Mettre la partie variable directement dans la vue et l'enlever du template.
  2. Mettre les informations directement dans le controller ou l'action et c'est cette information qui sera lu par le template
  3. Utiliser content_for directement dans la vue en utilisant le yield dans le template

Bien sûr la meilleur méthode est la troisième. Mais je vais expliquer pourquoi et comment on peux mettre en oeuvre.

Mettre la partie variable directement dans la vue et l'enlever du template

Cette méthode est de loin la plus simple qui soit. On sait que notre partie n'est pas toujours affiché pour le layout indiqué, donc on la met pas dans le template. Mais voilà, si on a du code entre notre partie dynamique et le vrai contenu de notre vue, on a pas le choix il faut dupliquer cette partie et ainsi on se retrouve à faire plein de copier coller et comme on sait le copier coller c'est pas DRY. Il faut donc essayer de proscrire cette méthode. Le cas le plus horrible est bien sur la modification de la partie que vous avez mis dans chaque vue et ceux sur vos 100 vues. Bonne chance.

Mettre les informations directement dans le controller

Cette technique est un peu plus évolué que la précédente car elle permet d'être plus DRY. En effet, si on a du code entre sa partie "dynamique" et sa vue, on est pas obligé de l'intégrer dans chaque vue. Ce qui permet d'avoir un template plus important et des vues plus légères. Mais l'inconvénient de cette technique et qu'il faut gérer l'affichage et des techniques visuel directement dans le controller. Par exemple le nom des liens, etc.. Ce n'est pas du tout dans la logique MVC, car une partie de la vue est généré et controllé par le controlleur. Le controlleur ne doit que définir quel vue utiliser et donner les variables issues d'une ressources externes.

Utiliser content_for directement dans la vue en utilisant le yield dans le template

Pour que ça soit la vue qui gère le contenu dans le template sans que le controller s'en mêle, la meilleur technique est d'utiliser content_for. Cette méthode permet de mettre en interne un bloc de code HTML. Ce bloc de code HTML n'est lu que par l'utilisation d'un yield :le_nom_du_bloc. On peux ainsi définir le bloc dans la vue et donner sa position directement dans le layout. Si ensuite vous voulez réutiliser le même bloc de code, un render :partial permettra de factoriser encore le code. Voici un exemple d'utilisation de cette méthode avec un template et une vue.

Template :

<html>
  <head>
    <title>;Ma page</title>
  </head>
  <div id="menu">
   <ul>
    <%= yield :menu %>
  </ul>
  </div>
  <p>un peu de texte au milieu</p>
  <div id="content">
    <%= yield %>
  </div>
</html>

Vue :

<% content_for :menu do %>
  <li><% link_to 'Voir', page_url %></li>
<% end %>

<p>Ma vue compléte</p>

Vous trouvez pas ça génial ?

Sinon pour les anciens qui connaissaient la technique avant Rails 2.0 et qui utilisait @content_for_mon_block cette technique est déprécié. Il faut utiliser plutôt le yield depuis la version 2.0.x