Disclaimer

Ce qui va suivre est (probablement) très particulièrement sale. Il existe certainement d’autres manières beaucoup plus propre d’arriver au même résultat de manière simple, mais je ne les ai pas trouvées.

Tout ce que vous pourrez lire par la suite a été écrit sous la menace des deux inséparables compagnes Grey Goose et Red Bull et la responsabilité ne saurait m’être imputée de quelque manière que ce soit. Enfin, petit rappel : ne le faite pas chez vous, à moins de savoir ce que vous faites.

Ceci étant dit, passons aux choses sérieuses.

Introduction

Ma TODO s’enorgueillit depuis deux ans du souhait de doter Typo, le blogware basé sur le framework Ruby on Rails qui équipe ce site, d’un système de plugins modernes, dont le moindre défi n’est pas de ne nécessiter aucune intervention de l’utilisateur post installation.

Mes besoins côté utilisateur étaient simples :

  1. Le plugin devait offrir de nouvelles fonctionnalités en s’intégrant toalement dans l’application, et notamment en en reprenant le looks and feel.
  2. L’utilisateur devait pouvoir le déployer sans éditer ses templates ni manipuler le mondre code.
  3. Le plugin devait pouvoir également s’intégrer dans l’administration de l’outil en cas de besoin : paramétrage, gestion de contenus…

Et surtout, il était hors de question de demander aux utilisateurs de toucher à config/routes.rb

Si vous débutez dans le développement Rails, vous n’avez sans doutes pas encore joué avec les routes plus que nécessaire. Celles-ci permettent de définir que l’accès à telle ou telle URL dans l’application va entraîner l’appel à tel contrôleur, et à telle méthode.

C’est muni de ces problématiques que j’ai commencé à boire combattre l’insomnie et les crétins qui faisaient exploser des feux d’artifices à deux pas de la maison afin d’atteindre le Saint Graal : un formulaire de contact.

Ne rigolez pas. La création d’un plugin apparemment très simple devient tout de suite beaucoup plus complexe avec les pré requis exposés ci-dessus.

Création du plugin

Commencez par créer un plugin Rails des plus standards.

neuro$ ./script/generate plugin typo_contactform
      create  vendor/plugins/typo_contactform/lib
      create  vendor/plugins/typo_contactform/tasks
      create  vendor/plugins/typo_contactform/test
      create  vendor/plugins/typo_contactform/README
      create  vendor/plugins/typo_contactform/MIT-LICENSE
      create  vendor/plugins/typo_contactform/Rakefile
      create  vendor/plugins/typo_contactform/init.rb
      create  vendor/plugins/typo_contactform/install.rb
      create  vendor/plugins/typo_contactform/uninstall.rb
      create  vendor/plugins/typo_contactform/lib/typo_contactform.rb
      create  vendor/plugins/typo_contactform/tasks/typo_contactform_tasks.rake
      create  vendor/plugins/typo_contactform/test/typo_contactform_test.rb
      create  vendor/plugins/typo_contactform/test/test_helper.rb

Puis, dupliquez l’arborescence d’une application Rails normale dans votre plugin :

neuro$ cd vendor/plugins/typo_contactform/lib
neuro$ mkdir -p app/controllers/admin \
  app/models app/views/contact_form app/views/admin/contact_form \
  app/helpers

Ouvrez maintenant votre éditeur de texte favori. Si vous ne l’avez pas encore adopté, pourquoi n’essaieriez-vous pas Textmate ? Éditez votre nouveau plugin, les choses sérieuses commencent.

Forcez Rails à intégrer votre plugin dans ses dépendances

Éditez le fichier lib/typo_contactform.rb afin d’y ajouter le code suivant :

%w{ models controllers }.each do |dir|
  path = File.join(File.dirname(__FILE__), 'app', dir)
  $LOAD_PATH << path
  ActiveSupport::Dependencies.load_paths << path
  ActiveSupport::Dependencies.load_once_paths.delete(path)
end

ActionController::Base.send :helper, ContactformHelper

Ce bloc ajoute vos contrôleurs, modèles et helpers dans les dépendances Rails. Vos contrôleurs pourront notamment hériter d’autre chose que de la classe ActionController::Base, par exemple, d’ApplicationController.

Profitez-en également pour déclarer auprès de Rails vos helpers pour ce qu’ils sont, vous en aurez certainement besoin plus tard.

Création des tables

Toujours dans lib/typo_contactform.rb ajoutez le code suivant :

unless ContactForm.table_exists?
  ActiveRecord::Schema.create_table(ContactForm.table_name) do |t|
    t.column :name,           :string
    t.column :email,          :string
    t.column :address,        :string
    t.column :message,        :text
    t.column :created_at,     :datetime
    t.column :updated_at,     :datetime
  end
