6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

HameeAdvent Calendar 2021

Day 6

AsanaのAPIを使ってタスクのストーリー(アクティビティ)までを取得してみる

Posted at

こんにちは。

これはHameeの2021年のアドベントカレンダー、6日目の記事です。

同じくアドベントカレンダーの @yamamoto_hiroya さんの4日目の記事にもあるのですが、この11月からHameeの開発部署のプロジェクト管理をRedmineからAsanaに変更しました。その関係で、旧来できていた運用の再現や、手間をはぶく新しい運用の適用やツールの作成が急ピッチで進められていました。

そしてタスクのステータスをAsana上のセクションで管理するようになったのですが、あるとき、そのタスクの差し戻し回数や、保留されている時間を知り、プロジェクト管理の見える化をはかろうとしました。ここではそのためにおこなったAsanaのAPIの疎通方法を記載します。

Asanaとは

image.png
https://asana.com/ja

いま一つだけプロジェクトを管理ツールを選べと言われたらAsanaを選ぶ、僕にとってAsanaはいまやそういう存在のプロジェクト管理ツールになりました。ですが本当は「プロジェクト管理ツール」ではないらしいです。「ワークマネジメントツール」らしいです。なので、プロジェクトを管理する以上の機能がAsanaには詰め込まれています。

例えばポートフォリオやレポートといった機能でしょうか。メンバーがどのような稼働をしているのか、プロジェクトを横断的に確認することができたり、プロジェクトのステータスを収集したりしてくれます。

AsanaはもともとはFacebookのエンジニアが自分たちのプロジェクトを管理するためのツールとして生まれたそうです。多種多様で柔軟な機能群はそう言った実地で生み出されているのかもしれません。「それが欲しかったわー!」という、ワクワクするような新機能が実装され続けています。凄いね!

とはいえAsanaのワクワク機能実装をもってしても、すべての自社のわがままに付き合ってくれるわけではありません。しかしAPIを使えばある程度の情報をAsanaから引き出せたり、情報更新ができたりします。

ここでは試しに、Asana APIを使用し、あるプロジェクトに紐づくタスクのストーリー(アクティビティ)を持ってきます。

Asana APIを利用するための準備

ひとまずAsanaのAPIトークンを払い出します。Developer consoleで新しいアプリを作り、APIトークンを払い出すのが個人で使用するなら早いでしょう。

image.png

下記以降、ここで取得したトークンは ASANA_TOKEN という環境変数に入っているものとします。内容理解のために自分でPython書いてますが、最初にお伝えすると、すでにオフィシャルでライブラリがあるので、実務へはそちらを使うのがよろしいでしょう。

Projectを取得する

#!/usr/bin/env python

import os
import json
import urllib.request
from datetime import datetime, timezone
import dateutil.parser

class AsanaResource:
    end_point = 'https://app.asana.com/api/1.0'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + os.environ.get('ASANA_TOKEN')
    }

    @classmethod
    def get_response(cls, path):
        response = ''

        url = cls.end_point + path
        req = urllib.request.Request(url, method='GET', headers=AsanaResource.headers)
        with urllib.request.urlopen(req) as response:
            response = response.read().decode("utf-8")

        return response

class Project(AsanaResource):

    def __init__(self, project_gid):
        self.project_gid = project_gid

    def load(self):
        path = '/projects/{project_gid}?opt_fields=gid,name'.replace('{project_gid}', self.project_gid)

        response = AsanaResource.get_response(path)
        project_data = json.loads(response)['data']
        self.load_from_response(project_data)

    def load_from_response(self, project_data):
        self.project_gid = project_data['gid']
        self.name = project_data['name']

def main():

    project = Project('12345678')
    project.load()
    print(project.name)

if __name__ == '__main__':
    main()

project = Project('12345678')

この部分はプロジェクトのGIDとなります。(これは適当な値を入れています)基本、下記のようにURLに記載があるのでコピペで取得してください。あるいは、この値もAPIで取得できます

image.png

このスクリプトを実行すると、下記のようにプロジェクト名が取得できているのが分かります。

ito.masakuni % python ./asana_report
TEMP/MASAKUNI_NO_PJ

Project〜Task〜Storyを取得する

プロジェクトのデータ取得が確認したところで、ずあーっとタスクとそのアクティビティであるストーリーを取得します。全体的に下記のように変更します。

#!/usr/bin/env python

import os
import json
import urllib.request
from datetime import datetime, timezone
import dateutil.parser

