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.
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.
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
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.
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 :
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.
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 !
Nos conseils et ressources pour vos développements produit.