diff --git a/bugwarrior/services/mantisbt.py b/bugwarrior/services/mantisbt.py new file mode 100644 index 0000000..e54af0d --- /dev/null +++ b/bugwarrior/services/mantisbt.py @@ -0,0 +1,361 @@ +from builtins import filter +import re +import six + +import requests +from jinja2 import Template + +from bugwarrior.config import asbool, aslist, die +from bugwarrior.services import IssueService, Issue, ServiceClient + +import logging +log = logging.getLogger(__name__) + + +class MantisbtClient(ServiceClient): + def __init__(self, host, token): + self.host = host + self.session = requests.Session() + self.session.headers['Authorization'] = token + + def _api_url(self, path, **context): + """ Build the full url to the API endpoint """ + baseurl = "https://{}/api/rest".format(self.host) + return baseurl + path.format(**context) + + def get_user(self): + return self.json_response(self.session.get(self._api_url("/users/me"))) + + def get_projects(self): + return self._getter(self._api_url("/projects"), subkey="projects") + + def get_issues(self): + url = self._api_url("/issues?page_size=50") + return self._getter(url, page_size=50, subkey="issues") + + def get_assigned_issues(self): + """ Returns all issues assigned to authenticated user. + """ + url = self._api_url("/issues?page_size=50&filter_id=assigned") + return self._getter(url, page_size=50, subkey="issues") + + def get_monitored_issues(self): + """ Returns all issues monitored by authenticated user. + """ + url = self._api_url("/issues?page_size=50&filter_id=monitored") + return self._getter(url, page_size=50, subkey="issues") + + def get_reported_issues(self): + """ Returns all issues reported by authenticated user. + """ + url = self._api_url("/issues?page_size=50&filter_id=reported") + return self._getter(url, page_size=50, subkey="issues") + + def _getter(self, url, page_size=None, subkey=None): + """ Pagination utility. Obnoxious. """ + + results = [] + link = dict(next=url) + page_number = 1 + + while 'next' in link: + if page_size is not None: + response = self.session.get(link['next'] + "&page=" + str(page_number)) + else: + response = self.session.get(link['next']) + + json_res = self.json_response(response) + + if subkey is not None: + json_res = json_res[subkey] + + results += json_res + + if page_size is not None and len(json_res) == page_size: + page_number += 1 + else: + break + + return results + +class MantisbtIssue(Issue): + TITLE = 'mantisbttitle' + BODY = 'mantisbtbody' + CREATED_AT = 'mantisbtcreatedon' + UPDATED_AT = 'mantisbtupdatedat' + CLOSED_AT = 'mantisbtclosedon' + URL = 'mantisbturl' + PROJECT = 'mantisbtproject' + NUMBER = 'mantisbtnumber' + USER = 'mantisbtuser' + CATEGORY = 'mantisbtcategory' + STATE = 'mantisbtstate' + + UDAS = { + TITLE: { + 'type': 'string', + 'label': 'Mantisbt Title', + }, + BODY: { + 'type': 'string', + 'label': 'Mantisbt Body', + }, + CREATED_AT: { + 'type': 'date', + 'label': 'Mantisbt Created', + }, + UPDATED_AT: { + 'type': 'date', + 'label': 'Mantisbt Updated', + }, + CLOSED_AT: { + 'type': 'date', + 'label': 'Mantisbt Closed', + }, + PROJECT: { + 'type': 'string', + 'label': 'Mantisbt Project', + }, + URL: { + 'type': 'string', + 'label': 'Mantisbt URL', + }, + NUMBER: { + 'type': 'numeric', + 'label': 'Mantisbt Issue #', + }, + USER: { + 'type': 'string', + 'label': 'Mantisbt User', + }, + CATEGORY: { + 'type': 'string', + 'label': 'Mantisbt Category', + }, + STATE: { + 'type': 'string', + 'label': 'Mantisbt State', + } + } + UNIQUE_KEY = (URL, NUMBER, ) + + def _normalize_tag(self, label): + return re.sub(r'[^a-zA-Z0-9]', '_', label) + + def to_taskwarrior(self): + body = self.record.get('description') + if body: + body = body.replace('\r\n', '\n') + + created = self.parse_date(self.record.get('created_at')) + updated = self.parse_date(self.record.get('updated_at')) + closed_date = None + if self.record["status"]["name"] in ["closed", "resolved"]: + for history in self.record.get("history", []): + if history.get("field", {}).get("name", "") == "status"\ + and history.get("new_value", {}).get("name", "") in ["closed", "resolved"]: + closed_date = history["created_at"] + closed = self.parse_date(closed_date) + + return { + 'project': self.record['project']['name'], + 'priority': self.origin['default_priority'], + 'annotations': self.get_annotations(), + 'tags': self.get_tags(), + 'entry': created, + 'end': closed, + + self.TITLE: self.record.get('summary'), + self.BODY: body, + self.CREATED_AT: created, + self.UPDATED_AT: updated, + self.CLOSED_AT: closed, + self.URL: self.get_url(), + self.PROJECT: self.record['project'].get('name'), + self.NUMBER: self.record['id'], + self.USER: self.record['reporter'].get('name'), + self.CATEGORY: self.record['category'].get('name'), + self.STATE: self.record['status'].get('label'), + } + + def get_url(self): + return "https://{}view.php?id={}".format(self.extra['host'], self.record["id"]) + + def get_annotations(self): + annotations = [] + + context = self.record.copy() + annotation_template = Template(self.origin['annotation_template']) + + for annotation_dict in self.record.get('notes', []): + context.update({ + 'text': annotation_dict['text'], + 'date': annotation_dict['created_at'], + 'author': annotation_dict['reporter'].get('name', 'unknown'), + 'view': annotation_dict['view_state']['label'], + }) + annotations.append( + annotation_template.render(context) + ) + return annotations + + def get_tags(self): + tags = [] + + context = self.record.copy() + tag_template = Template(self.origin['tag_template']) + + for tag_dict in self.record.get('tags', []): + context.update({ + 'tag': self._normalize_tag(tag_dict['name']) + }) + tags.append( + tag_template.render(context) + ) + + return tags + + def get_default_description(self): + return self.build_default_description( + title=self.record['summary'], + url=self.get_processed_url(self.get_url()), + number=self.record['id'], + ) + + +class MantisbtService(IssueService): + ISSUE_CLASS = MantisbtIssue + CONFIG_PREFIX = 'mantisbt' + + def __init__(self, *args, **kw): + super(MantisbtService, self).__init__(*args, **kw) + + self.host = self.config.get('host', 'www.mantisbt.org/bugs/') + + token = self.get_password('token') + + self.client = MantisbtClient(self.host, token) + self.user = None + + self.exclude_projects = self.config.get('exclude_projects', [], aslist) + self.include_projects = self.config.get('include_projects', [], aslist) + + self.involved_issues = self.config.get( + 'involved_issues', default=True, to_type=asbool + ) + self.assigned_issues = self.config.get( + 'assigned_issues', default=False, to_type=asbool + ) + self.monitored_issues = self.config.get( + 'monitored_issues', default=False, to_type=asbool + ) + self.reported_issues = self.config.get( + 'reported_issues', default=False, to_type=asbool + ) + self.tag_template = self.config.get( + 'tag_template', default='{{tag}}', to_type=six.text_type + ) + self.annotation_template = self.config.get( + 'annotation_template', default='{{date}} {{author}} ({{view}}): {{text}}', to_type=six.text_type + ) + + def get_service_metadata(self): + return { + 'tag_template': self.tag_template, + 'annotation_template': self.annotation_template, + } + + def filter_involved_issues(self, issue): + _, issue = issue + user = self.client.get_user() + uid = user["id"] + if issue["reporter"]["id"] != uid and \ + issue.get("handler", {}).get("id") != uid and \ + all([ x.get("user", {}).get("id") != uid for x in issue.get("history", [])]) and \ + all([ x.get("user", {}).get("id") != uid for x in issue.get("monitors", [])]): + return False + return self.filter_project_name(issue["project"]["name"]) + + def filter_issues(self, issue): + _, issue = issue + return self.filter_project_name(issue["project"]["name"]) + + def filter_project_name(self, name): + if self.exclude_projects: + if name in self.exclude_projects: + return False + + if self.include_projects: + if name in self.include_projects: + return True + else: + return False + + return True + + @staticmethod + def get_keyring_service(service_config): + host = service_config.get('host', 'www.mantisbt.org/bugs/') + username = service_config.get('username', default='nousername') + return "mantisbt://{username}@{host}".format(username=username, + host=host) + + @staticmethod + def to_issue_dict(issues): + return { i['id']: i for i in issues } + + def get_owner(self, issue): + return issue.get("handler", {}).get("name") + + def get_author(self, issue): + return issue.get("reporter", {}).get("name") + + def issues(self): + issues = {} + is_limited = self.assigned_issues or self.monitored_issues or self.reported_issues + + if self.assigned_issues: + issues.update( + filter(self.filter_issues, self.to_issue_dict(self.client.get_assigned_issues()).items()) + ) + if self.monitored_issues: + issues.update( + filter(self.filter_issues, self.to_issue_dict(self.client.get_monitored_issues()).items()) + ) + if self.reported_issues: + issues.update( + filter(self.filter_issues, self.to_issue_dict(self.client.get_reported_issues()).items()) + ) + + if not is_limited: + all_issues = self.to_issue_dict(self.client.get_issues()) + if self.involved_issues: + issues.update( + filter(self.filter_involved_issues, all_issues.items()) + ) + else: + issues.update( + filter(self.filter_issues, all_issues.items()) + ) + + log.debug(" Found %i issues.", len(issues)) + if not is_limited: + issues = list(filter(self.include, issues.values())) + else: + issues = list(issues.values()) + log.debug(" Pruned down to %i issues.", len(issues)) + + for issue in issues: + issue_obj = self.get_issue_for_record(issue) + extra = { + 'host': self.host + } + issue_obj.update_extra(extra) + yield issue_obj + + @classmethod + def validate_config(cls, service_config, target): + if 'token' not in service_config: + die("[%s] has no 'mantisbt.token'" % target) + + super(MantisbtService, cls).validate_config(service_config, target) diff --git a/setup.py b/setup.py index d6d957a..665e36e 100644 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ setup(name='bugwarrior', activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service activecollab=bugwarrior.services.activecollab:ActiveCollabService jira=bugwarrior.services.jira:JiraService + mantisbt=bugwarrior.services.mantisbt:MantisbtService megaplan=bugwarrior.services.megaplan:MegaplanService phabricator=bugwarrior.services.phab:PhabricatorService versionone=bugwarrior.services.versionone:VersionOneService