Rails

Créer des traitements personnalisés pour CarrierWave

Si vous avez besoin de traiter des images sur une application rails, je ne peux que vous conseiller CarrierWave.

Cette librairie vous permettra aisément de gérer de l'upload de fichier. Mais également du post traitement automatisé sur les images, tel que le redimensionnement de celles-ci.

J'utilise CarrierWave sur ce blog, pour une refonte graphique qui devrait être publiée dans deux à trois semaines. Chaque catégorie et chaque article as ainsi une image. Cette image possède différentes versions qui sont chacune plus ou moins grande.

Pour l'une de ces versions, je souhaite obtenir des bords arrondis.
Le résultat désiré est l'image à gauche ici.

Voyons en détails comment j'en suis arrivé au résultat ci-contre.

 

Post processing CarrierWave

Imaginons le modèle d'upload suivant :

class CategoryUploader < CarrierWave::Uploader::Base
    include CarrierWave::RMagick
    version :thumb do
        process :resize_to_fill => [248, 163]
    end
end

Nous avons ici un uploader de catégorie qui créera une seconde image "thumb". Cette image sera de taille 248 * 163 pixels.

Vous pouvez trouver le processeur "resize_to_fill", qui redimensionnera l'image à lib/carrierwave/processing/rmagick.rb.

Afin de créer des bords arrondis, nous allons devoir créer notre propre processeur. Je l'ai placé dans lib/dmathieu/carrier_wave/round.rb.

unless defined? Magick 
    begin
        require 'rmagick'
    rescue LoadError
        require 'RMagick'
    rescue LoadError
        puts "WARNING: Failed to require rmagick, image processing may fail!"
    end
end

module Dmathieu
    module CarrierWave
        module Round

            module ClassMethods
                def rounded_corner(radius = 10)
                    process :rounded_corner => [radius]
                end
            end

            ##
            # Makes the image's corners round
            #
            #
            # === Parameters
            #
            # [radius (#to_s)] the corner radius
            #
            # === Yields
            #
            # [Magick::Image] additional manipulations to perform
            #
            def rounded_corner(radius = 10)
                #
                # Voir plus loin pour le code métier
                #
            end
        end
    end

Puis, dans votre uploader, vous devez inclure ce processeur nouvellement créé.

class CategoryUploader < CarrierWave::Uploader::Base
    include CarrierWave::RMagick
    include Dmathieu::CarrierWave::Round

    version :thumb do
        process :resize_to_fill => [248, 163]
        process :rounded_corner
    end
end

Vous pouvez constater que, dans notre module, nous créons une méthode rounded_corner, qui fera l'action d'ajouter des coins arrondis à notre image. Cette action sera appelée automatiquement par CarrierWave lors de la génération de cette version de l'image.

Le code métier

Afin de manipuler notre image, nous utilisons rmagick.

Pour créer notre image, nous allons faire la chose suivante :

  • Créer une nouvelle image de la même taille que la notre, qui contiendra un rectangle aux bords arrondis.
  • Placer ce rectangle au dessus de notre image.

Plutôt simple non ? ;-)

def rounded_corner(radius = 10)
    manipulate! do |img|
        masq = ::Magick::Image.new(img.columns, img.rows).matte_floodfill(1, 1)         
        ::Magick::Draw.new.
            fill('transparent').
            stroke('black').
            stroke_width(1).
            roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
            draw(masq)

        img.composite!(masq, 0, 0, Magick::LightenCompositeOp)
        img = yield(img) if block_given?
        img
    end
end

Nous venons ici de définir la méthode rounded_corner, qui se situe dans le module expliqué plus haut.

La méthode manipulate! se situe dans le processeur rmagick.rb et nous permet d'obtenir l'image courante, de la modifier et de la transmettre au processeur suivant (via le yield).

masq = ::Magick::Image.new(img.columns, img.rows).matte_floodfill(1, 1)

Nous créons ici une nouvelle image de la même taille que la précédente mais avec intégralement transparente.

::Magick::Draw.new.
        fill('transparent').
        stroke('black').
        stroke_width(1).
        roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
        draw(masq)

Nous dessinons maintenant dans l'image précédemment créée.

fill('transparent').

Le fonds du rectangle que nous dessinons sera transparent.

stroke('black').
stroke_width(1).

Notre rectangle aura une bordure noire de 1 pixel.

roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
draw(masq)

Nous créons ici le rectangle et l'ajoutons à l'image créée juste auparavant.

img.composite!(masq, 0, 0, Magick::LightenCompositeOp)

Enfin nous superposons cette image nouvellement créée à celle qui existe déjà, rendant l'effet escompté.

img = yield(img) if block_given?
img

CarrierWave fonctionne de manière imbriquée. Chaque processeur est exécuté l'un après l'autre, transmettant ensuite la main au suivant et retournant l'image modifiée. C'est ce que nous faisons ici avec yield.

Et enfin nous retournons l'image qui pourra alors être sauvegardée.

Conclusion

Comme vous pouvez le constater, CarrierWave permet de créer des processeurs de manière fortement générique afin de rendre vos images telles que vous en avez besoin.

Je suis d'ailleurs assez étonné qu'il n'y ait pas déjà des processeurs open source qui se baladent.

Rails : Modèles polymorphes

Supposons que vous soyez en train de développer un blog (du genre de celui-ci ;-) ). Vous avez en conséquent avoir des billets qui peuvent avoir des commentaires. Cependant ces commentaires, eux, peuvent être présent dans un billet ou bien dans une page. Afin de rendre notre code propre, nous ne désirons pas avoir plusieurs types de commentaires. Que vous postiez un commentaire sur une page ou sur un billet, il s'agit d'une entrée dans la même base de données et du même modèle.

Dans le modèle

Pour cela, notre modèle Commentaire sera lié à une page ou un billet de manière polymorphe.

class Comment < ActiveRecord::Base
    belongs_to :commentable, :polymorphic => true
end

class Post < ActiveRecord::Base
    has_many :comments, :as => :commentable
end

class Page < ActiveRecord::Base
    has_many :comments, :as => :commentable
