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

Last updated at Posted at 2025-12-15

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

Redmine Advent Calendar 2025

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

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

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

image.png

この記事の背景

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

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

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

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

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

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

image.png

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

前回の記事の続きです。

指示5

<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>なし</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>なし</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>なし</td></tr></tbody></table>

n/aはなくなりましたが、リアクション数が2なので、リアクション名も2名が
表示される必要がありますが、「なし」になっています。

下図のように「リアクション名も2名が表示される必要がありますが、「なし」になっている」と指摘しました。
image.png

ご指摘ありがとうございます。リアクション数が2であるにもかかわらず「リアクション者名」が「なし」と表示されるのは、ご提示いただいたパッチのメソッド reaction_user_names が、リアクションデータが存在するにもかかわらず、空の文字列や空の配列など、Ruby/Railsで「データがない」と判断される値(blank?present?false になる値)を返している可能性が高いです。

特に、reaction_user_names が空の配列 [] を返した場合、前回のコードでは raw_user_names.present?false になり、最終的に 'なし' が採用されてしまいます。

ここでは、reaction_user_names が返した値をまず文字列に変換し、その結果が空だった場合に、reaction_count が0より大きいかどうかで表示を切り替える、より堅牢なロジックに修正します。

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

init.rb に記述するコードを以下のように修正してください。特に、reaction_users の値の処理を強化しました。

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

          # reaction_user_names の戻り値を直接取得
          raw_user_names = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil
          
          # raw_user_namesを文字列に変換するロジック
          reaction_users_str = if raw_user_names.respond_to?(:join)
            # 配列の場合: joinして文字列化
            raw_user_names.join(', ')
          elsif raw_user_names.present?
            # 文字列の場合: そのまま利用
            raw_user_names.to_s
          else
            # nilや空の場合: 空文字列にする
            ''
          end
          
          # 最終的な表示調整
          reaction_users_display = if reaction_users_str.present?
            reaction_users_str
          elsif reaction_count.to_i > 0
            # **修正ポイント**: カウントがあるのに名前がない場合
            '(ユーザー名が取得できません)' 
          else
            'なし'
          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

        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. issue.reaction_user_namesユーザー名の配列 (["User A", "User B"]) を返した場合、"User A, User B" と表示されます。
  2. issue.reaction_user_namesカンマ区切りの文字列 ("User A, User B") を返した場合、そのまま表示されます。
  3. issue.reaction_user_names空の配列 ([]) や空の文字列 ("") を返したにもかかわらず、reaction_count が2などの正の値を返した場合、(ユーザー名が取得できません) と表示されます。

これで、データが存在するにもかかわらず「なし」と表示される矛盾が解消されるはずです。お手数ですが、このコードで再度動作確認をお願いいたします。

指示6

表形式で表示されなくなりました。また、「ユーザー名が取得できません」とでます。

下図のように表形式ではない状態になりました。

image.png

ご報告ありがとうございます。画像を確認いたしました。

表形式で表示されなくなった原因は、前回の修正でタスク一覧を表示する処理を Proc.new で定義した際に、HTMLの文字列連結が意図通りに機能しなかったことにある可能性が高いです。

特に、html << ... でHTMLを連結する処理が、Proc.new の外側の content 変数に正しく結合されず、結果として生成されたHTMLタグが単なる文字列として表示されてしまっています。

また、「ユーザー名が取得できません」と表示されるのは、ロジックは正しく機能しているものの、やはりご提示の環境では issue.reaction_user_names が値を返していないことを示しています。

🧐 修正方針

  1. HTML文字列連結の修正: Proc.new を使った処理をやめ、元のコードのように直接 content 変数に文字列を連結することで、HTML構造が壊れるのを防ぎます。
  2. リアクション者名の表示ロジックの簡素化: reaction_user_names が期待通りに動かない環境では、これ以上ロジックを複雑化するのではなく、reaction_count があればユーザー名があるはずだという前提で、よりシンプルなメッセージを表示します。

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

元のコード構造に戻しつつ、リアクション情報の表示を追加します。

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_user_names の戻り値を直接取得
          raw_user_names = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil
          
          # ユーザー名表示ロジック
          reaction_users_display = if reaction_count.to_i > 0
            # カウントがある場合: ユーザー名を取得して整形
            if raw_user_names.respond_to?(:join) && raw_user_names.present?
              raw_user_names.join(', ')
            elsif raw_user_names.present?
              raw_user_names.to_s
            else
              # ユーザー名が取れないがカウントはある場合の代替メッセージ
              "#{reaction_count}名リアクションあり"
            end
          else
            'なし'
          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
        raw_user_names = issue.respond_to?(:reaction_user_names) ? issue.reaction_user_names : nil
        
        # ユーザー名表示ロジック
        reaction_users_display = if reaction_count.to_i > 0
          if raw_user_names.respond_to?(:join) && raw_user_names.present?
            raw_user_names.join(', ')
          elsif raw_user_names.present?
            raw_user_names.to_s
          else
            "#{reaction_count}名リアクションあり"
          end
        else
          'なし'
        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

変更点の要約

  1. HTML生成の修正: Proc.new を使用せず、元のコードと同様に content << ... を使ってHTML文字列を直接連結するように戻しました。これにより、表形式が正しくレンダリングされるはずです。
  2. リアクション者名の代替表示: reaction_count が0より大きく、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?