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

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>"

    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.

Demain soir aura lieu le 4e apéro Ruby à Lyon.
Bien évidemment, je ne peux que vous conseiller de vous y rendre !
Je serai présent et j’entrainerai un jeune padawan avec moi afin de le trainer du côté clair de la force :)

C’est à l’Antre Autre, un petit bar sympa à côté des Terreaux.
Vous pouvez venir à partir de 19h. Et nous serons probablement plusieurs à manger sur place par la suite (mais bien évidemment, vous partez quand bon vous semble).

Que vous veniez ou pas, vous avez entendu parler de rails 3 !
Et peut-être de bundler, dont je ferai une présentation d’une dizaine de minutes (« lightning talk »).

Du coup, que vous veniez ou pas, vous pouvez visualiser en ligne cette introduction à bundler.
Enjoy! comme on dit. Si vous avez des remarques à faire dessus, n’hésitez pas à me les faire remonter (ou alors à forker le projet et à faire les modifictions).
Et je vous attends demain.

Parlons un peu de rack :)
Utilisé par rails et de nombreuses autres applications ruby afin de pouvoir être lancées par une large majorité de serveurs web (mongrel, unicorn, …) rack est particulièrement puissant.

La création d’une première application est on ne peut plus simple.
Dans un fichier test.rb :

class Test
    def call(env)
        [200, {"Content-Type" => "text/html"}, ["<html><head></head><body>Hello world!</body></html>"]]
    end
end

Exécutez votre application avec la ligne de commande suivante :

rackup test.rb

Simple mais efficace :)
Bien évidemment il faut ajouter des librairies à cela afin d’avoir quelque chose de plus puissant.

Maintenant découvronr autre chose : les middlewares.
En effet rack peut embriquer les méthodes call et ainsi vous permettre d’exécuter du code juste après l’initialisation de rack. Ou bien juste avant l’exécution du code de votre page.

Le développement d’un rack ressemble fortement à celui d’une tâche rack « normale ».
Voici un exemple de middleware vous permettant d’ajouter, juste avant la balise , le temps d’exécution de la page.

Dans un fichier response_timer.rb

class ResponseTimer
    attr_reader :message, :app

    def initialize(app, message = "Response Time")
        @message = message
        @app = app
    end

    def call(env)
        start = Time.now
        status, headers, response = app.call(env)
        stop = Time.now

        body = ''
        response.each { |part| body << part }
        index = body.rindex('</body>')
        if index
            body.insert(index, "<!-- #{message}: #{stop - start} -->\n")
            headers["Content-Length"] = body.length.to_s
            response = [body]
        end
        [status, headers, response]
    end
end

En revanche, nous allons maintenant avoir besoin d’un fichier de configuration pour rack, afin de pouvoir définir quel middleware nous incluons.
Nous devons pour cela créer un fichier nommé config.ru

require 'test'
require 'response_timer'

use ResponseTimer
run Test.new

Et nous n’avons plus qu’à exécuter le serveur

rackup

Vous noterez trois choses :

  • Nous incluons les deux librairies que nous venons de créer. Notre application et le middleware
  • Avec la méthode « use », nous ajoutons le middle à ceux natif à rack
  • Nous exécutons notre application

Comment ce middleware fonctionne-t-il ?
Comme votre application ! :)
Vous constaterez que les deux ont une méthode call. Et que les deux retournent un tableau contenant status, headers et réponse.

La seule différence entre les deux étant cette ligne :

status, headers, response = app.call(env)

Dans le middleware :
Grace au app.call, nous appellons le middleware suivant. Et ce, jusqu’à ce que nous arrivions au bout de la chaine : notre classe « test ».
Nous calculons donc assez aisément le temps d’exécution de notre application. Il nous suffit de faire la différence entre le timestamp avant l’exécution de cette méthode et après.
Et comme nous avons accès au contenu de la page retournée même après l’exécution de la méthode, nous pouvons y ajouter un commentaire juste avant la balise (si elle existe) contenant le temps d’exécution de la page.

Et dans rails ?

Comme dit plus haut, toute application rails tourne avec rack. Il est donc tout à fait possible d’ajouter notre middleware dans votre application.
Dans rails, les middlewares sont à ajouter dans le fichier config/environment.rb.

