Redmine wikiマクロを作成する方法４（ステータスごとのチケットの割合を一つのグラフで表示）

# Redmine wikiマクロを作成する方法４（ステータスごとのチケットの割合を一つのグラフで表示）

[Redmine Advent Calendar 2024](https://adventar.org/calendars/10311)の12日目の記事として作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/9651356f-f9f5-7cc8-4b1a-074c983ae9fe.png)


今年のRedmine Advent Calendarに空白が多く残っており、クリスマスが過ぎる前までになんとか空白の日付の記事を書いて、空白の日付を埋めることを優先して記事を作成しました。そのため、濃くない内容の記事になるかもしれませんが、ご容赦ください。


## 完成形
前回の[Redmine wikiマクロを作成する方法３（ステータスごとのチケットの割合をグラフで表示）](https://qiita.com/yam_asaki/items/ff108e081dc421fe2b10)は下図のように各ステータス毎に独立したグラフとして表示させましたが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/116cf94e-8a6a-31d7-9b36-4a0b53de8432.png)

今回の開発では、下図のように、ステータスごとのチケットの割合を一つのグラフで表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/1663b0a7-f10c-0ad4-6e27-f9449d6a45ff.png)

多少の違いはありますが、今回の開発は前回の開発と重複したソースコードが多く存在します。


* * *
## 背景

Redmineには自作のwikiマクロを作成して登録することができます。
その方法としてはプラグインを作成しますがwikiマクロの登録のためのプラグインは比較的開発が簡単であるため、プラグインを作ったことがない方でも簡単に試してみることができるメリットがあります。

wikiマクロでは色々な処理をすることができるためRedmineの構造を理解したり、Rubyを理解したり、プラグインの仕組みを理解したりするためには良い教材になるかと思います。
今年のRedmine Advent Calendarを通して、何回かに分けてwikiマクロの作り方を紹介したいと思います。

今回の記事では「Redmine wikiマクロを作成する方法４」として「ステータスごとのチケットの割合を一つのグラフで表示」する方法を説明します。


## 前提条件：
使用するwikiマクロ登録用プラグインは[Redmine wikiマクロを作成する方法１](https://qiita.com/yam_asaki/items/1dfd349e078fc6d0f764)で説明したプラグインを使用します。/plugins/wiki_macro_plugin/init.rbの「init.rb」ファイルにwikiマクロを登録します。


---

### 1. **マクロを実装**

1. /plugins/wiki_macro_plugin/init.rbの「`init.rb`」ファイルを編集して、Wikiマクロを登録します：
   ```ruby
   Redmine::Plugin.register :wiki_macro_plugin do
    省略
   end

    Redmine::WikiFormatting::Macros.register do
      desc "Display a single progress bar with sections for each status in a project"
      macro :combined_progress_bar do |obj, args|
        project = Project.find_by_identifier(args.first || obj.project.identifier)
        return "プロジェクトが見つかりません。" unless project
    
        # プロジェクト内のチケットを取得
        issues = project.issues
        return "チケットがありません。" if issues.empty?
    
        # ステータスごとの件数を取得
        total_count = issues.count
        status_counts = issues.group(:status).count
    
        # プログレスバーを作成
        progress_bar = "<div style='width: 100%; display: flex; border: 1px solid #ccc; background-color: #e0e0e0;'>"
    
        # 各ステータスごとにバーのセクションを作成
        status_counts.each do |status, count|
          percentage = (count.to_f / total_count * 100).round(2)
          color = "##{Digest::MD5.hexdigest(status.name)[0..5]}" # ステータス名を元に色を生成
          progress_bar << "<div style='width: #{percentage}%; background-color: #{color}; text-align: center; color: white; font-size: 0.8em;' title='#{status.name}: #{count}件 (#{percentage}%)'>"
          progress_bar << "#{percentage}%"
          progress_bar << "</div>"
        end
    
        progress_bar << "</div>"
    
        # プログレスバーの説明を追加
        legend = "<ul style='list-style: none; padding: 0; margin: 10px 0;'>"
        status_counts.each do |status, count|
    
          # ステータスへのリンクを生成
          status_link = link_to(status.name, project_issues_path(project, set_filter: 1, f: ['status_id'], op: { 'status_id' => '=' }, v: { 'status_id' => [status.id] }))
          
          color = "##{Digest::MD5.hexdigest(status.name)[0..5]}"
          legend << "<li style='margin: 5px 0;'>"
          legend << "<span style='display: inline-block; width: 15px; height: 15px; background-color: #{color}; margin-right: 5px;'></span>"
    
          legend << "#{status_link}: #{count}件"
          legend << "</li>"
        end
        legend << "</ul>"
    
        (progress_bar + legend).html_safe
      end
    end
   ```


以下、コードを1行ずつ解説します。

---

```ruby
Redmine::WikiFormatting::Macros.register do
```
- **説明**: RedmineにカスタムWikiマクロを登録するための宣言です。このブロック内で新しいマクロを定義します。

---

```ruby
  desc "Display a single progress bar with sections for each status in a project"
```
- **説明**: マクロの説明文を指定しています。この内容は、`{{macro_list}}`を実行するとマクロの説明の欄に情報が反映されます。

`{{macro_list}}`を実行画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/18556345-54f6-ab30-5565-9311c40a0888.png)

`{{macro_list}}`を実行した画面で`desc`の内容が表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/da3ae049-a1a4-2afa-840e-4e75f30a54b8.png)


