Hacking the Taiga

by  David Barragán Merino /  @bameda




Hi!


David Barragán Merino

bameda on Github

@bameda on Twitter


#FFF8E7 at Kaleidos Open Source

Grouchy Smurf at Taiga Agile

What's Taiga?

Free. Open Source. Powerful. Taiga is a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable.


https://taiga.io

How it's made?


Back (API)

Front (SPA)

...Did you say API?...





























...let's start the Party!!...

Drawing graphs


I want to...

...draw graphics about the statistics of the issues of my project

But...

What about the API authentication?

How can I get the data?

How can I draw the graphs?

What about the API authentication?

Token Based Authentication: JSON Web Tokens (JWT)


1.1.1. Standard token authentication

To authenticate requests an http header called "Authorization" should be added. Its format should be:


Authorization: Bearer ${AUTH_TOKEN}
                                

This token ${AUTH_TOKEN} can be received through the login API.

3.1. Login

To login a user send a POST request containing the following data:

  • type: with value normal
  • username (required): a valid username or email
  • password (required): the user passowrd

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{
        "type": "normal",
        "username": "'${USERNAME}'",
        "password": "'${PASSWORD}'"
      }' \
  https://api.taiga.io/api/v1/auth
                            

When the login is successful, the HTTP response is a 200 OK and the response body is a JSON user auth detail object


{
    "id": 7,
    "is_active": true,
    "username": "beta.tester",
    "email": "beta.testing@taiga.io",
    "full_name": "Beta testing",
    "auth_token": "eyJ1c2VyX2F1dGhlbnRpY2F0aW9uX2lkIjo3fq:1XmPud:LKXVD9Z0rmHJjiyy0m4YaaHlQS1",
    (...)
}
                            

How can I get the data?


10.11. Issue stats

To get a project issue stats send a GET request specifying the project_id in the url:


curl -X GET \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  https://api.taiga.io/api/v1/projects/1/issues_stats
                            

The HTTP response is a 200 OK and the response body is a JSON project issue stats object.


{
    "total_issues": 668,
    "opened_issues": 108,
    "closed_issues": 560,
    "issues_per_type": {
        "1": {
            "id": 1,
            "name": "Bug",
            "color": "#f57900",
            "count": 501
        },
        (...)
    },
    "issues_per_status": {
        "1": {
            "id": 1,
            "name": "New",
            "color": "#8C2318",
            "count": 90
        },
        (...)
    },
    "issues_per_owner": {
        "11": {
            "id": 11,
            "name": "Anler Hern\u00e1ndez Peral",
            "username": "anler.hernandez",
            "color": "#8f0030",
            "count": 1
        },
        (...)
    },
    "issues_per_assigned_to": {
        "0": {
            "id": 0,
            "name": "Unassigned",
            "username": "Unassigned",
            "color": "black",
            "count": 185
        },
        (...)
    },
    "issues_per_priority": {
        "1": {
            "id": 1,
            "name": "Low",
            "color": "#888a85",
            "count": 50
        },
        (...)
    },
    "issues_per_severity": {
        "1": {
            "id": 1,
            "name": "Wishlist",
            "color": "#888a85",
            "count": 9
        },
        (...)
    },
    "last_four_weeks_days": {
        "by_open_closed": {
            "closed": [8, 3, 2, 0, (...)],
            "open": [7, 3, 2, 0, (...)]
        },
        "by_priority": {
            "1": {
                "id": 1,
                "name": "Low",
                "color": "#888a85",
                "data": [18, 17, 17, (...)]
            },
        },
        "by_severity": {
            "1": {
                "id": 1,
                "name": "Wishlist",
                "color": "#888a85",
                "data": [8, 8, 8, (...)]
            },
        },
        "by_status": {(...)}
    },
}
                            

10.1. List Project

To list projects send a GET request with the following parameters:


curl -X GET \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  https://api.taiga.io/api/v1/projects
                            