Voici comment j’ai inclu le rack ResponseTimer :

Rails::Initializer.run do |config|
    config.middleware.insert_before Rack::Lock, "ResponseTimer", "Load Time"
end

Le middleware Rack::Lock est le tout premier à être exécuté (vous pouvez avoir la liste de tous vos middlewares avec rake middleware).
Nous exécutons donc, juste avant celui-ci, le calcul du temps d’exécution de notre page :)

Trois méthodes vous permettent d’ajouter de nouveaux middlewares :

  • config.middleware.use – Le middleware sera ajouté à la fin de la pile
  • config.middleware.insert_before – Le middleware sera ajouté avant celui passé en premier paramètre
  • config.middleware.insert_after – Le middleware sera ajouté après celui passé en premier paramètre

Vous pouvez également supprimer un middleware précédemment ajouté : config.middleware.delete
Et remplacer un middleware par un autre : config.middleware.swap

Par ailleurs de nombreux middlewares sont disponibles en Open Source. Vous en trouverez une liste sur le wiki de rack.
Ainsi que dans le projet rack-contrib.

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.

Je vous ai présenté il y a quelques jours CouchDB.
La librairie que je vous ai présenté dans le précédent article était très bien dans un but éducatif. Mais en pratique, c’est vraiment pas puissant. Aujourd’hui nous allons donc parler de CouchREST.

Commencons par installer le gem. Si vous n’avez pas Rubygems configuré pour Gemcutter, faites le maintenant !

sudo gem install couchrest

Zou ! Maintenant on se connecte, on crée une base et un document dans celle-ci !

require 'rubygems"
require "couchrest"

@db = CouchRest.database!("http://127.0.0.1:5984/my_database")
response = @db.save_doc({:name => 'My Document', :content => 'The content'})
p @db.get(response['id']).inspect

Super, mais jusque la, ça ne va pas beaucoup plus loin que ce que nous avons fait précédemment.
Voyons donc comment créer une classe qui représentera un modèle de données CouchDB.

class Document < CouchRest::ExtendedDocument
    use_database 'my_database'

    property  :name
    property  :content

    timestamps!
end

Nous créons un modèle « Document », qui possèdera les champs name et content. Mais également created_at et updated_at (remplis automatiquement grâce à timestamps!).
Maintenant nous pouvons faire les choses d’une manière similaire de ce que nous faisons avec ActiveRecord.

Créer un nouveau document.

doc = Document.new(:name => 'My Doc', :content => 'The Content')
doc.save!

Récupérer tous les documents existants

docs = Document.all

Et également appeller n’importe quelle vue grâce à des méthodes virtuelles. Par exemple si vous avez une vue « by_name ».

docs = Document.by_name

CouchRest gère même la pagination des documents (avec les méthodes de pagination internes à CouchDB).

docs = Document.all.paginate(:page => 1, :per_page => 10)

Vous pouvez donc utiliser CouchDB de manière particulièrement avancée, comme vous le faites déjà avec ActiveRecord pour vos SGBDR de type SQL.
Cependant avec CouchREST il vous faut toujours gérer la connexion à votre base de données « manuellement », dans un fichiers que vous placez dans config/initializers/ par exemple. Pas super pratique.
Heureusement il existe CouchREST Rails, qui implémente CouchREST (et uniquement CouchREST) mais gère automatiquement la connexion au serveur.

Commencez par installer le plugin.

script/plugin install git://github.com/hpoydar/couchrest-rails.git

Puis générez les divers fichiers de configuration relatifs à celui-ci.

script/generate couch_rest_rails

Vous allez maintenant notamment avoir un fichier config/couchdb.yml, qui contient la configuration relative à CouchDB en fonction de votre environnement.
Voici le contenu par défaut de ce document.

base: &base
 database_prefix:
 database_suffix: _<%= RAILS_ENV %>

development:
  host: localhost
  port: 5984
  <<: *base

test:
  host: localhost
  port: 5984
  <<: *base

A vous de configurer celui-ci comme vous le désirez en fonction de vos divers environnements et de vos besoins :)
Attention : vos modèles CouchREST ne doivent maintenant plus dépendre de ExtendedDocument mais de CouchRestRails::Document

