Skip to content

Commit

Permalink
Merge pull request #56 from kostajh/activecollab3
Browse files Browse the repository at this point in the history
Add activeCollab 3.x support
  • Loading branch information
ralphbean committed Feb 25, 2013
2 parents eedb0f8 + 00b6f78 commit 2e6fabc
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 3 deletions.
27 changes: 24 additions & 3 deletions bugwarrior/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ It currently supports the following remote resources:
- `teamlab <http://www.teamlab.com/>`_
- `redmine <http://www.redmine.org/>`_
- `jira <http://www.atlassian.com/software/jira/overview>`_
- `activecollab 2.x <http://www.activecollab.com>`_
- `activecollab <http://www.activecollab.com>`_ (2.x and 3.x)

Configuring
-----------
Expand Down Expand Up @@ -128,7 +128,7 @@ Create a ``~/.bugwarriorrc`` file with the following contents.
# Here's an example of a jira project. The ``jira-python`` module is
# a bit particular, and jira deployments, like Bugzilla, tend to be
# reasonably customized. So YMMV. The ``base_uri`` must not have a
# have a trailing slash. In this case we fetch comments and
# have a trailing slash. In this case we fetch comments and
# cases from jira assigned to 'ralph' where the status is not closed or
# resolved.
[jira.project]
Expand Down Expand Up @@ -157,8 +157,29 @@ Create a ``~/.bugwarriorrc`` file with the following contents.
user_id = 7
project_name = redmine

# Here's an example of an activecollab3 target. This is only valid for
# activeCollab 3.x, see below for activeCollab 2.x.
#
# Obtain your user ID and API url by logging in, clicking on your avatar on
# the lower left-hand of the page. When on that page, look at the URL. The
# number that appears after "/user/" is your user ID.
#
# On the same page, go to Options and API Subscriptions. Generate a read-only
# API key and add that to your bugwarriorrc file.
#
# Bugwarrior will only gather tasks and subtasks for projects in your "Favorites"
# list. Note that if you have 10 projects in your favorites list, bugwarrior
# will make 21 API calls on each run: 1 call to get a list of favorites, then
# 2 API calls per projects, one for tasks and one for subtasks.

[activecollab3]
service = activecollab3
url = https://ac.example.org/api.php
key = your-api-key
user_id = 15

# Here's an example of an activecollab2 target. Note that this will only work
# with ActiveCollab 2.x and *not* with ActiveCollab 3.x.
# with ActiveCollab 2.x - see above for 3.x.
#
# You can obtain your user ID and API url by logging into ActiveCollab and
# clicking on "Profile" and then "API Settings". When on that page, look
Expand Down
2 changes: 2 additions & 0 deletions bugwarrior/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def get_owner(self, issue):
from redmine import RedMineService
from jira import JiraService
from activecollab2 import ActiveCollab2Service
from activecollab3 import ActiveCollab3Service


# Constant dict to be used all around town.
Expand All @@ -137,6 +138,7 @@ def get_owner(self, issue):
'redmine': RedMineService,
'jira': JiraService,
'activecollab2': ActiveCollab2Service,
'activecollab3': ActiveCollab3Service,
}


Expand Down
205 changes: 205 additions & 0 deletions bugwarrior/services/activecollab3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from twiggy import log

from bugwarrior.services import IssueService
from bugwarrior.config import die
from bugwarrior.db import MARKUP

import urllib2
import time
import json
import datetime

api_count = 0
task_count = 0

class ActiveCollabApi():
def call_api(self, uri, key, url):
global api_count
api_count += 1
url = url.rstrip("/") + "?auth_api_token=" + key + "&path_info=" + uri + "&format=json"
req = urllib2.Request(url)
res = urllib2.urlopen(req)
return json.loads(res.read())

class Client(object):
def __init__(self, url, key, user_id, projects):
self.url = url
self.key = key
self.user_id = user_id
self.projects = projects

# Return a UNIX timestamp from an ActiveCollab date
def format_date(self, date):
if date is None:
return
d = datetime.datetime.fromtimestamp(time.mktime(time.strptime(
date, "%Y-%m-%d")))
timestamp = int(time.mktime(d.timetuple()))
return timestamp

# Return a priority of L, M, or H based on AC's priority index of -2 to 2
def format_priority(self, priority):
priority = str(priority)
priority_map = {'-2': 'L', '-1': 'L', '0': 'M', '1': 'H', '2': 'H'}
return priority_map[priority]

def find_issues(self, user_id=None, project_id=None, project_name=None):
ac = ActiveCollabApi()
user_tasks_data = []
user_subtasks_data = []
user_tasks_data = ac.call_api("/projects/" + str(project_id) + "/tasks", self.key, self.url)
user_subtasks_data = ac.call_api("/projects/" + str(project_id) + "/subtasks", self.key, self.url)
global task_count
assigned_tasks = []

