OPS

PythonでBacklogのプロジェクト横断ガントチャートを表示してみた。

2018.11.16

本記事のポイント

Backlogの複数プロジェクトをガントチャート表示してみました。
環境:Ubuntu16.04LTS + Python3 + python_gannt

はじめに

JIG-SAWではnulabのbacklog を使っています。

とても使いやすいツールで、中でもガントチャートの表示が見やすく重宝しております。

しかしながら、記事作成時点で複数プロジェクトを横断した表示ができません。

各自が様々なプロジェクトに参加しているので、チーム内の忙しさみたいなものが把握できず困っていました。

追加要件として、今回やりたい事は以下のような内容です。

・できるだけ簡単に公開したい。

・人手を介さず自動生成したい。

公式対応に期待しつつ、簡単に表示してみました。

結果

まずは結果です。

以下のようなガントチャートが画像で出力されました。

※表示の都合上 png ファイルですが、出力結果は svg ファイルとなります。

実装

環境

色々と検討しましたが Python のライブラリ発見したので作ってみる事にしました。

https://xael.org/pages/python-gantt-en.html

環境は Ubuntu16.04LTS + Python3 + Python-Gantt になります。

全体像

処理の全体的な流れから説明します。

1. backlog から表示対象の課題を取得する。

2. Python-Gantt の Task に課題を登録する。

3. Task をソートする。

4. Python-Gantt の Project に Task を登録する。

5. 画像を出力する。

# 1. backlog から表示対象の課題を取得する。
result = webAPICall(api, param)

# 2. python-gantt の Task に課題を登録する。
task_list = create_task_list(result)

# 3. Task をソートする。
task_list = task_sort(task_list)

# 4. python-gantt の Project に Task を登録する。
project = create_project(task_list, filename)

# 5. 画像を出力する。
create_svg(project, filename)

1. backlog から表示対象の課題を取得する。

backlog からの課題取得は backlog API を利用しています。

https://developer.nulab-inc.com/ja/docs/backlog/

条件を指定して、対象の課題を JSON 形式で取得します。

import requests
import json

# backlog APIキー
apiKey = '************************'

# backlog URL
base_url = 'https://backlog.url.jp{}apiKey={}'

# backlog API 選択
api = '/api/v2/issues?'

# backlog API 検索条件
param = "&count={}".format(100)
param = param + "&assigneeId[]={}".format(253156)
param = param + "&assigneeId[]={}".format(84848)
param = param + "&assigneeId[]={}".format(267795)
param = param + "&assigneeId[]={}".format(107914)
param = param + "&assigneeId[]={}".format(9597)
param = param + "&assigneeId[]={}".format(254058)
param = param + "&statusId[]={}".format(1)
param = param + "&statusId[]={}".format(2)
param = param + "&statusId[]={}".format(3)


def webAPICall(api, param):

    # URLを組み立てる。
    url = base_url.format(api, apiKey) + param

    # 読み込むオブジェクトの作成
    readObj = requests.get(url)

    # webAPIからのJSONを取得
    response = json.loads(readObj.text)

    # JSONを返す
    return response

2. python-gantt の Task に課題を登録する。

ここから Python-Gantt を使って、 backlog から取得した JSON を加工していきます。

とはいえ、 Python-Gantt のサンプルソースがそのまま使えます。

苦労したのは Python-Gantt 関係ない部分でした。

backlog の課題をガントチャートのどの日付に表示するか?という問題があったので、下記のようにしました。

・開始日と期限日が設定されていれば、その期間を表示する。

・開始日だけ設定されていれば、開始日の1日だけ表示する。

・期限日だけ設定されていれば、期限日の1日だけ表示する。

・開始日も期限日も設定されていなければ、表示しない。

from datetime import datetime as dt
from datetime import timedelta
import gantt

# 出力ファイル名
filename = "all schedule"


def create_task_list(tasks):

    # フォントを変更します。
    gantt.define_font_attributes(fill='black',
                                 stroke='black',
                                 stroke_width=0,
                                 font_family="Verdana")

    task_list = []

    # 課題の分だけ繰り返します。
    for key in tasks:

        # 表示項目は キー + タイトル とします。
        name = "{} {}".format(key["issueKey"], key["summary"].replace("\t", ""))

        time_format = '%Y-%m-%dT%H:%M:%SZ'
        if (key["startDate"] is None):
            if (key["dueDate"] is None):
                continue
            else:
                start = dt.strptime(key["dueDate"], time_format)
                duration = 1
        else:
            start = dt.strptime(key["startDate"], time_format)
            if (key["dueDate"] is None):
                duration = 1
            else:
                dueDate = dt.strptime(key["dueDate"], time_format)
                duration = (dueDate - start) // timedelta(days=1)

        percent_done = 0  # 今回は進捗率は使いません。
        resources = []
        resources.append(gantt.Resource(key["assignee"]["name"]))
        color = "#FF8080"

        t = gantt.Task(name=name,
                       start=start,
                       duration=duration,
                       percent_done=percent_done,
                       resources=resources,
                       color=color)

        task_list.append(t)

    return task_list

3. Task をソートする。

ソートするには Task の開始日など必要になります。

これらの取得方法も Python-Gantt で用意されていました。

なお、私はドキュメント上から取得方法など見つけられませんでした。

公式サイトにてライブラリのソースも公開されております。

(少し長いですが)難しくありませんでしたので、困ったら読んでみる事をオススメします。

ソートアルゴリズムは簡単な選択ソートです。