Notre modèle précédent sera donc maintenant ainsi

class Document < CouchRestRails::Document

Redémarrez votre application et accédez directement à chacun de vos documents CouchDB via le modèle approprié :)
Si vous regardez le contenu de votre base de données, vous verrez que CouchREST fonctionne d’une manière similaire aux Single Table Inheritance de ActiveRecord.
Chaque document a un champ « couchrest-type », qui définit le modèle relatif au document.
Vous pouvez donc particulièrement aisément transformer un document en un autre document.

Supposons que vous ayez des documents de type « File » et d’autres de type « Image », vous transformerez toutes vos images en fichiers de la manière suivante.

File.all.each do |file|
    doc['couchrest-type'] = 'Image'
    doc.save!
end

Ne faites pas cela sans avoir intégralement testé toutes les conséquences possibles bien évidemment (sauf si avez le désir de mourir dans d’affreuses souffrances ;) ).

Comme vous le constatez, avec CouchREST, (même si le projet a quelques problèmes de communication), vous pouvez gérer vos données stockées dans CouchDB de manière aussi aisée que toute donnée stockée dans un SGBDR geré par ActiveRecord.

Dans un prochain (et dernier) article à propos de CouchDB, nous ferons un peu de javascript avec les vues, comment les implémenter (avec CouchREST) et comment s’en servir afin de profiter de toutes les fonctionnalités du SGBD.

L’annonce a été faite il y a de cela un an, Rails 3 est en cours de développement.
Cela sera la prochaine version du meilleur framework web (sauf possibles releases de sécurité).

A l’heure actuelle, la 3.0 est une 3.0.pre. L’utiliser sur une application en production est donc fortement déconseillé. Et même en développement, c’est pas évident.
En revanche il est tout à fait possible d’installer cette nouvelle version dans un but de test.

Allons y pour cette installation.
Vous devez commencer par récupérer la version edge du framework. Pour cela il faut que GIT soit installé sur votre machine.
Faites un git clone du repository rails dans le répertoire de votre choix.

> cd ~/
> git clone git://github.com/rails/rails.git

A partir de ce moment, vous pouvez créer une nouvelle application rails3.

~/rails/railties/bin/rails test3

Cependant il va nous manquer quelques dépendances pour pouvoir exécuter cette application.
Tout d’abord il nous faut installer bundler, le nouveau gestionnaire de dépendances développé par Yehuda Catz.

sudo gem install bundler

Puis rendez vous dans votre application et éditez le fichier Gemfile
Placez-y les dépendances suivantes :

gem "rack",          "1.0.1"
gem "rack-mount",    :git => "git://github.com/rails/rack-mount.git"
gem "rack-test",     "~> 0.5.0"
gem "erubis",        "~> 2.6.0"
gem "arel",          :git => "git://github.com/rails/arel.git"
gem "sqlite3-ruby"
gem "rails", "3.0.pre", :git => "git://github.com/rails/rails.git"

Toutes ces dépendances seront installées automatiquement lorsque le framework passera en Beta1 et qu’il sera disponible sous la forme d’un gem.
En attendant il vous faut les installer de manière semi automatique.

Après avoir placé ces dépendances, vous pouvez les installer avec un

gem bundle

Et voila ! Vous avez votre application rails3 fonctionnelle !
Un petit

ruby script/server

Et en vous rendant à l’adresse http://localhost:3000/, vous verrez votre nouvelle application lancée.

Pour commencer à tester, faites comme d’habitude.
Générez un contrôleur et un modèle.

ruby script/generate controller index
ruby script/generate model page

Et commencez à développer.
Vos contrôleurs, modèles et vues ne changent pas de place. Mais certaines fonctionnalités peuvent ne plus être disponibles.
A vous de découvrir les nouveautés au fur et à mesure. Je ferai peut-être quelques articles ici pour certaines d’entre elles.

Il n’est pas rare d’avoir besoin de créer des tâches asynchrones dans vos applications. Si ces tâches sont à déclencher suite à l’action d’un utilisateur (édition d’une page, …), vous pouvez utiliser delayed job.
Mais si cette tâche doit être exécutée à interval régulier dans le temps, utiliser delayed job n’est pas faisable. Et il vous faudra créer un cron.