end

Vous pouvez donc assigner un commentaire à un billet ou à une page (mais pas aux deux évidemment). Pour fonctionner, une relation polymorphe requiert tout de même que vous ajoutiez un champ "type" à la base comments.

Ainsi dans vos migrations :

class CreateComments < ActiveRecord::Migration
    def self.up
        create_table :comments do |t|
            t.integer  :commentable_id
            t.string    :type

            t.timestamps
        end
    end

    def self.down
        drop_table :comments
    end
end

Le type correspondra à Post ou Page et permettra à rails de savoir quel est le type de relation du commentaire. Ainsi, depuis votre commentaire, vous pouvez faire :

@comment.commentable

Ce qui retournera l'objet commentable. Une page ou un billet, en fonction du type donné.

Et depuis la page et le billet, vous pouvez faire :

@object.comments

Ce qui retournera tous les commentaires relatifs à cette page ou à ce billet.

Dans le contrôleur

Nos modèles gèrent assez aisément les relations polymorphes. Mais qu'en est-il de notre contrôleur ? Il n'existe pas de solution native. Mais en bidouillant un peu, nous pouvons obtenir quelque chose de propre, nous permettant d'avoir un seul contrôleur pour tous nos commentaires.

Dans le répertoire lib de notre application, créons un module que nous nommerons ParentObject. Votre fichier doit par conséquent être nommé parent_object.rb

module ParentObject

    def self.included(klass)
        klass.class_eval do
            class << self
                attr_reader :parents

                protected
                def parent_resources(*parents)
                    @parents = parents
                end
            end

            def parent_id(parent)
                params["#{parent.to_s}_id"]
            end

            def parent_type
                self.class.parents.detect { |parent| parent_id(parent) }
            end

            def parent_class
                parent_type && parent_type.to_s.classify.constantize
            end

            def parent_object
                parent_class && parent_class.find(parent_id(parent_type))
            end
        end
    end
end

Nous créons un module ruby que nous devrons par la suite inclure dans notre contrôleur. Pour les détails, je vous invite à vous renseigner sur ce en quoi consiste la méta programmation en ruby. Nous n'allons pas sortir du sujet ici.

class << self
    attr_reader :parents

    protected
    def parent_resources(*parents)
        @parents = parents
    end
end

Ici, nous définissons une méthode qui sera accessible à la base de notre contrôleur (en dehors des actions) et qui nous permettra de définir la liste de tous les objets qui pourront être parents de nos commentaires.

def parent_id(parent)
    params["#{parent.to_s}_id"]
end

Cette méthode, à laquelle nous transmettons un type de parent (par exemple "post"), nous retournera l'identifiant de l'élément passé en paramètre. Celui-ci doit être nommé "parent_id" (par exemple "post_id").

def parent_type
    self.class.parents.detect { |parent| parent_id(parent) }
end

Cette méthode parcourera tous les parents transmis et cherchera pour chacun d'eux si un paramètre y est présent. Dès qu'elle en trouvera un, elle retournera ce parent. Ainsi, nous savons si nous avons affaire à un post ou une page.

def parent_class
    parent_type && parent_type.to_s.classify.constantize
end

Cette méthode, en se basant sur la méthode précédente (qui retourne une chaine de caractère) nous retournera la classe de l'objet parent.

def parent_object
    parent_class && parent_class.find(parent_id(parent_type))
end

Enfin cette méthode prends la classe de l'objet parent et effectue la requête appropriée dans la base de données afin d'en retourner l'objet.

Nous n'avons plus qu'à utiliser ceci dans notre application !

class ApplicationController < ActionController::Base
    include ParentObject
end

Dans notre contrôleur principal, nous incluons notre module dernièrement créé, afin de pouvoir l'utiliser dans n'importe quel contrôleur.

class CommentsController < ApplicationController
    parent_resources :post, :page

     def new
         @parent = parent_object
         @comment = Comment.new
     end

     def create
         @parent = parent_object
         @comment = @parent.comments.build params[:comment]

         if @comment.valid? and @comment.save
             redirect_to @parent
         else
             render :new
         end
     end
end

Assez simple non ? :-) Que faisons-nous ?

parent_resources :post, :page

Nous définissons, pour ce contrôleur, la liste de tous les parents possibles. Puis dans les actions new et create nous récupérons l'objet parent et permettons de poster un nouveau commentaire relatif à cet objet.

Les routes

Lorsque nous créons nos routes relatives à ces commentaires, nous utilisons bien évidemment les ressources. Et c'est là le seul endroit ou nous avons un peu de répétition de code à faire.

resources :posts do
    resources :comments
end
resources :pages do
    resources :comments
end

Pour chaque post et chaque page, nous ajoutons des ressources de commentaires.

Ces routes fonctionneront avec rails3. Adaptez les en fonction pour une application rails 2 !

Tester ceci

Vu que j'ai utilisé ceci dans un cas réel avant d'écrire cet article, j'ai également écrit des tests. Vous pouvez visualiser et utiliser ceux-ci comme bon vous semble. Vous les trouverez sur gist@github

Et nos contrôleurs ?

Lorsque vous allez écrire des tests fonctionnels pour vos contrôleurs, vous allez vouloir tester chacun des différents types d'objets parents. Il est cependant inutile de répéter 50 fois votre floppée de tests. Cela ne serait pas très DRY non plus.

require 'spec_helper'
describe CommentsController do

    [:post, :page].each do |parent|

        before :each do
            @parent = Factory(parent)
        end

        describe 'new' do
            it 'should display the form page' do
                get :new, "#{parent}_id".to_sym => @parent.id
            end
        end

        describe 'create' do
            it 'should create a new comment' do
                lambda do
                    post :create,
                        :comment => Factory.attributes_for(:comment),
                        "#{parent}_id".to_sym => @parent.id
                end
            end
        end
    end
end

Que faisons-nous ici ?

[:post, :page].each do |parent|
    ...
end

