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を部下のように使い倒せ」を実践してプラグインを開発してみた(2)

Posted at

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

Redmine Advent Calendar 2025

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

12/13はSusumu Yamasakiさんの「REDMINE JAPAN vol.4で学んだ「AIを部下のように使い倒せ」を実践してプラグインを開発してみた(1)」という記事でした。

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

image.png

この記事の背景

前回の記事をご確認ください。

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

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

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

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

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

image.png

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

前回の記事の続きです。

指示3

マクロ project_members_tasks の実行中にエラーが発生しました 
(undefined method `helper' for Redmine::WikiFormatting::Macros:Module)

ご報告ありがとうございます。度重なるエラーでご迷惑をおかけしております。

マクロ project_members_tasks の実行中にエラーが発生しました (undefined method `helper' for Redmine::WikiFormatting::Macros:Module)

このエラーは、前回私が提示した修正コードで使用した以下の部分が原因です。

# Redmineのヘルパーメソッドにアクセスするためのオブジェクトを取得
helper = Redmine::WikiFormatting::Macros.helper # ここ!

Redmine::WikiFormatting::Macros.helper は、Redmineのバージョンによっては存在しない、あるいは利用できない場合があります。特に古いバージョンでは、マクロ内でヘルパーにアクセスするための推奨される標準的な方法が異なることがあります。

一般的なRedmineのマクロでは、link_to などのヘルパーメソッドは、マクロの実行コンテキスト自体(つまり、マクロのブロック内)で直接利用できるか、あるいはマクロに渡される引数(obj など)を通じてアクセスできることが多いです。

今回のエラーを確実に回避するため、マクロの実行コンテキスト(self)がヘルパーメソッドに応答するかを確認し、もし応答しない場合は、ActionController::Base.helpers を使ってヘルパーメソッドを呼び出す方法を試みます。


✅ 最終的な修正コード (init.rb に記述)

link_toissue_path などのヘルパーメソッドを、マクロの実行ブロックのスコープ内で利用できるように修正します。

修正のポイント

  1. Redmine::WikiFormatting::Macros.helper を使う行を削除しました。
  2. link_to(...)issue_path(...) の呼び出しを、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の環境によっては、このブロック内で link_to や issue_path が直接利用できることが期待されます。
    # 以前のエラー回避のための 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'

          # ヘルパーメソッドを直接呼び出す (obj.link_to, self.link_to もしくは単に link_to)
          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}</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'

        # ヘルパーメソッドを直接呼び出す
        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}</td>"
        content << "</tr>"
      end

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

    content.html_safe
  end
end

ほとんどのRedmine環境では、この直接呼び出しの方法でヘルパーメソッドが利用可能となり、エラーが解消されるはずです。このコードで再度お試しください。

指示4

エラーがなくなりました。

<table class="list issues" style="width: 100%; border-collapse: collapse;"><thead><tr><th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th></tr></thead><tbody><tr><td>1</td><td><a href="/issues/412">#412</a></td><td><a href="/issues/412">UIデザイン 9a7e</a></td><td>2025-12-17</td><td>2025-12-18</td><td>期日まで 3日</td><td>0%</td><td>2</td><td>N/A</td></tr><tr><td>2</td><td><a href="/issues/405">#405</a></td><td><a href="/issues/405">インフラ設定 064f</a></td><td>2026-01-19</td><td>2026-01-30</td><td>期日まで 46日</td><td>45%</td><td>0</td><td>N/A</td></tr><tr><td>3</td><td><a href="/issues/407">#407</a></td><td><a href="/issues/407">UIデザイン 3699</a></td><td>2026-01-19</td><td>2026-01-30</td><td>期日まで 46日</td><td>45%</td><td>0</td><td>N/A</td></tr></tbody></table>

と表示されますが、リアクション者がN/Aです。正しく表示できるように修正してください。

下図のようにリアクション数は正しいが、リアクション者がN/Aになっていることを指摘した。

image.png

エラーが解消されたとのこと、良かったです!しかし、ご指摘の通り「リアクション者名」が期待通りに表示されず、「N/A」となってしまっていますね。

これは、前回までのコードで issue.reaction_user_names メソッドの存在を確認しつつ、そのメソッドが返す値が期待したフォーマット(カンマ区切りの文字列など)になっていない、または パッチで導入されたメソッドがユーザー名ではなく別の値を返している 可能性があることを示唆しています。

