Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Kanboard service (fixes: #223) #794

Merged
merged 5 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ matrix:
env: JIRAVERSION=1.0.10

install:
- pip install .[jira,megaplan,activecollab,bts,bugzilla,trac,gmail]
- pip install .[jira,megaplan,activecollab,bts,bugzilla,trac,gmail,kanboard]
- pip install codecov
- pip install coverage
script: nosetests -w tests --with-coverage --cover-branches --cover-package=bugwarrior -v
Expand Down
2 changes: 2 additions & 0 deletions bugwarrior/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ It currently supports the following remote resources:
- `gitlab <https://gitlab.com>`_ (api v3)
- `gmail <https://www.google.com/gmail/about/>`_
- `jira <https://www.atlassian.com/software/jira/overview>`_
- `kanboard <https://kanboard.org/>`_
- `megaplan <https://www.megaplan.ru/>`_
- `pagure <https://pagure.io/>`_
- `phabricator <http://phabricator.org/>`_
Expand Down Expand Up @@ -72,3 +73,4 @@ Contributors
- BinaryBabel (contributed support for YouTrack)
- Matthew Cengia (contributed extra support for Trello)
- Andrew Demas (contributed support for PivotalTracker)
- Florian Preinstorfer (contributed support for Kanboard)
15 changes: 15 additions & 0 deletions bugwarrior/docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ Example Configuration
jira.version = 5
jira.add_tags = enterprisey,work


# This is a kanboard example.
[my_kanboard]
service = kanboard
kanboard.url = https://kanboard.example.org
kanboard.username = ralphbean

# Your password or, even better, API token
kanboard.password = my_api_token

# A custom query to search for open issues. By default, assigned and open
# tasks are queried.
kanboard.query = status:open assignee:me


# Here's an example of a phabricator target
[my_phabricator]
service = phabricator
Expand Down
3 changes: 2 additions & 1 deletion bugwarrior/docs/getting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Installing from https://pypi.python.org/pypi/bugwarrior is easy with

By default, ``bugwarrior`` will be installed with support for the following
services: Bitbucket, Github, Gitlab, Pagure, Phabricator, Redmine, Teamlab, and
Versionone. There is optional support for Jira, Megaplan.ru, Active Collab,
Versionone. There is optional support for Jira, Kanboard, Megaplan.ru, Active Collab,
Debian BTS, Trac, Bugzilla, and but those require extra dependencies that are
installed by specifying ``bugwarrior[service]`` in the commands above. For
example, if you want to use bugwarrior with Jira::
Expand All @@ -43,6 +43,7 @@ The following extra dependency sets are available:

- keyring (See also `linux installation instructions <https://github.com/jaraco/keyring#linux>`_.)
- jira
- kanboard
- megaplan
- activecollab
- bts
Expand Down
61 changes: 61 additions & 0 deletions bugwarrior/docs/services/kanboard.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Kanboard
========

You can import tasks from your Kanboard instance using the ``kanboard`` service name.

Additional Requirements
-----------------------

Install the following package using ``pip``:

* ``kanboard``

Example Service
---------------

Here's an example of a Kanboard project::

[my_issue_tracker]
service = kanboard
kanboard.url = https://kanboard.example.org
kanboard.username = ralph
kanboard.password = my_api_token

The above example is the minimum required to import issues from Kanboard. It is
recommended to use a personal API token instead of a password, which can be
created on the Kanboard settings page. You can also feel free to use any of the
configuration options described in `Service Features`_ below.

Service Features
----------------

Specify the Query to Use for Gathering Issues
+++++++++++++++++++++++++++++++++++++++++++++

By default, all open issues assigned to the specified username are imported.
One may use the `kanboard.query` parameter to modify the search query.

For example, to import all open issues assigned to 'frank', use the following
configuration option::

kanboard.query = status:open assignee:frank


Provided UDA Fields
-------------------

+-----------------------------+---------------------+---------------------+
| Field Name | Description | Type |
+=============================+=====================+=====================+
| ``kanboardtaskid`` | Task ID | Number (numeric) |
+-----------------------------+---------------------+---------------------+
| ``kanboardtasktitle`` | Task Title | Text (string) |
+-----------------------------+---------------------+---------------------+
| ``kanboardtaskdescription`` | Task Description | Text (string) |
+-----------------------------+---------------------+---------------------+
| ``kanboardprojectid`` | Project ID | Number (numeric) |
+-----------------------------+---------------------+---------------------+
| ``kanboardprojectname`` | Project Name | Text (string) |
+-----------------------------+---------------------+---------------------+
| ``kanboardurl`` | URL | Text (string) |
+-----------------------------+---------------------+---------------------+
169 changes: 169 additions & 0 deletions bugwarrior/services/kanboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import datetime
import logging
import re
from urllib.parse import urlparse

from dateutil.tz.tz import tzutc
from kanboard import Client

from bugwarrior.config import die
from bugwarrior.services import Issue, IssueService

log = logging.getLogger(__name__)


class KanboardIssue(Issue):
TASK_ID = "kanboardtaskid"
TASK_TITLE = "kanboardtasktitle"
TASK_DESCRIPTION = "kanboardtaskdescription"
PROJECT_ID = "kanboardprojectid"
PROJECT_NAME = "kanboardprojectname"
URL = "kanboardurl"

UDAS = {
TASK_ID: {"type": "numeric", "label": "Kanboard Task ID"},
TASK_TITLE: {"type": "string", "label": "Kanboard Task Title"},
TASK_DESCRIPTION: {"type": "string", "label": "Kanboard Task Description"},
PROJECT_ID: {"type": "numeric", "label": "Kanboard Project ID"},
PROJECT_NAME: {"type": "string", "label": "Kanboard Project Name"},
URL: {"type": "string", "label": "Kanboard URL"},
}
UNIQUE_KEY = (TASK_ID,)

PRIORITY_MAP = {"0": None, "1": "L", "2": "M", "3": "H"}

def to_taskwarrior(self):
return {
"project": self.get_project(),
"priority": self.get_priority(),
"annotations": self.get_annotations(),
"tags": self.get_tags(),
"due": self.get_due(),
"entry": self.get_entry(),
self.TASK_ID: self.get_task_id(),
self.TASK_TITLE: self.get_task_title(),
self.TASK_DESCRIPTION: self.get_task_description(),
self.PROJECT_ID: self.get_project_id(),
self.PROJECT_NAME: self.get_project_name(),
self.URL: self.get_url(),
}

def get_default_description(self):
return self.build_default_description(
title=self.get_task_title(),
url=self.get_processed_url(self.get_url()),
number=self.get_task_id(),
)

def get_task_id(self):
return int(self.record["id"])

def get_task_title(self):
return self.record["title"]

def get_task_description(self):
return self.record["description"]

def get_project_id(self):
return int(self.record["project_id"])

def get_project_name(self):
return self.record["project_name"]

def get_project(self):
value = self.get_project_name()
value = re.sub(r"[^a-zA-Z0-9]", "_", value)
return value.strip("_")

def get_url(self):
return self.extra["url"]

def get_tags(self):
return self.extra.get("tags", [])

def get_due(self):
return self._convert_timestamp_from_field("date_due")

def get_entry(self):
return self._convert_timestamp_from_field("date_creation")

def get_annotations(self):
return self.extra.get("annotations", [])

def _convert_timestamp_from_field(self, field):
timestamp = int(self.record.get(field, 0))
if timestamp:
return (
datetime.datetime.fromtimestamp(timestamp)
.astimezone(tzutc())
.replace(microsecond=0)
)


class KanboardService(IssueService):
ISSUE_CLASS = KanboardIssue
CONFIG_PREFIX = "kanboard"

def __init__(self, *args, **kw):
super().__init__(*args, **kw)
username = self.config.get("username")
password = self.get_password("password", username)
url = self.config.get("url").rstrip("/")
self.client = Client(f"{url}/jsonrpc.php", username, password)
default_query = f"status:open assignee:{username}"
self.query = self.config.get("query", default_query)

def annotations(self, task, url):
comments = []
if int(task.get("nb_comments", 0)):
comments = self.client.get_all_comments(**{"task_id": task["id"]})
return self.build_annotations(
((c["name"], c["comment"]) for c in comments), url
)

def issues(self):
# The API provides only a per-project search. Retrieve the list of
# projects first and query each project in turn.
projects = self.client.get_my_projects_list()
tasks = []
for project_id, project_name in projects.items():
log.debug(
"Search for tasks in project %r using query %r",
project_name,
self.query,
)
params = {"project_id": project_id, "query": self.query}
response = self.client.search_tasks(**params)
log.debug("Found %d task(s) in project %r", len(response), project_name)
tasks.extend(response)

for task in tasks:
task_id = task["id"]
extra = {}

# Resolve a task's URL.
response = self.client.get_task(task_id=task_id)
extra["url"] = response["url"]

# Resolve a task's tags.
response = self.client.get_task_tags(task_id=task_id)
extra["tags"] = [v for v in response.values()]

# Resolve a task's comments.
extra["annotations"] = self.annotations(task, extra["url"])

yield self.get_issue_for_record(task, extra)

@classmethod
def validate_config(cls, service_config, target):
for option in ("url", "username", "password"):
if option not in service_config:
die(f"[{target}] has no 'kanboard.{option}'")

IssueService.validate_config(service_config, target)

@staticmethod
def get_keyring_service(service_config):
parsed = urlparse(service_config.get("url"))
username = service_config.get("username")
return f"kanboard://{username}@{parsed.netloc}"
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"bugzilla": ["python-bugzilla>=2.0.0"],
"gmail": ["google-api-python-client", "google-auth-oauthlib"],
"jira": ["jira>=0.22"],
"kanboard": ["kanboard"],
"keyring": ["keyring"],
"megaplan": ["megaplan>=1.4"],
"phabricator": ["phabricator"],
Expand All @@ -60,7 +61,8 @@
"bugwarrior[gmail]",
"bugwarrior[trac]",
"bugwarrior[bugzilla]",
"bugwarrior[phabricator]"
"bugwarrior[phabricator]",
"bugwarrior[kanboard]",
],
test_suite='nose.collector',
entry_points="""
Expand All @@ -75,6 +77,7 @@
trac=bugwarrior.services.trac:TracService
bts=bugwarrior.services.bts:BTSService
bugzilla=bugwarrior.services.bz:BugzillaService
kanboard=bugwarrior.services.kanboard:KanboardService
teamlab=bugwarrior.services.teamlab:TeamLabService
redmine=bugwarrior.services.redmine:RedMineService
activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service
Expand Down
Loading