Nous définissons un tableau contenant tous les types de parents que nous pouvons avoir pour ce contrôleur. Et nous parcourons ce tableau. Tous nos tests seront effectués à l'intérieur de la boucle. Ainsi ils seront effectués pour chacun des éléments parents.

before :each do
    @parent = Factory(parent)
end

Avant chaque test, nous définissons l'objet parent. Ici, nous utilisons factory girl et chaque modèle a sa factory du même nom.

Enfin nous créons nos tests.

get :new, "#{parent}_id".to_sym => @parent.id

Lorsque nous faisons un appel à une page, nous devons transmettre le paramètre qui correspond au parent donné.

Conclusion

Comme vous pouvez le constater ici, il est assez simple d'éviter d'inutiles répétitions de code lorsque vos commentaires sont tous similaires. Si ce n'était pas le cas, peut-être qu'utiliser Single Table Inheritance serait plus utile et modulaire.

Création de générateurs pour rails 3

Lorsque vous développez un plugin pour rails, il peut être utile que l'utilisateur de ce plugin crée divers fichiers (par exemple de configuration) dans son projet. Par exemple, rspec crée le fichier spec/spec_helper.rb. Ainsi que vos fichiers de test à chaque fois que vous créez un nouveau modèle, contrôleur ou autre.

Pour faciliter la création de ces fichiers dans votre projet, rails propose divers helpers permettant de créer des templates de fichiers. Supposons un plugin que nous nommerons foo. Dans ce plugin, nous désirons créer le modèle User. Ce modèle contiendra, comme son nom l'indique, des utilisateurs !

Un premier générateur

Commencez par créer le dossier foo/lib/generators, dans lequel vous créerez le fichier user_generator.rb. Dans ce fichier, nous placerons la classe suivante :

Nous créons une classe du nom de notre générateur (le nom est très important et doit correspondre au nom du fichier. Sinon rails ne trouvera pas le générateur).

Puis nous définissons le chemin vers les fichiers de template :

source_root File.expand_path("../templates", __FILE__)

Enfin nous définissons une méthode qui créera le fichier de notre modèle et y insérera le contenu du template :

def add_user_model
    template "user_model.rb", "app/models/user.rb"
end

Vous pouvez créer autant de méthodes que vous le désirez. Toutes les méthodes publiques seront exécutées dans l'ordre dans lequel elles sont définies.

Puis créez un fichier foo/lib/generators/templates/user_model.rb Qui contiendra ceci :

class User < ActiveRecord::Base

end

Parce que notre générateur s'appelle "user", vous pouvez l'exécuter après avoir installé le plugin dans votre projet rails, en tapant :

script/rails generate foo:user

Ce qui aura pour effet de créer le modèle User dans le répertoire approprié.

Générateurs nommés

Si vous avez un petit peu d'expérience avec rails, vous avez probablement déjà utilisé l'un des générateurs natifs. Par exemple nous pourrions créer notre précédent modèle User de la manière suivante :

script/rails generate model user

Si vous souhaitez créer ce type de générateur, vous pouvez faire hériter celui-ci de Rails::Generators::NamedBase. Ainsi, diverses méthodes supplémentaires seront à votre disposition.

  • class_path et file_name sont extraits des paramètres passés. Par exemple si vous avez entré admin/posts, class_path sera égal à admin et file_name à posts.
  • file_path est class_path et file_name séparés par un /.
  • class_name est le nom de la classe. Par exemple admin/posts deviendrait Admin::Posts
  • human_name est le nom de la classe "humanisé". Par exemple admin/posts deviendrait Admin posts.
  • plural_name est le nom du fichier au pluriel. Par exemple post deviendrait posts.
  • i18n_scope est le file_path avec les slash (/) remplacé par des points (.). Par exemple admin/posts deviendrait admin.posts.

Ainsi nous pourrions créer un générateur plus générique avec la commande

script/rails generate foo:user people

Qui créerait un modèle nommé People et non plus User. Exemple :

Vous noterez l'utilisation de la variable file_name nous permettant de définir de manière dynamique le nom du fichier qui sera créé.

Et le contenu de notre template :

class <%= class_name %> < ActiveRecord::Base

end

Vous noterez l'utilisation de la variable class_name qui nous permet de définir de manière dynamique le nom de notre nouveau modèle.

Conclusion

Avec les générateurs, vous pouvez ainsi créer tous les fichiers dont votre plugin ou gem aura besoin pour tourner correctement autour de votre application rails. Si vous souhaitez en savoir plus quant à ces générateurs, je vous invite à lire l'article Creating and Customizing Rails Generators.

Suivi des modifications d'un modèle : ActiveModel::Dirty

Depuis sa version 2.1 (2008 donc), vous pouvez suivre les modifications apportées à vos modèles rails grâce à ActiveModel::Dirty. Mise en application !

En quoi ça consiste ?

Lorsque vous prenez l'un des modèles ActiveRecord de votre application et modifiez un attribut, vous pouvez désirer savoir, lors de la validation des données ou simplement avant l'enregistrement, quels champs ont été modifiés et quelles sont les anciennes et nouvelles valeurs. ActiveModel::Dirty s'occupe de cela pour vous, en suivant les changements dans vos modèles et en les sauvegardant afin d'en faire le suivi par la suite.

Ceci n'est pas un système de contrôle de version. Le suivi n'est sauvegardé nulle part d'autre que dans la RAM. En conséquent à partir du moment ou vous détruisez votre objet, vous perdez le suivi précédent. En revanche, cela permet de vérifier l'ancienne et la nouvelle valeur d'un champs lors de la sauvegarde d'un modèle, ce qui peut être plutôt utile dans certains cas.

Cas concret : Exécuter une action lors de la publication d'un billet sur un blog

Je commence à vraiment en avoir marre de Wordpress et de son code pourri. En conséquent j'ai décidé de commencer à développer mon propre moteur de blog. Cette application tournera sous Rails 3. Ne vous attendez pas à une release avant septembre en revanche.

Dans cette application, je souhaite, lorsque je publie un billet, mettre à jour mon statut Twitter afin de notifier les personnes qui me suivent sur ce réseau social de la présence d'un nouveau contenu sur le blog. Chacun de mes billets a un champ "published" qui est à vrai ou faux. Je ne veux bien évidemment mettre à jour mon status que lorsque je publie le billet. Donc lorsque le champ passe de faux à vrai.