if user_tasks_data:
for key, task in enumerate(user_tasks_data):
task_count += 1
assigned_task = dict()
# Load Task data
# @todo Implement threading here.
if ((task[u'assignee_id'] == int(self.user_id)) and (task[u'completed_on'] is None)):
assigned_task['permalink'] = task[u'permalink']
assigned_task['task_id'] = task[u'task_id']
assigned_task['id'] = task[u'id']
assigned_task['project_id'] = task[u'project_id']
assigned_task['project'] = project_name
assigned_task['description'] = task[u'name']
assigned_task['type'] = "task"
assigned_task['created_on'] = task[u'created_on'][u'mysql']
assigned_task['created_by_id'] = task[u'created_by_id']
if 'priority' in task:
assigned_task['priority'] = self.format_priority(task[u'priority'])
else:
assigned_task['priority'] = self.default_priority
if task[u'due_on'] is not None:
assigned_task['due'] = self.format_date(task[u'due_on'][u'mysql'])
if assigned_task:
assigned_tasks.append(assigned_task)

# Subtasks
if user_subtasks_data:
for key, subtask in enumerate(user_subtasks_data):
task_count += 1
assigned_task = dict()
if ((subtask[u'assignee_id'] == int(self.user_id)) and (subtask[u'completed_on'] is None)):
# Get permalink
assigned_task['permalink'] = (self.url).rstrip('api.php') + 'projects/' + str(project_id) + '/tasks'
if assigned_tasks:
for k, t in enumerate(assigned_tasks):
if 'id' in t:
if subtask[u'parent_id'] == t[u'id']:
assigned_task['permalink'] = t[u'permalink']
assigned_task['task_id'] = subtask[u'id']
assigned_task['project'] = project_name
assigned_task['project_id'] = project_id
assigned_task['description'] = subtask['body']
assigned_task['type'] = 'subtask'
assigned_task['created_on'] = subtask[u'created_on']
assigned_task['created_by_id'] = subtask[u'created_by_id']
if 'priority' in subtask:
assigned_task['priority'] = self.format_priority(subtask[u'priority'])
else:
assigned_task['priority'] = self.default_priority
if subtask[u'due_on'] is not None:
assigned_task['due'] = self.format_date(subtask[u'due_on'])
if assigned_task:
assigned_tasks.append(assigned_task)

return assigned_tasks

class ActiveCollab3Service(IssueService):
def __init__(self, *args, **kw):
super(ActiveCollab3Service, 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')

# Get a list of favorite projects
projects = []
ac = ActiveCollabApi()
data = ac.call_api("/projects", self.key, self.url)
for item in data:
if item[u'is_favorite'] == 1:
projects.append(dict([(item[u'id'], item[u'name'])]))

self.projects = projects

self.client = Client(self.url, self.key, self.user_id, self.projects)

@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 issue['permalink']

def get_project_name(self, issue):
return issue['project']

def description(self, title, project_id, task_id="", cls="task"):

cls_markup = {
'task': '#',
'subtask': 'Subtask #',
}

return "%s%s%s - %s" % (
MARKUP, cls_markup[cls], str(task_id),
title[:45],
)

def format_annotation(self, created, permalink):
return (
"annotation_%i" % time.mktime(created.timetuple()),
"%s" % (permalink),
)

def annotations(self, issue):
return dict([
self.format_annotation(
datetime.datetime.fromtimestamp(time.mktime(time.strptime(
issue['created_on'], "%Y-%m-%d %H:%M:%S"))),
issue['permalink'],
)])

def issues(self):
# Loop through each project
start = time.time()
issues = []
projects = self.projects
# @todo Implement threading here.
log.name(self.target).debug(" {0} projects in favorites list.", len(projects))
for project in projects:
for project_id, project_name in project.iteritems():
log.name(self.target).debug(" Getting tasks for #" + str(project_id) + " " + str(project_name) + '"')
issues += self.client.find_issues(self.user_id, project_id, project_name)

log.name(self.target).debug(" Found {0} total.", len(issues))
global api_count
log.name(self.target).debug(" {0} API calls", api_count)
log.name(self.target).debug(" {0} tasks and subtasks analyzed", task_count)
log.name(self.target).debug(" Elapsed Time: %s" % (time.time() - start))

formatted_issues = []

for issue in issues:
formatted_issue = dict(
description=self.description(
issue["description"],
issue["project_id"], issue["task_id"], issue["type"],
),
project=self.get_project_name(issue),
priority=issue["priority"],
**self.annotations(issue)
)
if "due" in issue:
formatted_issue["due"] = issue["due"]
formatted_issues.append(formatted_issue)
log.name(self.target).debug(" {0} tasks assigned to you", len(formatted_issues))
return formatted_issues

0 comments on commit 2e6fabc

Please sign in to comment.