Passed
Push — master ( fbd03e...630b73 )
by Alexander
02:15
created

Redmine.redmine_tracker_by_name()   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 3
nop 2
1
"""
2
    This module implements Kiwi TCMS interface to external issue tracking systems.
3
    Refer to each implementor class for integration specifics!
4
"""
5
6
from urllib.parse import urlencode
7
8
import github
9
import gitlab
10
import jira
11
import redminelib
12
from django.conf import settings
13
14
from tcms.core.contrib.linkreference.models import LinkReference
15
16
from tcms.issuetracker.bugzilla_integration import Bugzilla  # noqa, pylint: disable=unused-import
17
from tcms.issuetracker import (github_integration,
18
                               gitlab_integration, jira_integration,
19
                               redmine_integration)
20
from tcms.issuetracker.base import IssueTrackerType
21
22
23
# conditional import b/c this App can be disabled
24
if 'tcms.bugs.apps.AppConfig' in settings.INSTALLED_APPS:
25
    from tcms.issuetracker.kiwitcms import KiwiTCMS  # noqa, pylint: disable=unused-import
26
27
28
def from_name(name):
29
    """
30
        Return the class which matches ``name`` if it exists inside this
31
        module or raise an exception.
32
    """
33
    if name not in globals():
34
        raise NotImplementedError('IT of type %s is not supported' % name)
35
    return globals()[name]
36
37
38
class JIRA(IssueTrackerType):
39
    """
40
        Support for JIRA. Requires:
41
42
        :base_url: - the URL of this JIRA instance
43
        :api_username: - a username registered in JIRA
44
        :api_password: - the password for this username
45
46
        Additional control can be applied via the ``JIRA_OPTIONS`` configuration
47
        setting (in ``product.py``). By default this setting is not provided and
48
        the code uses ``jira.JIRA.DEFAULT_OPTIONS`` from the ``jira`` Python module!
49
    """
50
    it_class = jira_integration.JiraThread
51
52
    def _rpc_connection(self):
53
        if hasattr(settings, 'JIRA_OPTIONS'):
54
            options = settings.JIRA_OPTIONS
55
        else:
56
            options = None
57
58
        return jira.JIRA(
59
            self.bug_system.base_url,
60
            basic_auth=(self.bug_system.api_username, self.bug_system.api_password),
61
            options=options,
62
        )
63
64
    def is_adding_testcase_to_issue_disabled(self):
65
        return not (self.bug_system.base_url
66
                    and self.bug_system.api_username
67
                    and self.bug_system.api_password)
68
69
    @classmethod
70
    def bug_id_from_url(cls, url):
71
        """
72
            Jira IDs are the last group of chars at the end of the URL.
73
            For example https://issues.jenkins-ci.org/browse/JENKINS-31044
74
        """
75
        return url.strip().split('/')[-1]
76
77
    def details(self, url):
78
        try:
79
            issue = self.rpc.issue(self.bug_id_from_url(url))
80
            return {
81
                'title': issue.fields.summary,
82
                'description': issue.fields.description,
83
            }
84
        except jira.exceptions.JIRAError:
85
            return super().details(url)
86
87
    def report_issue_from_testexecution(self, execution, user):
88
        """
89
            JIRA Project == Kiwi TCMS Product, otherwise defaults to the first found
90
            Issue Type == Bug or the first one found
91
92
            If 1-click bug report doesn't work then fall back to manual
93
            reporting!
94
95
            For the HTML API description see:
96
            https://confluence.atlassian.com/display/JIRA050/Creating+Issues+via+direct+HTML+links
97
        """
98
        try:
99
            project = self.rpc.project(execution.run.plan.product.name)
100
        except jira.exceptions.JIRAError:
101
            project = self.rpc.projects()[0]
102
103
        try:
104
            issue_type = self.rpc.issue_type_by_name('Bug')
105
        except KeyError:
106
            issue_type = self.rpc.issue_types()[0]
107
108
        try:
109
            new_issue = self.rpc.create_issue(
110
                project=project.id,
111
                issuetype={'name': issue_type.name},
112
                summary='Failed test: %s' % execution.case.summary,
113
                description=self._report_comment(execution),
114
            )