Avec ActiveModel::Dirty, je vais donc pouvoir faire :

class Post < ActiveRecord::Base
    validates_presence_of    :title, :content, :published
    
    before_save :publish_tweet
    
    private
    def publish_tweet
        # We don't publish if the post isn't published
        return if !published?
        # We don't publish if the post was already published before
        return if !published_changed?</p>

<p>        Resque.enqueue(Jobs::PublishTweet, self.title)
    end
end

Que faisons-nous ?

before_save :publish_tweet
Nous définissons un callback before_save permettant à notre méthode publish_tweet d'être appelée directement à chaque fois que l'on sauvegarde le billet.

Dans cette méthode, nous commençons par filtrer.

return if !published?
Nous ignorons le billet si il n'est pas encore publié.

return if !published_changed?
C'est ici que nous utilisons ActiveModel::Dirty. Nous ignorons le billet si la valeur du champ published n'a pas changé. Vu que c'est un booléen il n'a que deux valeurs possibles. Et vu que dans le filtre précédent, nous nous arrêtons si le billet n'est pas publié, notre booléen est donc maintenant forcément à vrai. En conséquent, si le billet était déjà publié auparavant, on publie pas le tweet une seconde fois.

Et enfin on publie le tweet.

Resque.enqueue(Jobs::PublishTweet, self.title)
J'utilise Resque pour les tâches asynchrones côté serveur d'où cette ligne permettant d'exécuter en tâche de fond la méthode qui publiera le tweet.

L'API en détail

C'est bien gentil de voir un cas concret mais au final on a pas vu beaucoup de ActiveModel::Dirty ci-dessus. Voyons donc plus en détail les fonctionnalités qu'apporte cette librairie.

Voyons le code suivant tiré de la documentation

person = Person.find_by_name('Bob')
person.changed? # => false

Nous récupérons l'objet de type personne. Vu que nous n'avons encore rien modifié, la méthode changed? retourne faux.

person.name = 'Joe'
person.changed? # => true
person.name_changed? # => true
person.name_was # => 'Bob'

Sur cette même personne, nous modifions son nom. ActiveModel::Dirty nous annonce alors que ce nom a été changé et nous permet d'obtenir la précédente valeur.

person.name_change # => ['Bob', 'Joe']
person.name = 'Bill'
person.name_change # => ['Bob', 'Joe', 'Bill']

Toujours dans le même objet, peut visualiser un tableau contenant toutes les versions de la valeur. Si nous modifions à nouveau ce nom, on constate que name_change contient les 3 valeurs que le nom a porté.

person.save
person.changed? # => false
person.name_changed? # => false

Nous sauvegardons l'objet dans la base. Du coup ActiveModel::Dirty est remis à zéro. On ne constate plus aucun changement.

person.name = 'Bill'
person.name_changed? # => false
person.name_change # => nil

Si nous modifions à nouveau ce nom en lui redonnant la valeur qu'il possède déjà, on ne constate aucun changement en réalité.

person.name = 'Bob'
person.changed # => ['name']
person.changes # => { 'name' => ['Bill', 'Bob'] }

La méthode changed nous retourne la liste de tous les champs ayant été modifiés. Et changes nous retourne un hash contenant toutes les modifications apportées champ par champ.

person.reset_name! # => 'Bill'
person.changed? # => false
person.name_changed? # => false
person.name # => 'Bill'

Enfin on peut annuler un changement. La valeur est restaurée.

Conclusion

Comme vous pouvez le constater, cette API ActiveModel::Dirty peut s'avérer particulièrement puissante et améliorer fortement la qualité de votre code. Vous n'avez plus besoin de tracker les changements apportés à vos modèles par vous même. Rails s'en charge et vous n'avez plus qu'à récupérer les informations.

La puissance de l'intégration de rack dans rails 3

