Passed
Push — master ( fdd007...c06fae )
by Alexander
02:28
created

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

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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