Blog tech

Initiation au développement de plugin Redmine

Rédigé par Cédric Brancourt | 13 mai 2015

Initiation au développement de plugin Redmine

Redmine est un outil de gestion de projet que j’affectionne pour sa simplicité et son accessibilité. Dans sa version originale il permet de gérer un éventail très large de types de projets, pas uniquement a destination des équipes de développement, mais aussi pour les bureaux d’études, administrations … Cependant, il peut arriver que l’on ai besoin de fonctionnalités supplémentaires ou de revoir ses fonctionnalités de bases pour l’adapter à son propre besoin. Comme c’est un logiciel libre, il est possible de le « forker » pour l’adapter. Mais ça à l’inconvénient de rendre l’import de fonctionnalités de nouvelles versions beaucoup plus délicat. Il est beaucoup plus avisé de s’appuyer sur le système de plugins de Redmine qui permet d’étendre et de modifier ses fonctionalités. Les pré-requis au développement de plugins sont la connaissance de Ruby avec certaines notions de meta-programmation, la connaissance du fonctionnel et du code de Redmine.

Notre cas d’étude

Redmine permet de changer le statut d’une demande de plusieurs manières : - lorsqu’on consulte une liste de demandes, au travers d’un click droit. Pas commun et peu utilisable sur un smartphone par exemple. - lorsqu’on consulte une demande, en cliquant sur « modifier » puis en sélectionnant le statut. Et enfin en cliquant sur « Enregistrer », ça fait beaucoup de clics si on a plusieurs demandes à traiter.

Il m’a été demandé récemment de créer un plugin mettant à disposition un bouton permettant de fermer une demande directement depuis l’interface de consultation. Je vais rendre cette demande plus générique il s’agit de mettre a disposition un bouton pour chaque transition de statut disponible pour l’utilisateur courant. Défini comme ceci on reste compatible avec la possibilité personnaliser les statuts et les workflow de Redmine. C’est ce que l’on va voir en détail dans la suite.

Tout commence par un init.rb

Les plugins de Redmine sont à ajouter dans le répertoire plugins, mon plugin s’appellera issue_workflow_buttons, je crée donc un répertoire plugins/issue_workflow_button

A l’initialisation de Redmine les fichiers init.rb contenus dans les sous-répertoires de plugin sont évalués. C’est ce qui permet d’enregistrer le plugin. Je créé donc mon fichier init.rb dans le répertoire du plugin. La méthode register de la classe Redmine::Plugin permet de déclarer le plugin et ses meta-données. Pour cela la meilleure documentation est le fichier de la classe

Redmine::Plugin.register :issue_workflow_button do
  name 'Issue workflow button'
  author 'Cedric Brancourt @ Synbioz'
  description 'Add button(s) for available workflow transitions on issue view'
  version '0.0.1'
end

Le contenu du block sera évalué lors de l’initialisation de Redmine (et donc Rails). Ce n’est pas anodin, c’est ce qui nous permettra d’étendre les classes et module de Redmine.

Une fois l’init.rb ajouté et Redmine redémarré, notre plugin, qui pour l’instant ne fait rien, est bien reconnu dans la partie administration > plugins

Surcharger une vue ?

Puisque nous allons ajouter des boutons à la vue d’une demande nous allons devoir étendre la vue de base de Redmine

Redmine permet de « customizer » les vues de bases, soit en utilisant un système de hooks, soit en remplaçant directement la vue d’origine.

Le remplacement de la vue d’origine se fait en copiant le fichier de vue dans un fichier ayant le même chemin dans le répertoire du plugin. A l’initialisation des plugins le répertoire app du plugin est ajouté à l’autoload path. C’est une solution limitée, car elle induit des conflits dès que plus d’un plugin surcharge la même vue.

Les hooks de Redmine sont disséminés dans l’application, la plus grande partie se trouvent dans les vues. Ils permettent d’insérer du contenu à partir d’un point d’insertion défini.

Si je regarde dans le fichier de ma vue show ligne 70 j’ai un hook avec lequel je vais pouvoir interagir. Je vais pouvoir créer une vue partielle et un listener pour ce hook.

Dans le répertoire app/views/issues/ de mon plugin je crée un fichier de partial _workflow_buttons.html.erb qui contiendra les actions permettant de modifier le statut. Je remarque que dans le issues_controller de Redmine les statuts autorisés sont déjà assignés à une variable @allowed_statuses Je vais pouvoir itérer dessus pour créer les contrôles.

<% @allowed_statuses.each do |allowed_status| %>
    <%= link_to(allowed_status, issue_path(@issue, issue: {status_id: allowed_status.id}), method: :put, class: 'link-button') %>
<% end %>

Il suffit maintenant d’ajouter un hook listener qui fera un render de ce partial, toujours dans mon plugin, je crée le fichier (et l’arborescence) lib/issue_workflow_buttons/view_hook_listeners.rb

