Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

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

Posted at

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

Redmine Advent Calendar 2025

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

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

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

image.png

この記事の背景

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

前回作成した「リアクション数とリアクション者名」リストの改善

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

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

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

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

1)リアクションをした最新の日付順にリアクション者名を表示

image.png

2)リアクションをした日時をツールチップで表示

image.png

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

改修指示1

まずは設計を依頼しました。

前回作ってもらった「project_members_tasks」を下記のように
修正をしたいのですが、対応は可能でしょうか?
すぐにコードを提示する必要はありません。
実現可能であるならば、実現するためにどのような方針で対応をするのか、
設計書を作成してください。
私が設計書を確認をして問題がなければコードの提示をお願いする予定です。

|リアクション者名|
|佐藤 太郎, 林星 菜|

現在上記のように表示されますが、

1)リアクション者名でリアクションをした日付である
reactionsのcreated_at(DATETIME型)の降順でソートして表示

2)「佐藤 太郎」などのリアクション名にカーソルを近づけると
ツールチップで上記のcreated_atの値が表示

ご要望ありがとうございます。前回作成したWikiマクロ project_members_tasks の「リアクション者名」の表示について、ソート順の変更ツールチップによるリアクション日時の表示を追加する件、承知いたしました。

これらの機能追加は実現可能です。

まずは、実現に向けた**設計書(対応方針)**を作成します。内容をご確認いただき、問題なければ次のステップ(コードの提示)に進ませていただきます。


📑 修正設計書:project_members_tasks マクロ機能追加

1. 🎯 目的

既存のWikiマクロ project_members_tasks において、以下の機能を追加し、リアクション情報の視認性と詳細度を向上させる。

  1. 「リアクション者名」の表示順を、最新のリアクション日時(reactions.created_at)が一番最初になるように降順でソートする。
  2. 各リアクション者名にマウスカーソルを合わせた際、そのリアクションがなされた日時(reactions.created_at)をツールチップで表示する。

2. 📝 対応方針と技術要素

この修正は、主にIssueオブジェクトからリアクション情報を取得し、HTML文字列を組み立てる render_task_list Proc の内部処理を変更することで実現します。

No. 要件 方針(処理の場所) 技術要素
1. リアクション者名の降順ソート render_task_listraw_user_names を処理するブロック Ruby/ActiveRecord: 関連付けられた reactionscreated_at の降順でソートし、その結果からユーザー名と日時を取得する。
2. ツールチップ表示 render_task_list:HTML生成部分 HTML/CSS: ユーザー名を表示するHTMLタグに、日時情報を格納するための属性(例: title 属性)を追加する。

3. ⚙️ 詳細設計

3.1. リアクションデータの取得とソート

現在の問題点: 前回修正版では、issue.reaction_user_names または手動で取得したユーザー名の配列を join(', ') しているため、日時の情報は失われており、ソート処理も含まれていません。

修正後の動作:

  1. Issueに関連付けられた全リアクション(issue.reactions)を取得します。
  2. このリアクションリストを created_at の降順でソートします。
  3. ソートされたリアクションリストから、リアクションを行ったユーザー名と、そのリアクションの発生日時のペア(例:{ name: '佐藤 太郎', time: '2025-12-16 10:00:00' })を作成します。
  4. このソート済みリストを使用し、表示用の「リアクション者名」のカンマ区切り文字列と、ツールチップ用の情報を生成します。

3.2. ツールチップ(title 属性)の実装

HTMLでのツールチップ表示は、最も簡単な方法として<a>タグや<span>タグの**title属性**を使用します。

実装手順:

  1. ステップ 3.1 で作成したソート済みリストを反復処理します。
  2. 各リアクション者名(例: '佐藤 太郎')に対して、以下の形式のHTMLを組み立てます。
<span title="[created_atの値]">リアクション者名</span>

  1. すべてのユーザーに対してこの<span>タグを生成した後、全体をカンマとスペースで結合します(例:<span title="2025/12/16">佐藤 太郎</span>, <span title="2025/12/15">林星 菜</span>)。
  • この際、HTMLの表示にはカンマとスペースを含める必要があります。

4. 変更が影響する主なコードブロック

以下の処理が大きく変更されます。

  • render_task_list 内の「リアクション情報の取得」から「html << ...」までのブロック。
  • 特に、raw_user_names の取得部分を、ユーザー名と日時を含む構造化されたデータ(例:配列)に置き換えます。

上記が、ご要望を実現するための設計方針となります。

この方針で問題ございませんでしょうか?ご確認いただけましたら、具体的なRubyコードの提示に進ませていただきます。

改修指示2

この方針で問題ありません。具体的なRubyコードの提示をお願いします。

ご確認いただきありがとうございます。設計方針に基づき、リアクション者名のソートおよびツールチップ表示機能を追加した project_members_tasks マクロの修正コードを提示します。

🛠️ 修正されたWikiマクロコード (project_members_tasks)

