Passed
Push — master ( 174f4e...fbd03e )
by Alexander
01:57
created

tcms.issuetracker.types.JIRA.details()   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
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 quote, 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_url: - the API URL for your JIRA instance
44
        :api_username: - a username registered in JIRA
45
        :api_password: - the password for this username
46
47
        Additional control can be applied via the ``JIRA_OPTIONS`` configuration
48
        setting (in ``product.py``). By default this setting is not provided and
49
        the code uses ``jira.JIRA.DEFAULT_OPTIONS`` from the ``jira`` Python module!
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.api_url,
61
            basic_auth=(self.bug_system.api_username, self.bug_system.api_password),
62
            options=options,
63
        )
64
65
    @classmethod
66
    def bug_id_from_url(cls, url):
67
        """
68
            Jira IDs are the last group of chars at the end of the URL.
69
            For example https://issues.jenkins-ci.org/browse/JENKINS-31044
70
        """
71
        return url.strip().split('/')[-1]
72
73
    def details(self, url):
74
        try:
75
            issue = self.rpc.issue(self.bug_id_from_url(url))
76
            return {
77
                'title': issue.fields.summary,
78
                'description': issue.fields.description,
79
            }
80
        except jira.exceptions.JIRAError:
81
            return super().details(url)
82
83
    def report_issue_from_testexecution(self, execution, user):
84
        """
85
            JIRA Project == Kiwi TCMS Product, otherwise defaults to the first found
86
            Issue Type == Bug or the first one found
87
88
            If 1-click bug report doesn't work then fall back to manual
89
            reporting!
90
91
            For the HTML API description see:
92
            https://confluence.atlassian.com/display/JIRA050/Creating+Issues+via+direct+HTML+links
93
        """
94
        try:
95
            project = self.rpc.project(execution.run.plan.product.name)
96
        except jira.exceptions.JIRAError:
97
            project = self.rpc.projects()[0]
98
99
        try:
100
            issue_type = self.rpc.issue_type_by_name('Bug')
101
        except KeyError:
102
            issue_type = self.rpc.issue_types()[0]
103
104
        try:
105
            new_issue = self.rpc.create_issue(
106
                project=project.id,
107
                issuetype={'name': issue_type.name},
108
                summary='Failed test: %s' % execution.case.summary,
109
                description=self._report_comment(execution),
110
            )
111
            new_url = self.bug_system.base_url + "/browse/" + new_issue.key
112
113
            # add a link reference that will be shown in the UI
114
            LinkReference.objects.get_or_create(
115
                execution=execution,
116
                url=new_url,
117
                is_defect=True,
118
            )
119
120
            return new_url
121
        except jira.exceptions.JIRAError:
122
            pass
123
124
        args = {
125
            'pid': project.id,
126
            'issuetype': issue_type.id,
127
            'summary': 'Failed test: %s' % execution.case.summary,
128
            'description': self._report_comment(execution),
129
        }
130
131
        url = self.bug_system.base_url
132
        if not url.endswith('/'):
133
            url += '/'
134
135
        return url + '/secure/CreateIssueDetails!init.jspa?' + urlencode(args, True)
136
137
138
class GitHub(IssueTrackerType):
139
    """
140
        Support for GitHub. Requires:
141
142
        :base_url: - URL to a GitHub repository for which we're going to report issues
143
        :api_password: - GitHub API token - needs ``repo`` or ``public_repo``
144
                         permissions.
145
146
        .. note::
147
148
            You can leave the ``api_url`` and ``api_username`` fields blank because
149
            the integration code doesn't use them!
150
    """
151
    it_class = github_integration.GitHubThread
152
153
    def _rpc_connection(self):
154
        # NOTE: we use an access token so only the password field is required
155
        return github.Github(self.bug_system.api_password)
156
157
    def is_adding_testcase_to_issue_disabled(self):
158
        return not (self.bug_system.base_url and self.bug_system.api_password)
159
160
    def report_issue_from_testexecution(self, execution, user):
161
        """
162
            GitHub only supports title and body parameters
163
        """
164
        args = {
165
            'title': 'Failed test: %s' % execution.case.summary,
166
            'body': self._report_comment(execution),
167
        }
168
169
        try:
170
            repo_id = self.it_class.repo_id(self.bug_system)
171
            repo = self.rpc.get_repo(repo_id)
172
            issue = repo.create_issue(**args)
173
174
            # add a link reference that will be shown in the UI
175
            LinkReference.objects.get_or_create(
176
                execution=execution,
177
                url=issue.html_url,
178
                is_defect=True,
179
            )
180
181
            return issue.html_url
182
        except Exception:  # pylint: disable=broad-except
