0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

REDMINE JAPAN vol.4で学んだ「AIを部下のように使い倒せ」を実践してプラグインを開発してみた(1)

Last updated at Posted at 2025-12-15

REDMINE JAPAN vol.4で学んだ「AIを部下のように使い倒せ」を実践してプラグインを開発してみた(1)

Redmine Advent Calendar 2025

今回の記事は下記の「Redmine Advent Calendar 2025」の12/13の記事として作成しました。

12/12はSusumu Yamasakiさんの「Redmineとプラグインのライセンスに関してGeminiに聞いてみた(3)」という記事でした。

急に寒くなり今年も残り数日です。今年もRedmine Advent Calendarの時期になりました。Advent Calendarに参加してRedmine界隈を盛り上げていけたらと思っています。

image.png

この記事の背景

REDMINE JAPAN vol.4で石井 大地 氏の「AI革命の現在地:変貌する業務/人材マネジメント」という講演は非常に印象深く、自分の中ではかなり衝撃的な内容であった。
初めはただ単に聞いていたが、途中から慌ててメモをしたのだが、当時下記のようなメモを残した。

欲望とマネージメント 
やるべき事を指示しろ

エアいはマネージメント力が必要

人に聞かないから早い

コミュニケーションをしないので効率化

部下と専門家がAI

プロジェクト管理に時間がかかる
時間の調整に時間がかかる

自分とAIで仕事は終わらせよ

AIに命令せよ
指示をせよ
自分で働くな

せっかくAIがあるが、人間が資料や文書などを作成してAIに添削や修正を依頼をしてはいけないというのである。人間はあくまで指示のみを与え、正しくない回答をした場合、そこで人間が手直しをしないで、誤りを指摘して最後までAIに目標を完成させるようにせよ。という内容であった。
AIには強気で指示をして、命令をしてこそ価値があるのだと。

言うならばAIを奴隷のようにとことん使い倒し、一切の妥協や忖度も必要がないというのである。
自分の仕事をサポートさせるために自分とAIが相互に協働をするのではなく、あくまで指示出しと成果物の承認だけを行い、実作業には人間は手を出してはいけないというのである。

文章だけ読むと、普通にAIを使うのと何が違うのか?という疑問点も生じるかもしれないが、生で講演を聞いたせいか、非常に腑に落ちた説明だった。

AIに仕事を任せると一度ではうまくいかないことは多々ある。特にプログラムの開発をAIに委ねると簡単な開発は満足の行く回答をしても複雑な開発はまだまだ厳しいという印象である。何回指示出しをしてもエラーの連続で、せっかくエラーが直ったのにも関わらず、再度指示出しをすると解決したエラーが再度発生することもある。
そのため、ある程度試してうまく行かないと途中で諦めたり、自分の手を動かす方が早いので自分で開発をしてしまう傾向があった。
しかし、いくら自分が手を動かくほうが早いとしても自分で開発をしてはいけないというのだ。なぜならば自分の役割、人間の役割は作業者の役割ではなく指示者、監督、PMの役割に徹してこそ、AIを使いこなしていると言えるからだ。

そのような意味で「AIを奴隷のように使いこなせ!」という認識、意識を持って働く必要があり、この認識の転換こそが講演で受けた衝撃であった。

「リアクション数とリアクション者名」リストの開発

上記の背景をもとに下記の方法でRedmineの「リアクション数とリアクション者名」リストの開発を行った。

開発方法:Geminiに完全一任(自分は一切開発のために手を出さない)

実現方法:wikiマクロ({{project_members_tasks2(demo-project)}}だけで実現)

開発できた画面
プロジェクトに属しているユーザーとチケット一覧にリアクション数とリアクション者名を表示

image.png

実際にAIへ行った指示とその回答

これからはGeminiに何を指示し、それに対してどのような回答があり、どのようなエラーがあったのかを示します。はっきり言って試行錯誤の連続であり、簡単には正解が出ませんでした。あまりにもゴールにたどり着けないので途中心が折れそうになりましたが、部下であるAIを最後まで信じて指示を行いました。そのため「AIに愛はいらない」という認識で指示をしましたが、最後まで信じて指示したということは「AIの能力を認めて最後まで信じ切る」という意味では「AIに対する愛情」が必要だったのかも?という気にもなります。

何はともあれ、試行錯誤の歴史をこの記事で再現したいと思います。

指示1

すでにRedmineのwikiマクロとして下記のマクロがあります。



