diff options
-rw-r--r-- | environments/immae-eu.nix | 1 | ||||
-rw-r--r-- | overlays/bugwarrior/default.nix | 5 | ||||
-rw-r--r-- | overlays/bugwarrior/mantisbt.patch | 379 | ||||
-rw-r--r-- | overlays/default.nix | 1 |
4 files changed, 386 insertions, 0 deletions
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 | |||
106 | 106 | ||
107 | # todolist/time management | 107 | # todolist/time management |
108 | taskwarrior vit timewarrior | 108 | taskwarrior vit timewarrior |
109 | bugwarrior | ||
109 | 110 | ||
110 | # video/music | 111 | # video/music |
111 | youtube-dl ncmpc ncmpcpp ffmpeg | 112 | 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 @@ | |||
1 | self: super: { | ||
2 | bugwarrior = super.python3Packages.bugwarrior.overridePythonAttrs(old: rec { | ||
3 | patches = old.patches or [] ++ [ ./mantisbt.patch ]; | ||
4 | }); | ||
5 | } | ||
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 @@ | |||
1 | diff --git a/bugwarrior/services/mantisbt.py b/bugwarrior/services/mantisbt.py | ||
2 | new file mode 100644 | ||
3 | index 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) | ||
368 | diff --git a/setup.py b/setup.py | ||
369 | index 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 | ||
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 @@ | |||
6 | bitlbee-discord = import ./bitlbee-discord; | 6 | bitlbee-discord = import ./bitlbee-discord; |
7 | bonfire = import ./bonfire; | 7 | bonfire = import ./bonfire; |
8 | bundix = import ./bundix; | 8 | bundix = import ./bundix; |
9 | bugwarrior = import ./bugwarrior; | ||
9 | dwm = import ./dwm; | 10 | dwm = import ./dwm; |
10 | elinks = import ./elinks; | 11 | elinks = import ./elinks; |
11 | gitweb = import ./gitweb; | 12 | gitweb = import ./gitweb; |