---

```ruby
  macro :combined_progress_bar do |obj, args|
```
- **説明**: `:combined_progress_bar`という名前のWikiマクロを定義します。
- **引数**:
  - `obj`: Wikiページやプロジェクトなど、現在のコンテキストオブジェクト。
  - `args`: マクロに渡された引数の配列。

---

```ruby
    project = Project.find_by_identifier(args.first || obj.project.identifier)
```
- **説明**: 引数からプロジェクト識別子を取得し、該当するプロジェクトをデータベースから検索します。引数がない場合は、現在のプロジェクトを使用します。

---

```ruby
    return "プロジェクトが見つかりません。" unless project
```
- **説明**: プロジェクトが見つからない場合にメッセージを返し、処理を終了します。

---

```ruby
    issues = project.issues
```
- **説明**: 指定したプロジェクト内のすべてのチケット（Issue）を取得します。

---

```ruby
    return "チケットがありません。" if issues.empty?
```
- **説明**: プロジェクト内にチケットがない場合にメッセージを返し、処理を終了します。

---

```ruby
    total_count = issues.count
```
- **説明**: チケットの総数をカウントし、変数`total_count`に代入します。

---

```ruby
    status_counts = issues.group(:status).count
```
- **説明**: チケットをステータスごとにグループ化し、各ステータスの件数をカウントします。

---

```ruby
    progress_bar = "<div style='width: 100%; display: flex; border: 1px solid #ccc; background-color: #e0e0e0;'>"
```
- **説明**: プログレスバー全体のHTML要素の開始タグを作成します。
  - **`flex`レイアウト**: 各ステータスセクションを横並びに表示します。

---

```ruby
    status_counts.each do |status, count|
```
- **説明**: 各ステータスとその件数について繰り返し処理を開始します。

---

```ruby
      percentage = (count.to_f / total_count * 100).round(2)
```
- **説明**: 各ステータスのチケット割合を計算し、小数点2桁に丸めます。

---

```ruby
      color = "##{Digest::MD5.hexdigest(status.name)[0..5]}"
```
- **説明**: ステータス名を元にMD5ハッシュを計算し、16進数の先頭6文字を取り出して色コードとして使用します。これにより、ステータスごとに一意な色を生成します。

---

```ruby
      progress_bar << "<div style='width: #{percentage}%; background-color: #{color}; text-align: center; color: white; font-size: 0.8em;' title='#{status.name}: #{count}件 (#{percentage}%)'>"
```
- **説明**: プログレスバーのセクションをHTMLで生成します。
  - `width`: セクションの幅はステータス割合（`percentage`）に比例。
  - `background-color`: ステータス固有の色。
  - `title`: マウスオーバーで詳細（ステータス名、件数、割合）が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/57bb9805-8341-3091-058f-4206ca3e1717.png)

---

```ruby
      progress_bar << "#{percentage}%"
```
- **説明**: セクション内に割合をテキスト表示します。

---

```ruby
      progress_bar << "</div>"
```
- **説明**: プログレスバーセクションの終了タグを追加します。

---

```ruby
    end
```
- **説明**: ステータスごとの繰り返し処理を終了します。

---

```ruby
    progress_bar << "</div>"
```
- **説明**: プログレスバー全体の終了タグを追加します。