Redmine::WikiFormatting::Macros.register do

desc "Display task lists for all members of a project, including unassigned tasks with task numbers"

macro :project_members_tasks do |obj, args|

# プロジェクトの取得

project = Project.find_by_identifier(args.first || obj.project.identifier)

return "プロジェクトが見つかりません。" unless project


# プロジェクトメンバーの取得

members = project.members

content = "#{project.name}のメンバーとタスク一覧<br>"


# メンバーごとのタスクを収集

members.each do |member|

user = member.user

content << "<h3>#{user.name}のタスク</h3>"


# ユーザーのタスクを取得

issues = project.issues.where(assigned_to_id: user.id)

if issues.any?

content << "<table border='1' style='border-collapse: collapse; width: 100%;'>"

content << "<tr><th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th></tr>"


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

"<font color='red'>#{days_left.abs}日 遅れ</font>"

end

else

"期日未設定"

end


content << "<tr>"

content << "<td>#{index + 1}</td>"

content << "<td>#{link_to("##{issue.id}", issue_path(issue))}</td>"

content << "<td>#{issue.subject}</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 << "</tr>"

end


content << "</table>"

else

content << "タスクがありません。"

end

end


# 担当者が未指定のタスク

unassigned_issues = project.issues.where(assigned_to_id: nil)

if unassigned_issues.any?

content << "<h3>担当者未指定</h3>"

content << "<table border='1' style='border-collapse: collapse; width: 100%;'>"

content << "<tr><th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th></tr>"


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

"<font color='red'>#{days_left.abs}日 遅れ</font>"

end

else

"期日未設定"

end


content << "<tr>"

content << "<td>#{index + 1}</td>"

content << "<td>#{link_to("##{issue.id}", issue_path(issue))}</td>"

content << "<td>#{issue.subject}</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 << "</tr>"

end


content << "</table>"

end


content.html_safe

end

end





これを実行すると担当者ごとにチケットごとに

|No|#|題名|開始日|期日|状態|進捗率|

という情報が表示されます。



これに

https://www.redmine.org/attachments/34281の

lib/redmine/reaction.rbの

def reaction_user_names

def reaction_count

を利用して

|No|#|題名|開始日|期日|状態|進捗率|リアクション数|リアクション者名|

となるようにwikiマクロを作成してください。



wikiマクロのためのプラグインはすでにできているので、「init.rb」ファイルを編集して、Wikiマクロを登録する部分だけ提示してください。



不明な点があれば質問してください。

RedmineのWikiマクロの拡張に関するご依頼、承知いたしました。

ご提示いただいた既存のマクロをベースに、チケット(Issue)に設定されたリアクション数リアクションしたユーザー名を表示するように変更します。