The HTTP response is a 200 OK and the response body is a JSON list of project list entry objects


[
    {
        "anon_permissions": [],
        "created_date": "2014-09-16T15:39:49+0000",
        "creation_template": 1,
        "default_issue_status": 574,
        "default_issue_type": 127,
        "default_points": 977,
        "default_priority": 245,
        "default_severity": 408,
        "default_task_status": 411,
        "default_us_status": 352,
        "description": "Taiga",
        "i_am_owner": true,
        "id": 87,
        "is_backlog_activated": false,
        "is_issues_activated": true,
        "is_kanban_activated": true,
        "is_private": false,
        "is_liked": true,
        "is_watched": true,
        "is_wiki_activated": true,
        "members": [
            2,
              120,
              (...)
        ],
        "modified_date": "2014-10-29T07:35:38+0000",
        "my_permissions": [
            "admin_project_values",
            "view_tasks",
            "view_milestones",
            "view_project",
            (...)
        ],
        "name": "AIL",
        "owner": 2,
        "public_permissions": [],
        "slug": "ail",
        "likes": 3,
        "tags": null,
        "tags_colors": {
            "api": "#ce5c00",
            "cuidador": "#204a87",
            "gestor": "#73d216",
            (...)
        },
        "total_milestones": 3,
        "total_story_points": 20.0,
        "videoconferences": "appear-in",
        "videoconferences_salt": null,
        "voters": 1,
        "watchers": [
            7,
            (...)
        ]
    },
    (...)
]
                            

The results can be filtered using the following parameters:

  • member: user id
  • members: user ids

curl -X GET \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  https://api.taiga.io/api/v1/projects?member=1
                            

The results can be ordered using the order_by parameter with the values:

  • memberships__user_order: the project order specified by the user

curl -X GET \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  https://api.taiga.io/api/v1/projects?member=1&order_by=memberships__user_order
                            

How can I draw the graphs?


Requests

HTTP for Humans















Let's build it...


#!/usr/bin/env python

from urllib.parse import urljoin
import sys
import copy
import getpass
from datetime import datetime, timedelta

import requests
import pygal


BASE_HEADERS = {
    "content-type": "application/json; charset: utf8",
    "X-DISABLE-PAGINATION": "true",
}

API_HOST = "https://api.taiga.io/"

URLS = {
    "auth": "/api/v1/auth",
    "projects": "/api/v1/projects",
    "project-issues-stats": "/api/v1/projects/{}/issues_stats",
}