class AsanaResource:
    end_point = 'https://app.asana.com/api/1.0'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + os.environ.get('ASANA_TOKEN')
    }

    @classmethod
    def get_response(cls, path):
        response = ''

        url = cls.end_point + path
        req = urllib.request.Request(url, method='GET', headers=AsanaResource.headers)
        with urllib.request.urlopen(req) as response:
            response = response.read().decode("utf-8")

        return response

class Project(AsanaResource):

    def __init__(self, project_gid):
        self.project_gid = project_gid

    def load(self):
        path = '/projects/{project_gid}?opt_fields=gid,name'.replace('{project_gid}', self.project_gid)

        response = AsanaResource.get_response(path)
        project_data = json.loads(response)['data']
        self.load_from_response(project_data)

    def load_from_response(self, project_data):
        self.project_gid = project_data['gid']
        self.name = project_data['name']

    def get_tasks(self):
        tasks = []
        path = '/projects/{project_gid}/tasks?opt_fields=gid,name,assignee.name,created_at,memberships.section.name'.replace('{project_gid}', self.project_gid)

        response = AsanaResource.get_response(path)
        tasks_data = json.loads(response)['data']

        tasks = []
        for task_data in tasks_data:
            task = Task(task_data['gid'])
            task.load_from_response(task_data)
            tasks.append(task)

        return tasks

class Task(AsanaResource):

    def __init__(self, task_gid):
        self.task_gid = task_gid

    def load(self):
        path = '/tasks/{task_gid}?opt_fields=gid,name,assignee.name,created_at,memberships.section.name'.replace('{task_gid}', self.task_gid)

        response = AsanaResource.get_response(path)
        self.load_from_response(json.loads(response)['data'])

    def load_from_response(self, task_data):
        self.task_gid = task_data['gid']
        self.name = task_data['name']
        self.created_at = task_data['created_at']

        self.assignee = None
        if not task_data['assignee'] is None:
            self.assignee = task_data['assignee']['name']

        self.section_name = None
        for membership in task_data['memberships']:
            if 'section' in membership:
                self.section_name = membership['section']['name']

    def get_stories(self):
        stories = []
        path = '/tasks/{task_gid}/stories?opt_fields=text,created_at'.replace('{task_gid}', self.task_gid)

        response = AsanaResource.get_response(path)
        stories_data = json.loads(response)['data']

        for story_data in stories_data:
            story = Story(story_data['gid'])
            story.load_from_response(story_data)
            stories.append(story)

        return stories

class Story(AsanaResource):
    text = None
    created_at = None

    def __init__(self, story_gid):
        self.story_gid = story_gid

    def load(self):
        path = '/stories/{story_id}?opt_fields=gid,text,created_at'.replace('{story_id}', self.story_gid)

        response = AsanaResource.get_response(path)
        self.load_from_response(json.loads(response)['data'])

    def load_from_response(self, story_data):
        self.story_gid = story_data['gid']
        self.text = story_data['text']
        self.created_at = story_data['created_at']

def main():

    project = Project('12345678')

    for task in project.get_tasks():
        for story in task.get_stories():
            print(story.text)

if __name__ == '__main__':
    main()

この実行結果は下記となり、各タスクのストーリーが取得できているのが分かります。

ito.masakuni % python ./asana_report
Masakuni Ito added to https://app.asana.com/0/12345678/987654321
コメントを追加
Masakuni Ito changed the due date to December 7
Masakuni Ito assigned to you

image.png

画面上のストーリーとも一致することが分かります。

ちょっとだけ解説

path = '/projects/{project_gid}/tasks?opt_fields=gid,name,assignee.name,created_at,memberships.section.name'.replace('{project_gid}', self.project_gid)

AsanaのAPIは各リソースに複数のアクセス方法があるようです。上記はプロジェクトに紐付いているタスクを取得します。

path = '/tasks/{task_gid}?opt_fields=gid,name,assignee.name,created_at,memberships.section.name'.replace('{task_gid}', self.task_gid)

こちらはタスクのIDから取得します。

opt_fields

opt_fields をつけないとAsanaで定められたデフォルトのデータしか取得できないのですが、 opt_fields でパラメータを指定すると、そのデータが取得できるようになっています。ドキュメントの Input/Output Options をご覧ください。てゆーか、基本ここで取得できるものはCreate時のレスポンスと同じようなものだろうなと思っている(例えばTaskのCreate時のデータ)のですが、誰かこの一覧をご存じの方いらっしゃいませんか。

まとめ

APIを使用せずとも、さらに柔軟なレポートがAsana上で作れるようになると僕は信じています。

6
0
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
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?