On se tutoie ?

Aller au contenu | Aller au menu | Aller à la recherche

mardi, août 2 2011

couldn't parse YAML at line

Si vous avez une erreur de ce type au boot de votre application rails, il faut changer explicitement le moteur utilisé dans le boot.rb Bundler charge Psych comme moteur YAML par défaut et ce dernier a des soucis avec rails 3.x

require 'yaml'
YAML::ENGINE.yamler= 'syck'

mercredi, septembre 17 2008

Accèder à une base MS SQL en ruby sous linux, avec et sans ActiveRecord

Pré-requis

Cette procédure a été testé sous une debian lenny avec un kernel 2.6.18 (et sous OSX Leopard 10.5.4).
L'objectif est donc d'accéder à une base de données Microsoft SQL Server depuis un script ruby sous GNU / Linux.

Installation des packages

L'idée est de communiquer avec la base MS SQL par le biais des librairies FreeTDS et de l'API ODBC.

On commence par installer les packages nécessaires :

aptitude install unixodbc
aptitude install unixodbc-dev
aptitude install sqsh

Il est possible d'installer freetds par aptitude (install freetds-dev qui installe common) mais dans mon cas il manquait le fichier libtdsodbc.so.
Je l'ai donc installé depuis les sources :

wget ftp://ftp.ibiblio.org/pub/Linux/ALPHA/freetds/stable/freetds-stable.tgz
tar xzvf freetds-stable.tgz
cd freetds-0.82
./configure && make && sudo make install

Il faut maintenant définir quelques variables d'environnement définisant les emplacements des fichiers de configuration.
Soit dans le /etc/profile pour que tous les utilisateurs y ait accès, soit dans le .bashrc uniquement pour l'utilisateur courant :

export ODBCINI=/etc/odbc.ini
export ODBCSYSINI=/etc
export FREETDSCONF=/etc/freetds/freetds.conf

Rechargement de la configuration :

$ source /etc/profile

Configuration de la datasource

Dans le /etc/freetds/freetds.conf :

[MY_DSN]
	host = IP du serveur
	port = 1433
	tds version = 7.0

MyDSN représente le nom de la datasource, on peut y mettre le nom souhaité. On teste la connexion au datasource :

$ sqsh -S MY_DSN -U user -P password

Si tout se passe tu atteris sur un prompt de type :

1>

Il nous faut maintenant indiquer à ODBC d'utiliser FreeTDS :

$ vim /etc/odbc.ini
 
[MY_DSN]
Driver          = FreeTDS
Description     = ODBC connection via FreeTDS
Trace           = No
Servername      = MY_DSN
Database        = YOUR_ACTUAL_DB_NAME
$ vim /etc/odbcinst.ini
 
[FreeTDS]
Description     = TDS driver (Sybase/MS SQL)
Driver          = /usr/local/lib/libtdsodbc.so
FileUsage       = 1

On peut maintenant tester l'accès au datasource par le biais de isql, qui permet l'envoi de requêtes SQL sur le serveur :

$ isql MY_DSN user password
SQL> select top 10 * from users (pas de limit)

Note : La close top agit de la même façon que la close limit pour une base de données MySQL.

Accéder aux données MS SQL depuis ruby, sans ActiveRecord

Nous allons utiliser l'interface d'accès aux données DBI en utilisant le driver ODBC.

Il existe maintenant une gem pour ruby dbi :

# gem install dbi

Faisons un premier essai depuis un shell ruby :

$ irb

require 'rubygems'
require 'dbi'

DBI.connect('dbi:odbc:MY_DSN', login, password)
=> Unable to load driver 'odbc'

DBI ne parvient pas à charger le driver odbc. Actuellement odbc est installé sur notre machine mais il n'y a pas d'extension pour dialoguer avec.

On installe donc la librairie ODBC pour ruby :

$ wget http://www.ch-werner.de/rubyodbc/ruby-odbc-0.9995.tar.gz
$ tar zvxf ruby-odbc-0.9995.tar.gz
$ cd ruby-odbc-0.9995/
$ ruby extconf.rb
$ make
# make install