Cet article est fortement inspiré (mais n'est pas une traduction) de Rails and Merb Merge: Rack

Rack est une interface entre votre serveur web et votre application basé sur le standard CGI mais sans ses caractéristiques globales (variables d'environnement et sortie standard).

Dans Rails 2.3

Depuis Rails 2.3, toute application développée autour du framework tourne avec Rack. L'implémentation de rack est la suivante :

  • ActionController::Dispatcher.new est l'application rack de base
  • Le parseur de paramètres (ActionController::ParamsParser)est implémenté en tant que middleware
  • Le routeur est une application rack qui dispatche vers les contrôleurs
  • Chaque contrôleur est une application rack

cela a donc permis d'implémenter la notion de metal, des applications rack tournant autour de Ruby.

Vous pouvez trouver un exemple de metal dans mon article interdire l'accès aux crawlers dans l'environnement de développement.

Cependant les metal n'existent plus dans rails 3. Vous allez comprendre pourquoi dans cet article.

Mais ne nous attardons pas sur les choses qui sont vouées à disparaitre et passons plutôt directement à ce qui est intéressant : les nouveautés apportées par l'implémentation de Rack dans Rails 3.

Dans Rails 3

Vous l'avez déjà vu, Rails 3 implémente le concept d'application. Chaque application est un objet contenant un routeur et des paramètres et configuration ... Mais avant tout une application Rack !

Par conséquent les 10 lignes de code suivantes sont une application Rails 3 qui fonctionne. Dans un fichier nommé config.ru, placez :

require "action_controller/railtie"
 
class FooController < ActionController::Base
  def bar
    self.response_body = "Hello World !"
  end
end
 
class MyApp < Rails::Application
   config.session_store :disabled
 
   routes.draw do
     root :to => "foo#bar"
  end
end
 
run MyApp

Lancez l'application : rackup -p 4000. Rendez-vous sur localhost:4000 et vous verrez un bel "Hello World". Vos applications les plus simples n'ont plus rien à envier aux applications Sinatra.

Nous avons vu plus haut que Rails 2.3 commençait déjà à être conçu autour de middlewares rack. Dans cette nouvelle branche, cette intégration est encore plus flagrante. En effet Rails 3 inclue les fonctionnalités suivantes chacune dans un middleware différent :

  • Un middleware exécutant les preparation callbacks
  • Un middleware pour lire et écrire les cookies
  • Un middleware qui nettoie les flash déjà affichés
  • Un middleware qui gère les requêtes HEAD
  • Un middleware vérifiant les IP spoofing
  • Un middleware servant à rendre les fichiers statiques
  • Un middleware de gestion des exceptions de bas niveau
  • Un middleware pour les divers stockages en session
  • Un middleware pour synchroniser les requêtes non thread-safe Dépends de Rack, pas de rails
  • Un middleware pour mesurer le temps d'exécution d'une requête Dépends de Rack, pas de rails
  • Un middleware permettant d'implémenter la sémantique de Send-file dans les divers serveurs Dépends de Rack, pas de rails
  • Un middleware permettant de détecter les requêtes PUT et DELETE transmises en POST Dépends de Rack, pas de rails

Bien évidemment vous pouvez dans votre application ajouter, réordonner ou supprimer des middlewares.

Le routeur

Le routeur de Rails 3 a également été réécrit afin d'être mieux implémenté dans Rack. C'est Rack::Mount. Celui-ci reconnait des URL et les dispatche vers n'importe quelle application, même si elles ne sont pas rails. Il est ainsi tout à fait possible de faire la chose suivante

Rails.application.routes.draw do
  match "/blog" => BlogApplication
end

Vous pouvez également mettre un symbole et donc avoir match :blog => BlogApplication

Vous n'avez plus qu'à écrire votre application de blog et elle sera intégrable dans votre application principale sous l'url /blog. Vous pouvez ainsi beaucoup plus aisément diviser votre application en de multiples modules, rendant chaque brique plus légère et donc plus aisée à développer.

Actions

Comme dit plus haut, chaque contrôleur est lui même une application Rack. Cela va même plus loin que cela puisque chaque action est en elle même une application rack ! Que vous pouvez exécuter indépendamment de l'application rails en elle même.

Cela se fait grâce à

MyController.action :index

Supposons par exemple un contrôleur Posts et une action show que vous souhaitez exécuter. Le fichier config.ru suivant sera exécuté sans problème avec rack.

class PostsController < ActionController::Base
  append_view_path "/path/to/views"
 
  def show
    render
  end
end
 
run ArticlesController.action(:show)

N'oubliez pas, bien évidemment, de remplacer "/path/to/views" par le réel chemin vers vos vues

Votre rendu et tous vos callbacks seront bien évidemment exécutés. En fait, en interne, c'est grosso modo ce que rails lui même fait.

Dans les tests

L'utilité de ceci est une plus grande facilité d'exécution des tests. Dans rails 2.3, si votre application dépends beaucoup de middlewares, ceux-ci seront assez difficile à tester dans vos tests fonctionnels car ils ne seront pas exécutés. C'est notamment le cas si vous utilisez devise. La solution trouvée est de mocker les fonctions implémentées par le middleware.

Avec Rack::Test, vous pouvez fortement simplifier cela. Reprenons notre méthode show précédente et testons la.

Tout d'abord en instanciant l'application en elle même.

class TestApplication < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    MyApplication
  end
 
  def test_get
    get "/posts/1"
    assert_equal "Hello world !", last_response.body
  end
end

Et puis comme on peut ne faire que un appel à l'action, on se prive pas

class TestPosts < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    PostsController.action(:show)
  end
 
  def test_get
    get "/posts/1"
    assert_equal "Hello world !", last_response.body
  end
end

Ici, nous sommes dans un article présentant des fonctionnalités. C'est donc un cas d'école. Avant de vous amuser à appeler directement une action dans vos tests, demandez vous si c'est vraiment DRY.

In fine

Si Rails 2.3 a commencé l'implémentation de Rack, Rails 3 plonge à plein nez dedans, s'intégrant pleinement avec l'application. Cela permet de rendre votre application beaucoup plus modulaire et puissante.

Nous avons vu ici une partie des nouvelles fonctionnalités du routeur. Mais ce n'est pas tout à ce niveau ! Attendez vous à d'autres :)

La beauté des "scopes" dans Rails 3

Ceci est une traduction de l'article The skinny on scopes publié sur Edge Rails.

Je me souviens de mon cœur faisant des bonds lorsque le plugin has_finder de Nick Kallen a été implémenté dans rails core sous le nom de named_scope. named_scope a rapidement rejoint la liste de mes outils préférés de par sa merveilleuse manière de créer des requêtes logiques encapsulées et réutilisables. Alors qu'il avait ses points faibles (pour ne pas le nommer, le manque du support de :joins et :include), il a redéfini ma manière de penser la logique de mes modèles. Une fois que vous avez gouté au plaisir des named_scopes, vous ne pouvez jamais revenir en arrière.

Et maintenant Rails 3 arrive avec son refactoring complet de ActiveRecord. Que deviennent nos cher named_scope ? Pour faire simple, cela a été renommé en scope et vous pouvez les utiliser comme vous le faisiez déjà ... Mais de manière plus aisée. Voyons un peu ce que l'on peut faire avec ces scopes dans Rails 3.

Usage basique

Supposons un modèle Post avec des champs published_at, title et content. Dans rails 2.x, nous devrions définir les scopes published et recent.

class Post < ActiveRecord::Base
    named_scope :published, lambda { 
        { :conditions =>
            ["posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now]
        }
    }  
    named_scope :recent, : order => "posts.published_at DESC"
end

La raison pour laquelle nous utilisons un lambda ici est que cela permet de reporter l'exécution de Time.zone.now au moment ou la scope est effectivement invoquée. Sans ce lambda, le moment retourné serait celui ou la classé est évaluée. Pas la scope elle même.

Avec Rails 3, l'architecture d'ActiveRecord est maintenant basée sur une classe Relation. Classe que vous pouvez voir comme une sorte de "named_scope on steroids", permettant de de chainer chaque requête directement dans ActiveRecord.

Vous pouvez voir comment utiliser les méthodes where, ordre etc dans l'article de Pratik sur cette nouvelle interface ou bien encore dans ce Railscast. Comprendre ceci est important étant donné que les nouvelles scopes sont construites autour de cela.

Voyons comment. Voici nos deux différentes scopes portées sous Rails 3.

class Post < ActiveRecord::Base
    scope :published, lambda { 
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }  
    scope :recent, order("posts.published_at DESC")
end

Alors que la logique reste la même (les portions SQL), vous pouvez commencer à voir comment les scopes utilisent la nouvelle interface de requêtage pour construire directement la requête au lieu de construire un hash d'options comme c'était fait dans Rails 2. Ceci est le tout premier aperçu de la flexibilité que la nouvelle interface apporte à nos scopes. Elles ne sont plus construites différemment que n'importe quelle requête. Elles sont construites au dessus des mêmes méthodes que vous pouvez utiliser lorsque vous construisez directement vos requêtes. Cette consistance est maintenant présent partout dans ActiveRecord.

Mais ce n'est pas tout ...

Réutilisation des scopes

Supposons que nous désirions modifier notre scope "recent" pour n'inclure que les posts publiés. Nous avons déjà défini ce que published signifie et nous ne devrions donc pas avoir à le redéfinir pour créer une nouvelle scope. Aucun problème ! Nous pouvons chainer les scopes elles mêmes et c'est ce que nous allons faire ici.

class Post < ActiveRecord::Base
    scope :published, lambda { 
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }
    scope :published_since, lambda { |ago|
        published.where("posts.published_at >= ?", ago)
    }
    scope :recent, published.order("posts.published_at DESC")
end

Ca commence à devenir intéressant.

Construction dynamique

Déjà dans Rails 2.3, vous pouvez créer des scopes anonymes afin d'obtenir dynamiquement des scopes chainables selon vos besoins. Un cas typique d'utilisation est lorsque vous désirez créer une méthode de recherche à laquelle vous pouvez toujours ajouter d'autres manipulations.

Par exemple pour chercher parmi nos posts, nous créons cette méthode qui retournera un scope que vous pourrez par la suite filtrer (notez l'utilisation de "scoped" pour démarrer la chaine avec une scope vide à laquelle d'autres peuvent être ajoutées).

L'utilisation de inject ici masque l'intérêt de cette méthode si vous n'avez pas l'habitude de voir de telles itérations. Voici une version plus aisée à comprendre contenant les champs de recherche en dur (et qui n'utilise même pas de scope anonyme).

class Post < ActiveRecord::Base
    class << self</p>

<p>        # Moins dynamique mais probablement plus lisible
        def search(q)
            query = "%#{q}%"
            where("posts.title LIKE ?", query).where("posts.body LIKE ?", query)
        end
    end
end

Vu que nous construisons nos scopes autour de la nouvelle interface qui est fortement chainable, nous pouvons faire la chose suivante avec notre méthode de recherche :

# What's in the db, titles ~= publish date
Post.all.collect(&:title) #=> ["1 week from now", "Now", "1 week ago", "2 weeks ago"]
Post.published.collect(&:title) #=> ["Now", "1 week ago", "2 weeks ago"]</p>

<p># Combinaisons de recherche
Post.search('1').collect(&:title) #=> ["1 week from now", "1 week ago"]
Post.search('1').published.collect(&:title) #=> ["1 week ago"]
Post.search('w').published_since(10.days.ago).collect(&:title) #=> ["Now", "1 week ago"]
Post.search('w').order('created_at DESC').limit(2).collect(&:title) #=> ["2 weeks ago", "1 week ago"]

Vous pouvez imaginer un scénario ou des requêtes bien plus complexes pourront être construites en utilisant des scopes anonymes. Cool non ?

Scopes multi modèles

Les scopes sont parfaites pour manipuler uniquement les colonnes d'un modèle unique. Mais elles peuvent également être utilisées pour construire des requêtes multi modèles (qui requièrent un join). Ajoutons à nos posts des utilisateurs (qui peuvent être auteur ou commentateur) et écrivons quelques scopes sur le modèle User qui nous permettront de récupérer uniquement ceux ayant publié des billets et ceux qui ont commenté.

Notons également que ActiveRelation est également suffisamment intelligent pour savoir comment faire un join sur la définition de l'association, nous autorisant à placer cette relation en référence.

C'est une bonne pratique de référencer le nom complet de la colonne, avec sa table (posts.published_at au lieu de published_at). Cela permet d'éviter des ambiguïtés de noms de colonnes. Particulièrement important lorsque vous construisez des scopes multi modèles ou des colonnes venant de plus d'une table peuvent être jointes.

Pour êtres super flexible, vous pouvez toujours invoquer table_name au lieu de mettre cela en dur. Pour être franc, c'est quelque chose que je ne fais que rarement. where("#{table_name}.published_at IS NOT NULL")

Vu que nous avons l'arsenal complet des opérateurs ActiveRecord à notre disposition dans les scopes, nous pouvons faire des join et des group by dans les scopes qui seront chainées dans les requêtes complexes. Chose que named_scope n'arrivait que rarement à faire.

Comme fait ici, vous pouvez utiliser to_sql afin de visualiser la requête SQL qui sera exécutée. C'est très pratique lors de débogages.

Opérations CRUD sur les scopes

Vu que ActiveRelation vous permet d'invoquer toutes les méthodes builder/update/destroy, celles-ci sont également accessibles pour les scopes. Jouons un peu à faire des modifications sur nos scopes au lieu de se contenter de les récupérer.

Vous pouvez également créer un nouvel uplet avec des scopes existantes. Supposons que nous avons une scope (très peu utile) qui ne récupère que les posts avec un certain titre

class Post < ActiveRecord::Base
    scope :titled_luda, where(:title => 'Luda')
end

Nous pouvons utiliser cette scope pour créer de nouvelles instances (tout comme new, create, ...)

Post.titled_luda.size #=> 0
Post.titled_luda.build
  #=> #<Post id: nil, title: "Luda", ...>

Afin de pouvoir utiliser les méthodes de création dans une scope, celle-ci doit définir l'égalité de l'attribut de manière directe dans le where avec un hash comme fait précédemment. Si nous avions fait un where("title = 'Luda'"), le titre n'aurait pas été propagé dans le nouvel objet.

Pour les grands fou

Une chose qui m'a toujours choqué est la manière dont la logique pour ce qui fait qu'un billet est publié est séparée entre les scopes de la classe Post et celles de la classe User. Afin de se rafraichir la mémoire

class Post < ActiveRecord::Base
    scope :published, lambda { 
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }
end

Ainsi que

class User < ActiveRecord::Base</p>

<p>    scope :published, lambda {
        joins(:posts).
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).
        group("users.id")
    }
end

Tout bon développeur va instantanément noter la duplication du where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).

Heureusemnt il existe une méthode pour éviter cette répétition : un merge dont l'alias est '&'. Voyons comment nous pouvons utiliser scope#& afin de faire référence à Post.published dans la scope User.published.

class User < ActiveRecord::Base</p>

<p>    scope :published, lambda {
        joins(:posts).group("users.id") & Post.published
    }
end

Et voici la requête SQL qui sera générée :

User.published.to_sql
  #=> SELECT users.* FROM "users"
  #   INNER JOIN "posts" ON "posts"."author_id" = "users"."id"
  #   WHERE (posts.published_at IS NOT NULL AND posts.published_at <= '2010-02-27 02:55:45.063181')
  #   GROUP BY users.id

Notez comment les conditions définies dans Post.published sont mergées dans les relations join et group de la scope User.published ? Et ceci fonctionne avec toutes les relations mergables.

Conclusion

Ce billet dérive un petit peu dans une explication de la nouvelle interface de requêtage SQL avec ActiveRecord dans Rails 3 afin d'entrer dans les détails des scopes. Cependant aucune des nouvelles fonctionnalités des scopes n'aurait pu être mise en place sans ces nouveautés d'ActiveRecord. Donc si vous êtes encore un petit confus à propos de cela, n'hésitez pas à vous documenter encore un petit peu plus à propos de la nouvelle API ActiveRecord avant de vous penchez sur les scopes. Une fois que vous aurez ces bases, vous ne pourrez plus vous passer des scopes !

Authlogic: interdire les sessions multiples pour le même utilisateur

Pour compenser le fait qu'il y ait eu deux articles la semaine passée, celui de cette semaine sera light ;) Il existe plusieurs systèmes d'identifications pour les applications Rails. Mes deux préférés sont authlogic et devise. Mais clearance est pas mal non plus.

Je vais parler du premier. Lorsque vous avez implémenté votre système d'identification avec ce gem, vous vous rendrez rapidement compte qu'un même utilisateur peut être connecté plusieurs fois depuis des machines ou des navigateurs différents. Ce base, cela peut être intéressant afin d'éviter d'avoir à redemander à vos utilisateurs de se reconnecter trop régulièrement.

Mais dans d'autres cas (ou vous désirez éviter le partage de comptes par exemple), ce n'est pas l'effet désiré. Après avoir cherche un petit peu, j'ai posté la solution à ceci sur stackoverflow.

Dans votre modèle de session (par défaut UserSession), ajoutez le code suivant :

before_destroy :reset_persistence_token
before_create  :reset_persistence_token</p>

<p>def reset_persistence_token
    record.reset_persistence_token
end

Nous créons deux callbacks. Ainsi, notre méthode reset_persistence_token sera exécutée à chaque fois que nous créons ou supprimons une session utilisateur (à chaque fois que l'utilisateur se connecte ou se déconnecte).

Cette méthode réinitialise un token qui est également situé dans la session de l'utilisateur et qui nous permet de l'identifier. Nous sommes obligés de la définir car elle l'est à l'origine, uniquement dans le modèle User. C'est dans la session que nous désirons réinitialiser ce token. D'ou le record.. record retourne l'utilisateur auquel nous réinitialisons le token.

Ainsi, à chaque fois que notre utilisateur se connectera ou se déconnectera de l'application, le token sera modifié. Et nous n'aurons donc pas la possibilité d'avoir deux machines connectées avec la même session :)

Rails 3 : nouvelle API ActionMailer

L'une des grandes mises à jour de Rails 3 est la nouvelle API Action Mailer. Petit rappel : dans les versions précédentes de Rails, nous pouvons transmettre des emails en créant, dans le répertoire app/models un modèle ActionMailer. Celui-ci pourrait ressembler (dans rails 2.3) à ceci :

class UserMailer < ActionMailer::Base
    def welcome_email(user)
        recipients user.email
        from "I'm nobody <42@unknown>"
        subject "Hello World"
        body {:user => user }
    end
end

Et dans le répertoire app/views/user_mailer, nous pourrons créer un fichier welcome_email.text.erb qui sera le contenu de notre email. Maintenant supposons que nous désirions attacher une pièce jointe à notre email. Nous allons devoir ajouter dans notre méthode welcome_email cette pièce jointe.

attachment "application/pdf" do |a|
    a.body = contenu_du_pdf()
end
un "beurk" suffira !

Dans Rails 3, notre méthode d'envoi d'email devient la suivante :

class UserMailer < ActionMailer::Base
    default :from => "I'm nobody <42@unknown>"</p>

<p>    def welcome_email(user)
        @user = user
        mail(:to => user.email,  :subject => "Hello World")
    end
end

Et lorsque nous souhaitons ajouter une pièce jointe, nous n'avons qu'à faire :

attachments['terms.pdf'] = {:content => contenu_du_pdf() }
C'est déjà plus sympa.

Mais ce n'est pas tout ! Par défaut, les fichiers welcome_mail.text.erb et welcome_mail.html.erb sont inclus dans le mail. Ainsi la personne recevant l'email peut le lire en html ou en texte. Mais tout comme vous le faites dans vos contrôleurs pour l'html, le json, l'xml ou tout autre format, vous pouvez vouloir rendre quelque chose de différent en fonction du format html ou texte du mail.

Go ! :)

mail(:to => user.email,  :subject => "Hello World") do |format|
    format.text { render :text => "Mon email est en texte" }
    format.html { render :html => "Mon email est en <strong>HTML</strong>" }
end

Pour continuer lorsque vous envoyiez votre email, vous faisiez cela de la manière suivante :

UserMailer.deliver_welcome_email(@user)

Vous devrez maintenant faire :

UserMailer.welcome_email(@user).deliver

Le welcome_email vous renvoyant un objet Mail que vous pouvez ainsi modifier comme bon vous semble. Voir le stocker pour l'envoyer plus tard par exemple. Si vous désirez plus d'informations concernant cette nouvelle API, je vous recommande l'article sur guides.rails.info (encore en cours de rédaction). Et le gist qui a servi de spécification pour cette nouvelle API.

Rails3: va-t-on vers J2EE ?

Rails 3 étant proche de sa première beta, j'en profite pour multiplier les articles à son propos ! Qui plus est il y a matière à écrire. Je ne m'en prive pas donc :)