Pour cela je vais vous parler de Whenever.
L’utilisation est simpliste. Vous définissez tous vos crons dans config/schedule.rb. Et whenever se charge de générer automatiquement votre crontab.

Voici par exemple le document whenever pour RefStats.

every 1.minute do
  rake "crawler:work > #{RAILS_ROOT}/../shared/log/crawler.log"
end

every 1.day, :at => '2 am' do
  rake "maintenance:daily > #{RAILS_ROOT}/../shared/log/maintenance.log"
end

Toutes les minutes, le crawler récupérant les positions est lancé. Il s’agit d’une tâche rake.
Tous les jours à 2h du matin, la tâche « maintenance:daily » est lancée.

Suite à cela, depuis la base de votre projet, entrez en ligne de commande :

whenever

Vous verrez alors la crontab générée par whenever. Uniquement affichée, par mise à jour.
Si vous désirez mettre celle-ci à jour, c’est cependant simple.

whenever –update-crontab ApplicationName

L’option ApplicationName doit être différente pour chacune de vos applications. Ainsi whenever ne modifie que la crontab relative à cette application.

Du coup dans votre processus de déploiement Capistrano, vous ajoutez une tâche pour whenever :

after "deploy:symlink", "deploy:update_crontab"
namespace :deploy do
    desc "Update the crontab file"
    task :update_crontab, :roles => :db do
        run "cd #{release_path} && whenever --update-crontab #{application}"
    end
end

Et votre crontab sera remise automatiquement à jour à chaque fois que vous déploierez votre application. Plus à se soucier de cela manuellement :)

Dans toute application Rails, vous avez une tâche « rake » assez sympa : stats. Celle-ci vous retourne le nombre de lignes de code dans votre projet.
En séparant les contrôleurs, les modèles, les helpers et les librairies. Mais également en séparant le code applicatif du code de test.
Et en vous donnant un ratio code applicatif / code de test/

Voici un exemple de rake stats (avec des chiffres complètement fictifs).

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   419 |   289 |       7 |      43 |   6 |     4 |
| Helpers              |     9 |     9 |       0 |       1 |   0 |     7 |
| Models               |    34 |    29 |       4 |       2 |   0 |    12 |
| Libraries            |   359 |   212 |       2 |      36 |  18 |     3 |
| Model specs          |    78 |    60 |       0 |       0 |   0 |     0 |
| Controller specs     |   203 |   166 |       0 |       2 |   0 |    81 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |  1514 |  1028 |      13 |      85 |   6 |    10 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 539     Test LOC: 489     Code to Test Ratio: 1:0.9

Ici, en utilisant RSpec, nous avons les statistiques de nos specs car l’outil modifie cette fonctionnalité afin d’avoir les bons chiffres.
Cependant si en plus de RSpec, et comme moi, vous utilisez Cucumber, vous n’aurez pas ces chiffres. Normal ! RSpec redéfinit la tâche stats. Mais pas cucumber.

Modifions donc cette tâche afin d’avoir nos chiffres. Ouvrez le fichier suivant :

lib/tasks/rspec.rake

La aux alentours de la ligne 110, vous allez trouver la définition des répertoires de code à include dans la tâche stats.

task :statsetup do
    require 'code_statistics'
    ::STATS_DIRECTORIES << %w(Model\ specs spec/models) if File.exist?('spec/models')
    ::STATS_DIRECTORIES << %w(View\ specs spec/views) if File.exist?('spec/views')
    ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers) if File.exist?('spec/controllers')
    ::STATS_DIRECTORIES << %w(Helper\ specs spec/helpers) if File.exist?('spec/helpers')
    ::STATS_DIRECTORIES << %w(Library\ specs spec/lib) if File.exist?('spec/lib')
    ::STATS_DIRECTORIES << %w(Routing\ specs spec/routing) if File.exist?('spec/routing')
    ::STATS_DIRECTORIES << %w(Integration\ specs spec/integration) if File.exist?('spec/integration')
    ::CodeStatistics::TEST_TYPES << "Model specs" if File.exist?('spec/models')
    ::CodeStatistics::TEST_TYPES << "View specs" if File.exist?('spec/views')
    ::CodeStatistics::TEST_TYPES << "Controller specs" if File.exist?('spec/controllers')
    ::CodeStatistics::TEST_TYPES << "Helper specs" if File.exist?('spec/helpers')
    ::CodeStatistics::TEST_TYPES << "Library specs" if File.exist?('spec/lib')
    ::CodeStatistics::TEST_TYPES << "Routing specs" if File.exist?('spec/routing')
    ::CodeStatistics::TEST_TYPES << "Integration specs" if File.exist?('spec/integration')
  end