115
            new_url = self.bug_system.base_url + "/browse/" + new_issue.key
116
117
            # add a link reference that will be shown in the UI
118
            LinkReference.objects.get_or_create(
119
                execution=execution,
120
                url=new_url,
121
                is_defect=True,
122
            )
123
124
            return new_url
125
        except jira.exceptions.JIRAError:
126
            pass
127
128
        args = {
129
            'pid': project.id,
130
            'issuetype': issue_type.id,
131
            'summary': 'Failed test: %s' % execution.case.summary,
132
            'description': self._report_comment(execution),
133
        }
134
135
        url = self.bug_system.base_url
136
        if not url.endswith('/'):
137
            url += '/'
138
139
        return url + '/secure/CreateIssueDetails!init.jspa?' + urlencode(args, True)
140
141
142
class GitHub(IssueTrackerType):
143
    """
144
        Support for GitHub. Requires:
145
146
        :base_url: - URL to a GitHub repository for which we're going to report issues
147
        :api_password: - GitHub API token - needs ``repo`` or ``public_repo``
148
                         permissions.
149
150
        .. note::
151
152
            You can leave the ``api_url`` and ``api_username`` fields blank because
153
            the integration code doesn't use them!
154
    """
155
    it_class = github_integration.GitHubThread
156
157
    def _rpc_connection(self):
158
        # NOTE: we use an access token so only the password field is required
159
        return github.Github(self.bug_system.api_password)
160
161
    def is_adding_testcase_to_issue_disabled(self):
162
        return not (self.bug_system.base_url and self.bug_system.api_password)
163
164
    def report_issue_from_testexecution(self, execution, user):
165
        """
166
            GitHub only supports title and body parameters
167
        """
168
        args = {
169
            'title': 'Failed test: %s' % execution.case.summary,
170
            'body': self._report_comment(execution),
171
        }
172
173
        try:
174
            repo_id = self.it_class.repo_id(self.bug_system)
175
            repo = self.rpc.get_repo(repo_id)
176
            issue = repo.create_issue(**args)
177
178
            # add a link reference that will be shown in the UI
179
            LinkReference.objects.get_or_create(
180
                execution=execution,
181
                url=issue.html_url,
182
                is_defect=True,
183
            )
184
185
            return issue.html_url
186
        except Exception:  # pylint: disable=broad-except
187
            # something above didn't work so return a link for manually
188
            # entering issue details with info pre-filled
189
            url = self.bug_system.base_url
190
            if not url.endswith('/'):
191
                url += '/'
192
193
            return url + '/issues/new?' + urlencode(args, True)
194
195
    def details(self, url):
196
        """
197
            Use GitHub's API instead of OpenGraph to return bug
198
            details b/c it will work for both public and private URLs.
199
        """
200
        repo_id = self.it_class.repo_id(self.bug_system)
201
        repo = self.rpc.get_repo(repo_id)
202
        issue = repo.get_issue(self.bug_id_from_url(url))
203
        return {
204
            'title': issue.title,
205
            'description': issue.body,
206
        }
207
208
209
class Gitlab(IssueTrackerType):
210
    """
211
        Support for Gitlab. Requires:
212
213
        :base_url: URL to a GitLab repository for which we're going to report issues
214
        :api_url: URL to GitLab instance. Usually gitlab.com!
215
        :api_password: GitLab API token.
216
217
        .. note::
218
219
            You can leave ``api_username`` field blank because
220
            the integration code doesn't use it!
221
    """
222
    it_class = gitlab_integration.GitlabThread
223
224
    def _rpc_connection(self):
225
        # we use an access token so only the password field is required
226
        return gitlab.Gitlab(self.bug_system.api_url,
227
                             private_token=self.bug_system.api_password)
228
229
    def is_adding_testcase_to_issue_disabled(self):
230
        return not (self.bug_system.api_url and self.bug_system.api_password)
231
232
    def report_issue_from_testexecution(self, execution, user):
233
        repo_id = self.it_class.repo_id(self.bug_system)