class IssueWorkflowButtons::ViewHookListeners < Redmine::Hook::ViewListener
  render_on(:view_issues_show_details_bottom, partial: 'issues/workflow_buttons')
end

Et dans le init.rb je require mon hook listener en ajoutant en entête :

require_dependency 'issue_workflow_buttons/view_hook_listeners'

Je redémarre Redmine, et les liens sont bien présents sur la vue de ma demande :

Le résultat n’est pas très joli, on va ajouter un peu de CSS pour rendre ça plus propre.

Ajouter du style

Que ce soit pour ajouter du javascript ou des styles, Redmine permet d’ajouter des assets dans les plugins. Les assets sont à ajouter dans les répertoires assets/(stylesheets|javascript|images) a la racine du plugin. Les fichiers assets sont des plugins sont copiés au démarrage de Redmine dans le répertoire public/plugin_assets/nom_du_plugin

Ajoutons notre fichier css : assets/stylesheets/issue_workflow_buttons.css

a.link-button {
    padding: 5px 8px;
    background: #628DB6;
    color: #FFF;
    margin: 3px;
    text-decoration: none;
    font-weight: bold;
    -webkit-transition-duration: 0.2s;
    -moz-transition-duration: 0.2s;
    transition-duration: 0.2s;
}

a.link-button:active {
    background-color: #83C1EB;
}

.actions {
    padding: 10px 0px;
}

Pour inclure la feuille de style au chargement de la page, ça reste assez trivial, en utilisant content_for dans notre partial :

<% content_for :header_tags do %>
    <%= stylesheet_link_tag 'workflow_buttons', :plugin => 'issue_workflow_buttons' %>
<% end %>

Il faut préciser le plugin dans le helper des assets tags car il est possible d’utiliser les assets d’un plugin dans un autre plugin. Une fois Redmine redémarré on respire et c’est plus propre :

Encore un détail

Il reste un petit détail agaçant le statut actuel de la tâche est inclus dans les statuts disponibles. Même si il est réellement disponible il n’y a pas d’intérêt à effectuer ce genre de transition. Je pourrais utiliser un reject dans ma vue pour éliminer le statut actuel de la liste.

Mais le but est de découvrir comment étendre Redmine alors on va ajouter une nouvelle méthode au modèle Issue (qui correspond aux demandes) et qui nous donnera les statuts disponibles, mais pas le statut courant.

Pour étendre les classes existantes de Redmine il suffit d’utiliser la mécanique des modules de Ruby.

Je crée un module IssueWorkflowButtons::IssueExtension dans lib

module IssueWorkflowButtons::IssueExtension
  def self.included(issue_class)
    issue_class.send(:include, InstanceMethods)
  end

  module InstanceMethods
    def next_transitions_for(user)
      self.new_statuses_allowed_to(user).reject {|s| s == self.status}
    end
  end

end

Ce module va étendre les méthodes d’instance de la classe receveuse lorsqu’il sera inclus. La méthode next_transitions_for sera ajoutée. Cette méthode fournit une liste des statuts filtrée.

Il ne reste plus qu’à inclure le module dans la classe Issue, dans notre init.rb

require_dependency 'issue_workflow_buttons/view_hook_listeners'
require_dependency 'issue_workflow_buttons/issue_extension'

Redmine::Plugin.register :issue_workflow_buttons do
  name 'Issue workflow buttons'
  author 'Cedric Brancourt @ Synbioz'
  description 'Add button(s) for available workflow transitions on issue view'
  version '0.0.1'

  Issue.send(:include, IssueWorkflowButtons::IssueExtension)
end

Et dans la vue utiliser notre nouvelle méthode à la quelle on passe User.current qui est l’utilisateur connecté.

<div class="actions">
    <% @issue.next_transitions_for(User.current).each do |allowed_status| %>
        <%= link_to(allowed_status, issue_path(@issue, issue: {status_id: allowed_status.id}), method: :put, class: 'link-button') %>
    <% end %>
</div>

<% content_for :header_tags do %>
    <%= stylesheet_link_tag 'workflow_buttons', :plugin => 'issue_workflow_buttons' %>
<% end %>

Et voila, un redémarage plus tard nos transitions sont filtrées.

Mais encore ?

Oui il y a bien des choses qui pourraient être couvertes, mais nous avons vu l’essentiel. Les plugins Redmine étant des Rails Engine patchés il est possible d’ajouter tout ce que vous trouveriez dans une application Rails classique dans app. Les routes du plugin se gèrent dans un fichier ‘config/routes.rb’, l’I18n dans config/locales et cætera.

Vous trouverez quelques documentations sur le développement de plugins dans les pages wiki de Redmine Des exemples dans les plugins existants

Vous trouverez les sources de ce plugin sur notre profile Github

Peut-être avez-vous des idées de plugins ? Alors foncez !