]>
Commit | Line | Data |
---|---|---|
9970dfe6 IB |
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 |