tcms.issuetracker.types   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 191
dl 0
loc 370
rs 9.6
c 0
b 0
f 0

20 Methods

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