Hier matin je suis tombé sur un article particulièrement intéressant The path to Rails3: Introduction, qui explique plutôt bien le maitre mot de cette nouvelle version du framework : découplage. Ce découplage a pour but de faciliter l'utilisation de rails par blocs uniquement lorsque son utilisation en entier n'est pas forcément nécessaire.

Vous pouvez ainsi valider vos modèles sans forcément être dans une application rails, ni utiliser Active Record. Avec arel, vous pouvez générer des requêtes SQL sans dépendre de rails (à terme du moins. Pour le moment, vous dépendez toujours d'ActiveRecord).

Avec bundler, vous gérez les dépendances de votre projet, qu'il utilise rails ou pas. Regardez comment je fais pour jesus ! Je ferai une présentation de bundler au prochain apéro Ruby à Lyon. Venez donc y assister !

Je découvre donc cet article. Et le trouvant intéressant, je le partage avec Julien qui bosse dans le même bureau que moi. Sa réaction a été "c'est vraiment en train de devenir inaccessible pour les débutants, rails. Trop compliqué" (je répète pas les choses mot pour mot, désolé).

Ce à quoi je réponds : NON !. Pour un débutant découvrant le framework, celui-ci reste toujours aussi simple. Le screencast créer un blog avec rails en 15 minutes est parfaitement adaptable pour rails 3.

Et ce parce que de base, rien ne change ! L'idéologie de rails est toujours convention over configuration et les API ne changent pas fondamentalement (sauf peut-être pour ActionMailer. Mais ce n'est pas encore implémenté).

