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'

jeudi, mars 25 2010

Génération d'url avec des ressources imbriquées en rails

Je suis tombé récemment sur ce qui me semble être un bug dans la génération d'url pour les nested resources, d'ailleurs je l'ai remonté;

Prenons un exemple d'une famille avec plusieurs personnes

map.resources :families, :has_many => :people

La définition des resources dans le fichier de routes permet la mise à disposition des générateurs d'urls. Par exemple:

family_people_path(1) => /families/1/people

Par contre le passage d'un id ou objet nil devrait générer une exception. Or dans une vue ou un contrôleur:

family_people_path(nil) => /families//people

Pourtant en console une exception est bien lancée:

include ActionController::UrlWriter
family_people_path(nil)
ActionController::RoutingError: family_people_url failed to generate from {:action=>"index", :family_id=>nil, :controller=>"people"}, expected: {:action=>"index", :controller=>"people"}, diff: {:family_id=>nil}

Est ce que cela signifie que les exceptions de génération d'URL sont capturées (et étouffées) à un plus haut niveau dans une application rails ?

mardi, janvier 12 2010

I18n et formulaires imbriqués en rails

En rails 2.3.4 je trouve le comportement du error_messages un peu surprenant avec les formulaires imbriqués.

class Person < ActiveRecord::Base
  accepts_nested_attributes_for :address, :allow_destroy => true
end
class Person < ActiveRecord::Base
  validates_presence_of :label
end
<% form_for(@person) do |f| -%>
  <% f.fields_for :address do |address| %>
    <p>
      <%= address.label :label %>
      <%= address.text_field :label %>
    </p>
  <% end -%>
<% end -%>

D'après http://weblog.rubyonrails.org/2009/1/26/nested-model-forms, les erreurs sont recopiées dans l'objet parent. Ils disent que c'est sujet à changement mais apparement c'est toujours le cas. En créant les objets depuis la console:

>> p  = Person.new
>> p.build_address
>> p.save
>> p.errors
=>
#<ActiveRecord::Errors:0x102d94560 @errors={"address_label"=>[#<ActiveRecord::Error:0x102d7be48 …

Du coup dans mon fichier de traduction je mets logiquement:

person:
  address_label: "foo"

Perdu ! En maintant les mains dans le cambouis vendor/rails je suis remonté jusqu'à generate_full_message qui se base en fait sur la classe de base de l'objet (imbriqué ou non). Du coup il va chercher dans:

address:
  address_label: "foo"

C'est pas vraiment logique, surtout que dans 99% des cas on a déjà la traduction:

address:
  label: "foo"

Ce serait cool s'il pouvait aller le chercher là.
Ah et interdiction de répondre t-as-qu-a-faire-un-patch :-)

lundi, octobre 12 2009

Internet Explorer accepte n'importe quoi

Raison de plus pour ne plus l'accepter !

Tout à commencer par le classique:

Client: Ça marche pas quand je rentre dans ma fiche produit.
Développeur: Ça marche chez moi ©.

Voilà le contexte: L'application, en ruby on rails, permet de faire une recherche de produit. Parmi la liste des produits trouvés on peut entrer dans sa fiche et depuis celle-ci faire un export au format excel.

Là en rails on se dit que c'est assez cool à faire. On commence par enregistrer le type Mime des fichers excel dans l'initializer qui va bien (mime_types.rb):

Mime::Type.register "application/vnd.ms-excel", :xls

Ensuite dans l'action il suffit de distinguer les différents formats de rendu:

respond_to do |format|
  format.html
  format.xls {
    # some stuff
  }
end

et dans la vue utiliser la route formatée en passant l'extension enregistrée précédemment (xls).

Sur les navigateurs modernes ça fonctionne bien, sauf que pour mon client sous IE, quand il cliquait sur le lien permettant d'accéder à la fiche produit il avait directement le fichier excel.

Là je commence à me dire qu'il y a un souci dans les headers accept d'IE. Sauf que quand j'essaie de reproduire le le bug sur mon windows virtuel pas moyen. Que ce soit IE7 ou 8 ça fonctionne normalement.

Voilà à quoi ressemble les en-têtes http accept d'un IE standard (je les ai récupérés sur le net d'une personne sous windows 7):
image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*

Là ça fonctionne, mon lien « classique » ne correspond à aucune des type mime précédent, du coup je passe dans le * et pour IE comme il ne sait pas trop quoi en faire il le traite comme du HTML. Que je sache pour les autres navigateurs c'est l'inverse. On regarde d'abord si ce n'est pas du html, du xhtml+xml ou du xml et après on avise.

Mais pour Internet Explorer le web c'est d'abord des documents binaires, pas du texte.

Pour en revenir au problème là ou ça devient vicieux c'est que les en-têtes http accept sont différents si Office est installé sur la machine (ce qui est le cas pour client et pas moi). Là ça devient:
image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-shockwave-flash, application/x-silverlight-2-b2, application/x-silverlight, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*