---

```ruby
    legend = "<ul style='list-style: none; padding: 0; margin: 10px 0;'>"
```
- **説明**: プログレスバーの説明（凡例）用のHTMLリストの開始タグを作成します。

---

```ruby
    status_counts.each do |status, count|
```
- **説明**: 各ステータスについて繰り返し処理を開始します（凡例作成用）。

---

```ruby
      status_link = link_to(status.name, project_issues_path(project, set_filter: 1, f: ['status_id'], op: { 'status_id' => '=' }, v: { 'status_id' => [status.id] }))
```
- **説明**: ステータス名をクリック可能なリンクとして生成します（リンク先はそのステータスに対応するチケット一覧ページ）。


以下、このコード行を詳しく解説します：


---

### **1. `link_to` メソッド**
- **役割**: Railsの`link_to`メソッドは、HTMLの`<a>`タグを生成するために使います。
- **構文**: 
  ```ruby
  link_to(link_text, url, html_options = {})
  ```
  - `link_text`: リンクに表示する文字列。
  - `url`: リンクの宛先（今回の場合、チケットリストページのURL）。
  - `html_options`: 必要に応じて追加のHTML属性（今回使われていない）。

- この例では:
  - `link_text` = `status.name`（ステータス名）
  - `url` = `project_issues_path(...)`で生成されるURL。

---

### **2. `project_issues_path`**
- **役割**: 特定のプロジェクトのチケットリストのURLを生成します。
- **構文**:
  ```ruby
  project_issues_path(project, params = {})
  ```
  - `project`: リンク先のプロジェクトオブジェクト。
  - `params`: URLに付加するパラメータ。

- 今回の例では、以下のようなパラメータを追加しています。

---

#### **2.1 `set_filter: 1`**
- **役割**: チケットリストのフィルターを有効化します。
- **効果**: URLでフィルター条件を指定した場合、このパラメータが必要です。

---

#### **2.2 `f: ['status_id']`**
- **役割**: フィルターの対象フィールドを指定します。
- **詳細**:
  - `'status_id'`: ステータスIDをフィルタリング条件に設定。

---

#### **2.3 `op: { 'status_id' => '=' }`**
- **役割**: フィルター条件での演算子を指定します。
- **詳細**:
  - `'status_id' => '='`: ステータスIDが特定の値に等しいという条件を意味します。

---

#### **2.4 `v: { 'status_id' => [status.id] }`**
- **役割**: フィルター条件の値を指定します。
- **詳細**:
  - `'status_id' => [status.id]`:
    - `status.id`は現在のステータスオブジェクトのID。
    - ステータスIDが`status.id`と一致するチケットをフィルタリングします。

---

### **3. 全体の動作**
- **生成されるリンク**:
  - `status.name`がリンクのテキスト（例: "Open", "Closed"など）。
  - リンク先は`project_issues_path`で生成されたURL。具体例として次のような形式になります：
    ```
    /projects/example_project/issues?set_filter=1&f[]=status_id&op[status_id]==&v[status_id][]=2
    ```
    - 上記は、「ステータスIDが2のチケットのみを表示」という条件をURLで表現したものです。

---

### **4. 結果**
このコードによって、次の動作が実現されます：
- ユーザーがリンクをクリックすると、選択したステータス（例: "Open", "Closed"など）に対応するチケットリストが表示されます。



---

```ruby
      color = "##{Digest::MD5.hexdigest(status.name)[0..5]}"
```
- **説明**: 進捗バーで使用したのと同じ色を生成します。

---

```ruby
      legend << "<li style='margin: 5px 0;'>"
```
- **説明**: 凡例リストの項目を開始します。

---

```ruby
      legend << "<span style='display: inline-block; width: 15px; height: 15px; background-color: #{color}; margin-right: 5px;'></span>"
```
- **説明**: 各ステータスに対応する色付きの四角（凡例アイコン）を生成します。

---

```ruby
      legend << "#{status_link}: #{count}件"
```
- **説明**: ステータス名（リンク）と件数を凡例リスト項目として追加します。

---

```ruby
      legend << "</li>"
```
- **説明**: 凡例リストの項目を終了します。

---

```ruby
    end
```
- **説明**: 凡例作成用の繰り返し処理を終了します。

---

```ruby
    legend << "</ul>"
```
- **説明**: 凡例リストの終了タグを追加します。

