Un peu de contexte peut-être
J’ai envie de mettre par écrit ce petit cas d’usage client pour l’avoir un peu sous le coude, et surtout au propre !
On change un peu les noms, et le domaine, et ça donne :
Un client qui fait de l’aménagement de bâtiments, utilise Redmine pour suivre les anomalies de chantier, notamment en période de garantie. À chaque anomalie déclarée :
- elle est analysée,
- puis validée (ou non) par le responsable du dossier,
- et des actions correctives sont décidées.
Jusque-là, rien d’exotique.
En revanche, plusieurs autres personnes doivent également pouvoir suivre certaines anomalies : responsables de zone, conducteurs de travaux, encadrement, etc.
Et avec Redmine ?
Redmine est configuré pour que la majorité des utilisateurs ne puissent voir que les tickets :
- dont ils sont assignés,
- ou qu’ils ont créés.

Mais pour que d’autres utilisateurs puissent afficher des tickets, on a utilisé ce petit plugin : Redmine Extended Watchers qui permet d’ouvrir la liste des observateurs aux membres du projet, et surtout de leur donner la possibilité d’afficher les tickets dont ils sont observateurs.

Il fait même partie de ma petite liste des plugins 2025 d’ailleurs 😉
auto-promo !!
Le problème ?
L’ajout des observateurs reste manuel. Et si quelqu’un est oublié :
- il ne voit pas le ticket,
- il ne reçoit pas les notifications,
- et l’information ne circule plus correctement.
Il fallait donc trouver un moyen d’ajouter automatiquement les bons observateurs, sans compliquer le fonctionnement existant.
Et la solution ?
La solution qu’on a envisagée, c’est de déclarer la liste des dossiers, des responsables et des personnes qui veulent suivre les dossiers, dans une page wiki de Redmine.
Lorsqu’un utilisateur saisit le numéro de dossier dans un ticket :
- le nom du dossier est complété automatiquement,
- le responsable est ajouté en observateur,
- les autres personnes à suivre sont ajoutées également.
L’utilisateur saisit le numéro de dossier du chantier et bim le reste arrive en fonction de ce qui a été défini dans le tableau.
2 champs personnalisés bien placés
Eh bien c’est pas si compliqué, on va créer 2 champs personnalisés de type texte pour commencer :
- N° de dossier
- Nom du dossier
Et surtout on ne les met pas en obligatoire ! Je t’explique après…
Voici la tête du tableau dans la page du wiki :

Et c’est tout ?
…Pour le moment.
On a maintenant l’interface avec les 2 petits champs :