183
            # something above didn't work so return a link for manually
184
            # entering issue details with info pre-filled
185
            url = self.bug_system.base_url
186
            if not url.endswith('/'):
187
                url += '/'
188
189
            return url + '/issues/new?' + urlencode(args, True)
190
191
    def details(self, url):
192
        """
193
            Use GitHub's API instead of OpenGraph to return bug
194
            details b/c it will work for both public and private URLs.
195
        """
196
        repo_id = self.it_class.repo_id(self.bug_system)
197
        repo = self.rpc.get_repo(repo_id)
198
        issue = repo.get_issue(self.bug_id_from_url(url))
199
        return {
200
            'title': issue.title,
201
            'description': issue.body,
202
        }
203
204
205
class Gitlab(IssueTrackerType):
206
    """
207
        Support for Gitlab. Requires:
208
209
        :base_url: URL to a GitLab repository for which we're going to report issues
210
        :api_url: URL to GitLab instance. Usually gitlab.com!
211
        :api_password: GitLab API token.
212
213
        .. note::
214
215
            You can leave ``api_username`` field blank because
216
            the integration code doesn't use it!
217
    """
218
    it_class = gitlab_integration.GitlabThread
219
220
    def _rpc_connection(self):
221
        # we use an access token so only the password field is required
222
        return gitlab.Gitlab(self.bug_system.api_url,
223
                             private_token=self.bug_system.api_password)
224
225
    def is_adding_testcase_to_issue_disabled(self):
226
        return not (self.bug_system.api_url and self.bug_system.api_password)
227
228
    def report_issue_from_testexecution(self, execution, user):
229
        repo_id = self.it_class.repo_id(self.bug_system)
230
        project = self.rpc.projects.get(repo_id)
231
        new_issue = project.issues.create({
232
            'title': 'Failed test: %s' % execution.case.summary,
233
            'description': self._report_comment(execution),
234
        })
235
236
        # and also add a link reference that will be shown in the UI
237
        LinkReference.objects.get_or_create(
238
            execution=execution,
239
            url=new_issue.attributes['web_url'],
240
            is_defect=True,
241
        )
242
        return new_issue.attributes['web_url']
243
244
    def details(self, url):
245
        """
246
            Use Gitlab API instead of OpenGraph to return bug
247
            details b/c it will work for both public and private URLs.
248
        """
249
        repo_id = self.it_class.repo_id(self.bug_system)
250
        project = self.rpc.projects.get(repo_id)
251
        issue = project.issues.get(self.bug_id_from_url(url))
252
        return {
253
            'title': issue.title,
254
            'description': issue.description,
255
        }
256
257
258
class Redmine(IssueTrackerType):
259
    """
260
        Support for Redmine. Requires:
261
262
        :base_url: - the URL for this Redmine instance
263
        :api_url: - the API URL for your Redmine instance
264
        :api_username: - a username registered in Redmine
265
        :api_password: - the password for this username
266
    """
267
    it_class = redmine_integration.RedmineThread
268
269
    def _rpc_connection(self):
270
        return redminelib.Redmine(
271
            self.bug_system.api_url,
272
            username=self.bug_system.api_username,
273
            password=self.bug_system.api_password
274
        )
275
276
    def find_project_by_name(self, name):
277
        """
278
            Return a Redmine project which matches the given product name.
279
280
            .. note::
281
282
                If there is no match then return the first project in Redmine.
283
        """
284
        try:
285
            return self.rpc.project.get(name)
286
        except redminelib.exceptions.ResourceNotFoundError:
287
            projects = self.rpc.project.all()
288
            return projects[0]
289
290
    @staticmethod
291
    def find_issue_type_by_name(project, name):
292
        """
293
            Return a Redmine tracker matching name ('Bug').
294
295
            .. note::
296
297
                If there is no match then return the first one!
298
        """
299
        for trk in project.trackers:
300
            if str(trk).lower() == name.lower():
301
                return trk
302
303
        return project.trackers[0]
304
305
    def report_issue_from_testexecution(self, execution, user):
306
        project = self.find_project_by_name(execution.run.plan.product.name)
307
308
        issue_type = self.find_issue_type_by_name(project, 'Bug')
309
310
        query = "issue[tracker_id]=" + str(issue_type.id)
311
        query += "&issue[subject]=" + quote('Failed test: %s' % execution.case.summary)
312
313
        comment = self._report_comment(execution)
314
        query += "&issue[description]=%s" % quote(comment)
315
316
        url = self.bug_system.base_url
317
        if not url.endswith('/'):
318
            url += '/'
319
320
        return url + '/projects/%s/issues/new?' % project.id + query
321