---

```ruby
    (progress_bar + legend).html_safe
```
- **説明**: プログレスバーと凡例を結合し、HTMLをエスケープせずにそのまま出力できるようにします。

---

```ruby
  end
end
```
- **説明**: マクロ定義の終了部分です。











---

### 2. **マクロの使い方:**

wikiページで下記の形式でwikiマクロを実行します。
   
   ```ruby
    {{combined_progress_bar(project_identifier)}}
   ```

   - project_identifierにプロジェクトの識別子を引数として渡します。引数が無い場合は、該当のwikiが所属しているプロジェクトがprogress_bar_by_statusに渡されます。

引数は有っても無くても動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/0630721d-a488-e7ae-94e5-a95356f2eec2.png)


下図のようにwikiマクロの実行結果が表示されます。（上の出力結果が引数がない場合で、下の出力結果が引数が有る場合です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/3eaac7fe-6a4c-0210-7a6b-0375b8eec24f.png)

「ステータス」の欄のステータス名をクリックすると、下図のようにチケット一覧ページに移動できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/c0747e01-2a9d-1701-f50f-2c5901286509.png)

* * *
## 感想

* wikiマクロ登録用のプラグインがあれば、簡単にwikiマクロを登録できることが分かります。
* 各ステータスごとのチケット件数の割合を一つのグラフで表示でき、どのステータスのチケットが多いのかがグラフから簡単に把握できます。
* 各ステータスごとのチケット件数の取得など、Redmineプラグインで必要な要素技術の習得に寄与します。
* 該当のステータスのチケット一覧へ移動できるリンクが有り、簡単に該当のチケット一覧を表示させることが可能。
* グラフの色を動的に作成できるメリットが有る。ただし、似たような色になってしまうデメリットが有り、今後、改善の余地はある。


* * *
### この記事の作成者の紹介
* * *
山崎進
* 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マクロを作成する方法１](https://qiita.com/yam_asaki/items/1dfd349e078fc6d0f764)

 *　[Redmine wikiマクロを作成する方法２（完了チケットの割合をグラフで表示）](https://qiita.com/yam_asaki/items/5e09143f93104c5590a1)

 *　[Redmine wikiマクロを作成する方法３（ステータスごとのチケットの割合をグラフで表示）](https://qiita.com/yam_asaki/items/ff108e081dc421fe2b10)

 *　[Redmine wikiマクロを作成する方法４（ステータスごとのチケットの割合を一つのグラフで表示）](https://qiita.com/yam_asaki/items/d0f0bba1b0caa892bd46)

 *　[Redmine wikiマクロを作成する方法５（特定のユーザーのチケット状態の確認）](https://qiita.com/yam_asaki/items/4138dc71505c7c33f63f)

 *　[Redmine wikiマクロを作成する方法６（プロジェクトに所属する全てのユーザーのチケット状態の確認）](https://qiita.com/yam_asaki/items/e80f9252a50caee14f1d)

### 関連記事（2023年のRedmine Advent Calendarの記事です。）
 *　[JqueryでRedmineのメニューにアイコンを入れる方法](https://qiita.com/yam_asaki/items/32097598cb0a653dd916)

 *　[JqueryでRedmineのメニューを閉じたり開いたりする方法](https://qiita.com/yam_asaki/items/5ac4a1127beeed53059c)

　*　[JqueryでRedmineの「活動ページ」をもっと便利に(タイトルを開閉したり、曜日を入れたり)](https://qiita.com/yam_asaki/items/718513b52b30b30f65bc)

　*　[JqueryでRedmineの「活動ページ」をもっと便利に②(フィルタリング機能と移動機能)](https://qiita.com/yam_asaki/items/2428610fd89bc73ce0ab)

　*　[JqueryでRedmineの「wikiページ」をもっと便利に(フィルタリング機能と移動機能)](https://qiita.com/yam_asaki/items/a6a32c45dbbc72a28836)

　*　[JqueryでRedmineの「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)](https://qiita.com/yam_asaki/items/176d07588793b3e66b5d)

　*　[RedmineのパスワードやログインIDを忘れた時の復旧方法](https://qiita.com/yam_asaki/items/6881c92b30dc85f589ad)

　*　[「rails console」を使ってRedmine上のスケジュールを一括変更する方法](https://qiita.com/yam_asaki/items/1d41dc8653773f3522e6)