Il nous faut ajouter nos features Cucumber, et ce deux fois : une pour les statistiques « générales ». Et une seconde afin de comptabiliser ces données comme des données de test. La tâche statsetup sera donc la suivante :

task :statsetup do
    require 'code_statistics'
    ::STATS_DIRECTORIES << %w(Model\ specs spec/models) if File.exist?('spec/models')
    ::STATS_DIRECTORIES << %w(View\ specs spec/views) if File.exist?('spec/views')
    ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers) if File.exist?('spec/controllers')
    ::STATS_DIRECTORIES << %w(Helper\ specs spec/helpers) if File.exist?('spec/helpers')
    ::STATS_DIRECTORIES << %w(Library\ specs spec/lib) if File.exist?('spec/lib')
    ::STATS_DIRECTORIES << %w(Routing\ specs spec/routing) if File.exist?('spec/routing')
    ::STATS_DIRECTORIES << %w(Integration\ specs spec/integration) if File.exist?('spec/integration')
    ::CodeStatistics::TEST_TYPES << "Model specs" if File.exist?('spec/models')
    ::CodeStatistics::TEST_TYPES << "View specs" if File.exist?('spec/views')
    ::CodeStatistics::TEST_TYPES << "Controller specs" if File.exist?('spec/controllers')
    ::CodeStatistics::TEST_TYPES << "Helper specs" if File.exist?('spec/helpers')
    ::CodeStatistics::TEST_TYPES << "Library specs" if File.exist?('spec/lib')
    ::CodeStatistics::TEST_TYPES << "Routing specs" if File.exist?('spec/routing')
    ::CodeStatistics::TEST_TYPES << "Integration specs" if File.exist?('spec/integration')

    #
    # Adding Cucumber features to the stats
    #
    ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features')
    ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features')
  end

Et du coup nous avons maintenant nos données Cucumber ajoutées à nos stats.
Avec les même données fictives que plus haut, nous aurons donc un rake stats équivalent à celui-ci :

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   419 |   289 |       7 |      43 |   6 |     4 |
| Helpers              |     9 |     9 |       0 |       1 |   0 |     7 |
| Models               |    34 |    29 |       4 |       2 |   0 |    12 |
| Libraries            |   359 |   212 |       2 |      36 |  18 |     3 |
| Model specs          |    78 |    60 |       0 |       0 |   0 |     0 |
| Cucumber features    |   412 |   263 |       0 |       1 |   0 |   261 |
| Controller specs     |   203 |   166 |       0 |       2 |   0 |    81 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |  1514 |  1028 |      13 |      85 |   6 |    10 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 539     Test LOC: 489     Code to Test Ratio: 1:0.9

Nos statistiques de projet sont plus réalistes et le ratio entre le code applicatif et le code de test est plus révélateur de ce que vous avez réellement dans votre application.

Avec rails comme avec tout framework, vous allez rapidement avoir besoin de gérer l’identification des utilisateurs et leurs autorisations.
Pour leur identification, il existe divers plugins. Notamment Restful Authentication, AuthLogic et (mon préféré) Clearance.
Pour la gestion des autorisations, je vais vous parler de ACL9.

L’idée est simple. Après avoir identifié votre utilisateur, vous avez accès à un objet current_user, qui contient l’objet relatif à l’utilisateur actif (généralement une instance du model User. Mais cela peut être n’importe quoi).
ACL9 va ajouter deux tables :
- Role, qui contiendra tous les rôles utilisateurs existants. Un peu comme des groupes. Chaque rôle pouvant être associé à une instance d’un objet.
Vous pourrez ainsi avoir un rôle « admin », global, qui permettra de définir les accès de l’administrateur.
Et un rôle « éditeur », relatif uniquement à l’objet ayant pour id « 1″ du modèle « News ».