if __name__ == "__main__":
    headers = copy.deepcopy(BASE_HEADERS)

    # Login
    username = input("Type your username or email:\n> ")
    password = getpass.getpass("Type your password:\n> ")

    url = urljoin(API_HOST, URLS["auth"])
    data = {
        "username": username,
        "password": password,
        "type": "normal"
    }
    response = requests.post(url, json=data, headers=headers)
    me = response.json()

    headers["Authorization"] = "Bearer {}".format(me["auth_token"])

    # List all my projects
    url = urljoin(API_HOST, URLS["projects"])
    params = {
        "member": me["id"]
    }
    response = requests.get(url, params=params, headers=headers)
    projects = response.json()

    project_list_display = "\n".join(
        ["  {} - {}".format(i, p["name"]) for i, p in enumerate(projects)]
    )
    index = int(input("Select project: \n{}\n> ".format(project_list_display)))
    project = projects[index]

    # Get issues stats data
    url = urljoin(API_HOST, URLS["project-issues-stats"].format(project["id"]))
    response = requests.get(url, headers=headers)
    issues_stats = response.json()

    # GRAPHS
    style = pygal.style.NeonStyle()
    style.background = "transparent"

    # Draw graph: Issues by status
    chart = pygal.Pie(show_legend=False, style=style)
    chart.title = "Issues by Status in {}".format(project["name"])
    for item in issues_stats["issues_per_status"].values():
        chart.add(item["name"], [{
            "value": item["count"],
            "color": item["color"]
        }])
    chart.render_to_file("../../output/chart_issues_by_status.svg")
    print("Generate graph: 'Issues by Status in {}'".format(project["name"]))

    # Draw graph: Issues per Assigned to
    chart = pygal.HorizontalBar(style=style)
    chart.title = "Issues by Assignation in {}".format(project["name"])
    for item in issues_stats["issues_per_assigned_to"].values():
        if item["count"] > 0:
            chart.add(item["name"], item["count"])
    chart.render_to_file("../../output/chart_issues_by_assigned_to.svg")
    print("Generate graph: 'Issues by Assignation in {}'".format(project["name"]))

    # Draw graph: Issues open/closed last month
    today = datetime.today()
    last_four_weeks = [today - timedelta(days=x) for x in range(28, 0, -1)]

    chart = pygal.Dot(x_label_rotation=30, style=style)
    chart.title = "Issues Open/Closed last 4 weeks in {}".format(project["name"])
    chart.x_labels = [d.strftime("%Y %b %d") for d in last_four_weeks]
    chart.add("Open",
              issues_stats["last_four_weeks_days"]["by_open_closed"]["open"])
    chart.add("Closed",
              issues_stats["last_four_weeks_days"]["by_open_closed"]["closed"])
    chart.render_to_file("../../output/chart_issues_open_close_last_4_weeks.svg")
    print("Generate graph: 'Issues Open/Closed last 4 weeks in {}'".format(
                                                            project["name"]))
                        

...you can do it better!!!

But you are not alone...















...use the community.















nephila/python-taiga

A Python module for communicating with the Taiga API


#!/usr/bin/env python

import getpass
from datetime import datetime, timedelta

from taiga import TaigaAPI
import pygal


API_HOST = "https://api.taiga.io/"


if __name__ == "__main__":
    api = TaigaAPI(host=API_HOST)

    # Login
    username = input("Type your username or email:\n> ")
    password = getpass.getpass("Type your password:\n> ")

    api.auth(username=username, password=password)
    me = api.me()

    # List all my projects
    projects = api.projects.list(member=me.id)

    project_list_display = "\n".join(
        ["  {} - {}".format(i, p.name) for i, p in enumerate(projects)]
    )
    index = int(input("Select project: \n{}\n> ".format(project_list_display)))
    project = projects[index]

    # Get issues stats data
    issues_stats = project.issues_stats()

    # GRAPHS
    style = pygal.style.NeonStyle()
    style.background = "transparent"

    # Draw graph: Issues by status
    chart = pygal.Pie(show_legend=False, style=style)
    chart.title = "Issues by Status in {}".format(project.name)
    for item in issues_stats["issues_per_status"].values():
        chart.add(item["name"], [{
            "value": item["count"],
            "color": item["color"]
        }])
    chart.render_to_file("../../output/chart_issues_by_status.v2.svg")
    print("Generate graph: 'Issues by Status in {}'".format(project.name))

    # Draw graph: Issues per Assigned to
    chart = pygal.HorizontalBar(style=style)
    chart.title = "Issues by Assignation in {}".format(project.name)
    for item in issues_stats["issues_per_assigned_to"].values():
        if item["count"] > 0:
            chart.add(item["name"], item["count"])
    chart.render_to_file("../../output/chart_issues_by_assigned_to.v2.svg")
    print("Generate graph: 'Issues by Assignation in {}'".format(project.name))

    # Draw graph: Issues open/closed last month
    today = datetime.today()
    last_four_weeks = [today - timedelta(days=x) for x in range(28, 0, -1)]

    chart = pygal.Dot(x_label_rotation=30, style=style)
    chart.title = "Issues Open/Closed last 4 weeks in {}".format(project.name)
    chart.x_labels = [d.strftime("%Y %b %d") for d in last_four_weeks]
    chart.add("Open",
              issues_stats["last_four_weeks_days"]["by_open_closed"]["open"])
    chart.add("Closed",
              issues_stats["last_four_weeks_days"]["by_open_closed"]["closed"])
    chart.render_to_file(
        "../../output/chart_issues_open_close_last_4_weeks.v2.svg")
    print("Generate graph: 'Issues Open/Closed last 4 weeks in {}'".format(
                                                               project.name))
                        