主に、render_task_list の内部でリアクション情報の取得とHTML生成を行う部分を大幅に修正し、リアクション日時によるソートと、title 属性を使用したツールチップを実装しました。

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 with detailed information."
  macro :project_members_tasks do |obj, args|
    # ヘルパーメソッドの利用を有効化
    extend ActionView::Helpers::TagHelper
    extend ActionView::Helpers::UrlHelper
    extend Redmine::I18n

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

    # Issueを取得する共通処理を定義
    # NOTE: リアクション情報を取得するために、:reactions を明示的に includes する
    fetch_issues = Proc.new do |assignee_id|
      # Reaction に user が含まれていることを前提とする
      project.issues
             .where(assigned_to_id: assignee_id)
             .includes(:reactions => :user) 
             .order(:due_date, :id)
    end


    # タスク一覧を表示する処理をメソッド化 (再利用するため)
    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

          # リアクション情報の取得とソート、HTML生成
          reaction_count = 0
          reaction_users = 'なし'

          if issue.respond_to?(:reactions) && issue.reactions.any?
            # 1. リアクション日時 (created_at) の降順でソート
            sorted_reactions = issue.reactions.sort_by { |r| r.created_at || DateTime.new(1970) }.reverse
            
            # 2. リアクション者ごとのリストを作成 (ユーザー名と最新日時)
            user_reactions = {}
            sorted_reactions.each do |reaction|
              user = reaction.user
              # ユーザーが存在し、まだ記録されていない、またはより新しいリアクションの場合のみ記録
              if user && (!user_reactions[user.id] || reaction.created_at > user_reactions[user.id][:created_at])
                user_reactions[user.id] = {
                  name: user.name,
                  created_at: reaction.created_at
                }
              end
            end
            
            # 3. リアクション日時が新しい順にソート(既にソートされているが、念のため)
            # user_reactions の値 (Hash) を created_at で降順ソート
            sorted_user_details = user_reactions.values.sort_by { |d| d[:created_at] }.reverse

            # 4. HTMLタグを生成 (ツールチップ付き)
            user_htmls = sorted_user_details.map do |detail|
              # リアクション日時を Redmine 標準の format_time でフォーマット
              # format_time は Wikiマクロのコンテキストではそのまま利用できない場合があるため、DateTime#strftimeを使用する。
              # ただし、Redmine::I18n を extend しているため、format_time が利用可能と仮定する。
              formatted_time = format_time(detail[:created_at])
              
              # span タグに title 属性 (ツールチップ) を設定
              content_tag('span', detail[:name], title: "#{detail[:name]} がリアクション: #{formatted_time}", class: 'reaction-user-tooltip')
            end
            
            reaction_users = user_htmls.join(', ').html_safe
            reaction_count = issue.reactions.size
          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>" # days_info は状態ではなく期日までの日数情報
          html << "<td>#{issue.done_ratio}%</td>"
          html << "<td>#{reaction_count}</td>"
          html << "<td>#{reaction_users}</td>" # HTML生成されたリアクション者名
          html << "</tr>"
        end

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

    # 担当者が未指定のタスクのレンダリング
    unassigned_issues = fetch_issues.call(nil)
    content << render_task_list.call(unassigned_issues, "❓ 担当者未指定のタスク")

    content.html_safe
  end
end

変更点の概要

  1. ヘルパーメソッドのインクルード:
    マクロ内で link_tocontent_tagformat_time などのRedmine標準のヘルパーメソッドを使用できるようにするため、マクロの冒頭に以下を追加しました。
extend ActionView::Helpers::TagHelper
extend ActionView::Helpers::UrlHelper
extend Redmine::I18n

  1. リアクション情報の収集とソート (実装手順 3.1):
  • issue.reactionscreated_at の降順でソートしました。
  • 複数のリアクションを行ったユーザーがいる場合、最も新しいリアクション日時を採用し、重複したユーザーを排除するロジック (user_reactions ハッシュの利用) を導入しました。
  • これにより、リアクション者リストは最新のリアクション日時順に並びます。
  1. ツールチップ付きHTMLの生成 (実装手順 3.2):
  • ソートされたユーザー詳細リストを反復処理し、各ユーザー名に対して content_tag ヘルパーを使用して <span> タグを生成しました。
  • 生成された <span> タグには、リアクション日時を title 属性として設定しています。
  • 例: <span title="佐藤 太郎 がリアクション: 2025/12/16 10:00">佐藤 太郎</span>

これにより、ユーザー名にカーソルを合わせるとリアクション日時が表示され、リスト自体も最新のリアクション順に表示されるようになりました。

このコードをご利用のRedmine環境に適用し、動作をご確認ください。


感想

すぐに開発を依頼をすると私が意図した通りに開発をしてくれない可能性があるので、まずは設計を依頼した。設計に問題がなかったので、設計をもとに開発を依頼した。意図した通り、修正をしてくれた。
誰が、いつ、どれだけ多くの人がリアクションをしたのか?は重要な情報であり、wikiマクロで簡単に実現ができ、利便性が向上したと思います。


この記事の作成者の紹介


山崎進

  • 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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?