]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - overlays/bugwarrior/mantisbt.patch
Add bugwarrior
[perso/Immae/Config/Nix.git] / overlays / bugwarrior / mantisbt.patch
CommitLineData
9970dfe6
IB
1diff --git a/bugwarrior/services/mantisbt.py b/bugwarrior/services/mantisbt.py
2new file mode 100644
3index 0000000..e54af0d
4--- /dev/null
5+++ b/bugwarrior/services/mantisbt.py
6@@ -0,0 +1,361 @@
7+from builtins import filter
8+import re
9+import six
10+
11+import requests
12+from jinja2 import Template
13+
14+from bugwarrior.config import asbool, aslist, die
15+from bugwarrior.services import IssueService, Issue, ServiceClient
16+
17+import logging
18+log = logging.getLogger(__name__)
19+
20+
21+class MantisbtClient(ServiceClient):
22+ def __init__(self, host, token):
23+ self.host = host
24+ self.session = requests.Session()
25+ self.session.headers['Authorization'] = token
26+
27+ def _api_url(self, path, **context):
28+ """ Build the full url to the API endpoint """
29+ baseurl = "https://{}/api/rest".format(self.host)
30+ return baseurl + path.format(**context)
31+
32+ def get_user(self):
33+ return self.json_response(self.session.get(self._api_url("/users/me")))
34+
35+ def get_projects(self):
36+ return self._getter(self._api_url("/projects"), subkey="projects")
37+
38+ def get_issues(self):
39+ url = self._api_url("/issues?page_size=50")
40+ return self._getter(url, page_size=50, subkey="issues")
41+
42+ def get_assigned_issues(self):
43+ """ Returns all issues assigned to authenticated user.
44+ """
45+ url = self._api_url("/issues?page_size=50&filter_id=assigned")
46+ return self._getter(url, page_size=50, subkey="issues")
47+
48+ def get_monitored_issues(self):
49+ """ Returns all issues monitored by authenticated user.
50+ """
51+ url = self._api_url("/issues?page_size=50&filter_id=monitored")
52+ return self._getter(url, page_size=50, subkey="issues")
53+
54+ def get_reported_issues(self):
55+ """ Returns all issues reported by authenticated user.
56+ """
57+ url = self._api_url("/issues?page_size=50&filter_id=reported")
58+ return self._getter(url, page_size=50, subkey="issues")
59+
60+ def _getter(self, url, page_size=None, subkey=None):
61+ """ Pagination utility. Obnoxious. """
62+
63+ results = []
64+ link = dict(next=url)
65+ page_number = 1
66+
67+ while 'next' in link:
68+ if page_size is not None:
69+ response = self.session.get(link['next'] + "&page=" + str(page_number))
70+ else:
71+ response = self.session.get(link['next'])
72+
73+ json_res = self.json_response(response)
74+
75+ if subkey is not None:
76+ json_res = json_res[subkey]
77+
78+ results += json_res
79+
80+ if page_size is not None and len(json_res) == page_size:
81+ page_number += 1
82+ else:
83+ break
84+
85+ return results
86+
87+class MantisbtIssue(Issue):
88+ TITLE = 'mantisbttitle'
89+ BODY = 'mantisbtbody'
90+ CREATED_AT = 'mantisbtcreatedon'
91+ UPDATED_AT = 'mantisbtupdatedat'
92+ CLOSED_AT = 'mantisbtclosedon'
93+ URL = 'mantisbturl'
94+ PROJECT = 'mantisbtproject'
95+ NUMBER = 'mantisbtnumber'
96+ USER = 'mantisbtuser'
97+ CATEGORY = 'mantisbtcategory'
98+ STATE = 'mantisbtstate'
99+
100+ UDAS = {
101+ TITLE: {
102+ 'type': 'string',
103+ 'label': 'Mantisbt Title',
104+ },
105+ BODY: {
106+ 'type': 'string',
107+ 'label': 'Mantisbt Body',
108+ },
109+ CREATED_AT: {
110+ 'type': 'date',
111+ 'label': 'Mantisbt Created',
112+ },
113+ UPDATED_AT: {
114+ 'type': 'date',
115+ 'label': 'Mantisbt Updated',
116+ },
117+ CLOSED_AT: {
118+ 'type': 'date',
119+ 'label': 'Mantisbt Closed',
120+ },
121+ PROJECT: {
122+ 'type': 'string',
123+ 'label': 'Mantisbt Project',
124+ },
125+ URL: {
126+ 'type': 'string',
127+ 'label': 'Mantisbt URL',
128+ },
129+ NUMBER: {
130+ 'type': 'numeric',
131+ 'label': 'Mantisbt Issue #',
132+ },
133+ USER: {
134+ 'type': 'string',
135+ 'label': 'Mantisbt User',
136+ },
137+ CATEGORY: {
138+ 'type': 'string',
139+ 'label': 'Mantisbt Category',
140+ },
141+ STATE: {
142+ 'type': 'string',
143+ 'label': 'Mantisbt State',
144+ }
145+ }
146+ UNIQUE_KEY = (URL, NUMBER, )
147+
148+ def _normalize_tag(self, label):
149+ return re.sub(r'[^a-zA-Z0-9]', '_', label)
150+
151+ def to_taskwarrior(self):
152+ body = self.record.get('description')
153+ if body:
154+ body = body.replace('\r\n', '\n')
155+
156+ created = self.parse_date(self.record.get('created_at'))
157+ updated = self.parse_date(self.record.get('updated_at'))
158+ closed_date = None
159+ if self.record["status"]["name"] in ["closed", "resolved"]:
160+ for history in self.record.get("history", []):
161+ if history.get("field", {}).get("name", "") == "status"\
162+ and history.get("new_value", {}).get("name", "") in ["closed", "resolved"]:
163+ closed_date = history["created_at"]
164+ closed = self.parse_date(closed_date)
165+
166+ return {
167+ 'project': self.record['project']['name'],
168+ 'priority': self.origin['default_priority'],
169+ 'annotations': self.get_annotations(),
170+ 'tags': self.get_tags(),
171+ 'entry': created,
172+ 'end': closed,
173+
174+ self.TITLE: self.record.get('summary'),
175+ self.BODY: body,
176+ self.CREATED_AT: created,
177+ self.UPDATED_AT: updated,
178+ self.CLOSED_AT: closed,
179+ self.URL: self.get_url(),
180+ self.PROJECT: self.record['project'].get('name'),
181+ self.NUMBER: self.record['id'],
182+ self.USER: self.record['reporter'].get('name'),
183+ self.CATEGORY: self.record['category'].get('name'),
184+ self.STATE: self.record['status'].get('label'),
185+ }
186+
187+ def get_url(self):
188+ return "https://{}view.php?id={}".format(self.extra['host'], self.record["id"])
189+
190+ def get_annotations(self):
191+ annotations = []
192+
193+ context = self.record.copy()
194+ annotation_template = Template(self.origin['annotation_template'])
195+
196+ for annotation_dict in self.record.get('notes', []):
197+ context.update({
198+ 'text': annotation_dict['text'],
199+ 'date': annotation_dict['created_at'],
200+ 'author': annotation_dict['reporter'].get('name', 'unknown'),
201+ 'view': annotation_dict['view_state']['label'],
202+ })
203+ annotations.append(
204+ annotation_template.render(context)
205+ )
206+ return annotations
207+
208+ def get_tags(self):
209+ tags = []
210+
211+ context = self.record.copy()
212+ tag_template = Template(self.origin['tag_template'])
213+
214+ for tag_dict in self.record.get('tags', []):
215+ context.update({
216+ 'tag': self._normalize_tag(tag_dict['name'])
217+ })
218+ tags.append(
219+ tag_template.render(context)
220+ )
221+
222+ return tags
223+
224+ def get_default_description(self):
225+ return self.build_default_description(
226+ title=self.record['summary'],
227+ url=self.get_processed_url(self.get_url()),
228+ number=self.record['id'],
229+ )
230+
231+
232+class MantisbtService(IssueService):
233+ ISSUE_CLASS = MantisbtIssue
234+ CONFIG_PREFIX = 'mantisbt'
235+
236+ def __init__(self, *args, **kw):
237+ super(MantisbtService, self).__init__(*args, **kw)
238+
239+ self.host = self.config.get('host', 'www.mantisbt.org/bugs/')
240+
241+ token = self.get_password('token')
242+
243+ self.client = MantisbtClient(self.host, token)
244+ self.user = None
245+
246+ self.exclude_projects = self.config.get('exclude_projects', [], aslist)
247+ self.include_projects = self.config.get('include_projects', [], aslist)
248+
249+ self.involved_issues = self.config.get(
250+ 'involved_issues', default=True, to_type=asbool
251+ )
252+ self.assigned_issues = self.config.get(
253+ 'assigned_issues', default=False, to_type=asbool
254+ )
255+ self.monitored_issues = self.config.get(
256+ 'monitored_issues', default=False, to_type=asbool
257+ )
258+ self.reported_issues = self.config.get(
259+ 'reported_issues', default=False, to_type=asbool
260+ )
261+ self.tag_template = self.config.get(
262+ 'tag_template', default='{{tag}}', to_type=six.text_type
263+ )
264+ self.annotation_template = self.config.get(
265+ 'annotation_template', default='{{date}} {{author}} ({{view}}): {{text}}', to_type=six.text_type
266+ )
267+
268+ def get_service_metadata(self):
269+ return {
270+ 'tag_template': self.tag_template,
271+ 'annotation_template': self.annotation_template,
272+ }
273+
274+ def filter_involved_issues(self, issue):
275+ _, issue = issue
276+ user = self.client.get_user()
277+ uid = user["id"]
278+ if issue["reporter"]["id"] != uid and \
279+ issue.get("handler", {}).get("id") != uid and \
280+ all([ x.get("user", {}).get("id") != uid for x in issue.get("history", [])]) and \
281+ all([ x.get("user", {}).get("id") != uid for x in issue.get("monitors", [])]):
282+ return False
283+ return self.filter_project_name(issue["project"]["name"])
284+
285+ def filter_issues(self, issue):
286+ _, issue = issue
287+ return self.filter_project_name(issue["project"]["name"])
288+
289+ def filter_project_name(self, name):
290+ if self.exclude_projects:
291+ if name in self.exclude_projects:
292+ return False
293+
294+ if self.include_projects:
295+ if name in self.include_projects:
296+ return True
297+ else:
298+ return False
299+
300+ return True
301+
302+ @staticmethod
303+ def get_keyring_service(service_config):
304+ host = service_config.get('host', 'www.mantisbt.org/bugs/')
305+ username = service_config.get('username', default='nousername')
306+ return "mantisbt://{username}@{host}".format(username=username,
307+ host=host)
308+
309+ @staticmethod
310+ def to_issue_dict(issues):
311+ return { i['id']: i for i in issues }
312+
313+ def get_owner(self, issue):
314+ return issue.get("handler", {}).get("name")
315+
316+ def get_author(self, issue):
317+ return issue.get("reporter", {}).get("name")
318+
319+ def issues(self):
320+ issues = {}
321+ is_limited = self.assigned_issues or self.monitored_issues or self.reported_issues
322+
323+ if self.assigned_issues:
324+ issues.update(
325+ filter(self.filter_issues, self.to_issue_dict(self.client.get_assigned_issues()).items())
326+ )
327+ if self.monitored_issues:
328+ issues.update(
329+ filter(self.filter_issues, self.to_issue_dict(self.client.get_monitored_issues()).items())
330+ )
331+ if self.reported_issues:
332+ issues.update(
333+ filter(self.filter_issues, self.to_issue_dict(self.client.get_reported_issues()).items())
334+ )
335+
336+ if not is_limited:
337+ all_issues = self.to_issue_dict(self.client.get_issues())
338+ if self.involved_issues:
339+ issues.update(
340+ filter(self.filter_involved_issues, all_issues.items())
341+ )
342+ else:
343+ issues.update(
344+ filter(self.filter_issues, all_issues.items())
345+ )
346+
347+ log.debug(" Found %i issues.", len(issues))
348+ if not is_limited:
349+ issues = list(filter(self.include, issues.values()))
350+ else:
351+ issues = list(issues.values())
352+ log.debug(" Pruned down to %i issues.", len(issues))
353+
354+ for issue in issues:
355+ issue_obj = self.get_issue_for_record(issue)
356+ extra = {
357+ 'host': self.host
358+ }
359+ issue_obj.update_extra(extra)
360+ yield issue_obj
361+
362+ @classmethod
363+ def validate_config(cls, service_config, target):
364+ if 'token' not in service_config:
365+ die("[%s] has no 'mantisbt.token'" % target)
366+
367+ super(MantisbtService, cls).validate_config(service_config, target)
368diff --git a/setup.py b/setup.py
369index d6d957a..665e36e 100644
370--- a/setup.py
371+++ b/setup.py
372@@ -80,6 +80,7 @@ setup(name='bugwarrior',
373 activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service
374 activecollab=bugwarrior.services.activecollab:ActiveCollabService
375 jira=bugwarrior.services.jira:JiraService
376+ mantisbt=bugwarrior.services.mantisbt:MantisbtService
377 megaplan=bugwarrior.services.megaplan:MegaplanService
378 phabricator=bugwarrior.services.phab:PhabricatorService
379 versionone=bugwarrior.services.versionone:VersionOneService