From 9970dfe647ff13341d32899bd5d283f119651a48 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Sat, 24 Oct 2020 10:40:25 +0200 Subject: [PATCH] Add bugwarrior --- environments/immae-eu.nix | 1 + overlays/bugwarrior/default.nix | 5 + overlays/bugwarrior/mantisbt.patch | 379 +++++++++++++++++++++++++++++ overlays/default.nix | 1 + 4 files changed, 386 insertions(+) create mode 100644 overlays/bugwarrior/default.nix create mode 100644 overlays/bugwarrior/mantisbt.patch diff --git a/environments/immae-eu.nix b/environments/immae-eu.nix index 04a9e9c..4ceee82 100644 --- a/environments/immae-eu.nix +++ b/environments/immae-eu.nix @@ -106,6 +106,7 @@ let # todolist/time management taskwarrior vit timewarrior + bugwarrior # video/music youtube-dl ncmpc ncmpcpp ffmpeg diff --git a/overlays/bugwarrior/default.nix b/overlays/bugwarrior/default.nix new file mode 100644 index 0000000..2b25985 --- /dev/null +++ b/overlays/bugwarrior/default.nix @@ -0,0 +1,5 @@ +self: super: { + bugwarrior = super.python3Packages.bugwarrior.overridePythonAttrs(old: rec { + patches = old.patches or [] ++ [ ./mantisbt.patch ]; + }); +} diff --git a/overlays/bugwarrior/mantisbt.patch b/overlays/bugwarrior/mantisbt.patch new file mode 100644 index 0000000..85e5af1 --- /dev/null +++ b/overlays/bugwarrior/mantisbt.patch @@ -0,0 +1,379 @@ +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 diff --git a/overlays/default.nix b/overlays/default.nix index 7444e15..1db1819 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -6,6 +6,7 @@ bitlbee-discord = import ./bitlbee-discord; bonfire = import ./bonfire; bundix = import ./bundix; + bugwarrior = import ./bugwarrior; dwm = import ./dwm; elinks = import ./elinks; gitweb = import ./gitweb; -- 2.41.0