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

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

[Redmine Advent Calendar 2024](https://adventar.org/calendars/10311)の10日目の記事として作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/2b54f953-46eb-5fff-e696-61e6b12cb7ad.png)

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


## 完成形
下図のようにwikiマクロの実行結果が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/116cf94e-8a6a-31d7-9b36-4a0b53de8432.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 progress bars for each status in a project with clickable links to issue lists"
      macro :progress_bar_by_status 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
    
        # プログレスバーを生成
        content = "#{project.name}のステータスごとの進捗<br>"
        content << "<table border='1' style='border-collapse: collapse; width: 100%;'>"
        content << "<tr><th>ステータス</th><th>件数</th><th>割合</th><th>進捗バー</th></tr>"
    
        status_counts.each do |status, count|
          percentage = (count.to_f / total_count * 100).round(2)
          progress_bar = "<div style='width: 100%; background-color: #e0e0e0; border: 1px solid #ccc;'>
                            <div style='width: #{percentage}%; background-color: #4caf50; text-align: center; color: white;'>
                              #{percentage}%
                            </div>
                          </div>"
    
          # ステータスへのリンクを生成
          status_link = link_to(status.name, project_issues_path(project, set_filter: 1, f: ['status_id'], op: { 'status_id' => '=' }, v: { 'status_id' => [status.id] }))
    
          content << "<tr>"
          content << "<td>#{status_link}</td>"
          content << "<td>#{count}</td>"
          content << "<td>#{percentage}%</td>"
          content << "<td>#{progress_bar}</td>"
          content << "</tr>"
        end
    
        content << "</table>"
        content.html_safe
      end
    end
   ```

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

---

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

---

```ruby
  desc "Display progress bars for each status in a project with clickable links to issue lists"
```
- **説明**: マクロの説明文を指定しています。この内容は、`{{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/43835a71-a76d-7b7a-07bb-ee084ed86037.png)

---

```ruby
  macro :progress_bar_by_status do |obj, args|
```
- **説明**: `:progress_bar_by_status`という名前のマクロを定義します。`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
    content = "#{project.name}のステータスごとの進捗<br>"
```
- **説明**: 出力するHTMLコンテンツの開始部分を定義しています。プロジェクト名を表示するテキストを追加しています。

---

```ruby
    content << "<table border='1' style='border-collapse: collapse; width: 100%;'>"
```
- **説明**: HTMLテーブルの開始タグを追加します。テーブルの幅は100%、セルのボーダーが崩れないようスタイルを設定しています。

---

```ruby
    content << "<tr><th>ステータス</th><th>件数</th><th>割合</th><th>進捗バー</th></tr>"
```
- **説明**: テーブルのヘッダー行を追加しています。各列の名前（ステータス、件数、割合、進捗バー）を定義しています。

---

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

---

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

---

```ruby
      progress_bar = "<div style='width: 100%; background-color: #e0e0e0; border: 1px solid #ccc;'>
                        <div style='width: #{percentage}%; background-color: #4caf50; text-align: center; color: white;'>
                          #{percentage}%
                        </div>
                      </div>"
```
- **説明**: HTMLとインラインCSSでプログレスバーを生成しています。バー全体を灰色にし、進捗部分を緑色で表示しています。

---

```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
      content << "<tr>"
```
- **説明**: テーブルの新しい行を開始します。

---

```ruby
      content << "<td>#{status_link}</td>"
```
- **説明**: ステータス名（リンク）をテーブルのセルとして追加します。

---

```ruby
      content << "<td>#{count}</td>"
```
- **説明**: ステータスごとの件数をテーブルのセルとして追加します。

---

```ruby
      content << "<td>#{percentage}%</td>"
```
- **説明**: 割合をテーブルのセルとして追加します。

---

```ruby
      content << "<td>#{progress_bar}</td>"
```
- **説明**: プログレスバーをテーブルのセルとして追加します。

---

```ruby
      content << "</tr>"
```
- **説明**: テーブルの行を終了します。

---

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

---

```ruby
    content << "</table>"
```
- **説明**: テーブルの閉じタグを追加します。

---

```ruby
    content.html_safe
```
- **説明**: HTMLコンテンツを`html_safe`メソッドでマークし、HTMLのエスケープを防ぎます。

---

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



---

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

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

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

引数は有っても無くても動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/17162b45-95f4-7d17-7ffd-33d19740e743.png)


下図のようにwikiマクロの実行結果が表示されます。（上の出力結果が引数がない場合で、下の出力結果が引数が有る場合です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/8ab77a81-1ddf-fa16-07db-8eba33311428.png)

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

* * *
## 感想

* wikiマクロ登録用のプラグインがあれば、簡単にwikiマクロを登録できることが分かります。
* 各ステータスごとのチケット件数の割合をグラフで表示でき、どのステータスのチケットが多いのかがグラフから簡単に把握できます。
* 各ステータスごとのチケット件数の取得など、Redmineプラグインで必要な要素技術の習得に寄与します。
* 出力結果が表形式で出力され、可読性の高い出力が可能。
* 表の各欄で、降順、昇順で並べ替えができます。並べ替えると他の欄も連動して並べ変わります。件数の多い順に並べることもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/930138d9-e0f5-4133-31ff-dae57479799a.png)





* * *
### この記事の作成者の紹介
* * *
山崎進
* 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)