Redmineのチケットにリアクション機能を追加するパッチ(#34281)の reaction_user_namesreaction_count メソッドを利用することを前提とします。これらのメソッドは、チケット(Issue)オブジェクトが応答できることが必要ですが、標準のRedmineには存在しないため、パッチやプラグインで拡張されているものと仮定してコードを作成します。

💡 Wikiマクロ登録コード (init.rb に記述)

init.rb ファイルに記述する、新しいWikiマクロを登録するコードは以下の通りです。マクロ名は既存と同じ :project_members_tasks とします。

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|
    # 必要なRedmineヘルパーの読み込み
    include ActionView::Helpers::UrlHelper
    include Redmine::Core::UrlHelper

    # プロジェクトの取得
    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

          # リアクション情報の取得
          # issueが reaction_count メソッドに応答できるか確認 (パッチ適用を前提)
          reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 'N/A'
          # issueが reaction_user_names メソッドに応答できるか確認 (パッチ適用を前提)
          reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : 'N/A'

          content << "<tr>"
          content << "<td>#{index + 1}</td>"
          content << "<td>#{link_to("##{issue.id}", issue_path(issue))}</td>"
          content << "<td>#{link_to(issue.subject, issue_path(issue))}</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}</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 : 'N/A'
        reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : 'N/A'

        content << "<tr>"
        content << "<td>#{index + 1}</td>"
        content << "<td>#{link_to("##{issue.id}", issue_path(issue))}</td>"
        content << "<td>#{link_to(issue.subject, issue_path(issue))}</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}</td>"
        content << "</tr>"
      end

      content << "</tbody></table>"
    end

    content.html_safe
  end
end

変更点の概要

  1. 必要なヘルパーのインクルード: link_toissue_path を使用するために、マクロの冒頭で以下のヘルパーをインクルードしました。
    include ActionView::Helpers::UrlHelper
    include Redmine::Core::UrlHelper
    
  2. リアクション情報の取得: 各チケットのループ内で、以下の処理を追加しました。
    # issueが reaction_count メソッドに応答できるか確認 (パッチ適用を前提)
    reaction_count = issue.respond_to?(:reaction_count) ? issue.reaction_count : 'N/A'
    # issueが reaction_user_names メソッドに応答できるか確認 (パッチ適用を前提)
    reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : 'N/A'
    
    • issue.respond_to?(:method_name) を使用することで、もしパッチが適用されていないRedmine環境でこのコードを実行してもエラーにならないように配慮しています。その場合、値は 'N/A' になります。
  3. テーブルヘッダーとデータの追加:
    • テーブルヘッダー (<tr><th>...</th></tr>) に <th>リアクション数</th><th>リアクション者名</th> を追加しました。
    • テーブルの行データ (<tr><td>...</td></tr>) の末尾に <td>#{reaction_count}</td><td>#{reaction_users}</td> を追加しました。

これで、ご要望の通り、担当者別および未指定のタスク一覧にリアクション情報が表示されるようになります。


他にRedmineのマクロやプラグインに関するご質問はありますか?

指示2

マクロ project_members_tasks の実行中にエラーが発生しました (undefined method `include' for #<ActionView::Base:0x0000000002de38>)

ご提示いただいたエラーメッセージ、

マクロ project_members_tasks の実行中にエラーが発生しました (undefined method `include' for #ActionView::Base:0x0000000002de38)

これは、前回私が提供したコードの以下の部分が原因で発生しています。

    # 必要なRedmineヘルパーの読み込み
    include ActionView::Helpers::UrlHelper
    include Redmine::Core::UrlHelper

Wikiマクロ(macro :project_members_tasks do |obj, args| ... end のブロック内)は、Redmineの内部でレンダリングコンテキスト(ActionView::Base のインスタンスなど)のスコープ内で実行されますが、ローカルに include を呼び出してモジュールを組み込むことはできません

このエラーを回避しつつ、link_toissue_path などのヘルパーメソッドを使用するには、ヘルパーを呼び出すための特殊なオブジェクトを利用するか、Redmineがマクロに渡すオブジェクト(obj)を通じてヘルパーを呼び出す必要があります。


✅ 修正後のWikiマクロ登録コード

ヘルパーメソッドを正しく呼び出すために、ActionController::Base.helpers を利用するか、ApplicationController に含まれるメソッドを obj 経由で呼び出すのが一般的です。

今回は、Redmineのマクロコンテキストで安全に実行できる方法でヘルパーメソッドを呼び出すように修正します。

init.rb に記述するコードを以下のように修正してください。

修正のポイント

  1. include の行を削除しました。
  2. link_toissue_path などのヘルパーメソッドを呼び出す際に、Redmine::WikiFormatting::Macros.helper というヘルパーへのアクセス手段を使用するように変更します。
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|
    # Redmineのヘルパーメソッドにアクセスするためのオブジェクトを取得
    helper = Redmine::WikiFormatting::Macros.helper

    # プロジェクトの取得
    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 : 'N/A'
          reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : 'N/A'

          # ヘルパーメソッドの呼び出しを helper.method_name(...) に変更
          issue_link = helper.link_to("##{issue.id}", helper.issue_path(issue))
          subject_link = helper.link_to(issue.subject, helper.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}</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 : 'N/A'
        reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : 'N/A'

        # ヘルパーメソッドの呼び出しを helper.method_name(...) に変更
        issue_link = helper.link_to("##{issue.id}", helper.issue_path(issue))
        subject_link = helper.link_to(issue.subject, helper.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}</td>"
        content << "</tr>"
      end

      content << "</tbody></table>"
    end

    content.html_safe
  end
end

この修正により、include エラーが解消され、正しくリンクが生成されるはずです。お手数ですが、このコードで再度お試しください。

他にご不明な点や、この後の作業で何かお手伝いできることがあればお知らせください。


次回に続く


この記事の作成者の紹介


山崎進

  • 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マクロを作成する方法1

* 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の「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)

 * RedmineのパスワードやログインIDを忘れた時の復旧方法

 * 「rails console」を使ってRedmine上のスケジュールを一括変更する方法

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?