On installe le driver ODBC pour ruby dbi qui utilise odbc.so installé ci dessus :

# gem install dbd-odbc

Désormais le code testé ci dessus fonctionne.
Note : Pas besoin de require 'odbc', dbi inclus automagiquement les drivers présents.

Il est maintenant possible de faire des requêtes manuelles sans passer par AR.
Des exemples sont disponibles sur la doc officielle de ruby-dbi.

Accéder aux données MS SQL depuis ruby, avec ActiveRecord

Il faut utiliser l'adapter odbc, qui n'est plus inclus de base avec Rails :

# gem install activerecord-odbc-adapter

Une fois effectué, la syntaxe pour établir une connection est la suivante :

ActiveRecord::Base.establish_connection({ :adapter => 'odbc', :dsn => 'MY_DSN', :username => 'user', :password => 'password'})

Note : Ce tutoriel fonctionne sous OSX. Sous Leopard, les fichiers de conf odbc (odbc.ini, odbcinst.ini) doivent se situer dans/Library/ODBC.
Les packages à installer peuvent s'installer avec les port ou être directement compilés.

jeudi, février 28 2008

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.

lundi, octobre 8 2007

www.synbioz.com

Je me laisse tenter par un peu d'auto-promotion pour dire que ça y est, on est en ligne. Si ça t'intéresse il y a même une section pour déposer un avis.

jeudi, août 23 2007

ActionMailer et TLS sont dans un bateau ...

TLS est un protocole de chiffrement des données qui remplace SSL.

Par mesure de sécurité c'est ce que j'utilise quand je mets en place un serveur SMTP, cela permet de crypter les données lors de la transmission du couple login / pass au serveur.

Mais quel rapport avec le module ActionMailer vas tu me dire ?

Aucun, justement. ActionMailer ne gère pas TLS en natif. Comme je n'ai pas envie de me logguer en clair sur mon SMTP, j'ai cherché une solution plus clean. Et c'est Kyle Maxwell qui l'offre, via un plug-in.

Comment mettre en place ce plug-in ?

Commence par récupérer le source directement du trunk :

$ svn co http://svn.kylemaxwell.com/rails_plugins/action_mailer_optional_tls/trunk

Une fois réalisé tu copies les lib (action_mailer_tls.rb et smtp_tls.rb) dans le répertoire lib de ton application rails, et le plus dur est fait.

Il nous reste à inclure ces lib au démarrage de l'application, dans le environnement.rb :

require "#{File.dirname(__FILE__)}/../lib/action_mailer_tls"
require "#{File.dirname(__FILE__)}/../lib/smtp_tls"

Note : Il faut éviter les chemins absolus qui rendent une application difficile à migrer.

Au niveau de la configuration de la connexion au SMTP, ça donne ça (en mode développement, donc dans le development.rb) :

config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_charset = "utf-8"
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
        :address => "smtp.domain.tld",
        :port => 25,
        :user_name =>'login',
        :password =>'pass',
        :authentication => :login,
        :domain => "domain.tld",
        :tls => true
}
config.action_mailer.perform_deliveries = true

Attention, en prod on n'utiliserait pas le même genre de configuration puisque l'on ne voudrait pas que l'appli se gauffre lamentablement devant le client en cas d'échec d'envoi du mail.
On mettrait donc le raise_delivery_errors à false.

De même en dev on n'a pas forcément envie d'envoyer les mails pour de vrai une fois que l'on sait que ça marche.

config.action_mailer.perform_deliveries = false

Dans ce cas on peut stocker les mails dans des variables et les soumettre à des test unitaires.

config.action_mailer.delivery_method = :test

Pour le reste rien ne change, on peut utiliser ActionMailer comme d'habitude, ce qui n'est pas l'objet de ce billet :-)

Note : Pense à rajouter les parenthèses autour du start_method à la ligne 24 du smtp_tls.rb pour éviter les warning au démarrage du serveur ( warning: parenthesize argument(s) for future version), et avoir un code plus conforme.