end

Au chargement de votre application, Rails créera la table contact_forms si cette dernière n’existe pas encore.

Ajout des routes à la volée

C’est là que les choses passent de pas super propres à franchement SALE. Si vous avez peur de ne pas le supporter, servez-vous un grand verre de vodka bien glacée et buvez-le cul sec. Ça ira certainement mieux après.

Éditez init.rb et ajoutez le code suivant au tout début du fichier :

ActionController::Routing::Routes.draw do |map|
  map.connect '/contactform/:action', :controller => 'contactform'
  map.connect '/admin/contactform/:action', :controller => 'admin/contactform'
end

ActionController::Routing::Routes.draw do |map|
  map.connect '/contactform/:action', :controller => 'contactform'
end  

ActionController::Routing::Routes.draw est appelé par Rails après le chargement des plugins. Il commence par appeler la méthode clear! qui vide la table de routage de votre application de son contenu, en commençant par les règles que votre plugin vient de définir. Pour cette raison, vous devez d’abord la surcharger.

Extension du chemin des vues

Le code contenu dans ce paragraphe concerne plus particulièrement Typo, qui possède un système de template particulier. Vous devrez sans doutes l’adapter un peu à votre application. Éditez pour cela le fichier app/controllers/contactform_controller.rb, et faites le commencer comme suit :

class ContactformController < ContentController
  unloadable
  layout :theme_layout
  before_filter :template_root
  
  def template_root
    self.view_paths = ::ActionController::Base.view_paths.dup.unshift("#{RAILS_ROOT}/vendor/plugins/typo_contact_form/lib/app/views")
  end
end

Si jamais vous n’utilisiez pas Typo, le bloc de code ci-dessous ressemblerait très probablement à :

class ContactformController < ApplicationController
  unloadable
  layout :theme_layout
  before_filter :template_root
  
  def theme_layout
    "#{RAILS_ROOT}/app/views/layouts/default.html.erb"
  end
  
  def template_root
    self.view_paths = ::ActionController::Base.view_paths.dup.unshift("#{RAILS_ROOT}/vendor/plugins/typo_contact_form/lib/app/views")
  end
end

Vous indiquez d’abord au plugin où aller chercher le layout de l’application. Puis, vous étendez le chemin d’accès aux vues afin de permettre à Rails de trouver celles de votre plugin.

L’appel à unloadable résoud les plantages à base de A copy of ApplicationController has been removed from the module tree but is still active! qui surviennent quand vous allez chercher à charger la page de votre plugin une seconde fois.

À ce stade, si vous avez mal à la tête, servez-vous une autre vodka, ça passera tout seul.

Et l’admin dans tout ça ?

Je vous avais promis que votre plugin serait administrable. Chose promise, chose due – et non rose promise chomedu comme le disait un tract de l’UNI à la fin des années 90. Prenez un verre, remplissez-le à moitié de vodka, et à moitié de sirop de cramberrie, détendez-vous, ça arrive.

Éditez à nouveau le fichier link/typocontactform.rb, et ajoutez-y le code suivant :

unless ContactForm.table_exists?
  admin = Profile.find_by_label('admin')
  admin.modules << :contactform
  admin.save
end

AccessControl.map :require => [ :admin, :publisher, :contributor ]  do |map|
  map.project_module :contactform, nil do |project|
    project.menu    "Contact",  { :controller => "admin/contactform", :action => "index"  }
end

Vous venez d’un seul coup d’ajouter l’entrée Contact Form dans votre administration – si vous utilisez le Typo Login System – et d’ajouter les ACL qui vont bien aux administrateurs de votre application.

Créez un fichier app/controllers/admin/contactform_controller.rb.

module Admin; end
class Admin::ContactformController < Admin::BaseController
  unloadable
  layout :set_layout
  before_filter :template_root
  
  def template_root
    self.view_paths = ::ActionController::Base.view_paths.dup.unshift("#{RAILS_ROOT}/vendor/plugins/typo_contact_form/lib/app/views")
  end
  
  def set_layout
    "#{RAILS_ROOT}/app/views/layouts/administration.html.erb"
  end
end

Ça ressemble pas mal à ce que nous avons vu précédemment non ?

Conclusion

Et voilà, c’est terminé, vous connaissez le plus compliqué. Pour le reste, c’est du développement Rails des plus standards. J’ai posé le code du Typo Contact Form sur Gighub. Tout n’est pas totalement terminé, mais le proof of concept est là. En espérant que ça vous donne envie d’en faire d’autres.