【Python】RedmineチケットのカスタムフィールドにPythonからファイル添付する

PythonでRedmine APIを操作するための`python-redmine`モジュールを使用したファイルタイプのカスタムフィールド更新を説明したいと思います。

# 通常のカスタムフィールド更新方法

ファイルタイプの更新方法について説明する前に、通常のカスタムフィールドの更新方法をおさらいします。

`redminelib`はPythonでRedmine APIを使用するためのモジュールです。
`pip install python-redmine`でインストールできます。

ざっくりとした流れは、以下です。

1. Redmineの接続情報(APIキーとURL)を指定
1. チケットIDを指定
1. カスタムフィールドIDを指定(分からない場合は、カスタムフィールド名から求める)
1. チケットIDとカスタムフィールドID・カスタムフィールドに設定する値を指定して、チケットを更新

詳細はソースのコメントに書いています。

```python:通常のカスタムフィールドの更新
from redminelib import Redmine

# Redmine接続情報

## 個人設定から参照したAPIキー
api_key = "c25xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx04"

## RedmineのURL
url = "http://redmine.example:3000/"

# Redmineの接続情報を渡してインスタンス生成
redmine = Redmine(url, key=api_key)

# チケットID
issue_id = 227

# 1.と2.のどちらかを実装

## 1. カスタムフィールドのIDが分かる場合
custom_field_id = 15

## 2. カスタムフィールドのIDが分からない場合

### カスタムフィールドの名前からIDを取得
custom_field_name = "usual_cf"

### 全てのカスタムフィールドのIDと名前を取得
custom_fields = redmine.custom_field.all().values("id", "name")

### for文でカスタムフィールドの情報を調べる
for cf in custom_fields:
    #### カスタムフィールド名が一致する情報がある
    if cf.get("name") == custom_field_name:
        ##### カスタムフィールドIDを取得
        custom_field_id = cf.get("id")

# チケットの更新
redmine.issue.update(
    issue_id, ## 更新したいチケットのID
    custom_fields=[
        {
            "id": custom_field_id,  ## カスタムフィールドのID
            "value": "usual"
        }
    ]
)
```

# ファイルタイプのカスタムフィールド更新方法

ファイルタイプのカスタムフィールドの更新を行うためには、ファイルを値として直接渡すのではなく、事前にアップロードする必要があります。
その際に使用するのは、`Redmine.upload`です。第一引数にアップロードしたいファイルパス、第二引数にアップロード後のファイル名を指定します。

```python:ファイルのアップロード
# ファイルをRedmineに事前にアップロード
data = redmine.upload(filepath, filename)
```

そうすると、レスポンスでファイルのIDとトークンが返ってきます。
このときのトークンはアップロードしたファイルを表すものですので、このトークンとファイル名をチケット更新の際にカスタムフィールドの値に指定することで、ファイル添付ができます。
ファイルを添付するカスタムフィールドの値の指定方法は以下です。

```python:ファイルを添付するカスタムフィールドの値の指定
"value":{
    "filename": filename, ## チケットに添付するファイル名
    "token": data["token"] ## 事前にアップロードしたファイルのトークン
}
```

流れをまとめるとこのような感じです。

1. Redmineの接続情報(APIキーとURL)を指定
1. チケットIDを指定
1. カスタムフィールドIDを指定(分からない場合は、カスタムフィールド名から求める)
1. ★ファイルを事前にアップロード
1. ★アップロード処理のレスポンスからトークンを取得
1. チケットIDとカスタムフィールドID・カスタムフィールドに設定する値(★ファイル名・トークン)を指定して、チケットを更新

通常の処理に書き足すとこんな感じです。

```python:ファイルタイプのカスタムフィールドの更新
from redminelib import Redmine
import os

# Redmine接続情報

## 個人設定から参照したAPIキー
api_key = "c25xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx04"

## RedmineのURL
url = "http://redmine.example:3000/"

# Redmineの接続情報を渡してインスタンス生成
redmine = Redmine(url, key=api_key)

# チケットID
issue_id = 227

# 1.と2.のどちらかを実装

## 1. カスタムフィールドのIDが分かる場合
custom_field_id = 18

## 2. カスタムフィールドのIDが分からない場合

### カスタムフィールドの名前からIDを取得
custom_field_name = "file_cf"

### 全てのカスタムフィールドのIDと名前を取得
custom_fields = redmine.custom_field.all().values("id", "name")

### for文でカスタムフィールドの情報を調べる
for cf in custom_fields:
    #### カスタムフィールド名が一致する情報がある
    if cf.get("name") == custom_field_name:
        ##### カスタムフィールドIDを取得
        custom_field_id = cf.get("id")

# 添付したいファイル名
filename = "example_file.csv"

# 添付したいファイルのパス
filepath = "./attached_file/" + filename

# このファイルがあるディレクトリに移動
os.chdir(os.path.dirname(os.path.abspath(__file__)))

# ファイルをRedmineに事前にアップロード
data = redmine.upload(filepath, filename)
## (参考) dataの中身の例
## {'id': 7167, 'token': '7167.ed1ccdb093229ca1bd0b043618d88743'}

# アップロードしたレスポンスから取得したトークンを使用して、カスタムフィールドにファイルを添付
redmine.issue.update(
    issue_id, ## 更新したいチケットのID
    custom_fields=[
        {
            "id": custom_field_id,  ## カスタムフィールドのID
            "value":{
                "filename": filename, ## チケットに添付するファイル名
                "token": data["token"] ## 事前にアップロードしたファイルのトークン
            }
        }
    ]
)
```

# さいごに

PythonからRedmineを操作する方法や`python-redmine`モジュールの基本的な使用方法についての情報は見つかりましたが、カスタムフィールドへのファイル添付は有益な情報が見つからなかったのでまとめてみました。
ちなみに今回は、RedmineをWebブラウザから開いて操作したときの通信をChromeのディベロッパーツールを使って調べました。
ディベロッパーツールから見えるGUIはJavaScriptなのでPythonとは多少違いますが、必要な処理などはなんとなく分かります。そこから得た情報をもとにリファレンスで検索をすると解決策が見つかるかもしれません。