Ainsi votre contrôleur ressemblera toujours à ceci :

class PostController < ApplicationController
  def index
    @posts = Post.all
  end
end

Votre modèle ressemblera toujours à cela

class Post < ActiveRecord::Base
  validates_presence_of :title, :content
end

Et votre vue ressemblera toujours à cela :

<% @posts.each do |post|  %>
    <p>
        <%= post.title %>
        <%= post.content %>
    </p>
<% end %>

Vos routes quant à elles, au lieu de ressembler à cela :

ActionController::Routing::Routes.draw do |map|
  map.resource :post
end

Ressembleront à cela :

ApplicationRails3::Application.routes.draw do |map|
    resources :post
end
Et rien qu'avec ça, vous avez le début de votre blog vous permettant déjà de visualiser la liste de tous vos articles.

"seul" le fonctionnement en interne change (et ce radicalement). Les API utilisées dans votre application ne changent, pour la plupart pas. Et si elles changent, la retro compatibilité devrait évidemment être assurée pendant pendant une version.

Moralité : non, rails ne se dirige pas vers une usine à gaz tel que J2EE. Oui, rails conserve sa simplicité. Et oui rails prends énormément en puissance.

Rails: Gestion des exceptions dans les contrôleurs

Vous pouvez, en Ruby, comme avec tout langage évolué, générer et gérer des exceptions.