- UserRole, qui fera la relation entre un rôle et un utilisateur (has_and_belongs_to_many).

Voici les migrations nécessaires pour ces deux tables :

create_table :roles, :force => true do |t|
    t.column   :name,                            :string,                    :limit => 40
    t.column      :authorizable_type",      :string,                    :limit => 40
    t.column      :authorizable_id,           :integer
    t.timestamps
end
create_table :roles_users", :id => false, :force => true do |t|
    t.column :user_id,                          :integer
    t.integer  :role_id,                           :integer
    t.timestamps
end

Vous devez bien évidemment créer un modèle « Role », qui représentera cette table. Il est inutile de créer un modèle RolesUser. Cela sera géré automatiquement par Rails.

class Role < ActiveRecord::Base
    acts_as_authorization_role
end

Vous noterez l’ajout de « acts_as_authorization_role », qui va implémenter les méthodes de rôles relatives à ACL9.
Dans notre table user, nous allons également devoir ajouter un appel à une méthode spécifique de ACL9 afin d’ajouter les méthodes relatives à l’utilisateur.

class User < ActiveRecord::Base
    acts_as_authorization_subject
end

Et dans tous les modèles ou nous désirons avoir une gestion des droits d’accès, nous allons ajouter :

class Foo < ActiveRecord::Base
    acts_as_authorization_object
end

ACL9 vous a maintenant ajouté diveres méthodes dans vos objets :
Dans votre modèle utilisateur :

  • has_role?(role, object = nil) – Retournera « true » si l’utilisateur a le rôle demandé en global ou sur l’objet précisé.
  • has_role!(role, object = nil) – Donnera le rôle nommé à l’utilisateur
  • has_no_role!(role, object = nil) – Supprimera le rôle nommé pour l’utilisateur
  • has_roles_for?(object) – Retourne true si l’utilisateur a un quelconque rôle sur l’objet
  • has_role_for?(object) – Alias de has_roles_for?
  • roles_for(object) – Retourne tous les rôles de l’utilisateur pour cet objet
  • has_no_roles_for!(object) – Supprime tous les rôles de l’utilisateur pour l’objet
  • has_no_roles! – Supprime tous les rôles de l’utilisateur

Et dans chacun de vos modèles objets (Foo plus haut) :

  • accepts_role?(role, subject) – Retourne true si subject a accès au rôle courant
  • accepts_role!(role, subject) – Donne accès à subject au rôle courant
  • accepts_no_role!(role, subject) – Supprime l’accès utilisateur au rôle
  • accepts_roles_by?(subject) – Retourne true si l’utilisateur a un rôle quelconque sur l’objet
  • accepts_role_by?(subject) – Alias de accepts_roles_by?
  • accepted_roles_by(subject) – Retourne tous les utilisateurs ayant un rôle pour cet objet

Maintenant que nous avons implémenté nos autorisations et que nous avons vu les diverses méthodes pour les utiliser, limitons les accès à nos actions !

class MyOwnController < ApplicationController
    access_control do
        # Autorise tous les utilisateurs ayant le rôle "superadmin" à accéder à l'objet
        allow :superadmin

        # Autorise tous les utilisateurs ayant le rôle "creator" sur @news
        allow :creator, :at => :news

        # Autorise les utilisateurs anonymes et ceux qui sont enregistrés sur la page d'accueil
        # Et refuse les utilisateurs ayant le rôle "banned"
        action :index do
            allow anonymous, logged_in
            deny :banned
        end

        # Autorise les utilisateurs ayant le rôle "manager" sur l'objet @news. Sauf pour l'action "destroy"
        allow :manager, :at => :news, :except => [:destroy]
    end

    def index
        @news = News.find_by_id params[:id]
        # Le code relatif à l'action "index"
    end
end

Du coup on peut maintenant gérer de manière particulièrement avancée les rôles dans notre application.

 
Fork me on GitHub