C'est la que le format excel prend le pas. Donc dans la négociation avec le serveur, IE demande en priorité du format excel plutôt qu'html.

Ca a été remonté sur le tracker de ruby on rails, même si ça n'a rien à voir avec le framework. Toutefois une solution est proposé pour contourner ce comportement d'IE, c'est de forcer le passage du paramètre format à HTML s'il n'est pas défini. Il faut savoir que ce n'est pas fiable à 100% puisque ça se base sur le user agent qui peut être modifié. Mais forcer le format à html pour les navigateurs qui fonctionne déjà correctement ne devrait pas avoir de conséquences fâcheuses.

Avant de tomber sur cette solution j'ai essayé d'utiliser une route formatée dans laquelle je spécifié html comme format mais ça n'a pas fonctionné. Je me retrouvais bien avec un lien se terminant par .html mais le format n'était toujours pas récupéré dans les paramètres de l'action, je ne sais pas vraiment pourquoi.

Bref, encore un post sur le net pour dire une chose: « Meurs IE, meurs ! »

mercredi, juillet 29 2009

Pourquoi ActiveRecord n'utilise pas le nom des associations pour faire ses jointures SQL ?

C'est une vrai question à laquelle je n'ai pas de réponse. Pourtant ça me paraît tellement naturel que je me demande s'il y a une raison technique à ça.

Ci dessous quelques exemples pour illustrer:

class Contract < ActiveRecord::Base
  belongs_to :sender, :class_name => "Company"
  belongs_to :receiver, :class_name => "Company"
end

On commence à imaginer le problème qui va se poser, deux relations qui pointent vers une même classe. Voyons un peu le SQL généré en faisant des jointures.

Contract.all(:joins => [:sender, :receiver])

=>

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` ON `companies`.id = `contracts`.sender_id INNER JOIN `companies` receivers_contracts ON `receivers_contracts`.id = `contracts`.receiver_id

Pour sender AR va donc conserver le nom de la table originale. Pour receiver comme le nom de table companies est déjà utilisé il va devoir en utiliser un autre (receivers_contracts).

D'où ma question: pourquoi ne pas utiliser le nom de l'association (et de façon systématique) ?

Je ne vois pas d'obstacle à cela dans la mesure où le nom de l'association risque fort d'être unique. Le seul problème que je vois est une jointure du même nom dans une classe déjà jointe. Par exemple si Company possédait une jointure nommée sender qu'on voulait utiliser dans le même temps.

Mais dans ce cas précis on pourrait utiliser des alias du style receivers_contracts partout pour générer un:

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` senders_contracts ON `senders_contracts`.id = `contracts`.sender_id INNER JOIN `companies` receivers_contracts ON `receivers_contracts`.id = `contracts`.receiver_id

Actuellement on ne peut pas faire de:

Contract.all(:joins => [:sender, :receiver], :conditions => { :sender => { :name => "foo" } })

Pire, quand on ne connaît pas le comportement par défaut d'AR il faut être devin pour savoir ce que fait:

Contract.all(:joins => [:sender, :receiver], :conditions => { :companies => { :name => "foo" } })

Est ce que je suis entrain de filtrer sur l'expéditeur ou le destinataire ?

Si on veut faire notre recherche en utilisant AR il faudrait donc faire:

Contract.all(:joins => [:sender, :receiver], :conditions => { :companies => { :name => "foo"}, :receivers_bills => { :name => "bar" } })

On ne peut pas dire que ce soit très élégant ni intuitif.

Pourtant ce SQL fonctionne:

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` sender ON `sender`.id = `contracts`.sender_id INNER JOIN companies receiver ON receiver.id = contracts.receiver_id WHERE (`sender`.`name` = 'foo' AND `receiver`.`name` = 'bar');

Si quelqu'un a une explication logique à ça je suis preneur.

mardi, avril 22 2008

ActionMailer et TLS : reloaded

J'avais déjà évoqué comment se connecter à un serveur SMTP en TLS dans un billet précédent mais quelques petites choses ont changé ; finalement c'est encore plus simple.

Étant donné que cela fait un moment que le dépôt de Kyle est HS, le plugin est maintenant disponible ici :
http://svn.douglasfshearer.com/rails/plugins/action_mailer_optional_tls

Pour l'installer il suffit d'utiliser le gestionnaire de plugins en commande :
ruby script/plugin install http://svn.douglasfshearer.com/rails/plugins/action_mailer_optional_tls

Côté configuration j'évite de tout coller dans le fichier environment.rb et je passe plutôt par les initializers qui sont chargés automatiquement au boot de l'appli, et dont c'est le rôle finalement.

Tu peux donc créer le fichier .rb de ton choix (par exemple tls.rb) dans le dossier initializers avec la même config qu'auparavant :

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

Voilà pour la petite mise à jour.

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.