Bon passons sur l’apparence de Redmine, j’ai laissé les 2 zones de texte en non obligatoire, et pas en liste, car je voulais que l’on puisse ajouter des anomalies sur d’autres projets, qui pouvaient ne pas être dans la liste. Chez mon client, on avait fait un petit lien pour ouvrir la page du wiki dans une popup, là, j’avoue, je ne l’ai pas fait :(
Bon maintenant, il manque juste un peu de magie, pour que quand on crée le ticket, le nom du dossier s’ajoute automatiquement, et les observateurs aussi, et pour ça, pour faire de la magie, il nous faut un peu de :
Custom workflow !
Avec ce petit plugin, on va créer un petit script côté serveur en Ruby, pour récupérer les éléments dans la page du wiki, ajouter le numéro de dossier, et ajouter les observateurs comme il faut.

Encore un plugin recensé dans cette merveilleuse liste de plugins …
bah quoi ??
Pour cela, on crée un nouveau workflow que l’on va activer sur notre projet, et on ajoute le script suivant dans la partie Saving observable objects :
On commence par définir les variables globales dont le script a besoin :
CS_CODE_FOLDER = 1
CS_NAME_FOLDER = 2
PAGE_NAME_FOLDERS = "Liste-dossiers"
- L’identifiant du champ personnalisé pour le code du dossier
- L’identifiant du champ personnalisé pour le nom du dossier
- Le nom de la page contenant la liste des dossiers
Ensuite, on découpe en petites fonctions pour éviter les grosses méthodes difficiles à maintenir.
Et commence, par une petite fonction qui va nous récupérer le tableau dans la page wiki sous forme de tableau en ruby :
def wiki_table_rows(page)
page.content.text
.each_line
.map(&:strip)
.select { |line| line.start_with?("|") }
.grep_v(/^\|[-\s|]+\|$/)
end
La magie du ruby, on va demander à nous fournir un hash à partir des en-têtes du tableau pour chaque ligne :
def parse_row(headers, row)
values = row.split("|").map(&:strip).reject(&:empty?)
hash = headers.zip(values).to_h
hash["Observateurs"] = hash["Observateurs"]
.split(",")
.map(&:strip)
.reject(&:empty?)
hash
end
Noter l’utilisation du petit zip qui va concaténer les en-têtes et la liste des valeurs dans un hash :
{
"N° Dossier" => "CH-2023-018",
"Nom du dossier" => "Rénovation immeuble rue Nationale",
"Responsable" => "@jmartin@redmine.org ",
"Observateurs" => ["@cdubois@redmine.org", "@tleroy@redmine.org"],
}
On va définir une méthode pour nous retourner le hash du dossier en fonction de son code à l’aide de la méthode find du hash.
def folder_search_code(code)
page = WikiPage.find_by(title: PAGE_NAME_FOLDERS)
return unless page
rows = wiki_table_rows(page)
headers = rows.first.split("|").map(&:strip).reject(&:empty?)
affaires = rows[1..].map { |row| parse_row(headers, row) }
affaires.find{|affaire| affaire["N° Dossier"] == code}
end
On arrive, enfin à l’exécution de la partie qui nous intéresse, oui je sais c’est long, mais il faut bien préparer l’histoire quand même.
Dans mon custom workflow je réagis si le champ “Numéro du dossier” a été saisi @issue.custom_field_value(CS_CODE_FOLDER) et
si le ticket a déjà été enregistré !@issue.new_record?, histoire d’avoir déjà une première validation et de ne pas polluer tout le monde.
Ensuite, on récupère les informations du dossier, et on ajoute son nom et les observateurs, et voilà !
if @issue.custom_field_value(CS_CODE_FOLDER) && !@issue.new_record?
affaire = folder_search_code(@issue.custom_field_value(CS_CODE_FOLDER))
if affaire
@issue.custom_field_values = {
CS_NAME_FOLDER.to_s => affaire["Nom du dossier"]
}
folder_add_watcher(@issue, User.find_by(login: affaire["Responsable"].gsub(/^@/, "")))
affaire["Observateurs"].each do |watcher|
folder_add_watcher(@issue, User.find_by(login: watcher.gsub(/^@/, "")))
end
end
end
J’ai pas détaillé la fonction folder_add_watcher, mais elle ajoute un observateur d’un dossier à une demande :
def folder_add_watcher(issue, watcher)
@issue.add_watcher(watcher) unless issue.watched_by?(watcher)
end
Et voilà !

L’utilisateur crée le ticket, et quand on l’enregistre une nouvelle fois, le titre et les observateurs sont directement ajoutés au ticket. Ils sont notifiés, et grâce au plugin Redmine Extended Watchers, ils peuvent avoir accès au ticket.
Alors, c’est qu’un exemple, mais on pourrait :
- Réagir directement quand le ticket est créé et uniquement à ce moment-là, on retire juste le
!:issue.custom_field_value(CS_CODE_FOLDER) && @issue.new_record? - Ou sur le changement à un statut particulier, dans ce cas, on récupère l’identifiant du statut, et on vérifie que celui-ci a changé :
issue.custom_field_value(CS_CODE_FOLDER) && @issue.status_id_changed? && @issue.status_id == 4
Après c’est déjà pas mal pour un début :)
Et pour développer, comment on fait ?
Oui, on va pas dire que développer un petit bout de script dans un navigateur à retester ce n’est quand même pas le plus pratique, donc mon astuce, c’est d’utiliser rails runner.
On se crée un petit fichier workflow.rb, et on colle le petit workflow à développer entre les 2 !
@issue = Issue.find(2)
# BEFORE CUSTOM_WORKFLOW
# =================================================================
# <<<<<<< COLLER VOTRE WORKFLOW ICI >>>>>>>
# =================================================================
# AFTER CUSTOM_WORKFLOW
@issue.save!
Surtout, ne pas coller le @issue.savedans le custom workflow ensuite, ça va un peu tourner en rond dans la console … true story !
Et ensuite, pour tester on lance le script ruby :
rails runner workflow.rb
Bien sûr, byebug est ton ami.
Et c’est tout ?
Bah oui et non, mais bon après faut bien s’arrêter, et comme on dit :
La perfection est atteinte, non pas lorsqu’il n’y a plus rien à ajouter, mais lorsqu’il n’y a plus rien à retirer.
– Antoine de Saint-Exupéry
Et en écrivant l’article, je me disais que je pourrai proposer une version plus simple à base de ViewCustomize, et de tickets pour gérer la configuration. Dans un projet dédié, on crée une catégorie pour les dossiers à suivre. On crée un ticket qui contient le nom du dossier, et le champ numéro du dossier. Et via ViewCustomize, on crée une liste déroulante auto-complétée, qui va rechercher via API Redmine dans une liste de tickets …
Hum oui, bonne idée finalement, je vais le tester du coup !!!