From c81999adceb279572c0146f908b7beff3329a4fd Mon Sep 17 00:00:00 2001 From: Justin Forest Date: Wed, 12 Dec 2012 01:16:08 +0400 Subject: [PATCH 1/2] Support for TeamLab TeamLab is an open source collaboration platform, which includes a task manager. This change adds basic support for that service (no annotations currenty). --- bugwarrior/README.rst | 13 +++- bugwarrior/services/__init__.py | 2 + bugwarrior/services/teamlab.py | 113 ++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 bugwarrior/services/teamlab.py diff --git a/bugwarrior/README.rst b/bugwarrior/README.rst index 5966c4937..4910bf50a 100644 --- a/bugwarrior/README.rst +++ b/bugwarrior/README.rst @@ -12,6 +12,7 @@ It currently supports the following remote resources: - `trac `_ - `bugzilla `_ - `megaplan `_ + - `teamlab `_ Configuring ----------- @@ -102,7 +103,7 @@ Create a ``~/.bugwarriorrc`` file with the following contents. bugzilla.username = rbean@redhat.com bugzilla.password = OMG_LULZ - # Here/s an example of a megaplan target. + # Here's an example of a megaplan target. [my_megaplan] service = megaplan @@ -113,6 +114,16 @@ Create a ``~/.bugwarriorrc`` file with the following contents. default_priority = H project_name = example + # Here's an example of a teamlab target. + [my_teamlab] + service = teamlab + + hostname = teamlab.example.com + login = alice + password = secret + + project_name = example_teamlab + .. example diff --git a/bugwarrior/services/__init__.py b/bugwarrior/services/__init__.py index be243744b..524569916 100644 --- a/bugwarrior/services/__init__.py +++ b/bugwarrior/services/__init__.py @@ -88,6 +88,7 @@ def get_owner(self, issue): from bitbucket import BitbucketService from trac import TracService from bz import BugzillaService +from teamlab import TeamLabService # Constant dict to be used all around town. @@ -96,6 +97,7 @@ def get_owner(self, issue): 'bitbucket': BitbucketService, 'trac': TracService, 'bugzilla': BugzillaService, + 'teamlab': TeamLabService, } diff --git a/bugwarrior/services/teamlab.py b/bugwarrior/services/teamlab.py new file mode 100644 index 000000000..ccdc73842 --- /dev/null +++ b/bugwarrior/services/teamlab.py @@ -0,0 +1,113 @@ +from twiggy import log + +from bugwarrior.services import IssueService +from bugwarrior.config import die + +import datetime +import json +import urllib +import urllib2 + + +class Client(object): + def __init__(self, hostname, verbose=False): + self.hostname = hostname + self.verbose = verbose + self.token = None + + def authenticate(self, login, password): + resp = self.call_api("/api/1.0/authentication.json", post={ + "userName": str(login), + "password": str(password), + }) + + self.token = str(resp["token"]) + + def get_task_list(self): + resp = self.call_api("/api/1.0/project/task/@self.json") + return resp + + def call_api(self, uri, post=None, get=None): + uri = "http://" + self.hostname + uri + + if post is None: + data = None + else: + data = urllib.urlencode(post) + + if get is not None: + uri += "?" + urllib.urlencode(get) + + self.log("Fetching %s" % uri) + + req = urllib2.Request(uri, data) + if self.token is not None: + req.add_header("Authorization", self.token) + req.add_header("Accept", "application/json") + + res = urllib2.urlopen(req) + if res.getcode() >= 400: + raise Exception("Error accessing the API: %s" % res.read()) + + response = res.read() + + return json.loads(response)["response"] + + def log(self, message): + if self.verbose: + print message + + +class TeamLabService(IssueService): + def __init__(self, *args, **kw): + super(TeamLabService, self).__init__(*args, **kw) + + self.hostname = self.config.get(self.target, 'hostname') + _login = self.config.get(self.target, 'login') + _password = self.config.get(self.target, 'password') + + self.client = Client(self.hostname) + self.client.authenticate(_login, _password) + + self.project_name = self.hostname + if self.config.has_option(self.target, "project_name"): + self.project_name = self.config.get(self.target, "project_name") + + @classmethod + def validate_config(cls, config, target): + for k in ('login', 'password', 'hostname'): + if not config.has_option(target, k): + die("[%s] has no '%s'" % (target, k)) + + IssueService.validate_config(config, target) + + def get_issue_url(self, issue): + return "http://%s/products/projects/tasks.aspx?prjID=%d&id=%d" \ + % (self.hostname, issue["projectOwner"]["id"], issue["id"]) + + def get_priority(self, issue): + if issue["priority"] == 1: + return "H" + else: + return "M" + + def issues(self): + issues = self.client.get_task_list() + log.debug(" Remote has {0} total issues.", len(issues)) + if not issues: + return [] + + # Filter out closed tasks. + issues = filter(lambda i: i["status"] == 1, issues) + log.debug(" Remote has {0} active issues.", len(issues)) + + return [dict( + description=self.description( + issue["title"], + self.get_issue_url(issue), + issue["id"], cls="issue", + ), + project=self.project_name, + priority=self.get_priority(issue), + ) for issue in issues] + From 39aeb8b098c7785b919d34d5cac64f76b70c0858 Mon Sep 17 00:00:00 2001 From: Justin Forest Date: Wed, 12 Dec 2012 02:44:01 +0400 Subject: [PATCH 2/2] Support for RedMine --- bugwarrior/README.rst | 9 ++++ bugwarrior/services/__init__.py | 2 + bugwarrior/services/redmine.py | 79 +++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 bugwarrior/services/redmine.py diff --git a/bugwarrior/README.rst b/bugwarrior/README.rst index 4910bf50a..c7bc44a11 100644 --- a/bugwarrior/README.rst +++ b/bugwarrior/README.rst @@ -13,6 +13,7 @@ It currently supports the following remote resources: - `bugzilla `_ - `megaplan `_ - `teamlab `_ + - `redmine `_ Configuring ----------- @@ -124,6 +125,14 @@ Create a ``~/.bugwarriorrc`` file with the following contents. project_name = example_teamlab + # Here's an example of a redmine target. + [my_redmine] + service = redmine + url = http://redmine.example.org/ + key = c0c4c014cafebabe + user_id = 7 + project_name = redmine + .. example diff --git a/bugwarrior/services/__init__.py b/bugwarrior/services/__init__.py index 524569916..5a05a742d 100644 --- a/bugwarrior/services/__init__.py +++ b/bugwarrior/services/__init__.py @@ -89,6 +89,7 @@ def get_owner(self, issue): from trac import TracService from bz import BugzillaService from teamlab import TeamLabService +from redmine import RedMineService # Constant dict to be used all around town. @@ -98,6 +99,7 @@ def get_owner(self, issue): 'trac': TracService, 'bugzilla': BugzillaService, 'teamlab': TeamLabService, + 'redmine': RedMineService, } diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py new file mode 100644 index 000000000..516533598 --- /dev/null +++ b/bugwarrior/services/redmine.py @@ -0,0 +1,79 @@ +from twiggy import log + +from bugwarrior.services import IssueService +from bugwarrior.config import die + +import datetime +import urllib +import urllib2 +import json + + +class Client(object): + def __init__(self, url, key): + self.url = url + self.key = key + + def find_issues(self, user_id=None): + args = {} + if user_id is not None: + args["assigned_to_id"] = user_id + return self.call_api("/issues.json", args)["issues"] + + def call_api(self, uri, get=None): + url = self.url.rstrip("/") + uri + + if get: + url += "?" + urllib.urlencode(get) + + req = urllib2.Request(url) + req.add_header("X-Redmine-API-Key", self.key) + + res = urllib2.urlopen(req) + + return json.loads(res.read()) + + +class RedMineService(IssueService): + def __init__(self, *args, **kw): + super(RedMineService, self).__init__(*args, **kw) + + self.url = self.config.get(self.target, 'url').rstrip("/") + self.key = self.config.get(self.target, 'key') + self.user_id = self.config.get(self.target, 'user_id') + + self.client = Client(self.url, self.key) + + self.project_name = None + if self.config.has_option(self.target, "project_name"): + self.project_name = self.config.get(self.target, "project_name") + + @classmethod + def validate_config(cls, config, target): + for k in ('url', 'key', 'user_id'): + if not config.has_option(target, k): + die("[%s] has no '%s'" % (target, k)) + + IssueService.validate_config(config, target) + + def get_issue_url(self, issue): + return self.url + "/issues/" + str(issue["id"]) + + def get_project_name(self, issue): + if self.project_name: + return self.project_name + return issue["project"]["name"] + + def issues(self): + issues = self.client.find_issues(self.user_id) + log.debug(" Found {0} total.", len(issues)) + + return [dict( + description=self.description( + issue["subject"], + self.get_issue_url(issue), + issue["id"], cls="issue", + ), + project=self.get_project_name(issue), + priority=self.default_priority, + ) for issue in issues]