##// END OF EJS Templates
integrations: fixed code for python3
super-admin -
r5064:9a69dbc2 default
parent child Browse files
Show More
@@ -1,243 +1,243 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX, add_route_requirements
24 24 from rhodecode.lib.utils2 import safe_int
25 25 from rhodecode.model.db import Repository, Integration, RepoGroup
26 26 from rhodecode.integrations import integration_type_registry
27 27 from rhodecode.integrations.views import GlobalIntegrationsView
28 28 from rhodecode.integrations.views import RepoGroupIntegrationsView
29 29 from rhodecode.integrations.views import RepoIntegrationsView
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class ValidIntegrationPredicate(object):
35 35 def __init__(self, val, config):
36 36 self.val = val
37 37
38 38 def text(self):
39 return 'valid_integration_route = %s' % self.val
39 return f'valid_integration_route = {self.val}'
40 40
41 41 phash = text
42 42
43 43 def __call__(self, info, request):
44 44 integration_type = info['match']['integration']
45 45 integration_id = info['match'].get('integration_id')
46 46
47 47 if integration_type not in integration_type_registry:
48 48 return False
49 49
50 50 if integration_id:
51 51 if not safe_int(integration_id):
52 52 return False
53 53
54 54 integration = Integration.get(integration_id)
55 55 if not integration:
56 56 return False
57 57 if integration.integration_type != integration_type:
58 58 return False
59 59
60 60 # match types to repo or repo group
61 61 repo_name = info['match'].get('repo_name')
62 62 repo_group_name = info['match'].get('repo_group_name')
63 63 repo, repo_group = None, None
64 64 if repo_name:
65 65 repo = Repository.get_by_repo_name(repo_name)
66 66 if not repo:
67 67 return False
68 68
69 69 if repo_group_name:
70 70 repo_group = RepoGroup.get_by_group_name(repo_group_name)
71 71 if not repo_group:
72 72 return False
73 73
74 74 if repo and repo.repo_id != integration.repo_id:
75 75 return False
76 76 if repo_group and repo_group.group_id != integration.repo_group_id:
77 77 return False
78 78
79 79 return True
80 80
81 81
82 82 def includeme(config):
83 83 config.add_route_predicate(
84 84 'valid_integration', ValidIntegrationPredicate)
85 85
86 86 # global integrations
87 87 config.add_route('global_integrations_new',
88 88 ADMIN_PREFIX + '/integrations/new')
89 89 config.add_view(GlobalIntegrationsView,
90 90 attr='new_integration',
91 91 renderer='rhodecode:templates/admin/integrations/new.mako',
92 92 request_method='GET',
93 93 route_name='global_integrations_new')
94 94
95 95 config.add_route('global_integrations_home',
96 96 ADMIN_PREFIX + '/integrations')
97 97 config.add_route('global_integrations_list',
98 98 ADMIN_PREFIX + '/integrations/{integration}')
99 99 for route_name in ['global_integrations_home', 'global_integrations_list']:
100 100 config.add_view(GlobalIntegrationsView,
101 101 attr='integration_list',
102 102 renderer='rhodecode:templates/admin/integrations/list.mako',
103 103 request_method='GET',
104 104 route_name=route_name)
105 105
106 106 config.add_route('global_integrations_create',
107 107 ADMIN_PREFIX + '/integrations/{integration}/new',
108 108 valid_integration=True)
109 109 config.add_route('global_integrations_edit',
110 110 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
111 111 valid_integration=True)
112 112
113 113 for route_name in ['global_integrations_create', 'global_integrations_edit']:
114 114 config.add_view(GlobalIntegrationsView,
115 115 attr='settings_get',
116 116 renderer='rhodecode:templates/admin/integrations/form.mako',
117 117 request_method='GET',
118 118 route_name=route_name)
119 119 config.add_view(GlobalIntegrationsView,
120 120 attr='settings_post',
121 121 renderer='rhodecode:templates/admin/integrations/form.mako',
122 122 request_method='POST',
123 123 route_name=route_name)
124 124
125 125 # repo group integrations
126 126 config.add_route('repo_group_integrations_home',
127 127 add_route_requirements('/{repo_group_name}/_settings/integrations'),
128 128 repo_group_route=True)
129 129
130 130 config.add_view(RepoGroupIntegrationsView,
131 131 attr='integration_list',
132 132 renderer='rhodecode:templates/admin/integrations/list.mako',
133 133 request_method='GET',
134 134 route_name='repo_group_integrations_home')
135 135
136 136 config.add_route('repo_group_integrations_new',
137 137 add_route_requirements('/{repo_group_name}/_settings/integrations/new'),
138 138 repo_group_route=True)
139 139 config.add_view(RepoGroupIntegrationsView,
140 140 attr='new_integration',
141 141 renderer='rhodecode:templates/admin/integrations/new.mako',
142 142 request_method='GET',
143 143 route_name='repo_group_integrations_new')
144 144
145 145 config.add_route('repo_group_integrations_list',
146 146 add_route_requirements('/{repo_group_name}/_settings/integrations/{integration}'),
147 147 repo_group_route=True,
148 148 valid_integration=True)
149 149 config.add_view(RepoGroupIntegrationsView,
150 150 attr='integration_list',
151 151 renderer='rhodecode:templates/admin/integrations/list.mako',
152 152 request_method='GET',
153 153 route_name='repo_group_integrations_list')
154 154
155 155 config.add_route('repo_group_integrations_create',
156 156 add_route_requirements('/{repo_group_name}/_settings/integrations/{integration}/new'),
157 157 repo_group_route=True,
158 158 valid_integration=True)
159 159 config.add_view(RepoGroupIntegrationsView,
160 160 attr='settings_get',
161 161 renderer='rhodecode:templates/admin/integrations/form.mako',
162 162 request_method='GET',
163 163 route_name='repo_group_integrations_create')
164 164 config.add_view(RepoGroupIntegrationsView,
165 165 attr='settings_post',
166 166 renderer='rhodecode:templates/admin/integrations/form.mako',
167 167 request_method='POST',
168 168 route_name='repo_group_integrations_create')
169 169
170 170 config.add_route('repo_group_integrations_edit',
171 171 add_route_requirements('/{repo_group_name}/_settings/integrations/{integration}/{integration_id}'),
172 172 repo_group_route=True,
173 173 valid_integration=True)
174 174
175 175 config.add_view(RepoGroupIntegrationsView,
176 176 attr='settings_get',
177 177 renderer='rhodecode:templates/admin/integrations/form.mako',
178 178 request_method='GET',
179 179 route_name='repo_group_integrations_edit')
180 180 config.add_view(RepoGroupIntegrationsView,
181 181 attr='settings_post',
182 182 renderer='rhodecode:templates/admin/integrations/form.mako',
183 183 request_method='POST',
184 184 route_name='repo_group_integrations_edit')
185 185
186 186 # repo integrations
187 187 config.add_route('repo_integrations_home',
188 188 add_route_requirements('/{repo_name}/settings/integrations'),
189 189 repo_route=True)
190 190 config.add_view(RepoIntegrationsView,
191 191 attr='integration_list',
192 192 request_method='GET',
193 193 renderer='rhodecode:templates/admin/integrations/list.mako',
194 194 route_name='repo_integrations_home')
195 195
196 196 config.add_route('repo_integrations_new',
197 197 add_route_requirements('/{repo_name}/settings/integrations/new'),
198 198 repo_route=True)
199 199 config.add_view(RepoIntegrationsView,
200 200 attr='new_integration',
201 201 renderer='rhodecode:templates/admin/integrations/new.mako',
202 202 request_method='GET',
203 203 route_name='repo_integrations_new')
204 204
205 205 config.add_route('repo_integrations_list',
206 206 add_route_requirements('/{repo_name}/settings/integrations/{integration}'),
207 207 repo_route=True,
208 208 valid_integration=True)
209 209 config.add_view(RepoIntegrationsView,
210 210 attr='integration_list',
211 211 request_method='GET',
212 212 renderer='rhodecode:templates/admin/integrations/list.mako',
213 213 route_name='repo_integrations_list')
214 214
215 215 config.add_route('repo_integrations_create',
216 216 add_route_requirements('/{repo_name}/settings/integrations/{integration}/new'),
217 217 repo_route=True,
218 218 valid_integration=True)
219 219 config.add_view(RepoIntegrationsView,
220 220 attr='settings_get',
221 221 renderer='rhodecode:templates/admin/integrations/form.mako',
222 222 request_method='GET',
223 223 route_name='repo_integrations_create')
224 224 config.add_view(RepoIntegrationsView,
225 225 attr='settings_post',
226 226 renderer='rhodecode:templates/admin/integrations/form.mako',
227 227 request_method='POST',
228 228 route_name='repo_integrations_create')
229 229
230 230 config.add_route('repo_integrations_edit',
231 231 add_route_requirements('/{repo_name}/settings/integrations/{integration}/{integration_id}'),
232 232 repo_route=True,
233 233 valid_integration=True)
234 234 config.add_view(RepoIntegrationsView,
235 235 attr='settings_get',
236 236 renderer='rhodecode:templates/admin/integrations/form.mako',
237 237 request_method='GET',
238 238 route_name='repo_integrations_edit')
239 239 config.add_view(RepoIntegrationsView,
240 240 attr='settings_post',
241 241 renderer='rhodecode:templates/admin/integrations/form.mako',
242 242 request_method='POST',
243 243 route_name='repo_integrations_edit')
@@ -1,269 +1,269 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23 from rhodecode.model.db import Integration
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.integrations import integration_type_registry
26 26
27 27
28 28 def route_path(name, **kwargs):
29 29 return {
30 30 'home': '/',
31 31 }[name].format(**kwargs)
32 32
33 33
34 34 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
35 35 admin_view):
36 36 """
37 37 Posts form data to create integration at the url given then deletes it and
38 38 checks if the redirect url is correct.
39 39 """
40 40 repo_name = repo.repo_name
41 41 repo_group_name = repo_group.group_name
42 42 app.post(url, params={}, status=403) # missing csrf check
43 43 response = app.post(url, params={'csrf_token': csrf_token})
44 44 assert response.status_code == 200
45 45 response.mustcontain('Errors exist')
46 46
47 47 scopes_destinations = [
48 48 ('global',
49 49 ADMIN_PREFIX + '/integrations'),
50 50 ('root-repos',
51 51 ADMIN_PREFIX + '/integrations'),
52 52 ('repo:%s' % repo_name,
53 53 '/%s/settings/integrations' % repo_name),
54 54 ('repogroup:%s' % repo_group_name,
55 55 '/%s/_settings/integrations' % repo_group_name),
56 56 ('repogroup-recursive:%s' % repo_group_name,
57 57 '/%s/_settings/integrations' % repo_group_name),
58 58 ]
59 59
60 60 for scope, destination in scopes_destinations:
61 61 if admin_view:
62 62 destination = ADMIN_PREFIX + '/integrations'
63 63
64 64 form_data = [
65 65 ('csrf_token', csrf_token),
66 66 ('__start__', 'options:mapping'),
67 67 ('name', 'test integration'),
68 68 ('scope', scope),
69 69 ('enabled', 'true'),
70 70 ('__end__', 'options:mapping'),
71 71 ('__start__', 'settings:mapping'),
72 72 ('test_int_field', '34'),
73 73 ('test_string_field', ''), # empty value on purpose as it's required
74 74 ('__end__', 'settings:mapping'),
75 75 ]
76 76 errors_response = app.post(url, form_data)
77 assert 'Errors exist' in errors_response.body
77 assert 'Errors exist' in errors_response.text
78 78
79 79 form_data[-2] = ('test_string_field', 'data!')
80 80 assert Session().query(Integration).count() == 0
81 81 created_response = app.post(url, form_data)
82 82 assert Session().query(Integration).count() == 1
83 83
84 84 delete_response = app.post(
85 85 created_response.location,
86 86 params={'csrf_token': csrf_token, 'delete': 'delete'})
87 87
88 88 assert Session().query(Integration).count() == 0
89 89 assert delete_response.location.endswith(destination)
90 90
91 91
92 92
93 93 @pytest.mark.usefixtures('app', 'autologin_user')
94 94 class TestIntegrationsView(object):
95 95 pass
96 96
97 97
98 98 class TestGlobalIntegrationsView(TestIntegrationsView):
99 99 def test_index_no_integrations(self):
100 100 url = ADMIN_PREFIX + '/integrations'
101 101 response = self.app.get(url)
102 102
103 103 assert response.status_code == 200
104 104 response.mustcontain('exist yet')
105 105
106 106 def test_index_with_integrations(self, global_integration_stub):
107 107 url = ADMIN_PREFIX + '/integrations'
108 108 response = self.app.get(url)
109 109
110 110 assert response.status_code == 200
111 111 response.mustcontain(no=['exist yet'])
112 112 response.mustcontain(global_integration_stub.name)
113 113
114 114 @pytest.mark.parametrize(
115 115 'IntegrationType', integration_type_registry.values())
116 116 def test_new_integration_page(self, IntegrationType):
117 117 url = ADMIN_PREFIX + '/integrations/new'
118 118
119 119 response = self.app.get(url, status=200)
120 120 if not IntegrationType.is_dummy:
121 121 url = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
122 122 integration=IntegrationType.key)
123 123 response.mustcontain(url)
124 124
125 125 @pytest.mark.parametrize(
126 126 'IntegrationType', integration_type_registry.values())
127 127 def test_get_create_integration_page(self, IntegrationType):
128 128 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
129 129 integration_key=IntegrationType.key)
130 130 if IntegrationType.is_dummy:
131 131 self.app.get(url, status=404)
132 132 else:
133 133 response = self.app.get(url, status=200)
134 134 response.mustcontain(IntegrationType.display_name)
135 135
136 136 def test_post_integration_page(self, StubIntegrationType, csrf_token,
137 137 test_repo_group, backend_random):
138 138 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
139 139 integration_key=StubIntegrationType.key)
140 140
141 141 _post_integration_test_helper(
142 142 self.app, url, csrf_token, admin_view=True,
143 143 repo=backend_random.repo, repo_group=test_repo_group)
144 144
145 145
146 146 class TestRepoIntegrationsView(TestIntegrationsView):
147 147 def test_index_no_integrations(self, backend_random):
148 148 url = '/{repo_name}/settings/integrations'.format(
149 149 repo_name=backend_random.repo.repo_name)
150 150 response = self.app.get(url)
151 151
152 152 assert response.status_code == 200
153 153 response.mustcontain('exist yet')
154 154
155 155 def test_index_with_integrations(self, repo_integration_stub):
156 156 url = '/{repo_name}/settings/integrations'.format(
157 157 repo_name=repo_integration_stub.repo.repo_name)
158 158 stub_name = repo_integration_stub.name
159 159
160 160 response = self.app.get(url)
161 161
162 162 assert response.status_code == 200
163 163 response.mustcontain(stub_name)
164 164 response.mustcontain(no=['exist yet'])
165 165
166 166 @pytest.mark.parametrize(
167 167 'IntegrationType', integration_type_registry.values())
168 168 def test_new_integration_page(self, backend_random, IntegrationType):
169 169 repo_name = backend_random.repo.repo_name
170 170 url = '/{repo_name}/settings/integrations/new'.format(
171 171 repo_name=repo_name)
172 172
173 173 response = self.app.get(url, status=200)
174 174
175 175 url = '/{repo_name}/settings/integrations/{integration}/new'.format(
176 176 repo_name=repo_name,
177 177 integration=IntegrationType.key)
178 178 if not IntegrationType.is_dummy:
179 179 response.mustcontain(url)
180 180
181 181 @pytest.mark.parametrize(
182 182 'IntegrationType', integration_type_registry.values())
183 183 def test_get_create_integration_page(self, backend_random, IntegrationType):
184 184 repo_name = backend_random.repo.repo_name
185 185 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
186 186 repo_name=repo_name, integration_key=IntegrationType.key)
187 187 if IntegrationType.is_dummy:
188 188 self.app.get(url, status=404)
189 189 else:
190 190 response = self.app.get(url, status=200)
191 191 response.mustcontain(IntegrationType.display_name)
192 192
193 193 def test_post_integration_page(self, backend_random, test_repo_group,
194 194 StubIntegrationType, csrf_token):
195 195 repo_name = backend_random.repo.repo_name
196 196 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
197 197 repo_name=repo_name, integration_key=StubIntegrationType.key)
198 198
199 199 _post_integration_test_helper(
200 200 self.app, url, csrf_token, admin_view=False,
201 201 repo=backend_random.repo, repo_group=test_repo_group)
202 202
203 203
204 204 class TestRepoGroupIntegrationsView(TestIntegrationsView):
205 205 def test_index_no_integrations(self, test_repo_group):
206 206 url = '/{repo_group_name}/_settings/integrations'.format(
207 207 repo_group_name=test_repo_group.group_name)
208 208 response = self.app.get(url)
209 209
210 210 assert response.status_code == 200
211 211 response.mustcontain('exist yet')
212 212
213 213 def test_index_with_integrations(
214 214 self, test_repo_group, repogroup_integration_stub):
215 215
216 216 url = '/{repo_group_name}/_settings/integrations'.format(
217 217 repo_group_name=test_repo_group.group_name)
218 218
219 219 stub_name = repogroup_integration_stub.name
220 220 response = self.app.get(url)
221 221
222 222 assert response.status_code == 200
223 223 response.mustcontain(no=['exist yet'])
224 224 response.mustcontain(stub_name)
225 225
226 226 def test_new_integration_page(self, test_repo_group):
227 227 repo_group_name = test_repo_group.group_name
228 228 url = '/{repo_group_name}/_settings/integrations/new'.format(
229 229 repo_group_name=test_repo_group.group_name)
230 230
231 231 response = self.app.get(url)
232 232
233 233 assert response.status_code == 200
234 234
235 235 for integration_key, integration_obj in integration_type_registry.items():
236 236 if not integration_obj.is_dummy:
237 237 nurl = (
238 238 '/{repo_group_name}/_settings/integrations/{integration}/new').format(
239 239 repo_group_name=repo_group_name,
240 240 integration=integration_key)
241 241 response.mustcontain(nurl)
242 242
243 243 @pytest.mark.parametrize(
244 244 'IntegrationType', integration_type_registry.values())
245 245 def test_get_create_integration_page(
246 246 self, test_repo_group, IntegrationType):
247 247
248 248 repo_group_name = test_repo_group.group_name
249 249 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
250 250 ).format(repo_group_name=repo_group_name,
251 251 integration_key=IntegrationType.key)
252 252
253 253 if not IntegrationType.is_dummy:
254 254 response = self.app.get(url, status=200)
255 255 response.mustcontain(IntegrationType.display_name)
256 256 else:
257 257 self.app.get(url, status=404)
258 258
259 259 def test_post_integration_page(self, test_repo_group, backend_random,
260 260 StubIntegrationType, csrf_token):
261 261
262 262 repo_group_name = test_repo_group.group_name
263 263 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
264 264 ).format(repo_group_name=repo_group_name,
265 265 integration_key=StubIntegrationType.key)
266 266
267 267 _post_integration_test_helper(
268 268 self.app, url, csrf_token, admin_view=False,
269 269 repo=backend_random.repo, repo_group=test_repo_group)
@@ -1,450 +1,452 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import string
23 23 import collections
24 24 import logging
25 25 import requests
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29 from requests.adapters import HTTPAdapter
28 30 from requests.packages.urllib3.util.retry import Retry
29 31
30 32 from mako import exceptions
31 33
32 34 from rhodecode.lib.utils2 import safe_str
33 35 from rhodecode.translation import _
34 36
35 37
36 38 log = logging.getLogger(__name__)
37 39
38 40
39 41 class UrlTmpl(string.Template):
40 42
41 43 def safe_substitute(self, **kws):
42 44 # url encode the kw for usage in url
43 45 kws = {k: urllib.parse.quote(safe_str(v)) for k, v in kws.items()}
44 46 return super(UrlTmpl, self).safe_substitute(**kws)
45 47
46 48
47 49 class IntegrationTypeBase(object):
48 50 """ Base class for IntegrationType plugins """
49 51 is_dummy = False
50 52 description = ''
51 53
52 54 @classmethod
53 55 def icon(cls):
54 56 return '''
55 57 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
56 58 <svg
57 59 xmlns:dc="http://purl.org/dc/elements/1.1/"
58 60 xmlns:cc="http://creativecommons.org/ns#"
59 61 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
60 62 xmlns:svg="http://www.w3.org/2000/svg"
61 63 xmlns="http://www.w3.org/2000/svg"
62 64 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
63 65 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
64 66 viewBox="0 -256 1792 1792"
65 67 id="svg3025"
66 68 version="1.1"
67 69 inkscape:version="0.48.3.1 r9886"
68 70 width="100%"
69 71 height="100%"
70 72 sodipodi:docname="cog_font_awesome.svg">
71 73 <metadata
72 74 id="metadata3035">
73 75 <rdf:RDF>
74 76 <cc:Work
75 77 rdf:about="">
76 78 <dc:format>image/svg+xml</dc:format>
77 79 <dc:type
78 80 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
79 81 </cc:Work>
80 82 </rdf:RDF>
81 83 </metadata>
82 84 <defs
83 85 id="defs3033" />
84 86 <sodipodi:namedview
85 87 pagecolor="#ffffff"
86 88 bordercolor="#666666"
87 89 borderopacity="1"
88 90 objecttolerance="10"
89 91 gridtolerance="10"
90 92 guidetolerance="10"
91 93 inkscape:pageopacity="0"
92 94 inkscape:pageshadow="2"
93 95 inkscape:window-width="640"
94 96 inkscape:window-height="480"
95 97 id="namedview3031"
96 98 showgrid="false"
97 99 inkscape:zoom="0.13169643"
98 100 inkscape:cx="896"
99 101 inkscape:cy="896"
100 102 inkscape:window-x="0"
101 103 inkscape:window-y="25"
102 104 inkscape:window-maximized="0"
103 105 inkscape:current-layer="svg3025" />
104 106 <g
105 107 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
106 108 id="g3027">
107 109 <path
108 110 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
109 111 id="path3029"
110 112 inkscape:connector-curvature="0"
111 113 style="fill:currentColor" />
112 114 </g>
113 115 </svg>
114 116 '''
115 117
116 118 def __init__(self, settings):
117 119 """
118 120 :param settings: dict of settings to be used for the integration
119 121 """
120 122 self.settings = settings
121 123
122 124 def settings_schema(self):
123 125 """
124 126 A colander schema of settings for the integration type
125 127 """
126 128 return colander.Schema()
127 129
128 130 def event_enabled(self, event):
129 131 """
130 132 Checks if submitted event is enabled based on the plugin settings
131 133 :param event:
132 134 :return: bool
133 135 """
134 136 allowed_events = self.settings.get('events') or []
135 137 if event.name not in allowed_events:
136 138 log.debug('event ignored: %r event %s not in allowed set of events %s',
137 139 event, event.name, allowed_events)
138 140 return False
139 141 return True
140 142
141 143
142 144 class EEIntegration(IntegrationTypeBase):
143 145 description = 'Integration available in RhodeCode EE edition.'
144 146 is_dummy = True
145 147
146 148 def __init__(self, name, key, settings=None):
147 149 self.display_name = name
148 150 self.key = key
149 151 super(EEIntegration, self).__init__(settings)
150 152
151 153
152 154 # Helpers #
153 155 # updating this required to update the `common_vars` as well.
154 156 WEBHOOK_URL_VARS = [
155 157 # GENERAL
156 158 ('General', [
157 159 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
158 160 ('repo_name', 'Full name of the repository'),
159 161 ('repo_type', 'VCS type of repository'),
160 162 ('repo_id', 'Unique id of repository'),
161 163 ('repo_url', 'Repository url'),
162 164 ]
163 165 ),
164 166 # extra repo fields
165 167 ('Repository', [
166 168 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
167 169 ]
168 170 ),
169 171 # special attrs below that we handle, using multi-call
170 172 ('Commit push - Multicalls', [
171 173 ('branch', 'Name of each branch submitted, if any.'),
172 174 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
173 175 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
174 176 ]
175 177 ),
176 178 # pr events vars
177 179 ('Pull request', [
178 180 ('pull_request_id', 'Unique ID of the pull request.'),
179 181 ('pull_request_title', 'Title of the pull request.'),
180 182 ('pull_request_url', 'Pull request url.'),
181 183 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
182 184 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
183 185 'Changes after PR update'),
184 186 ]
185 187 ),
186 188 # commit comment event vars
187 189 ('Commit comment', [
188 190 ('commit_comment_id', 'Unique ID of the comment made on a commit.'),
189 191 ('commit_comment_text', 'Text of commit comment.'),
190 192 ('commit_comment_type', 'Type of comment, e.g note/todo.'),
191 193
192 194 ('commit_comment_f_path', 'Optionally path of file for inline comments.'),
193 195 ('commit_comment_line_no', 'Line number of the file: eg o10, or n200'),
194 196
195 197 ('commit_comment_commit_id', 'Commit id that comment was left at.'),
196 198 ('commit_comment_commit_branch', 'Commit branch that comment was left at'),
197 199 ('commit_comment_commit_message', 'Commit message that comment was left at'),
198 200 ]
199 201 ),
200 202 # user who triggers the call
201 203 ('Caller', [
202 204 ('username', 'User who triggered the call.'),
203 205 ('user_id', 'User id who triggered the call.'),
204 206 ]
205 207 ),
206 208 ]
207 209
208 210 # common vars for url template used for CI plugins. Shared with webhook
209 211 CI_URL_VARS = WEBHOOK_URL_VARS
210 212
211 213
212 214 class CommitParsingDataHandler(object):
213 215
214 216 def aggregate_branch_data(self, branches, commits):
215 217 branch_data = collections.OrderedDict()
216 218 for obj in branches:
217 219 branch_data[obj['name']] = obj
218 220
219 221 branches_commits = collections.OrderedDict()
220 222 for commit in commits:
221 223 if commit.get('git_ref_change'):
222 224 # special case for GIT that allows creating tags,
223 225 # deleting branches without associated commit
224 226 continue
225 227 commit_branch = commit['branch']
226 228
227 229 if commit_branch not in branches_commits:
228 230 _branch = branch_data[commit_branch] \
229 231 if commit_branch else commit_branch
230 232 branch_commits = {'branch': _branch,
231 233 'branch_head': '',
232 234 'commits': []}
233 235 branches_commits[commit_branch] = branch_commits
234 236
235 237 branch_commits = branches_commits[commit_branch]
236 238 branch_commits['commits'].append(commit)
237 239 branch_commits['branch_head'] = commit['raw_id']
238 240 return branches_commits
239 241
240 242
241 243 class WebhookDataHandler(CommitParsingDataHandler):
242 244 name = 'webhook'
243 245
244 246 def __init__(self, template_url, headers):
245 247 self.template_url = template_url
246 248 self.headers = headers
247 249
248 250 def get_base_parsed_template(self, data):
249 251 """
250 252 initially parses the passed in template with some common variables
251 253 available on ALL calls
252 254 """
253 255 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
254 256 common_vars = {
255 257 'repo_name': data['repo']['repo_name'],
256 258 'repo_type': data['repo']['repo_type'],
257 259 'repo_id': data['repo']['repo_id'],
258 260 'repo_url': data['repo']['url'],
259 261 'username': data['actor']['username'],
260 262 'user_id': data['actor']['user_id'],
261 263 'event_name': data['name']
262 264 }
263 265
264 266 extra_vars = {}
265 267 for extra_key, extra_val in data['repo']['extra_fields'].items():
266 268 extra_vars['extra__{}'.format(extra_key)] = extra_val
267 269 common_vars.update(extra_vars)
268 270
269 271 template_url = self.template_url.replace('${extra:', '${extra__')
270 272 for k, v in common_vars.items():
271 273 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
272 274 return template_url
273 275
274 276 def repo_push_event_handler(self, event, data):
275 277 url = self.get_base_parsed_template(data)
276 278 url_calls = []
277 279
278 280 branches_commits = self.aggregate_branch_data(
279 281 data['push']['branches'], data['push']['commits'])
280 282 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
281 283 # call it multiple times, for each branch if used in variables
282 284 for branch, commit_ids in branches_commits.items():
283 285 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
284 286
285 287 if '${branch_head}' in branch_url:
286 288 # last commit in the aggregate is the head of the branch
287 289 branch_head = commit_ids['branch_head']
288 290 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
289 291
290 292 # call further down for each commit if used
291 293 if '${commit_id}' in branch_url:
292 294 for commit_data in commit_ids['commits']:
293 295 commit_id = commit_data['raw_id']
294 296 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
295 297 # register per-commit call
296 298 log.debug(
297 299 'register %s call(%s) to url %s',
298 300 self.name, event, commit_url)
299 301 url_calls.append(
300 302 (commit_url, self.headers, data))
301 303
302 304 else:
303 305 # register per-branch call
304 306 log.debug('register %s call(%s) to url %s',
305 307 self.name, event, branch_url)
306 308 url_calls.append((branch_url, self.headers, data))
307 309
308 310 else:
309 311 log.debug('register %s call(%s) to url %s', self.name, event, url)
310 312 url_calls.append((url, self.headers, data))
311 313
312 314 return url_calls
313 315
314 316 def repo_commit_comment_handler(self, event, data):
315 317 url = self.get_base_parsed_template(data)
316 318 log.debug('register %s call(%s) to url %s', self.name, event, url)
317 319 comment_vars = [
318 320 ('commit_comment_id', data['comment']['comment_id']),
319 321 ('commit_comment_text', data['comment']['comment_text']),
320 322 ('commit_comment_type', data['comment']['comment_type']),
321 323
322 324 ('commit_comment_f_path', data['comment']['comment_f_path']),
323 325 ('commit_comment_line_no', data['comment']['comment_line_no']),
324 326
325 327 ('commit_comment_commit_id', data['commit']['commit_id']),
326 328 ('commit_comment_commit_branch', data['commit']['commit_branch']),
327 329 ('commit_comment_commit_message', data['commit']['commit_message']),
328 330 ]
329 331 for k, v in comment_vars:
330 332 url = UrlTmpl(url).safe_substitute(**{k: v})
331 333
332 334 return [(url, self.headers, data)]
333 335
334 336 def repo_commit_comment_edit_handler(self, event, data):
335 337 url = self.get_base_parsed_template(data)
336 338 log.debug('register %s call(%s) to url %s', self.name, event, url)
337 339 comment_vars = [
338 340 ('commit_comment_id', data['comment']['comment_id']),
339 341 ('commit_comment_text', data['comment']['comment_text']),
340 342 ('commit_comment_type', data['comment']['comment_type']),
341 343
342 344 ('commit_comment_f_path', data['comment']['comment_f_path']),
343 345 ('commit_comment_line_no', data['comment']['comment_line_no']),
344 346
345 347 ('commit_comment_commit_id', data['commit']['commit_id']),
346 348 ('commit_comment_commit_branch', data['commit']['commit_branch']),
347 349 ('commit_comment_commit_message', data['commit']['commit_message']),
348 350 ]
349 351 for k, v in comment_vars:
350 352 url = UrlTmpl(url).safe_substitute(**{k: v})
351 353
352 354 return [(url, self.headers, data)]
353 355
354 356 def repo_create_event_handler(self, event, data):
355 357 url = self.get_base_parsed_template(data)
356 358 log.debug('register %s call(%s) to url %s', self.name, event, url)
357 359 return [(url, self.headers, data)]
358 360
359 361 def pull_request_event_handler(self, event, data):
360 362 url = self.get_base_parsed_template(data)
361 363 log.debug('register %s call(%s) to url %s', self.name, event, url)
362 364 pr_vars = [
363 365 ('pull_request_id', data['pullrequest']['pull_request_id']),
364 366 ('pull_request_title', data['pullrequest']['title']),
365 367 ('pull_request_url', data['pullrequest']['url']),
366 368 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
367 369 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
368 370 ]
369 371 for k, v in pr_vars:
370 372 url = UrlTmpl(url).safe_substitute(**{k: v})
371 373
372 374 return [(url, self.headers, data)]
373 375
374 376 def __call__(self, event, data):
375 377 from rhodecode import events
376 378
377 379 if isinstance(event, events.RepoPushEvent):
378 380 return self.repo_push_event_handler(event, data)
379 381 elif isinstance(event, events.RepoCreateEvent):
380 382 return self.repo_create_event_handler(event, data)
381 383 elif isinstance(event, events.RepoCommitCommentEvent):
382 384 return self.repo_commit_comment_handler(event, data)
383 385 elif isinstance(event, events.RepoCommitCommentEditEvent):
384 386 return self.repo_commit_comment_edit_handler(event, data)
385 387 elif isinstance(event, events.PullRequestEvent):
386 388 return self.pull_request_event_handler(event, data)
387 389 else:
388 390 raise ValueError(
389 391 'event type `{}` has no handler defined'.format(event.__class__))
390 392
391 393
392 394 def get_auth(settings):
393 395 from requests.auth import HTTPBasicAuth
394 396 username = settings.get('username')
395 397 password = settings.get('password')
396 398 if username and password:
397 399 return HTTPBasicAuth(username, password)
398 400 return None
399 401
400 402
401 403 def get_web_token(settings):
402 404 return settings['secret_token']
403 405
404 406
405 407 def get_url_vars(url_vars):
406 408 items = []
407 409
408 410 for section, section_items in url_vars:
409 411 items.append('\n*{}*'.format(section))
410 412 for key, explanation in section_items:
411 413 items.append(' {} - {}'.format('${' + key + '}', explanation))
412 414 return '\n'.join(items)
413 415
414 416
415 417 def render_with_traceback(template, *args, **kwargs):
416 418 try:
417 419 return template.render(*args, **kwargs)
418 420 except Exception:
419 421 log.error(exceptions.text_error_template().render())
420 422 raise
421 423
422 424
423 425 STATUS_400 = (400, 401, 403)
424 426 STATUS_500 = (500, 502, 504)
425 427
426 428
427 429 def requests_retry_call(
428 430 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
429 431 session=None):
430 432 """
431 433 session = requests_retry_session()
432 434 response = session.get('http://example.com')
433 435
434 436 :param retries:
435 437 :param backoff_factor:
436 438 :param status_forcelist:
437 439 :param session:
438 440 """
439 441 session = session or requests.Session()
440 442 retry = Retry(
441 443 total=retries,
442 444 read=retries,
443 445 connect=retries,
444 446 backoff_factor=backoff_factor,
445 447 status_forcelist=status_forcelist,
446 448 )
447 449 adapter = HTTPAdapter(max_retries=retry)
448 450 session.mount('http://', adapter)
449 451 session.mount('https://', adapter)
450 452 return session
@@ -1,469 +1,469 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import deform
22 22 import logging
23 23 import peppercorn
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
26 26
27 27 from rhodecode.integrations import integration_type_registry
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode.apps._base.navigation import navigation_list
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, CSRFRequired, HasPermissionAnyDecorator,
32 32 HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.lib.helpers import Page
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.integration import IntegrationModel
39 39 from rhodecode.model.validation_schema.schemas.integration_schema import (
40 40 make_integration_schema, IntegrationScopeType)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class IntegrationSettingsViewBase(BaseAppView):
46 46 """
47 47 Base Integration settings view used by both repo / global settings
48 48 """
49 49
50 50 def __init__(self, context, request):
51 51 super(IntegrationSettingsViewBase, self).__init__(context, request)
52 52 self._load_view_context()
53 53
54 54 def _load_view_context(self):
55 55 """
56 56 This avoids boilerplate for repo/global+list/edit+views/templates
57 57 by doing all possible contexts at the same time however it should
58 58 be split up into separate functions once more "contexts" exist
59 59 """
60 60
61 61 self.IntegrationType = None
62 62 self.repo = None
63 63 self.repo_group = None
64 64 self.integration = None
65 65 self.integrations = {}
66 66
67 67 request = self.request
68 68
69 69 if 'repo_name' in request.matchdict: # in repo settings context
70 70 repo_name = request.matchdict['repo_name']
71 71 self.repo = Repository.get_by_repo_name(repo_name)
72 72
73 73 if 'repo_group_name' in request.matchdict: # in group settings context
74 74 repo_group_name = request.matchdict['repo_group_name']
75 75 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
76 76
77 77 if 'integration' in request.matchdict: # integration type context
78 78 integration_type = request.matchdict['integration']
79 79 if integration_type not in integration_type_registry:
80 80 raise HTTPNotFound()
81 81
82 82 self.IntegrationType = integration_type_registry[integration_type]
83 83 if self.IntegrationType.is_dummy:
84 84 raise HTTPNotFound()
85 85
86 86 if 'integration_id' in request.matchdict: # single integration context
87 87 integration_id = request.matchdict['integration_id']
88 88 self.integration = Integration.get(integration_id)
89 89
90 90 # extra perms check just in case
91 91 if not self._has_perms_for_integration(self.integration):
92 92 raise HTTPForbidden()
93 93
94 94 self.settings = self.integration and self.integration.settings or {}
95 95 self.admin_view = not (self.repo or self.repo_group)
96 96
97 97 def _has_perms_for_integration(self, integration):
98 98 perms = self.request.user.permissions
99 99
100 100 if 'hg.admin' in perms['global']:
101 101 return True
102 102
103 103 if integration.repo:
104 104 return perms['repositories'].get(
105 105 integration.repo.repo_name) == 'repository.admin'
106 106
107 107 if integration.repo_group:
108 108 return perms['repositories_groups'].get(
109 109 integration.repo_group.group_name) == 'group.admin'
110 110
111 111 return False
112 112
113 113 def _get_local_tmpl_context(self, include_app_defaults=True):
114 114 _ = self.request.translate
115 115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
116 116 include_app_defaults=include_app_defaults)
117 117 c.active = 'integrations'
118 118
119 119 return c
120 120
121 121 def _form_schema(self):
122 122 schema = make_integration_schema(IntegrationType=self.IntegrationType,
123 123 settings=self.settings)
124 124
125 125 # returns a clone, important if mutating the schema later
126 126 return schema.bind(
127 127 permissions=self.request.user.permissions,
128 128 no_scope=not self.admin_view)
129 129
130 130 def _form_defaults(self):
131 131 _ = self.request.translate
132 132 defaults = {}
133 133
134 134 if self.integration:
135 135 defaults['settings'] = self.integration.settings or {}
136 136 defaults['options'] = {
137 137 'name': self.integration.name,
138 138 'enabled': self.integration.enabled,
139 139 'scope': {
140 140 'repo': self.integration.repo,
141 141 'repo_group': self.integration.repo_group,
142 142 'child_repos_only': self.integration.child_repos_only,
143 143 },
144 144 }
145 145 else:
146 146 if self.repo:
147 147 scope = _('{repo_name} repository').format(
148 148 repo_name=self.repo.repo_name)
149 149 elif self.repo_group:
150 150 scope = _('{repo_group_name} repo group').format(
151 151 repo_group_name=self.repo_group.group_name)
152 152 else:
153 153 scope = _('Global')
154 154
155 155 defaults['options'] = {
156 156 'enabled': True,
157 157 'name': _('{name} integration').format(
158 158 name=self.IntegrationType.display_name),
159 159 }
160 160 defaults['options']['scope'] = {
161 161 'repo': self.repo,
162 162 'repo_group': self.repo_group,
163 163 }
164 164
165 165 return defaults
166 166
167 167 def _delete_integration(self, integration):
168 168 _ = self.request.translate
169 169 Session().delete(integration)
170 170 Session().commit()
171 171 h.flash(
172 172 _('Integration {integration_name} deleted successfully.').format(
173 173 integration_name=integration.name),
174 174 category='success')
175 175
176 176 if self.repo:
177 177 redirect_to = self.request.route_path(
178 178 'repo_integrations_home', repo_name=self.repo.repo_name)
179 179 elif self.repo_group:
180 180 redirect_to = self.request.route_path(
181 181 'repo_group_integrations_home',
182 182 repo_group_name=self.repo_group.group_name)
183 183 else:
184 184 redirect_to = self.request.route_path('global_integrations_home')
185 185 raise HTTPFound(redirect_to)
186 186
187 187 def _integration_list(self):
188 188 """ List integrations """
189 189
190 190 c = self.load_default_context()
191 191 if self.repo:
192 192 scope = self.repo
193 193 elif self.repo_group:
194 194 scope = self.repo_group
195 195 else:
196 196 scope = 'all'
197 197
198 198 integrations = []
199 199
200 200 for IntType, integration in IntegrationModel().get_integrations(
201 201 scope=scope, IntegrationType=self.IntegrationType):
202 202
203 203 # extra permissions check *just in case*
204 204 if not self._has_perms_for_integration(integration):
205 205 continue
206 206
207 207 integrations.append((IntType, integration))
208 208
209 209 sort_arg = self.request.GET.get('sort', 'name:asc')
210 210 sort_dir = 'asc'
211 211 if ':' in sort_arg:
212 212 sort_field, sort_dir = sort_arg.split(':')
213 213 else:
214 214 sort_field = sort_arg, 'asc'
215 215
216 216 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
217 217
218 218 integrations.sort(
219 219 key=lambda x: getattr(x[1], sort_field),
220 220 reverse=(sort_dir == 'desc'))
221 221
222 222 def url_generator(page_num):
223 223 query_params = {
224 224 'page': page_num
225 225 }
226 226 return self.request.current_route_path(_query=query_params)
227 227
228 228 page = safe_int(self.request.GET.get('page', 1), 1)
229 229
230 230 integrations = Page(
231 231 integrations, page=page, items_per_page=10, url_maker=url_generator)
232 232
233 233 c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc'
234 234
235 235 c.current_IntegrationType = self.IntegrationType
236 236 c.integrations_list = integrations
237 237 c.available_integrations = integration_type_registry
238 238
239 239 return self._get_template_context(c)
240 240
241 241 def _settings_get(self, defaults=None, form=None):
242 242 """
243 243 View that displays the integration settings as a form.
244 244 """
245 245 c = self.load_default_context()
246 246
247 247 defaults = defaults or self._form_defaults()
248 248 schema = self._form_schema()
249 249
250 250 if self.integration:
251 251 buttons = ('submit', 'delete')
252 252 else:
253 253 buttons = ('submit',)
254 254
255 255 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
256 256
257 257 c.form = form
258 258 c.current_IntegrationType = self.IntegrationType
259 259 c.integration = self.integration
260 260
261 261 return self._get_template_context(c)
262 262
263 263 def _settings_post(self):
264 264 """
265 265 View that validates and stores the integration settings.
266 266 """
267 267 _ = self.request.translate
268 268
269 controls = self.request.POST.items()
269 controls = list(self.request.POST.items())
270 270 pstruct = peppercorn.parse(controls)
271 271
272 272 if self.integration and pstruct.get('delete'):
273 273 return self._delete_integration(self.integration)
274 274
275 275 schema = self._form_schema()
276 276
277 277 skip_settings_validation = False
278 278 if self.integration and 'enabled' not in pstruct.get('options', {}):
279 279 skip_settings_validation = True
280 280 schema['settings'].validator = None
281 281 for field in schema['settings'].children:
282 282 field.validator = None
283 283 field.missing = ''
284 284
285 285 if self.integration:
286 286 buttons = ('submit', 'delete')
287 287 else:
288 288 buttons = ('submit',)
289 289
290 290 form = deform.Form(schema, buttons=buttons)
291 291
292 292 if not self.admin_view:
293 293 # scope is read only field in these cases, and has to be added
294 294 options = pstruct.setdefault('options', {})
295 295 if 'scope' not in options:
296 296 options['scope'] = IntegrationScopeType().serialize(None, {
297 297 'repo': self.repo,
298 298 'repo_group': self.repo_group,
299 299 })
300 300
301 301 try:
302 302 valid_data = form.validate_pstruct(pstruct)
303 303 except deform.ValidationFailure as e:
304 304 h.flash(
305 305 _('Errors exist when saving integration settings. '
306 306 'Please check the form inputs.'),
307 307 category='error')
308 308 return self._settings_get(form=e)
309 309
310 310 if not self.integration:
311 311 self.integration = Integration()
312 312 self.integration.integration_type = self.IntegrationType.key
313 313 Session().add(self.integration)
314 314
315 315 scope = valid_data['options']['scope']
316 316
317 317 IntegrationModel().update_integration(self.integration,
318 318 name=valid_data['options']['name'],
319 319 enabled=valid_data['options']['enabled'],
320 320 settings=valid_data['settings'],
321 321 repo=scope['repo'],
322 322 repo_group=scope['repo_group'],
323 323 child_repos_only=scope['child_repos_only'],
324 324 )
325 325
326 326 self.integration.settings = valid_data['settings']
327 327 Session().commit()
328 328 # Display success message and redirect.
329 329 h.flash(
330 330 _('Integration {integration_name} updated successfully.').format(
331 331 integration_name=self.IntegrationType.display_name),
332 332 category='success')
333 333
334 334 # if integration scope changes, we must redirect to the right place
335 335 # keeping in mind if the original view was for /repo/ or /_admin/
336 336 admin_view = not (self.repo or self.repo_group)
337 337
338 338 if self.integration.repo and not admin_view:
339 339 redirect_to = self.request.route_path(
340 340 'repo_integrations_edit',
341 341 repo_name=self.integration.repo.repo_name,
342 342 integration=self.integration.integration_type,
343 343 integration_id=self.integration.integration_id)
344 344 elif self.integration.repo_group and not admin_view:
345 345 redirect_to = self.request.route_path(
346 346 'repo_group_integrations_edit',
347 347 repo_group_name=self.integration.repo_group.group_name,
348 348 integration=self.integration.integration_type,
349 349 integration_id=self.integration.integration_id)
350 350 else:
351 351 redirect_to = self.request.route_path(
352 352 'global_integrations_edit',
353 353 integration=self.integration.integration_type,
354 354 integration_id=self.integration.integration_id)
355 355
356 356 return HTTPFound(redirect_to)
357 357
358 358 def _new_integration(self):
359 359 c = self.load_default_context()
360 360 c.available_integrations = integration_type_registry
361 361 return self._get_template_context(c)
362 362
363 363 def load_default_context(self):
364 364 raise NotImplementedError()
365 365
366 366
367 367 class GlobalIntegrationsView(IntegrationSettingsViewBase):
368 368 def load_default_context(self):
369 369 c = self._get_local_tmpl_context()
370 370 c.repo = self.repo
371 371 c.repo_group = self.repo_group
372 372 c.navlist = navigation_list(self.request)
373 373
374 374 return c
375 375
376 376 @LoginRequired()
377 377 @HasPermissionAnyDecorator('hg.admin')
378 378 def integration_list(self):
379 379 return self._integration_list()
380 380
381 381 @LoginRequired()
382 382 @HasPermissionAnyDecorator('hg.admin')
383 383 def settings_get(self):
384 384 return self._settings_get()
385 385
386 386 @LoginRequired()
387 387 @HasPermissionAnyDecorator('hg.admin')
388 388 @CSRFRequired()
389 389 def settings_post(self):
390 390 return self._settings_post()
391 391
392 392 @LoginRequired()
393 393 @HasPermissionAnyDecorator('hg.admin')
394 394 def new_integration(self):
395 395 return self._new_integration()
396 396
397 397
398 398 class RepoIntegrationsView(IntegrationSettingsViewBase):
399 399 def load_default_context(self):
400 400 c = self._get_local_tmpl_context()
401 401
402 402 c.repo = self.repo
403 403 c.repo_group = self.repo_group
404 404
405 405 self.db_repo = self.repo
406 406 c.rhodecode_db_repo = self.repo
407 407 c.repo_name = self.db_repo.repo_name
408 408 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
409 409 c.repository_artifacts = ScmModel().get_artifacts(self.repo)
410 410 c.repository_is_user_following = ScmModel().is_following_repo(
411 411 c.repo_name, self._rhodecode_user.user_id)
412 412 c.has_origin_repo_read_perm = False
413 413 if self.db_repo.fork:
414 414 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
415 415 'repository.write', 'repository.read', 'repository.admin')(
416 416 self.db_repo.fork.repo_name, 'summary fork link')
417 417 return c
418 418
419 419 @LoginRequired()
420 420 @HasRepoPermissionAnyDecorator('repository.admin')
421 421 def integration_list(self):
422 422 return self._integration_list()
423 423
424 424 @LoginRequired()
425 425 @HasRepoPermissionAnyDecorator('repository.admin')
426 426 def settings_get(self):
427 427 return self._settings_get()
428 428
429 429 @LoginRequired()
430 430 @HasRepoPermissionAnyDecorator('repository.admin')
431 431 @CSRFRequired()
432 432 def settings_post(self):
433 433 return self._settings_post()
434 434
435 435 @LoginRequired()
436 436 @HasRepoPermissionAnyDecorator('repository.admin')
437 437 def new_integration(self):
438 438 return self._new_integration()
439 439
440 440
441 441 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
442 442 def load_default_context(self):
443 443 c = self._get_local_tmpl_context()
444 444 c.repo = self.repo
445 445 c.repo_group = self.repo_group
446 446 c.navlist = navigation_list(self.request)
447 447
448 448 return c
449 449
450 450 @LoginRequired()
451 451 @HasRepoGroupPermissionAnyDecorator('group.admin')
452 452 def integration_list(self):
453 453 return self._integration_list()
454 454
455 455 @LoginRequired()
456 456 @HasRepoGroupPermissionAnyDecorator('group.admin')
457 457 def settings_get(self):
458 458 return self._settings_get()
459 459
460 460 @LoginRequired()
461 461 @HasRepoGroupPermissionAnyDecorator('group.admin')
462 462 @CSRFRequired()
463 463 def settings_post(self):
464 464 return self._settings_post()
465 465
466 466 @LoginRequired()
467 467 @HasRepoGroupPermissionAnyDecorator('group.admin')
468 468 def new_integration(self):
469 469 return self._new_integration()
General Comments 0
You need to be logged in to leave comments. Login now