Redmine Plugins #2 : patcher une classe de Redmine

ATTENTION: cet article n'est plus valable à partir de Redmine 2.x.

Il peut arriver qu’une classe de Redmine ne se comporte pas exactement comme vous le voudriez, ou que vous souhaitiez lui ajouter des propriétés.

C’est décrit en anglais sur la page Plugin Internals / Extending the Redmine Core du wiki officiel, qui renvoie vers la lecture de certains plugins d’Eric Davis pour des exemples.

Petit apparté, je partage assez l’analyse selon laquelle il est quasi inutile de maintenir une API pour surcharger les modèles / controlleurs. Cela dit, parfois les méthodes sont extrêmement longues et/ou sujettes à de fréquents changements. Toute surcharge dans un plugin induit donc un risque pour les futures versions du core…

Retour à nos moutons : admettons qu’on veuille ajouter au modèle Issue une méthode d’instance whoami qui retournerait “Je suis le ticket #XXX”. Exemple bidon, c’est pour la science.

Si on applique ce que préconise Eric, ça donne quelque chose de ce genre :

require_dependency 'issue_patch'
Issue.send(:include, IssuePatch) unless Issue.included_modules.include? IssuePatch


require_dependency 'issue'

def self.included(base)
  base.extend(ClassMethods)
  base.send(:include, InstanceMethods)
  base.class_eval do
    unloadable #permet de décharger la classe en mode dev
  end
end

#ici nos méthodes de classe
module ClassMethods
end

#ici nos méthodes d'instance
module InstanceMethods
  def whoami
    "Je suis le ticket ##{self.id}"
  end
end    

Classique, mais comme diraient certains amis “on voit pas trop ce que ça fait”.

Personnellement je préfère réouvrir la classe Issue, et ça a l’air de marcher tout aussi bien (en dev et en prod) :

config.to_prepare do
require_dependency 'issue_patch'


require_dependency 'issue'

def whoami
  "Je suis le ticket ##{self.id}"
end

Différences :

  1. utilisation de “config” au lieu de “Dispatcher” ; sans importance à mon avis. C’est discuté un peu ici.
  2. ré-ouverture de la classe plutôt qu’inclusion d’un module ; je trouve ça plus lisible pour ce coup-ci

Attention, je ne dis pas que ce que fait Eric fonctionne moins bien. Au contraire, c’est peut-être plus “propre”, mais n’étant pas un développeur confirmé, si je ne comprends pas au premier coup d’oeil ce que j’ai fait, j’ai plus de mal à maintenir mon code.

Au passage, c’est une mauvaise idée d’appeler son patch “lib/issue_patch.rb”. Si tout le monde fait ça, on ne pourra pas faire fonctionner 2 plugins qui patchent la même classe en même temps. Beurk. D’ailleurs, c’était le cas pour des plugins à moi, donc autant utiliser des noms a priori uniques : commit redmine_drafts/ec06b8