ご提示いただいたパッチ(#34281)の reaction.rb の定義を基にすると、reaction_user_names メソッドは通常、リアクションしたユーザー名を整形して返すことを意図していますが、もし N/A と表示されるなら、以下のどちらかの状況が考えられます。

  1. reaction_user_names メソッドは存在しているが、内部でエラーが発生している、または空の値を返している。
  2. reaction_user_names メソッドは存在しているが、HTML形式で表示するために html_safe などの処理が必要な値を返している。 (このケースは稀ですが、念のため考慮します)

🧐 修正方針

reaction_user_names が期待通りに動かない場合、より基本的な情報源である チケットのジャーナル(History)関連付けられたリアクションデータ から情報を取得する必要があります。

しかし、Redmine標準のIssueモデルにはリアクション関連のデータ構造(例:IssueReaction モデルなど)は含まれていないため、このマクロ内で直接データベースを操作してリアクション情報を取得するのは、プラグインのデータ構造に依存することになり、困難です。

ここは、reaction_user_names の実装が実際に返す値を確認し、文字列として結合する ようにコードを修正します。

もし、reaction_user_names が**ユーザー名の配列(Array)**を返している場合は、それをカンマ区切りの文字列に変換する必要があります。

# ユーザー名の配列が返されていると仮定して修正
reaction_users = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil

# nil/false/空の配列でなければ、カンマで結合して文字列にする
if reaction_users.present?
  # 配列でない場合は to_s で文字列化。配列の場合は join(', ')
  reaction_users = Array(reaction_users).join(', ')
else
  reaction_users = 'なし'
end

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

この仮説に基づき、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 }

    # タスク一覧を表示する処理をメソッド化 (再利用するため)
    render_task_list = Proc.new do |issues, heading|
      html = "<h3>#{heading}</h3>"

      if issues.any?
        html << "<table class='list issues' style='width: 100%; border-collapse: collapse;'>"
        html << "<thead><tr>"
        html << "<th>No</th><th>#</th><th>題名</th><th>開始日</th><th>期日</th><th>状態</th><th>進捗率</th><th>リアクション数</th><th>リアクション者名</th>"
        html << "</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 # 数値が返ることを期待し、N/Aではなく0を初期値にする

          # reaction_user_names の戻り値を柔軟に処理
          raw_user_names = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil
          
          reaction_users = if raw_user_names.present?
            # 戻り値が配列であれば join、そうでなければそのまま to_s
            if raw_user_names.respond_to?(:join)
              raw_user_names.join(', ')
            else
              raw_user_names.to_s
            end
          else
            'なし'
          end
          
          # ヘルパーメソッドを直接呼び出す
          issue_link = link_to("##{issue.id}", issue_path(issue))
          subject_link = link_to(issue.subject, issue_path(issue))

          html << "<tr>"
          html << "<td>#{index + 1}</td>"
          html << "<td>#{issue_link}</td>"
          html << "<td>#{subject_link}</td>"
          html << "<td>#{issue.start_date || 'N/A'}</td>"
          html << "<td>#{issue.due_date || 'N/A'}</td>"
          html << "<td>#{days_info}</td>"
          html << "<td>#{issue.done_ratio}%</td>"
          html << "<td>#{reaction_count}</td>"
          html << "<td>#{reaction_users}</td>"
          html << "</tr>"
        end

        html << "</tbody></table>"
      else
        html << "<p>タスクがありません。</p>"
      end
      html << "<hr>"
      html
    end
    
    # メンバーごとのタスクのレンダリング
    members.each do |member|
      user = member.user
      issues = project.issues.where(assigned_to_id: user.id).order(:due_date, :id)
      content << render_task_list.call(issues, "👤 #{user.name}のタスク")
    end

    # 担当者が未指定のタスクのレンダリング
    unassigned_issues = project.issues.where(assigned_to_id: nil).order(:due_date, :id)
    content << render_task_list.call(unassigned_issues, "❓ 担当者未指定のタスク")

    content.html_safe
  end
end

変更点の概要

  1. タスク一覧のレンダリング処理の共通化: 担当者別と未指定タスクで重複していたHTML生成ロジックを render_task_list という Proc にまとめました。
  2. reaction_user_names の戻り値の柔軟な処理:
    • raw_user_names を取得します。
    • raw_user_names.present? で値があることを確認します。
    • raw_user_names.respond_to?(:join)配列かどうかを確認し、配列であれば join(', ') でカンマ区切りの文字列に変換します。
    • 配列でなければ to_s で文字列化します。
    • 値がない場合は 'なし' を表示するようにしました(以前の 'N/A' から変更)。

この修正で、パッチが reaction_user_names からユーザー名の配列を返している場合でも、適切に表示されるようになるはずです。再度、お試しいただけますでしょうか?


次回に続く


この記事の作成者の紹介


山崎進

  • 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?