234
        project = self.rpc.projects.get(repo_id)
235
        new_issue = project.issues.create({
236
            'title': 'Failed test: %s' % execution.case.summary,
237
            'description': self._report_comment(execution),
238
        })
239
240
        # and also add a link reference that will be shown in the UI
241
        LinkReference.objects.get_or_create(
242
            execution=execution,
243
            url=new_issue.attributes['web_url'],
244
            is_defect=True,
245
        )
246
        return new_issue.attributes['web_url']
247
248
    def details(self, url):
249
        """
250
            Use Gitlab API instead of OpenGraph to return bug
251
            details b/c it will work for both public and private URLs.
252
        """
253
        repo_id = self.it_class.repo_id(self.bug_system)
254
        project = self.rpc.projects.get(repo_id)
255
        issue = project.issues.get(self.bug_id_from_url(url))
256
        return {
257
            'title': issue.title,
258
            'description': issue.description,
259
        }
260
261
262
class Redmine(IssueTrackerType):
263
    """
264
        Support for Redmine. Requires:
265
266
        :base_url: - the URL for this Redmine instance
267
        :api_username: - a username registered in Redmine
268
        :api_password: - the password for this username
269
    """
270
    it_class = redmine_integration.RedmineThread
271
272
    def is_adding_testcase_to_issue_disabled(self):
273
        return not (self.bug_system.base_url
274
                    and self.bug_system.api_username
275
                    and self.bug_system.api_password)
276
277
    def _rpc_connection(self):
278
        return redminelib.Redmine(
279
            self.bug_system.base_url,
280
            username=self.bug_system.api_username,
281
            password=self.bug_system.api_password
282
        )
283
284
    def details(self, url):
285
        try:
286
            issue = self.rpc.issue.get(self.bug_id_from_url(url))
287
            return {
288
                'title': issue.subject,
289
                'description': issue.description,
290
            }
291
        except redminelib.exceptions.ResourceNotFoundError:
292
            return super().details(url)
293
294
    def redmine_project_by_name(self, name):
295
        """
296
            Return a Redmine project which matches the given product name.
297
            If there is no match then return the first project in Redmine!
298
        """
299
        all_projects = self.rpc.project.all()
300
        for project in all_projects:
301
            if project.name == name:
302
                return project
303
304
        return all_projects[0]
305
306
    @staticmethod
307
    def redmine_tracker_by_name(project, name):
308
        """
309
            Return a Redmine tracker matching name ('Bugs').
310
            If there is no match then return the first one!
311
        """
312
        all_trackers = project.trackers
313
314
        for tracker in all_trackers:
315
            if tracker.name.lower() == name.lower():
316
                return tracker
317
318
        return all_trackers[0]
319
320
    def redmine_priority_by_name(self, name):
321
        all_priorities = self.rpc.enumeration.filter(resource='issue_priorities')
322
323
        for priority in all_priorities:
324
            if priority.name.lower() == name.lower():
325
                return priority
326
327
        return all_priorities[0]
328
329
    def report_issue_from_testexecution(self, execution, user):
330
        project = self.redmine_project_by_name(execution.run.plan.product.name)
331
        tracker = self.redmine_tracker_by_name(project, 'Bugs')
332
333
        # the first Issue Status in Redmine
334
        status = self.rpc.issue_status.all()[0]
335
336
        # try matching TC.priority with IssuePriority in Redmine
337
        priority = self.redmine_priority_by_name(execution.case.priority.value)
338
339
        new_issue = self.rpc.issue.create(
340
            subject='Failed test: %s' % execution.case.summary,
341
            description=self._report_comment(execution),
342
            project_id=project.id,
343
            tracker_id=tracker.id,
344
            status_id=status.id,
345
            priority_id=priority.id,
346
        )
347
        new_url = self.bug_system.base_url + "/issues/%d" % new_issue.id
348
349
        # and also add a link reference that will be shown in the UI
350
        LinkReference.objects.get_or_create(
351
            execution=execution,
352
            url=new_url,
353
            is_defect=True,
354
        )
355
356
        return new_url
357