def task_sort(task_list):
    if len(task_list) == 1:
        return task_list

    max = None
    max_num = 0
    cnt = 0
    for task in task_list:
        if (max is None):
            max = task
            continue
        cnt += 1
        if (max.start_date() > task.start_date()):
            continue
        max = task
        max_num = cnt

    max = task_list.pop(max_num)
    res_list = task_sort(task_list)
    res_list.append(max)
    return res_list

4. python-gantt の Project に Task を登録する。

ほとんどサンプル通りです。

def create_project(task_list, filename):
    # Create a project
    p = gantt.Project(name=filename)

    for task in task_list:
        p.add_task(task)

    return p

5. 画像を出力する。

こちらもほとんどサンプル通りです。

一番右側の課題の キー + タイトル が見えるように表示範囲の終了側を少しだけ伸ばしました。

def create_svg(project, filename):
    gant_today = dt.now()
    gant_today = gant_today.replace(hour=0, minute=0, second=0, microsecond=0)
    gant_start = task_list[0].start_date()
    gant_end = task_list[-1].end_date() + timedelta(days=14)

    project.make_svg_for_tasks(filename=filename+'.svg',
                               today=gant_today,
                               start=gant_start,
                               end=gant_end)

ソース(全体)

ソースを置いておきますので、よかったら動かしてみてください。

from datetime import datetime as dt
from datetime import timedelta
import gantt
import requests
import json

# 出力ファイル名
filename = "all schedule"

# backlog APIキー
apiKey = '************************'

# backlog URL
base_url = 'https://backlog.url.jp{}apiKey={}'

# backlog API 選択
api = '/api/v2/issues?'

# backlog API 検索条件
param = "&count={}".format(100)
param = param + "&assigneeId[]={}".format(253156)
param = param + "&assigneeId[]={}".format(84848)
param = param + "&assigneeId[]={}".format(267795)
param = param + "&assigneeId[]={}".format(107914)
param = param + "&assigneeId[]={}".format(9597)
param = param + "&assigneeId[]={}".format(254058)
param = param + "&statusId[]={}".format(1)
param = param + "&statusId[]={}".format(2)
param = param + "&statusId[]={}".format(3)


def webAPICall(api, param):

    # URLを組み立てる。
    url = base_url.format(api, apiKey) + param

    # 読み込むオブジェクトの作成
    readObj = requests.get(url)

    # webAPIからのJSONを取得
    response = json.loads(readObj.text)

    # JSONを返す
    return response


def task_sort(task_list):
    if len(task_list) == 1:
        return task_list

    max = None
    max_num = 0
    cnt = 0
    for task in task_list:
        if (max is None):
            max = task
            continue
        cnt += 1
        if (max.start_date() > task.start_date()):
            continue
        max = task
        max_num = cnt

    max = task_list.pop(max_num)
    res_list = task_sort(task_list)
    res_list.append(max)
    return res_list


def create_project(task_list, filename):
    # Create a project
    p = gantt.Project(name=filename)

    for task in task_list:
        p.add_task(task)

    return p


def create_svg(project, filename):
    gant_today = dt.now()
    gant_today = gant_today.replace(hour=0, minute=0, second=0, microsecond=0)
    gant_start = task_list[0].start_date()
    gant_end = task_list[-1].end_date() + timedelta(days=14)

    project.make_svg_for_tasks(filename=filename+'.svg',
                               today=gant_today,
                               start=gant_start,
                               end=gant_end)


def create_task_list(tasks):

    # フォントを変更します。
    gantt.define_font_attributes(fill='black',
                                 stroke='black',
                                 stroke_width=0,
                                 font_family="Verdana")

    task_list = []

    # 課題の分だけ繰り返します。
    for key in tasks:

        # 表示項目は キー + タイトル とします。
        name = "{} {}".format(key["issueKey"], key["summary"].replace("\t", ""))

        time_format = '%Y-%m-%dT%H:%M:%SZ'
        if (key["startDate"] is None):
            if (key["dueDate"] is None):
                continue
            else:
                start = dt.strptime(key["dueDate"], time_format)
                duration = 1
        else:
            start = dt.strptime(key["startDate"], time_format)
            if (key["dueDate"] is None):
                duration = 1
            else:
                dueDate = dt.strptime(key["dueDate"], time_format)
                duration = (dueDate - start) // timedelta(days=1)

        percent_done = 0  # 今回は進捗率は使いません。
        resources = []
        resources.append(gantt.Resource(key["assignee"]["name"]))
        color = "#FF8080"

        t = gantt.Task(name=name,
                       start=start,
                       duration=duration,
                       percent_done=percent_done,
                       resources=resources,
                       color=color)

        task_list.append(t)

    return task_list


# 1. backlog から表示対象の課題を取得する。
result = webAPICall(api, param)

# 2. python-gantt の Task に課題を登録する。
task_list = create_task_list(result)

# 3. Task をソートする。
task_list = task_sort(task_list)

# 4. python-gantt の Project に Task を登録する。
project = create_project(task_list, filename)

# 5. 画像を出力する。
create_svg(project, filename)

まとめ

Python-Gantt は日本語の情報が少なかったので記事にしようと思ったのですが、
Python初心者でも簡単に作る事ができました。

また、今回は画像での出力となりました。

画像ということで、こんなメリット/デメリットがありました。

■メリット

・公開する為にサーバーを準備する必要がない。

・既存のwiki等に張り付けできる。

■デメリット

・リンククリックで直接課題にアクセスできない。

・画像がSVG形式なので、ツール側で対応していない場合がある。

今回は俯瞰する事が目的だったので、そこそこ時間かけずに実現できたかと思います。

他にも手法あると思いますので、色々試してみると面白いと思います。