Exemple rapide :

begin
    raise "Only a test"
rescue
    # Le raise nous emmene ici
end

Ainsi, nous pouvons aisément gérer les erreurs générées par notre application et éviter de tout casser pour un simple enregistrement non trouvé dans la base de données. Tous vos modèles et vos contrôleurs dans Rails pourront soulever des exceptionset elles seront gêrées par l'application.

Ainsi si, dans votre contrôleur, vous avez :

Post.find params[:id]

Et que l'uplet ayant pour clé primaire params[:id] n'existe pas, une exception ActiveRecord::RecordNotFound sera soulevée. En développement vous verrez un beau message d'erreur et en production une belle erreur 500. Mais nous ne voulons pas de cette erreur 500. Si l'enregistrement ne peut pas être trouvé, cela signifie que la page n'existe pas et alors, on désire une erreur 404.

En Ruby pur, nous ferions donc :

begin
    Post.find params[:id]
rescue ActiveRecord::RecordNotFound
    # On affiche l'erreur 404
end
Vade retro beurk !

Rendons nous plutôt dans application_controller et utilisons rescue_from (ou dans rails 3). Nous allons donc avoir, dans ApplicationController :

rescue_from ActiveRecord::RecordNotFound, :with => :render_missing
def render_missing
    render :file => "#{RAILS_ROOT}/public/404.html", :status => 404
end
Rails s'occupe de faire l'appel à cette méthode lorsque l'exception est soulevée et nous avons bien notre erreur 404 générée :)

Bien évidemment, vous pouvez gérez n'importe quelle exception avec rescue_from et ainsi éviter les erreurs 500 pas jolies et qui anéantissent l'expérience utilisateur afin de les remplacer par des joli messages.

Attention cependant à ne pas partir dans des excès ! En voici typiquement un. Ok pour gérer les exceptions dans le contrôleur lorsqu'elles ont un impact direct sur l'utilisateur (service indisponible, erreur 404, ...). Mais pas pour gérer toutes les erreurs comme ceci. Ne vous amusez donc pas à utiliser rescue_from afin de gérer les enregistrements invalides. Travaillez intelligemment :)