REDMINE JAPAN vol.4で学んだ「AIを部下のように使い倒せ」を実践してプラグインを開発してみた(4)
Redmine Advent Calendar 2025
今回の記事は下記の「Redmine Advent Calendar 2025」の12/16の記事として作成しました。
12/15はSusumu Yamasakiさんの「REDMINE JAPAN vol.4で学んだ「AIを部下のように使い倒せ」を実践してプラグインを開発してみた(3)」という記事でした。
急に寒くなり今年も残り数日です。今年もRedmine Advent Calendarの時期になりました。Advent Calendarに参加してRedmine界隈を盛り上げていけたらと思っています。
この記事の背景
前回の記事をご確認ください。
「リアクション数とリアクション者名」リストの開発
上記の背景をもとに下記の方法でRedmineの「リアクション数とリアクション者名」リストの開発を行った。
開発方法:Geminiに完全一任(自分は一切開発のために手を出さない)
実現方法:wikiマクロ({{project_members_tasks2(demo-project)}}だけで実現)
開発できた画面
プロジェクトに属しているユーザーとチケット一覧にリアクション数とリアクション者名を表示
実際にAIへ行った指示とその回答
前回の記事の続きです。
指示7
表敬式に戻りましたが、リアクション名でユーザーの名前になっていません。
ご報告ありがとうございます。表形式が復活し、代替メッセージが表示されるようになったことを確認しました。
新しい画像(image_5fc1bb.png)では、リアクション者名が「2名リアクションあり」と表示されており、これは前回の修正で設定した代替メッセージです。
これは、やはりご使用のRedmine環境では、提供されたパッチのメソッド reaction_user_names がユーザー名のリストを正常に返していないことを強く示唆しています。
🚨 根本的な原因の切り分け
提供いただいた情報に基づくと、以下の状況が考えられます。
-
reaction_countは、Redmineのデータベースから正しく件数を取得できている。 -
reaction_user_namesは、メソッドは存在するものの、**ユーザー名の文字列(または配列)**を取得できていない(空の値を返している)。
もし、あなたがこのプラグインを開発中であり、かつ reaction_user_names がユーザー名の配列を返すことが確実なのであれば、環境設定(パッチの適用状況や依存関係)を確認する必要があります。
しかし、もしパッチがどのように実装されているか確認できない場合、マクロ側でできることは限られます。
✅ 最終提案:ユーザー名取得の強制表示(デバッグ目的)
ユーザー名がどのように格納されているかを確認するために、reaction_user_names が何を返しているかをデバッグ目的でそのまま表示するようにしてみます。
もし reaction_user_names が配列を返すことが確認されているなら、以下の方法で配列を強制的に文字列に変換し、それが表示されるか確認します。
# ユーザー名の取得と強制変換
reaction_users_display = if issue.respond_to?(:reaction_user_names)
raw_user_names = issue.reaction_user_names
if raw_user_names.respond_to?(:join) # 配列かどうか
raw_user_names.join(', ')
elsif raw_user_names.present?
raw_user_names.to_s
else
# カウントがあるのに名前がない場合の代替メッセージ(デバッグ用)
reaction_count.to_i > 0 ? "リアクションあり(データ未取得)" : 'なし'
end
else
'N/A'
end
このデバッグロジックを組み込んだ、最後の修正版を提供します。もし raw_user_names が空でない文字列や配列を返していれば、このコードでユーザー名が表示されるはずです。
Redmine::WikiFormatting::Macros.register do
desc "Display task lists for all members of a project, including unassigned tasks with task numbers, and show issue reactions."
macro :project_members_tasks do |obj, args|
# プロジェクトの取得
project = Project.find_by_identifier(args.first || obj.project.identifier)
return "プロジェクトが見つかりません。" unless project
content = "<h2>#{project.name}のメンバーとタスク一覧 (リアクション情報付き)</h2>"
# メンバーごとのタスクを収集
members = project.members.sort_by { |m| m.user.name }
members.each do |member|
user = member.user
issues = project.issues.where(assigned_to_id: user.id).order(:due_date, :id)
content << "<h3>👤 #{user.name}のタスク</h3>"
if issues.any?
content << "<table class='list issues' style='width: 100%; border-collapse: collapse;'>"
content << "<thead><tr>"
content << "<th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th>"
content << "</tr></thead><tbody>"
issues.each_with_index do |issue, index|
days_info = if issue.due_date
days_left = (issue.due_date - Date.today).to_i
if days_left > 0
"期日まで #{days_left}日"
elsif days_left == 0
"今日が期日"
else
"<span style='color: red;'>#{days_left.abs}日 遅れ</span>"
end
else
"期日未設定"
end
# リアクション情報の取得
reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 0
# ユーザー名表示ロジック (デバッグ強化版)
reaction_users_display = 'なし'
if reaction_count.to_i > 0
if issue.respond_to?(:reaction_user_names)
raw_user_names = issue.reaction_user_names
if raw_user_names.respond_to?(:join) && raw_user_names.present?
# 配列で、かつデータがある場合
reaction_users_display = raw_user_names.join(', ')
elsif raw_user_names.present?
# 配列ではないが、文字列などとしてデータがある場合
reaction_users_display = raw_user_names.to_s
else
# カウントはあるがデータが空の場合
reaction_users_display = "#{reaction_count}名リアクションあり(ユーザーデータ取得失敗)"
end
else
# reaction_user_names メソッドがない場合
reaction_users_display = "#{reaction_count}名リアクションあり(メソッド未定義)"
end
end
# ヘルパーメソッドを直接呼び出す
issue_link = link_to("##{issue.id}", issue_path(issue))
subject_link = link_to(issue.subject, issue_path(issue))
content << "<tr>"
content << "<td>#{index + 1}</td>"
content << "<td>#{issue_link}</td>"
content << "<td>#{subject_link}</td>"
content << "<td>#{issue.start_date || 'N/A'}</td>"
content << "<td>#{issue.due_date || 'N/A'}</td>"
content << "<td>#{days_info}</td>"
content << "<td>#{issue.done_ratio}%</td>"
content << "<td>#{reaction_count}</td>"
content << "<td>#{reaction_users_display}</td>"
content << "</tr>"
end
content << "</tbody></table>"
else
content << "<p>タスクがありません。</p>"
end
content << "<hr>"
end
# 担当者が未指定のタスク (処理はメンバーごとのループとほぼ同じ)
unassigned_issues = project.issues.where(assigned_to_id: nil).order(:due_date, :id)
if unassigned_issues.any?
content << "<h3>❓ 担当者未指定のタスク</h3>"
content << "<table class='list issues' style='width: 100%; border-collapse: collapse;'>"
content << "<thead><tr>"
content << "<th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th>"
content << "</tr></thead><tbody>"
unassigned_issues.each_with_index do |issue, index|
days_info = if issue.due_date
days_left = (issue.due_date - Date.today).to_i
if days_left > 0
"期日まで #{days_left}日"
elsif days_left == 0
"今日が期日"
else
"<span style='color: red;'>#{days_left.abs}日 遅れ</span>"
end
else
"期日未設定"
end
# リアクション情報の取得
reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 0
# ユーザー名表示ロジック (デバッグ強化版)
reaction_users_display = 'なし'
if reaction_count.to_i > 0
if issue.respond_to?(:reaction_user_names)
raw_user_names = issue.reaction_user_names
if raw_user_names.respond_to?(:join) && raw_user_names.present?
reaction_users_display = raw_user_names.join(', ')
elsif raw_user_names.present?
reaction_users_display = raw_user_names.to_s
else
reaction_users_display = "#{reaction_count}名リアクションあり(ユーザーデータ取得失敗)"
end
else
reaction_users_display = "#{reaction_count}名リアクションあり(メソッド未定義)"
end
end
# ヘルパーメソッドを直接呼び出す
issue_link = link_to("##{issue.id}", issue_path(issue))
subject_link = link_to(issue.subject, issue_path(issue))
content << "<tr>"
content << "<td>#{index + 1}</td>"
content << "<td>#{issue_link}</td>"
content << "<td>#{subject_link}</td>"
content << "<td>#{issue.start_date || 'N/A'}</td>"
content << "<td>#{issue.due_date || 'N/A'}</td>"
content << "<td>#{days_info}</td>"
content << "<td>#{issue.done_ratio}%</td>"
content << "<td>#{reaction_count}</td>"
content << "<td>#{reaction_users_display}</td>"
content << "</tr>"
end
content << "</tbody></table>"
end
content.html_safe
end
end
このデバッグ強化版で実行していただき、表示がどのように変わるかご確認をお願いいたします。もし「ユーザーデータ取得失敗」というメッセージが表示された場合、マクロ側の問題ではなく、Redmineのパッチやプラグイン側でユーザー名を取得する処理が正しく行われていない可能性が高いと判断できます。
指示8
正しく動作しないのですが、開発のヒントになる可能性の情報を与えるので、参考にしてください。
# IssueQueryとIssueモデルにパッチを適用
require File.expand_path('lib/redmine_ticket_reactions_enhancer/issue_query_patch', __dir__)
require File.expand_path('lib/redmine_ticket_reactions_enhancer/issue_patch', __dir__) # IssueQuery#issuesのprepend
require File.expand_path('lib/redmine_ticket_reactions_enhancer/issue_model_patch', __dir__) # Issueモデルへのメソッド追加
end
module RedmineTicketReactionsEnhancer
# IssueQuery#issues の prepend のためのモジュール
module IssuePatch
extend ActiveSupport::Concern
# IssueQuery#issuesをprependでラップし、includesを適用
def issues(options={})
# 修正: 'reaction_users' はActiveRecordの関連付けではないため、
# 'reactions' のみを含める。これにより、Issue#reaction_user_names
# が呼ばれたときに、reactions テーブルへのアクセスがプリロードされる。
options[:include] = (options[:include] || []) + [:reactions]
options[:distinct] = true
# 既存のIssueQuery#issuesメソッドを呼び出す
super(options)
end
end
end
# IssueQueryにパッチを適用
unless IssueQuery.included_modules.include?(RedmineTicketReactionsEnhancer::IssuePatch)
# prepend を使用して IssueQuery#issues を上書き
IssueQuery.send(:prepend, RedmineTicketReactionsEnhancer::IssuePatch)
end
貴重な情報をご提供いただき、ありがとうございます。このパッチ情報(IssuePatch)は、マクロ内の問題の根本的な原因を特定し、修正する上で決定的なヒントとなります。
🧐 提供された情報からの洞察
ご提供いただいたパッチ情報から、以下の点が判明しました。
-
リアクション情報はプリロードされている:
これにより、チケット(
options[:include] = (options[:include] || []) + [:reactions]Issue)を取得する際に、関連するリアクションデータ(reactions)が事前にロードされていることがわかります。これはマクロのパフォーマンスにとって良いことです。 -
reaction_user_namesの実装の問題:
マクロが使用しているreaction_user_namesメソッドは、おそらくIssueモデルに追加されています(issue_model_patch.rb)。しかし、このメソッドが期待通りにユーザー名の文字列を返さず、空の値を返しているため、マクロで「リアクションあり」の代替メッセージが表示されていました。 -
マクロのコンテキスト:
ご提供のマクロはProject#issuesからチケットを取得しており、これはIssueQueryを経由していません。
🚨 マクロの問題点と修正点
元のマクロのコードでは、以下のクエリを使用してチケットを取得しています。
# ユーザーごとのタスクを取得
issues = project.issues.where(assigned_to_id: user.id)
# 担当者が未指定のタスク
unassigned_issues = project.issues.where(assigned_to_id: nil)
このクエリは、IssueQuery#issues を経由しないため、ご提供いただいた IssuePatch 内の options[:include] = ... + [:reactions] が実行されていません。
したがって、issue.reactions にアクセスするたびにデータベースへのクエリが発行されてしまい(N+1問題)、さらに Issue#reaction_user_names の処理が複雑な場合、期待通りの値が返されない、または実行環境によってはデータが空と見なされてしまう可能性があります。
✅ 最終修正後のWikiマクロ登録コード
この問題に対処するため、チケット取得時にマクロ内で明示的に :reactions を includes オプションに追加します。これにより、パッチが意図したデータプリロードと同じ効果を得て、reaction_user_names が正しく動作する可能性が高まります。
Redmine::WikiFormatting::Macros.register do
desc "Display task lists for all members of a project, including unassigned tasks with task numbers, and show issue reactions."
macro :project_members_tasks do |obj, args|
# プロジェクトの取得
project = Project.find_by_identifier(args.first || obj.project.identifier)
return "プロジェクトが見つかりません。" unless project
content = "<h2>#{project.name}のメンバーとタスク一覧 (リアクション情報付き)</h2>"
# メンバーごとのタスクを収集
members = project.members.sort_by { |m| m.user.name }
# ----------------------------------------------------
# ★ 修正ポイント: includes(:reactions) を明示的に追加
# ----------------------------------------------------
members.each do |member|
user = member.user
# ユーザーのタスクを取得。明示的に reactions をプリロード
issues = project.issues
.includes(:reactions) # リアクションデータをプリロード
.where(assigned_to_id: user.id)
.order(:due_date, :id)
content << "<h3>👤 #{user.name}のタスク</h3>"
if issues.any?
content << "<table class='list issues' style='width: 100%; border-collapse: collapse;'>"
content << "<thead><tr>"
content << "<th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th>"
content << "</tr></thead><tbody>"
issues.each_with_index do |issue, index|
days_info = if issue.due_date
days_left = (issue.due_date - Date.today).to_i
if days_left > 0
"期日まで #{days_left}日"
elsif days_left == 0
"今日が期日"
else
"<span style='color: red;'>#{days_left.abs}日 遅れ</span>"
end
else
"期日未設定"
end
# リアクション情報の取得
reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 0
# ユーザー名表示ロジック
reaction_users_display = 'なし'
if reaction_count.to_i > 0
if issue.respond_to?(:reaction_user_names)
raw_user_names = issue.reaction_user_names
if raw_user_names.respond_to?(:join) && raw_user_names.present?
# 配列で、かつデータがある場合
reaction_users_display = raw_user_names.join(', ')
elsif raw_user_names.present?
# 配列ではないが、文字列などとしてデータがある場合
reaction_users_display = raw_user_names.to_s
else
# カウントはあるがデータが空の場合 (この行が改善することを期待)
reaction_users_display = "#{reaction_count}名リアクションあり"
end
else
# reaction_user_names メソッドがない場合
reaction_users_display = "#{reaction_count}名リアクションあり(メソッド未定義)"
end
end
# ヘルパーメソッドを直接呼び出す
issue_link = link_to("##{issue.id}", issue_path(issue))
subject_link = link_to(issue.subject, issue_path(issue))
content << "<tr>"
content << "<td>#{index + 1}</td>"
content << "<td>#{issue_link}</td>"
content << "<td>#{subject_link}</td>"
content << "<td>#{issue.start_date || 'N/A'}</td>"
content << "<td>#{issue.due_date || 'N/A'}</td>"
content << "<td>#{days_info}</td>"
content << "<td>#{issue.done_ratio}%</td>"
content << "<td>#{reaction_count}</td>"
content << "<td>#{reaction_users_display}</td>"
content << "</tr>"
end
content << "</tbody></table>"
else
content << "<p>タスクがありません。</p>"
end
content << "<hr>"
end
# 担当者が未指定のタスク
unassigned_issues = project.issues
.includes(:reactions) # リアクションデータをプリロード
.where(assigned_to_id: nil)
.order(:due_date, :id)
if unassigned_issues.any?
content << "<h3>❓ 担当者未指定のタスク</h3>"
content << "<table class='list issues' style='width: 100%; border-collapse: collapse;'>"
content << "<thead><tr>"
content << "<th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th>"
content << "</tr></thead><tbody>"
unassigned_issues.each_with_index do |issue, index|
days_info = if issue.due_date
days_left = (issue.due_date - Date.today).to_i
if days_left > 0
"期日まで #{days_left}日"
elsif days_left == 0
"今日が期日"
else
"<span style='color: red;'>#{days_left.abs}日 遅れ</span>"
end
else
"期日未設定"
end
# リアクション情報の取得
reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 0
raw_user_names = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil
# ユーザー名表示ロジック
reaction_users_display = 'なし'
if reaction_count.to_i > 0
if issue.respond_to?(:reaction_user_names)
raw_user_names = issue.reaction_user_names
if raw_user_names.respond_to?(:join) && raw_user_names.present?
reaction_users_display = raw_user_names.join(', ')
elsif raw_user_names.present?
reaction_users_display = raw_user_names.to_s
else
reaction_users_display = "#{reaction_count}名リアクションあり"
end
else
reaction_users_display = "#{reaction_count}名リアクションあり(メソッド未定義)"
end
end
# ヘルパーメソッドを直接呼び出す
issue_link = link_to("##{issue.id}", issue_path(issue))
subject_link = link_to(issue.subject, issue_path(issue))
content << "<tr>"
content << "<td>#{index + 1}</td>"
content << "<td>#{issue_link}</td>"
content << "<td>#{subject_link}</td>"
content << "<td>#{issue.start_date || 'N/A'}</td>"
content << "<td>#{issue.due_date || 'N/A'}</td>"
content << "<td>#{days_info}</td>"
content << "<td>#{issue.done_ratio}%</td>"
content << "<td>#{reaction_count}</td>"
content << "<td>#{reaction_users_display}</td>"
content << "</tr>"
end
content << "</tbody></table>"
end
content.html_safe
end
end
次回に続く
なかなか正解にたどり着けないので、次回では、他の開発で使用したリアンクション者名取得のロジックをヒントとして提供をしました。
この記事の作成者の紹介
山崎進
- Redmine、Jquery、JavaScript,Rails、Ruby、SQL、VBA、RPAの開発を行なっています。
- 自動化、業務の効率化に高い関心があります。
- 下記の媒体で情報を発信しています。Redmineのプラグインの開発などに対応が可能ですので、お気軽にご連絡ください。
* Qiita:https://qiita.com/ankosoft
* Twitter:https://twitter.com/yamasaki24
* Redmine Advent Calendarで記事投稿
* redmine.tokyoで講演多数
* Redmine Japan Vol.1 前夜祭、Redmine Japan Vol.3で講演
* https://ankosoft.co.jp/blog/
* https://technology.ankosoft.co.jp/
関連記事(2024年のRedmine Advent Calendarの記事です。)
* Redmine wikiマクロを作成する方法2(完了チケットの割合をグラフで表示)
* Redmine wikiマクロを作成する方法3(ステータスごとのチケットの割合をグラフで表示)
* Redmine wikiマクロを作成する方法4(ステータスごとのチケットの割合を一つのグラフで表示)
* Redmine wikiマクロを作成する方法5(特定のユーザーのチケット状態の確認)
* Redmine wikiマクロを作成する方法6(プロジェクトに所属する全てのユーザーのチケット状態の確認)
関連記事(2023年のRedmine Advent Calendarの記事です。)
* JqueryでRedmineのメニューにアイコンを入れる方法
* JqueryでRedmineのメニューを閉じたり開いたりする方法
* JqueryでRedmineの「活動ページ」をもっと便利に(タイトルを開閉したり、曜日を入れたり)
* JqueryでRedmineの「活動ページ」をもっと便利に②(フィルタリング機能と移動機能)
* JqueryでRedmineの「wikiページ」をもっと便利に(フィルタリング機能と移動機能)
* JqueryでRedmineの「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)