Not really...















We are ninjas.

Create issues from the terminal


I want to...

...create issues in a Taiga project with a command from my terminal.

We use..

nephila/python-taiga

+


#!/usr/bin/env python

from taiga import TaigaAPI
import click

API_HOST = "https://api.taiga.io/"
api = TaigaAPI(host=API_HOST)


@click.group()
@click.option("--username", "-u", prompt=True, help="taiga.io useername or email")
@click.option("--password", "-p", prompt=True, hide_input=True,
              help="taiga.io password")
def cli(username, password):
    """The taiga command interface."""
    api.auth(username=username, password=password)


@cli.command()
@click.option("--project", "-p", "project_slug", prompt=True, help="project slug")
@click.option("--subject", "-s", prompt=True, help="issue subject")
@click.option("--description", "-d", help="issue description")
@click.option("--attachments", "-a", multiple=True, help="issue description")
def add_issue(project_slug, subject, description, attachments):
    """Create a new issue."""
    click.echo("  Getting project '{}'...".format(project_slug))
    project = api.projects.list().get(slug=project_slug)

    click.echo("  Creatting issue...'{}'".format(subject))
    description = description or click.edit("...write description here..") or ""
    issue = project.add_issue(subject,
                              project.default_priority,
                              project.default_issue_status,
                              project.default_issue_type,
                              project.default_severity,
                              description=description)

    if attachments:
        with click.progressbar(attachments,
                               label="  Attaching files...") as progressbar:
            for att in progressbar:
                issue.attach(att)


if __name__ == "__main__":
    cli()
                        

./taiga-ci
						
Usage: taiga-ci [OPTIONS] COMMAND [ARGS]...

  The taiga command interface.

Options:
  -u, --username TEXT  taiga.io useername or email
  -p, --password TEXT  taiga.io password
  --help               Show this message and exit.

Commands:
  add_issue  Create a new issue.
						

./taiga-ci add_issue --help
						
Username: bameda
Password: 
Usage: taiga-ci add_issue [OPTIONS]

  Create a new issue.

Options:
  -p, --project TEXT      project slug
  -s, --subject TEXT      issue subject
  -d, --description TEXT  issue description
  -a, --attachments TEXT  issue description
  --help                  Show this message and exit.
						

./taiga-ci add_issue -p sandbox \
                     -s "Test add issue cmd" \
                     -a ~/sample1.txt \
                     -a ~/sample2.txt
						
Username: bameda
Password:
  Getting project 'sandbox'...
  Creatting issue...'Test add issue cmd'
  Attaching files...  [####################################]  100%
						

Tell me more...

You can create a curses client

with

taigaio/taiga-ncurses

Taiga curses client for terminal lovers ;-)

...much more...


rochsystems/sentry-taiga

Creates issues in Taiga from Sentry exceptions.



ansible/taiga_issue module

create issues from Ansible (by lekum)

...and what's about webhooks?

Can I add new features?

Yes... creating plugins...

...and how I can contribute?

  • Feedback
  • UI enhancements
  • Bug reports
  • Code patches
  • Patch reviews
  • Creating plugins
  • Creating themes
  • Translations
  • Documentation improvements

Resources

Thank you!!!















Hacking the Taiga

Version 1.0 2015-12-04
© 2015 David Barragán Merino



Creative Commons License This work is licensed under a Creative Commons Attribution 4.0 International License.
Based on a work at https://github.com/bameda/slides/tree/master/2015/pycones_2015/hacking_the_taiga.

Hacking the Taiga

by  David Barragán Merino /  @bameda