##// END OF EJS Templates
integrations: refactor/cleanup + features, fixes #4181...
dan -
r731:7a6d3636 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
9
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
13
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
18 log = logging.getLogger(__name__)
19
20
21 def upgrade(migrate_engine):
22 """
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_4_0_1
28
29 tbl = db_4_4_0_1.Integration.__table__
30 child_repos_only = db_4_4_0_1.Integration.child_repos_only
31 child_repos_only.create(table=tbl)
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
@@ -0,0 +1,187 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import deform
24 import colander
25
26 from rhodecode.translation import _
27 from rhodecode.model.db import Repository, RepoGroup
28 from rhodecode.model.validation_schema import validators, preparers
29
30
31 def integration_scope_choices(permissions):
32 """
33 Return list of (value, label) choices for integration scopes depending on
34 the permissions
35 """
36 result = [('', _('Pick a scope:'))]
37 if 'hg.admin' in permissions['global']:
38 result.extend([
39 ('global', _('Global (all repositories)')),
40 ('root_repos', _('Top level repositories only')),
41 ])
42
43 repo_choices = [
44 ('repo:%s' % repo_name, '/' + repo_name)
45 for repo_name, repo_perm
46 in permissions['repositories'].items()
47 if repo_perm == 'repository.admin'
48 ]
49 repogroup_choices = [
50 ('repogroup:%s' % repo_group_name, '/' + repo_group_name + ' (group)')
51 for repo_group_name, repo_group_perm
52 in permissions['repositories_groups'].items()
53 if repo_group_perm == 'group.admin'
54 ]
55 result.extend(
56 sorted(repogroup_choices + repo_choices,
57 key=lambda (choice, label): choice.split(':', 1)[1]
58 )
59 )
60 return result
61
62
63 @colander.deferred
64 def deferred_integration_scopes_validator(node, kw):
65 perms = kw.get('permissions')
66 def _scope_validator(_node, scope):
67 is_super_admin = 'hg.admin' in perms['global']
68
69 if scope in ('global', 'root_repos'):
70 if is_super_admin:
71 return True
72 msg = _('Only superadmins can create global integrations')
73 raise colander.Invalid(_node, msg)
74 elif isinstance(scope, Repository):
75 if (is_super_admin or perms['repositories'].get(
76 scope.repo_name) == 'repository.admin'):
77 return True
78 msg = _('Only repo admins can create integrations')
79 raise colander.Invalid(_node, msg)
80 elif isinstance(scope, RepoGroup):
81 if (is_super_admin or perms['repositories_groups'].get(
82 scope.group_name) == 'group.admin'):
83 return True
84
85 msg = _('Only repogroup admins can create integrations')
86 raise colander.Invalid(_node, msg)
87
88 msg = _('Invalid integration scope: %s' % scope)
89 raise colander.Invalid(node, msg)
90
91 return _scope_validator
92
93
94 @colander.deferred
95 def deferred_integration_scopes_widget(node, kw):
96 if kw.get('no_scope'):
97 return deform.widget.TextInputWidget(readonly=True)
98
99 choices = integration_scope_choices(kw.get('permissions'))
100 widget = deform.widget.Select2Widget(values=choices)
101 return widget
102
103 class IntegrationScope(colander.SchemaType):
104 def serialize(self, node, appstruct):
105 if appstruct is colander.null:
106 return colander.null
107
108 if isinstance(appstruct, Repository):
109 return 'repo:%s' % appstruct.repo_name
110 elif isinstance(appstruct, RepoGroup):
111 return 'repogroup:%s' % appstruct.group_name
112 elif appstruct in ('global', 'root_repos'):
113 return appstruct
114 raise colander.Invalid(node, '%r is not a valid scope' % appstruct)
115
116 def deserialize(self, node, cstruct):
117 if cstruct is colander.null:
118 return colander.null
119
120 if cstruct.startswith('repo:'):
121 repo = Repository.get_by_repo_name(cstruct.split(':')[1])
122 if repo:
123 return repo
124 elif cstruct.startswith('repogroup:'):
125 repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
126 if repo_group:
127 return repo_group
128 elif cstruct in ('global', 'root_repos'):
129 return cstruct
130
131 raise colander.Invalid(node, '%r is not a valid scope' % cstruct)
132
133 class IntegrationOptionsSchemaBase(colander.MappingSchema):
134
135 name = colander.SchemaNode(
136 colander.String(),
137 description=_('Short name for this integration.'),
138 missing=colander.required,
139 title=_('Integration name'),
140 )
141
142 scope = colander.SchemaNode(
143 IntegrationScope(),
144 description=_(
145 'Scope of the integration. Group scope means the integration '
146 ' runs on all child repos of that group.'),
147 title=_('Integration scope'),
148 validator=deferred_integration_scopes_validator,
149 widget=deferred_integration_scopes_widget,
150 missing=colander.required,
151 )
152
153 enabled = colander.SchemaNode(
154 colander.Bool(),
155 default=True,
156 description=_('Enable or disable this integration.'),
157 missing=False,
158 title=_('Enabled'),
159 )
160
161
162
163 def make_integration_schema(IntegrationType, settings=None):
164 """
165 Return a colander schema for an integration type
166
167 :param IntegrationType: the integration type class
168 :param settings: existing integration settings dict (optional)
169 """
170
171 settings = settings or {}
172 settings_schema = IntegrationType(settings=settings).settings_schema()
173
174 class IntegrationSchema(colander.Schema):
175 options = IntegrationOptionsSchemaBase()
176
177 schema = IntegrationSchema()
178 schema['options'].title = _('General integration options')
179
180 settings_schema.name = 'settings'
181 settings_schema.title = _('{integration_type} settings').format(
182 integration_type=IntegrationType.display_name)
183 schema.add(settings_schema)
184
185 return schema
186
187
@@ -0,0 +1,66 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.html"/>
3 <%namespace name="widgets" file="/widgets.html"/>
4
5 <%def name="breadcrumbs_links()">
6 %if c.repo:
7 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
8 &raquo;
9 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
10 %elif c.repo_group:
11 ${h.link_to(_('Admin'),h.url('admin_home'))}
12 &raquo;
13 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
14 &raquo;
15 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
16 &raquo;
17 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
18 %else:
19 ${h.link_to(_('Admin'),h.url('admin_home'))}
20 &raquo;
21 ${h.link_to(_('Settings'),h.url('admin_settings'))}
22 &raquo;
23 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
24 %endif
25 &raquo;
26 ${_('Create new integration')}
27 </%def>
28 <%widgets:panel class_='integrations'>
29 <%def name="title()">
30 %if c.repo:
31 ${_('Create New Integration for repository: {repo_name}').format(repo_name=c.repo.repo_name)}
32 %elif c.repo_group:
33 ${_('Create New Integration for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
34 %else:
35 ${_('Create New Global Integration')}
36 %endif
37 </%def>
38
39 %for integration, IntegrationType in available_integrations.items():
40 <%
41 if c.repo:
42 create_url = request.route_path('repo_integrations_create',
43 repo_name=c.repo.repo_name,
44 integration=integration)
45 elif c.repo_group:
46 create_url = request.route_path('repo_group_integrations_create',
47 repo_group_name=c.repo_group.group_name,
48 integration=integration)
49 else:
50 create_url = request.route_path('global_integrations_create',
51 integration=integration)
52 %>
53 <a href="${create_url}" class="integration-box">
54 <%widgets:panel>
55 <h2>
56 <div class="integration-icon">
57 ${IntegrationType.icon|n}
58 </div>
59 ${IntegrationType.display_name}
60 </h2>
61 ${IntegrationType.description or _('No description available')}
62 </%widgets:panel>
63 </a>
64 %endfor
65 <div style="clear:both"></div>
66 </%widgets:panel>
@@ -0,0 +1,4 b''
1 <div class="form-control readonly"
2 id="${oid|field.oid}">
3 ${cstruct}
4 </div>
@@ -0,0 +1,262 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23 from webob.exc import HTTPNotFound
24
25 import rhodecode
26 from rhodecode.model.db import Integration
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
29 from rhodecode.tests.utils import AssertResponse
30 from rhodecode.integrations import integration_type_registry
31 from rhodecode.config.routing import ADMIN_PREFIX
32
33
34 @pytest.mark.usefixtures('app', 'autologin_user')
35 class TestIntegrationsView(object):
36 pass
37
38
39 class TestGlobalIntegrationsView(TestIntegrationsView):
40 def test_index_no_integrations(self, app):
41 url = ADMIN_PREFIX + '/integrations'
42 response = app.get(url)
43
44 assert response.status_code == 200
45 assert 'exist yet' in response.body
46
47 def test_index_with_integrations(self, app, global_integration_stub):
48 url = ADMIN_PREFIX + '/integrations'
49 response = app.get(url)
50
51 assert response.status_code == 200
52 assert 'exist yet' not in response.body
53 assert global_integration_stub.name in response.body
54
55 def test_new_integration_page(self, app):
56 url = ADMIN_PREFIX + '/integrations/new'
57
58 response = app.get(url)
59
60 assert response.status_code == 200
61
62 for integration_key in integration_type_registry:
63 nurl = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 integration=integration_key)
65 assert nurl in response.body
66
67 @pytest.mark.parametrize(
68 'IntegrationType', integration_type_registry.values())
69 def test_get_create_integration_page(self, app, IntegrationType):
70 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
71 integration_key=IntegrationType.key)
72
73 response = app.get(url)
74
75 assert response.status_code == 200
76 assert IntegrationType.display_name in response.body
77
78 def test_post_integration_page(self, app, StubIntegrationType, csrf_token,
79 test_repo_group, backend_random):
80 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
81 integration_key=StubIntegrationType.key)
82
83 _post_integration_test_helper(app, url, csrf_token, admin_view=True,
84 repo=backend_random.repo, repo_group=test_repo_group)
85
86
87 class TestRepoGroupIntegrationsView(TestIntegrationsView):
88 def test_index_no_integrations(self, app, test_repo_group):
89 url = '/{repo_group_name}/settings/integrations'.format(
90 repo_group_name=test_repo_group.group_name)
91 response = app.get(url)
92
93 assert response.status_code == 200
94 assert 'exist yet' in response.body
95
96 def test_index_with_integrations(self, app, test_repo_group,
97 repogroup_integration_stub):
98 url = '/{repo_group_name}/settings/integrations'.format(
99 repo_group_name=test_repo_group.group_name)
100
101 stub_name = repogroup_integration_stub.name
102 response = app.get(url)
103
104 assert response.status_code == 200
105 assert 'exist yet' not in response.body
106 assert stub_name in response.body
107
108 def test_new_integration_page(self, app, test_repo_group):
109 repo_group_name = test_repo_group.group_name
110 url = '/{repo_group_name}/settings/integrations/new'.format(
111 repo_group_name=test_repo_group.group_name)
112
113 response = app.get(url)
114
115 assert response.status_code == 200
116
117 for integration_key in integration_type_registry:
118 nurl = ('/{repo_group_name}/settings/integrations'
119 '/{integration}/new').format(
120 repo_group_name=repo_group_name,
121 integration=integration_key)
122
123 assert nurl in response.body
124
125 @pytest.mark.parametrize(
126 'IntegrationType', integration_type_registry.values())
127 def test_get_create_integration_page(self, app, test_repo_group,
128 IntegrationType):
129 repo_group_name = test_repo_group.group_name
130 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
131 ).format(repo_group_name=repo_group_name,
132 integration_key=IntegrationType.key)
133
134 response = app.get(url)
135
136 assert response.status_code == 200
137 assert IntegrationType.display_name in response.body
138
139 def test_post_integration_page(self, app, test_repo_group, backend_random,
140 StubIntegrationType, csrf_token):
141 repo_group_name = test_repo_group.group_name
142 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
143 ).format(repo_group_name=repo_group_name,
144 integration_key=StubIntegrationType.key)
145
146 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
147 repo=backend_random.repo, repo_group=test_repo_group)
148
149
150 class TestRepoIntegrationsView(TestIntegrationsView):
151 def test_index_no_integrations(self, app, backend_random):
152 url = '/{repo_name}/settings/integrations'.format(
153 repo_name=backend_random.repo.repo_name)
154 response = app.get(url)
155
156 assert response.status_code == 200
157 assert 'exist yet' in response.body
158
159 def test_index_with_integrations(self, app, repo_integration_stub):
160 url = '/{repo_name}/settings/integrations'.format(
161 repo_name=repo_integration_stub.repo.repo_name)
162 stub_name = repo_integration_stub.name
163
164 response = app.get(url)
165
166 assert response.status_code == 200
167 assert stub_name in response.body
168 assert 'exist yet' not in response.body
169
170 def test_new_integration_page(self, app, backend_random):
171 repo_name = backend_random.repo.repo_name
172 url = '/{repo_name}/settings/integrations/new'.format(
173 repo_name=repo_name)
174
175 response = app.get(url)
176
177 assert response.status_code == 200
178
179 for integration_key in integration_type_registry:
180 nurl = ('/{repo_name}/settings/integrations'
181 '/{integration}/new').format(
182 repo_name=repo_name,
183 integration=integration_key)
184
185 assert nurl in response.body
186
187 @pytest.mark.parametrize(
188 'IntegrationType', integration_type_registry.values())
189 def test_get_create_integration_page(self, app, backend_random,
190 IntegrationType):
191 repo_name = backend_random.repo.repo_name
192 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
193 repo_name=repo_name, integration_key=IntegrationType.key)
194
195 response = app.get(url)
196
197 assert response.status_code == 200
198 assert IntegrationType.display_name in response.body
199
200 def test_post_integration_page(self, app, backend_random, test_repo_group,
201 StubIntegrationType, csrf_token):
202 repo_name = backend_random.repo.repo_name
203 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
204 repo_name=repo_name, integration_key=StubIntegrationType.key)
205
206 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
207 repo=backend_random.repo, repo_group=test_repo_group)
208
209
210 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
211 admin_view):
212 """
213 Posts form data to create integration at the url given then deletes it and
214 checks if the redirect url is correct.
215 """
216
217 app.post(url, params={}, status=403) # missing csrf check
218 response = app.post(url, params={'csrf_token': csrf_token})
219 assert response.status_code == 200
220 assert 'Errors exist' in response.body
221
222 scopes_destinations = [
223 ('global',
224 ADMIN_PREFIX + '/integrations'),
225 ('root_repos',
226 ADMIN_PREFIX + '/integrations'),
227 ('repo:%s' % repo.repo_name,
228 '/%s/settings/integrations' % repo.repo_name),
229 ('repogroup:%s' % repo_group.group_name,
230 '/%s/settings/integrations' % repo_group.group_name),
231 ]
232
233 for scope, destination in scopes_destinations:
234 if admin_view:
235 destination = ADMIN_PREFIX + '/integrations'
236
237 form_data = [
238 ('csrf_token', csrf_token),
239 ('__start__', 'options:mapping'),
240 ('name', 'test integration'),
241 ('scope', scope),
242 ('enabled', 'true'),
243 ('__end__', 'options:mapping'),
244 ('__start__', 'settings:mapping'),
245 ('test_int_field', '34'),
246 ('test_string_field', ''), # empty value on purpose as it's required
247 ('__end__', 'settings:mapping'),
248 ]
249 errors_response = app.post(url, form_data)
250 assert 'Errors exist' in errors_response.body
251
252 form_data[-2] = ('test_string_field', 'data!')
253 assert Session().query(Integration).count() == 0
254 created_response = app.post(url, form_data)
255 assert Session().query(Integration).count() == 1
256
257 delete_response = app.post(
258 created_response.location,
259 params={'csrf_token': csrf_token, 'delete': 'delete'})
260
261 assert Session().query(Integration).count() == 0
262 assert delete_response.location.endswith(destination)
@@ -0,0 +1,120 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 import pytest
23
24 from rhodecode.model import validation_schema
25
26 from rhodecode.integrations import integration_type_registry
27 from rhodecode.integrations.types.base import IntegrationTypeBase
28 from rhodecode.model.validation_schema.schemas.integration_schema import (
29 make_integration_schema
30 )
31
32
33 @pytest.mark.usefixtures('app', 'autologin_user')
34 class TestIntegrationSchema(object):
35
36 def test_deserialize_integration_schema_perms(self, backend_random,
37 test_repo_group,
38 StubIntegrationType):
39
40 repo = backend_random.repo
41 repo_group = test_repo_group
42
43
44 empty_perms_dict = {
45 'global': [],
46 'repositories': {},
47 'repositories_groups': {},
48 }
49
50 perms_tests = {
51 ('repo:%s' % repo.repo_name, repo): [
52 ({}, False),
53 ({'global': ['hg.admin']}, True),
54 ({'global': []}, False),
55 ({'repositories': {repo.repo_name: 'repository.admin'}}, True),
56 ({'repositories': {repo.repo_name: 'repository.read'}}, False),
57 ({'repositories': {repo.repo_name: 'repository.write'}}, False),
58 ({'repositories': {repo.repo_name: 'repository.none'}}, False),
59 ],
60 ('repogroup:%s' % repo_group.group_name, repo_group): [
61 ({}, False),
62 ({'global': ['hg.admin']}, True),
63 ({'global': []}, False),
64 ({'repositories_groups':
65 {repo_group.group_name: 'group.admin'}}, True),
66 ({'repositories_groups':
67 {repo_group.group_name: 'group.read'}}, False),
68 ({'repositories_groups':
69 {repo_group.group_name: 'group.write'}}, False),
70 ({'repositories_groups':
71 {repo_group.group_name: 'group.none'}}, False),
72 ],
73 ('global', 'global'): [
74 ({}, False),
75 ({'global': ['hg.admin']}, True),
76 ({'global': []}, False),
77 ],
78 ('root_repos', 'root_repos'): [
79 ({}, False),
80 ({'global': ['hg.admin']}, True),
81 ({'global': []}, False),
82 ],
83 }
84
85 for (scope_input, scope_output), perms_allowed in perms_tests.items():
86 for perms_update, allowed in perms_allowed:
87 perms = dict(empty_perms_dict, **perms_update)
88
89 schema = make_integration_schema(
90 IntegrationType=StubIntegrationType
91 ).bind(permissions=perms)
92
93 input_data = {
94 'options': {
95 'enabled': 'true',
96 'scope': scope_input,
97 'name': 'test integration',
98 },
99 'settings': {
100 'test_string_field': 'stringy',
101 'test_int_field': '100',
102 }
103 }
104
105 if not allowed:
106 with pytest.raises(colander.Invalid):
107 schema.deserialize(input_data)
108 else:
109 assert schema.deserialize(input_data) == {
110 'options': {
111 'enabled': True,
112 'scope': scope_output,
113 'name': 'test integration',
114 },
115 'settings': {
116 'test_string_field': 'stringy',
117 'test_int_field': 100,
118 }
119 }
120
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 56 # defines current db version for migrations
54 __dbversion__ = 57 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -42,6 +42,7 b" STATIC_FILE_PREFIX = '/_static'"
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
45 46 # repo names can have a slash in them, but they must not end with a slash
46 47 'repo_name': r'.*?[^/]',
47 48 # file path eats up everything at the end
@@ -31,6 +31,15 b' log = logging.getLogger(__name__)'
31 31 def includeme(config):
32 32
33 33 # global integrations
34
35 config.add_route('global_integrations_new',
36 ADMIN_PREFIX + '/integrations/new')
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
38 attr='new_integration',
39 renderer='rhodecode:templates/admin/integrations/new.html',
40 request_method='GET',
41 route_name='global_integrations_new')
42
34 43 config.add_route('global_integrations_home',
35 44 ADMIN_PREFIX + '/integrations')
36 45 config.add_route('global_integrations_list',
@@ -48,15 +57,75 b' def includeme(config):'
48 57 config.add_route('global_integrations_edit',
49 58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
50 59 custom_predicates=(valid_integration,))
60
61
51 62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
52 63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
53 64 attr='settings_get',
54 renderer='rhodecode:templates/admin/integrations/edit.html',
65 renderer='rhodecode:templates/admin/integrations/form.html',
55 66 request_method='GET',
56 67 route_name=route_name)
57 68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
58 69 attr='settings_post',
59 renderer='rhodecode:templates/admin/integrations/edit.html',
70 renderer='rhodecode:templates/admin/integrations/form.html',
71 request_method='POST',
72 route_name=route_name)
73
74
75 # repo group integrations
76 config.add_route('repo_group_integrations_home',
77 add_route_requirements(
78 '{repo_group_name}/settings/integrations',
79 URL_NAME_REQUIREMENTS
80 ),
81 custom_predicates=(valid_repo_group,)
82 )
83 config.add_route('repo_group_integrations_list',
84 add_route_requirements(
85 '{repo_group_name}/settings/integrations/{integration}',
86 URL_NAME_REQUIREMENTS
87 ),
88 custom_predicates=(valid_repo_group, valid_integration))
89 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
90 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
91 attr='index',
92 renderer='rhodecode:templates/admin/integrations/list.html',
93 request_method='GET',
94 route_name=route_name)
95
96 config.add_route('repo_group_integrations_new',
97 add_route_requirements(
98 '{repo_group_name}/settings/integrations/new',
99 URL_NAME_REQUIREMENTS
100 ),
101 custom_predicates=(valid_repo_group,))
102 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
103 attr='new_integration',
104 renderer='rhodecode:templates/admin/integrations/new.html',
105 request_method='GET',
106 route_name='repo_group_integrations_new')
107
108 config.add_route('repo_group_integrations_create',
109 add_route_requirements(
110 '{repo_group_name}/settings/integrations/{integration}/new',
111 URL_NAME_REQUIREMENTS
112 ),
113 custom_predicates=(valid_repo_group, valid_integration))
114 config.add_route('repo_group_integrations_edit',
115 add_route_requirements(
116 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
117 URL_NAME_REQUIREMENTS
118 ),
119 custom_predicates=(valid_repo_group, valid_integration))
120 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
121 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
122 attr='settings_get',
123 renderer='rhodecode:templates/admin/integrations/form.html',
124 request_method='GET',
125 route_name=route_name)
126 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
127 attr='settings_post',
128 renderer='rhodecode:templates/admin/integrations/form.html',
60 129 request_method='POST',
61 130 route_name=route_name)
62 131
@@ -78,8 +147,21 b' def includeme(config):'
78 147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
79 148 attr='index',
80 149 request_method='GET',
150 renderer='rhodecode:templates/admin/integrations/list.html',
81 151 route_name=route_name)
82 152
153 config.add_route('repo_integrations_new',
154 add_route_requirements(
155 '{repo_name}/settings/integrations/new',
156 URL_NAME_REQUIREMENTS
157 ),
158 custom_predicates=(valid_repo,))
159 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
160 attr='new_integration',
161 renderer='rhodecode:templates/admin/integrations/new.html',
162 request_method='GET',
163 route_name='repo_integrations_new')
164
83 165 config.add_route('repo_integrations_create',
84 166 add_route_requirements(
85 167 '{repo_name}/settings/integrations/{integration}/new',
@@ -95,56 +177,12 b' def includeme(config):'
95 177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
96 178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
97 179 attr='settings_get',
98 renderer='rhodecode:templates/admin/integrations/edit.html',
180 renderer='rhodecode:templates/admin/integrations/form.html',
99 181 request_method='GET',
100 182 route_name=route_name)
101 183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
102 184 attr='settings_post',
103 renderer='rhodecode:templates/admin/integrations/edit.html',
104 request_method='POST',
105 route_name=route_name)
106
107
108 # repo group integrations
109 config.add_route('repo_group_integrations_home',
110 add_route_requirements(
111 '{repo_group_name}/settings/integrations',
112 URL_NAME_REQUIREMENTS
113 ),
114 custom_predicates=(valid_repo_group,))
115 config.add_route('repo_group_integrations_list',
116 add_route_requirements(
117 '{repo_group_name}/settings/integrations/{integration}',
118 URL_NAME_REQUIREMENTS
119 ),
120 custom_predicates=(valid_repo_group, valid_integration))
121 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
122 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
123 attr='index',
124 request_method='GET',
125 route_name=route_name)
126
127 config.add_route('repo_group_integrations_create',
128 add_route_requirements(
129 '{repo_group_name}/settings/integrations/{integration}/new',
130 URL_NAME_REQUIREMENTS
131 ),
132 custom_predicates=(valid_repo_group, valid_integration))
133 config.add_route('repo_group_integrations_edit',
134 add_route_requirements(
135 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
136 URL_NAME_REQUIREMENTS
137 ),
138 custom_predicates=(valid_repo_group, valid_integration))
139 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
140 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
141 attr='settings_get',
142 renderer='rhodecode:templates/admin/integrations/edit.html',
143 request_method='GET',
144 route_name=route_name)
145 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
146 attr='settings_post',
147 renderer='rhodecode:templates/admin/integrations/edit.html',
185 renderer='rhodecode:templates/admin/integrations/form.html',
148 186 request_method='POST',
149 187 route_name=route_name)
150 188
@@ -194,7 +232,7 b' def valid_integration(info, request):'
194 232 return False
195 233 if repo and repo.repo_id != integration.repo_id:
196 234 return False
197 if repo_group and repo_group.repo_group_id != integration.repo_group_id:
235 if repo_group and repo_group.group_id != integration.repo_group_id:
198 236 return False
199 237
200 238 return True
@@ -20,26 +20,52 b''
20 20
21 21 import colander
22 22
23 from rhodecode.translation import lazy_ugettext
23 from rhodecode.translation import _
24 24
25 25
26 class IntegrationSettingsSchemaBase(colander.MappingSchema):
27 """
28 This base schema is intended for use in integrations.
29 It adds a few default settings (e.g., "enabled"), so that integration
30 authors don't have to maintain a bunch of boilerplate.
31 """
26 class IntegrationOptionsSchemaBase(colander.MappingSchema):
32 27 enabled = colander.SchemaNode(
33 28 colander.Bool(),
34 29 default=True,
35 description=lazy_ugettext('Enable or disable this integration.'),
30 description=_('Enable or disable this integration.'),
36 31 missing=False,
37 title=lazy_ugettext('Enabled'),
32 title=_('Enabled'),
38 33 )
39 34
40 35 name = colander.SchemaNode(
41 36 colander.String(),
42 description=lazy_ugettext('Short name for this integration.'),
37 description=_('Short name for this integration.'),
43 38 missing=colander.required,
44 title=lazy_ugettext('Integration name'),
39 title=_('Integration name'),
45 40 )
41
42
43 class RepoIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
44 pass
45
46
47 class RepoGroupIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
48 child_repos_only = colander.SchemaNode(
49 colander.Bool(),
50 default=True,
51 description=_(
52 'Limit integrations to to work only on the direct children '
53 'repositories of this repository group (no subgroups)'),
54 missing=False,
55 title=_('Limit to childen repos only'),
56 )
57
58
59 class GlobalIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
60 child_repos_only = colander.SchemaNode(
61 colander.Bool(),
62 default=False,
63 description=_(
64 'Limit integrations to to work only on root level repositories'),
65 missing=False,
66 title=_('Root repositories only'),
67 )
68
69
70 class IntegrationSettingsSchemaBase(colander.MappingSchema):
71 pass
@@ -18,25 +18,84 b''
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 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
21 import colander
22 from rhodecode.translation import _
22 23
23 24
24 25 class IntegrationTypeBase(object):
25 26 """ Base class for IntegrationType plugins """
26 27
28 description = ''
29 icon = '''
30 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
31 <svg
32 xmlns:dc="http://purl.org/dc/elements/1.1/"
33 xmlns:cc="http://creativecommons.org/ns#"
34 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
35 xmlns:svg="http://www.w3.org/2000/svg"
36 xmlns="http://www.w3.org/2000/svg"
37 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
38 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
39 viewBox="0 -256 1792 1792"
40 id="svg3025"
41 version="1.1"
42 inkscape:version="0.48.3.1 r9886"
43 width="100%"
44 height="100%"
45 sodipodi:docname="cog_font_awesome.svg">
46 <metadata
47 id="metadata3035">
48 <rdf:RDF>
49 <cc:Work
50 rdf:about="">
51 <dc:format>image/svg+xml</dc:format>
52 <dc:type
53 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
54 </cc:Work>
55 </rdf:RDF>
56 </metadata>
57 <defs
58 id="defs3033" />
59 <sodipodi:namedview
60 pagecolor="#ffffff"
61 bordercolor="#666666"
62 borderopacity="1"
63 objecttolerance="10"
64 gridtolerance="10"
65 guidetolerance="10"
66 inkscape:pageopacity="0"
67 inkscape:pageshadow="2"
68 inkscape:window-width="640"
69 inkscape:window-height="480"
70 id="namedview3031"
71 showgrid="false"
72 inkscape:zoom="0.13169643"
73 inkscape:cx="896"
74 inkscape:cy="896"
75 inkscape:window-x="0"
76 inkscape:window-y="25"
77 inkscape:window-maximized="0"
78 inkscape:current-layer="svg3025" />
79 <g
80 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
81 id="g3027">
82 <path
83 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"
84 id="path3029"
85 inkscape:connector-curvature="0"
86 style="fill:currentColor" />
87 </g>
88 </svg>
89 '''
90
27 91 def __init__(self, settings):
28 92 """
29 93 :param settings: dict of settings to be used for the integration
30 94 """
31 95 self.settings = settings
32 96
33
34 97 def settings_schema(self):
35 98 """
36 99 A colander schema of settings for the integration type
37
38 Subclasses can return their own schema but should always
39 inherit from IntegrationSettingsSchemaBase
40 100 """
41 return IntegrationSettingsSchemaBase()
42
101 return colander.Schema()
@@ -26,11 +26,10 b' import colander'
26 26 from mako.template import Template
27 27
28 28 from rhodecode import events
29 from rhodecode.translation import _, lazy_ugettext
29 from rhodecode.translation import _
30 30 from rhodecode.lib.celerylib import run_task
31 31 from rhodecode.lib.celerylib import tasks
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34
36 35 log = logging.getLogger(__name__)
@@ -147,18 +146,79 b" repo_push_template_html = Template('''"
147 146 </html>
148 147 ''')
149 148
149 email_icon = '''
150 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
151 <svg
152 xmlns:dc="http://purl.org/dc/elements/1.1/"
153 xmlns:cc="http://creativecommons.org/ns#"
154 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
155 xmlns:svg="http://www.w3.org/2000/svg"
156 xmlns="http://www.w3.org/2000/svg"
157 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
158 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
159 viewBox="0 -256 1850 1850"
160 id="svg2989"
161 version="1.1"
162 inkscape:version="0.48.3.1 r9886"
163 width="100%"
164 height="100%"
165 sodipodi:docname="envelope_font_awesome.svg">
166 <metadata
167 id="metadata2999">
168 <rdf:RDF>
169 <cc:Work
170 rdf:about="">
171 <dc:format>image/svg+xml</dc:format>
172 <dc:type
173 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
174 </cc:Work>
175 </rdf:RDF>
176 </metadata>
177 <defs
178 id="defs2997" />
179 <sodipodi:namedview
180 pagecolor="#ffffff"
181 bordercolor="#666666"
182 borderopacity="1"
183 objecttolerance="10"
184 gridtolerance="10"
185 guidetolerance="10"
186 inkscape:pageopacity="0"
187 inkscape:pageshadow="2"
188 inkscape:window-width="640"
189 inkscape:window-height="480"
190 id="namedview2995"
191 showgrid="false"
192 inkscape:zoom="0.13169643"
193 inkscape:cx="896"
194 inkscape:cy="896"
195 inkscape:window-x="0"
196 inkscape:window-y="25"
197 inkscape:window-maximized="0"
198 inkscape:current-layer="svg2989" />
199 <g
200 transform="matrix(1,0,0,-1,37.966102,1282.678)"
201 id="g2991">
202 <path
203 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
204 id="path2993"
205 inkscape:connector-curvature="0"
206 style="fill:currentColor" />
207 </g>
208 </svg>
209 '''
150 210
151 class EmailSettingsSchema(IntegrationSettingsSchemaBase):
211 class EmailSettingsSchema(colander.Schema):
152 212 @colander.instantiate(validator=colander.Length(min=1))
153 213 class recipients(colander.SequenceSchema):
154 title = lazy_ugettext('Recipients')
155 description = lazy_ugettext('Email addresses to send push events to')
214 title = _('Recipients')
215 description = _('Email addresses to send push events to')
156 216 widget = deform.widget.SequenceWidget(min_len=1)
157 217
158 218 recipient = colander.SchemaNode(
159 219 colander.String(),
160 title=lazy_ugettext('Email address'),
161 description=lazy_ugettext('Email address'),
220 title=_('Email address'),
221 description=_('Email address'),
162 222 default='',
163 223 validator=colander.Email(),
164 224 widget=deform.widget.TextInputWidget(
@@ -169,8 +229,9 b' class EmailSettingsSchema(IntegrationSet'
169 229
170 230 class EmailIntegrationType(IntegrationTypeBase):
171 231 key = 'email'
172 display_name = lazy_ugettext('Email')
173 SettingsSchema = EmailSettingsSchema
232 display_name = _('Email')
233 description = _('Send repo push summaries to a list of recipients via email')
234 icon = email_icon
174 235
175 236 def settings_schema(self):
176 237 schema = EmailSettingsSchema()
@@ -29,29 +29,28 b' from celery.task import task'
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
41 class HipchatSettingsSchema(colander.Schema):
43 42 color_choices = [
44 ('yellow', lazy_ugettext('Yellow')),
45 ('red', lazy_ugettext('Red')),
46 ('green', lazy_ugettext('Green')),
47 ('purple', lazy_ugettext('Purple')),
48 ('gray', lazy_ugettext('Gray')),
43 ('yellow', _('Yellow')),
44 ('red', _('Red')),
45 ('green', _('Green')),
46 ('purple', _('Purple')),
47 ('gray', _('Gray')),
49 48 ]
50 49
51 50 server_url = colander.SchemaNode(
52 51 colander.String(),
53 title=lazy_ugettext('Hipchat server URL'),
54 description=lazy_ugettext('Hipchat integration url.'),
52 title=_('Hipchat server URL'),
53 description=_('Hipchat integration url.'),
55 54 default='',
56 55 preparer=strip_whitespace,
57 56 validator=colander.url,
@@ -61,15 +60,15 b' class HipchatSettingsSchema(IntegrationS'
61 60 )
62 61 notify = colander.SchemaNode(
63 62 colander.Bool(),
64 title=lazy_ugettext('Notify'),
65 description=lazy_ugettext('Make a notification to the users in room.'),
63 title=_('Notify'),
64 description=_('Make a notification to the users in room.'),
66 65 missing=False,
67 66 default=False,
68 67 )
69 68 color = colander.SchemaNode(
70 69 colander.String(),
71 title=lazy_ugettext('Color'),
72 description=lazy_ugettext('Background color of message.'),
70 title=_('Color'),
71 description=_('Background color of message.'),
73 72 missing='',
74 73 validator=colander.OneOf([x[0] for x in color_choices]),
75 74 widget=deform.widget.Select2Widget(
@@ -98,10 +97,12 b' in <a href="${data[\'repo\'][\'url\']}">${da'
98 97 ''')
99 98
100 99
101
102 100 class HipchatIntegrationType(IntegrationTypeBase):
103 101 key = 'hipchat'
104 display_name = lazy_ugettext('Hipchat')
102 display_name = _('Hipchat')
103 description = _('Send events such as repo pushes and pull requests to '
104 'your hipchat channel.')
105 icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
105 106 valid_events = [
106 107 events.PullRequestCloseEvent,
107 108 events.PullRequestMergeEvent,
@@ -29,21 +29,20 b' from celery.task import task'
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(colander.Schema):
43 42 service = colander.SchemaNode(
44 43 colander.String(),
45 title=lazy_ugettext('Slack service URL'),
46 description=h.literal(lazy_ugettext(
44 title=_('Slack service URL'),
45 description=h.literal(_(
47 46 'This can be setup at the '
48 47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 48 'slack app manager</a>')),
@@ -56,8 +55,8 b' class SlackSettingsSchema(IntegrationSet'
56 55 )
57 56 username = colander.SchemaNode(
58 57 colander.String(),
59 title=lazy_ugettext('Username'),
60 description=lazy_ugettext('Username to show notifications coming from.'),
58 title=_('Username'),
59 description=_('Username to show notifications coming from.'),
61 60 missing='Rhodecode',
62 61 preparer=strip_whitespace,
63 62 widget=deform.widget.TextInputWidget(
@@ -66,8 +65,8 b' class SlackSettingsSchema(IntegrationSet'
66 65 )
67 66 channel = colander.SchemaNode(
68 67 colander.String(),
69 title=lazy_ugettext('Channel'),
70 description=lazy_ugettext('Channel to send notifications to.'),
68 title=_('Channel'),
69 description=_('Channel to send notifications to.'),
71 70 missing='',
72 71 preparer=strip_whitespace,
73 72 widget=deform.widget.TextInputWidget(
@@ -76,8 +75,8 b' class SlackSettingsSchema(IntegrationSet'
76 75 )
77 76 icon_emoji = colander.SchemaNode(
78 77 colander.String(),
79 title=lazy_ugettext('Emoji'),
80 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
78 title=_('Emoji'),
79 description=_('Emoji to use eg. :studio_microphone:'),
81 80 missing='',
82 81 preparer=strip_whitespace,
83 82 widget=deform.widget.TextInputWidget(
@@ -102,10 +101,14 b" in <${data['repo']['url']}|${data['repo'"
102 101 ''')
103 102
104 103
104
105
105 106 class SlackIntegrationType(IntegrationTypeBase):
106 107 key = 'slack'
107 display_name = lazy_ugettext('Slack')
108 SettingsSchema = SlackSettingsSchema
108 display_name = _('Slack')
109 description = _('Send events such as repo pushes and pull requests to '
110 'your slack channel.')
111 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
109 112 valid_events = [
110 113 events.PullRequestCloseEvent,
111 114 events.PullRequestMergeEvent,
@@ -28,19 +28,19 b' from celery.task import task'
28 28 from mako.template import Template
29 29
30 30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import _
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34 log = logging.getLogger(__name__)
36 35
37 36
38 class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
37 class WebhookSettingsSchema(colander.Schema):
39 38 url = colander.SchemaNode(
40 39 colander.String(),
41 title=lazy_ugettext('Webhook URL'),
42 description=lazy_ugettext('URL of the webhook to receive POST event.'),
43 default='',
40 title=_('Webhook URL'),
41 description=_('URL of the webhook to receive POST event.'),
42 missing=colander.required,
43 required=True,
44 44 validator=colander.url,
45 45 widget=deform.widget.TextInputWidget(
46 46 placeholder='https://www.example.com/webhook'
@@ -48,18 +48,24 b' class WebhookSettingsSchema(IntegrationS'
48 48 )
49 49 secret_token = colander.SchemaNode(
50 50 colander.String(),
51 title=lazy_ugettext('Secret Token'),
52 description=lazy_ugettext('String used to validate received payloads.'),
51 title=_('Secret Token'),
52 description=_('String used to validate received payloads.'),
53 53 default='',
54 missing='',
54 55 widget=deform.widget.TextInputWidget(
55 56 placeholder='secret_token'
56 57 ),
57 58 )
58 59
59 60
61
62
60 63 class WebhookIntegrationType(IntegrationTypeBase):
61 64 key = 'webhook'
62 display_name = lazy_ugettext('Webhook')
65 display_name = _('Webhook')
66 description = _('Post json events to a webhook endpoint')
67 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
68
63 69 valid_events = [
64 70 events.PullRequestCloseEvent,
65 71 events.PullRequestMergeEvent,
@@ -18,23 +18,29 b''
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 import colander
22 import logging
23 21 import pylons
24 22 import deform
23 import logging
24 import colander
25 import peppercorn
26 import webhelpers.paginate
25 27
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 29 from pyramid.renderers import render
28 30 from pyramid.response import Response
29 31
30 32 from rhodecode.lib import auth
31 33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.helpers import Page
32 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
33 37 from rhodecode.model.scm import ScmModel
34 38 from rhodecode.model.integration import IntegrationModel
35 39 from rhodecode.admin.navigation import navigation_list
36 40 from rhodecode.translation import _
37 41 from rhodecode.integrations import integration_type_registry
42 from rhodecode.model.validation_schema.schemas.integration_schema import (
43 make_integration_schema)
38 44
39 45 log = logging.getLogger(__name__)
40 46
@@ -65,30 +71,45 b' class IntegrationSettingsViewBase(object'
65 71
66 72 request = self.request
67 73
68 if 'repo_name' in request.matchdict: # we're in a repo context
74 if 'repo_name' in request.matchdict: # in repo settings context
69 75 repo_name = request.matchdict['repo_name']
70 76 self.repo = Repository.get_by_repo_name(repo_name)
71 77
72 if 'repo_group_name' in request.matchdict: # we're in repo_group context
78 if 'repo_group_name' in request.matchdict: # in group settings context
73 79 repo_group_name = request.matchdict['repo_group_name']
74 80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
75 81
76 if 'integration' in request.matchdict: # we're in integration context
82
83 if 'integration' in request.matchdict: # integration type context
77 84 integration_type = request.matchdict['integration']
78 85 self.IntegrationType = integration_type_registry[integration_type]
79 86
80 87 if 'integration_id' in request.matchdict: # single integration context
81 88 integration_id = request.matchdict['integration_id']
82 89 self.integration = Integration.get(integration_id)
83 else: # list integrations context
84 integrations = IntegrationModel().get_integrations(
85 repo=self.repo, repo_group=self.repo_group)
86 90
87 for integration in integrations:
88 self.integrations.setdefault(integration.integration_type, []
89 ).append(integration)
91 # extra perms check just in case
92 if not self._has_perms_for_integration(self.integration):
93 raise HTTPForbidden()
90 94
91 95 self.settings = self.integration and self.integration.settings or {}
96 self.admin_view = not (self.repo or self.repo_group)
97
98 def _has_perms_for_integration(self, integration):
99 perms = self.request.user.permissions
100
101 if 'hg.admin' in perms['global']:
102 return True
103
104 if integration.repo:
105 return perms['repositories'].get(
106 integration.repo.repo_name) == 'repository.admin'
107
108 if integration.repo_group:
109 return perms['repositories_groups'].get(
110 integration.repo_group.group_name) == 'group.admin'
111
112 return False
92 113
93 114 def _template_c_context(self):
94 115 # TODO: dan: this is a stopgap in order to inherit from current pylons
@@ -102,6 +123,7 b' class IntegrationSettingsViewBase(object'
102 123 c.repo_group = self.repo_group
103 124 c.repo_name = self.repo and self.repo.repo_name or None
104 125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126
105 127 if self.repo:
106 128 c.repo_info = self.repo
107 129 c.rhodecode_db_repo = self.repo
@@ -112,23 +134,25 b' class IntegrationSettingsViewBase(object'
112 134 return c
113 135
114 136 def _form_schema(self):
115 if self.integration:
116 settings = self.integration.settings
117 else:
118 settings = {}
119 return self.IntegrationType(settings=settings).settings_schema()
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
138 settings=self.settings)
120 139
121 def settings_get(self, defaults=None, errors=None, form=None):
122 """
123 View that displays the plugin settings as a form.
124 """
125 defaults = defaults or {}
126 errors = errors or {}
140 # returns a clone, important if mutating the schema later
141 return schema.bind(
142 permissions=self.request.user.permissions,
143 no_scope=not self.admin_view)
144
145
146 def _form_defaults(self):
147 defaults = {}
127 148
128 149 if self.integration:
129 defaults = self.integration.settings or {}
130 defaults['name'] = self.integration.name
131 defaults['enabled'] = self.integration.enabled
150 defaults['settings'] = self.integration.settings or {}
151 defaults['options'] = {
152 'name': self.integration.name,
153 'enabled': self.integration.enabled,
154 'scope': self.integration.scope,
155 }
132 156 else:
133 157 if self.repo:
134 158 scope = _('{repo_name} repository').format(
@@ -139,11 +163,44 b' class IntegrationSettingsViewBase(object'
139 163 else:
140 164 scope = _('Global')
141 165
142 defaults['name'] = '{} {} integration'.format(scope,
143 self.IntegrationType.display_name)
144 defaults['enabled'] = True
166 defaults['options'] = {
167 'enabled': True,
168 'name': _('{name} integration').format(
169 name=self.IntegrationType.display_name),
170 }
171 if self.repo:
172 defaults['options']['scope'] = self.repo
173 elif self.repo_group:
174 defaults['options']['scope'] = self.repo_group
175
176 return defaults
145 177
146 schema = self._form_schema().bind(request=self.request)
178 def _delete_integration(self, integration):
179 Session().delete(self.integration)
180 Session().commit()
181 self.request.session.flash(
182 _('Integration {integration_name} deleted successfully.').format(
183 integration_name=self.integration.name),
184 queue='success')
185
186 if self.repo:
187 redirect_to = self.request.route_url(
188 'repo_integrations_home', repo_name=self.repo.repo_name)
189 elif self.repo_group:
190 redirect_to = self.request.route_url(
191 'repo_group_integrations_home',
192 repo_group_name=self.repo_group.group_name)
193 else:
194 redirect_to = self.request.route_url('global_integrations_home')
195 raise HTTPFound(redirect_to)
196
197 def settings_get(self, defaults=None, form=None):
198 """
199 View that displays the integration settings as a form.
200 """
201
202 defaults = defaults or self._form_defaults()
203 schema = self._form_schema()
147 204
148 205 if self.integration:
149 206 buttons = ('submit', 'delete')
@@ -152,23 +209,10 b' class IntegrationSettingsViewBase(object'
152 209
153 210 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
154 211
155 for node in schema:
156 setting = self.settings.get(node.name)
157 if setting is not None:
158 defaults.setdefault(node.name, setting)
159 else:
160 if node.default:
161 defaults.setdefault(node.name, node.default)
162
163 212 template_context = {
164 213 'form': form,
165 'defaults': defaults,
166 'errors': errors,
167 'schema': schema,
168 214 'current_IntegrationType': self.IntegrationType,
169 215 'integration': self.integration,
170 'settings': self.settings,
171 'resource': self.context,
172 216 'c': self._template_c_context(),
173 217 }
174 218
@@ -177,79 +221,90 b' class IntegrationSettingsViewBase(object'
177 221 @auth.CSRFRequired()
178 222 def settings_post(self):
179 223 """
180 View that validates and stores the plugin settings.
224 View that validates and stores the integration settings.
181 225 """
182 if self.request.params.get('delete'):
183 Session().delete(self.integration)
184 Session().commit()
185 self.request.session.flash(
186 _('Integration {integration_name} deleted successfully.').format(
187 integration_name=self.integration.name),
188 queue='success')
189 if self.repo:
190 redirect_to = self.request.route_url(
191 'repo_integrations_home', repo_name=self.repo.repo_name)
192 else:
193 redirect_to = self.request.route_url('global_integrations_home')
194 raise HTTPFound(redirect_to)
226 controls = self.request.POST.items()
227 pstruct = peppercorn.parse(controls)
228
229 if self.integration and pstruct.get('delete'):
230 return self._delete_integration(self.integration)
231
232 schema = self._form_schema()
233
234 skip_settings_validation = False
235 if self.integration and 'enabled' not in pstruct.get('options', {}):
236 skip_settings_validation = True
237 schema['settings'].validator = None
238 for field in schema['settings'].children:
239 field.validator = None
240 field.missing = ''
195 241
196 schema = self._form_schema().bind(request=self.request)
242 if self.integration:
243 buttons = ('submit', 'delete')
244 else:
245 buttons = ('submit',)
197 246
198 form = deform.Form(schema, buttons=('submit', 'delete'))
247 form = deform.Form(schema, buttons=buttons)
199 248
200 params = {}
201 for node in schema.children:
202 if type(node.typ) in (colander.Set, colander.List):
203 val = self.request.params.getall(node.name)
204 else:
205 val = self.request.params.get(node.name)
206 if val:
207 params[node.name] = val
249 if not self.admin_view:
250 # scope is read only field in these cases, and has to be added
251 options = pstruct.setdefault('options', {})
252 if 'scope' not in options:
253 if self.repo:
254 options['scope'] = 'repo:{}'.format(self.repo.repo_name)
255 elif self.repo_group:
256 options['scope'] = 'repogroup:{}'.format(
257 self.repo_group.group_name)
208 258
209 controls = self.request.POST.items()
210 259 try:
211 valid_data = form.validate(controls)
260 valid_data = form.validate_pstruct(pstruct)
212 261 except deform.ValidationFailure as e:
213 262 self.request.session.flash(
214 263 _('Errors exist when saving integration settings. '
215 264 'Please check the form inputs.'),
216 265 queue='error')
217 return self.settings_get(errors={}, defaults=params, form=e)
266 return self.settings_get(form=e)
218 267
219 268 if not self.integration:
220 269 self.integration = Integration()
221 270 self.integration.integration_type = self.IntegrationType.key
222 if self.repo:
223 self.integration.repo = self.repo
224 elif self.repo_group:
225 self.integration.repo_group = self.repo_group
226 271 Session().add(self.integration)
227 272
228 self.integration.enabled = valid_data.pop('enabled', False)
229 self.integration.name = valid_data.pop('name')
230 self.integration.settings = valid_data
273 scope = valid_data['options']['scope']
231 274
275 IntegrationModel().update_integration(self.integration,
276 name=valid_data['options']['name'],
277 enabled=valid_data['options']['enabled'],
278 settings=valid_data['settings'],
279 scope=scope)
280
281 self.integration.settings = valid_data['settings']
232 282 Session().commit()
233
234 283 # Display success message and redirect.
235 284 self.request.session.flash(
236 285 _('Integration {integration_name} updated successfully.').format(
237 286 integration_name=self.IntegrationType.display_name),
238 287 queue='success')
239 288
240 if self.repo:
241 redirect_to = self.request.route_url(
242 'repo_integrations_edit', repo_name=self.repo.repo_name,
289
290 # if integration scope changes, we must redirect to the right place
291 # keeping in mind if the original view was for /repo/ or /_admin/
292 admin_view = not (self.repo or self.repo_group)
293
294 if isinstance(self.integration.scope, Repository) and not admin_view:
295 redirect_to = self.request.route_path(
296 'repo_integrations_edit',
297 repo_name=self.integration.scope.repo_name,
243 298 integration=self.integration.integration_type,
244 299 integration_id=self.integration.integration_id)
245 elif self.repo:
246 redirect_to = self.request.route_url(
300 elif isinstance(self.integration.scope, RepoGroup) and not admin_view:
301 redirect_to = self.request.route_path(
247 302 'repo_group_integrations_edit',
248 repo_group_name=self.repo_group.group_name,
303 repo_group_name=self.integration.scope.group_name,
249 304 integration=self.integration.integration_type,
250 305 integration_id=self.integration.integration_id)
251 306 else:
252 redirect_to = self.request.route_url(
307 redirect_to = self.request.route_path(
253 308 'global_integrations_edit',
254 309 integration=self.integration.integration_type,
255 310 integration_id=self.integration.integration_id)
@@ -257,31 +312,60 b' class IntegrationSettingsViewBase(object'
257 312 return HTTPFound(redirect_to)
258 313
259 314 def index(self):
260 current_integrations = self.integrations
261 if self.IntegrationType:
262 current_integrations = {
263 self.IntegrationType.key: self.integrations.get(
264 self.IntegrationType.key, [])
265 }
315 """ List integrations """
316 if self.repo:
317 scope = self.repo
318 elif self.repo_group:
319 scope = self.repo_group
320 else:
321 scope = 'all'
322
323 integrations = []
324
325 for integration in IntegrationModel().get_integrations(
326 scope=scope, IntegrationType=self.IntegrationType):
327
328 # extra permissions check *just in case*
329 if not self._has_perms_for_integration(integration):
330 continue
331 integrations.append(integration)
332
333 sort_arg = self.request.GET.get('sort', 'name:asc')
334 if ':' in sort_arg:
335 sort_field, sort_dir = sort_arg.split(':')
336 else:
337 sort_field = sort_arg, 'asc'
338
339 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
340
341 integrations.sort(
342 key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
343
344
345 page_url = webhelpers.paginate.PageURL(
346 self.request.path, self.request.GET)
347 page = safe_int(self.request.GET.get('page', 1), 1)
348
349 integrations = Page(integrations, page=page, items_per_page=10,
350 url=page_url)
266 351
267 352 template_context = {
353 'sort_field': sort_field,
354 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
268 355 'current_IntegrationType': self.IntegrationType,
269 'current_integrations': current_integrations,
356 'integrations_list': integrations,
270 357 'available_integrations': integration_type_registry,
271 'c': self._template_c_context()
358 'c': self._template_c_context(),
359 'request': self.request,
272 360 }
361 return template_context
273 362
274 if self.repo:
275 html = render('rhodecode:templates/admin/integrations/list.html',
276 template_context,
277 request=self.request)
278 else:
279 html = render('rhodecode:templates/admin/integrations/list.html',
280 template_context,
281 request=self.request)
282
283 return Response(html)
284
363 def new_integration(self):
364 template_context = {
365 'available_integrations': integration_type_registry,
366 'c': self._template_c_context(),
367 }
368 return template_context
285 369
286 370 class GlobalIntegrationsView(IntegrationSettingsViewBase):
287 371 def perm_check(self, user):
@@ -293,7 +377,9 b' class RepoIntegrationsView(IntegrationSe'
293 377 return auth.HasRepoPermissionAll('repository.admin'
294 378 )(repo_name=self.repo.repo_name, user=user)
295 379
380
296 381 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
297 382 def perm_check(self, user):
298 383 return auth.HasRepoGroupPermissionAll('group.admin'
299 384 )(group_name=self.repo_group.group_name, user=user)
385
@@ -3481,7 +3481,6 b' class Integration(Base, BaseModel):'
3481 3481 integration_type = Column('integration_type', String(255))
3482 3482 enabled = Column('enabled', Boolean(), nullable=False)
3483 3483 name = Column('name', String(255), nullable=False)
3484
3485 3484 settings = Column(
3486 3485 'settings_json', MutationObj.as_mutable(
3487 3486 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
@@ -2036,6 +2036,8 b' class RepoGroup(Base, BaseModel):'
2036 2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 2038 user = relationship('User')
2039 integrations = relationship('Integration',
2040 cascade="all, delete, delete-orphan")
2039 2041
2040 2042 def __init__(self, group_name='', parent_group=None):
2041 2043 self.group_name = group_name
@@ -3481,6 +3483,8 b' class Integration(Base, BaseModel):'
3481 3483 integration_type = Column('integration_type', String(255))
3482 3484 enabled = Column('enabled', Boolean(), nullable=False)
3483 3485 name = Column('name', String(255), nullable=False)
3486 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3487 default=False)
3484 3488
3485 3489 settings = Column(
3486 3490 'settings_json', MutationObj.as_mutable(
@@ -3495,12 +3499,36 b' class Integration(Base, BaseModel):'
3495 3499 nullable=True, unique=None, default=None)
3496 3500 repo_group = relationship('RepoGroup', lazy='joined')
3497 3501
3498 def __repr__(self):
3502 @hybrid_property
3503 def scope(self):
3499 3504 if self.repo:
3500 scope = 'repo=%r' % self.repo
3501 elif self.repo_group:
3502 scope = 'repo_group=%r' % self.repo_group
3505 return self.repo
3506 if self.repo_group:
3507 return self.repo_group
3508 if self.child_repos_only:
3509 return 'root_repos'
3510 return 'global'
3511
3512 @scope.setter
3513 def scope(self, value):
3514 self.repo = None
3515 self.repo_id = None
3516 self.repo_group_id = None
3517 self.repo_group = None
3518 self.child_repos_only = False
3519 if isinstance(value, Repository):
3520 self.repo_id = value.repo_id
3521 self.repo = value
3522 elif isinstance(value, RepoGroup):
3523 self.repo_group_id = value.group_id
3524 self.repo_group = value
3525 elif value == 'root_repos':
3526 self.child_repos_only = True
3527 elif value == 'global':
3528 pass
3503 3529 else:
3504 scope = 'global'
3505
3506 return '<Integration(%r, %r)>' % (self.integration_type, scope)
3530 raise Exception("invalid scope: %s, must be one of "
3531 "['global', 'root_repos', <RepoGroup>. <Repository>]" % value)
3532
3533 def __repr__(self):
3534 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
@@ -29,7 +29,7 b' import traceback'
29 29
30 30 from pylons import tmpl_context as c
31 31 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy import or_
32 from sqlalchemy import or_, and_
33 33 from sqlalchemy.sql.expression import false, true
34 34 from mako import exceptions
35 35
@@ -39,7 +39,7 b' from rhodecode.lib import helpers as h'
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.lib.utils import PartialRenderer
41 41 from rhodecode.model import BaseModel
42 from rhodecode.model.db import Integration, User
42 from rhodecode.model.db import Integration, User, Repository, RepoGroup
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.integrations import integration_type_registry
45 45 from rhodecode.integrations.types.base import IntegrationTypeBase
@@ -61,28 +61,34 b' class IntegrationModel(BaseModel):'
61 61 raise Exception('integration must be int, long or Instance'
62 62 ' of Integration got %s' % type(integration))
63 63
64 def create(self, IntegrationType, enabled, name, settings, repo=None):
64 def create(self, IntegrationType, name, enabled, scope, settings):
65 65 """ Create an IntegrationType integration """
66 66 integration = Integration()
67 67 integration.integration_type = IntegrationType.key
68 integration.settings = {}
69 integration.repo = repo
70 integration.enabled = enabled
71 integration.name = name
72
73 68 self.sa.add(integration)
69 self.update_integration(integration, name, enabled, scope, settings)
74 70 self.sa.commit()
75 71 return integration
76 72
73 def update_integration(self, integration, name, enabled, scope, settings):
74 """
75 :param scope: one of ['global', 'root_repos', <RepoGroup>. <Repository>]
76 """
77
78 integration = self.__get_integration(integration)
79
80 integration.scope = scope
81 integration.name = name
82 integration.enabled = enabled
83 integration.settings = settings
84
85 return integration
86
77 87 def delete(self, integration):
78 try:
79 88 integration = self.__get_integration(integration)
80 89 if integration:
81 90 self.sa.delete(integration)
82 91 return True
83 except Exception:
84 log.error(traceback.format_exc())
85 raise
86 92 return False
87 93
88 94 def get_integration_handler(self, integration):
@@ -100,41 +106,108 b' class IntegrationModel(BaseModel):'
100 106 if handler:
101 107 handler.send_event(event)
102 108
103 def get_integrations(self, repo=None, repo_group=None):
104 if repo:
105 return self.sa.query(Integration).filter(
106 Integration.repo_id==repo.repo_id).all()
107 elif repo_group:
108 return self.sa.query(Integration).filter(
109 Integration.repo_group_id==repo_group.group_id).all()
109 def get_integrations(self, scope, IntegrationType=None):
110 """
111 Return integrations for a scope, which must be one of:
112
113 'all' - every integration, global/repogroup/repo
114 'global' - global integrations only
115 <Repository> instance - integrations for this repo only
116 <RepoGroup> instance - integrations for this repogroup only
117 """
110 118
119 if isinstance(scope, Repository):
120 query = self.sa.query(Integration).filter(
121 Integration.repo==scope)
122 elif isinstance(scope, RepoGroup):
123 query = self.sa.query(Integration).filter(
124 Integration.repo_group==scope)
125 elif scope == 'global':
111 126 # global integrations
112 return self.sa.query(Integration).filter(
113 Integration.repo_id==None).all()
127 query = self.sa.query(Integration).filter(
128 and_(Integration.repo_id==None, Integration.repo_group_id==None)
129 )
130 elif scope == 'root_repos':
131 query = self.sa.query(Integration).filter(
132 and_(Integration.repo_id==None,
133 Integration.repo_group_id==None,
134 Integration.child_repos_only==True)
135 )
136 elif scope == 'all':
137 query = self.sa.query(Integration)
138 else:
139 raise Exception(
140 "invalid `scope`, must be one of: "
141 "['global', 'all', <Repository>, <RepoGroup>]")
142
143 if IntegrationType is not None:
144 query = query.filter(
145 Integration.integration_type==IntegrationType.key)
146
147 result = []
148 for integration in query.all():
149 IntType = integration_type_registry.get(integration.integration_type)
150 result.append((IntType, integration))
151 return result
114 152
115 153 def get_for_event(self, event, cache=False):
116 154 """
117 155 Get integrations that match an event
118 156 """
119 query = self.sa.query(Integration).filter(Integration.enabled==True)
157 query = self.sa.query(
158 Integration
159 ).filter(
160 Integration.enabled==True
161 )
162
163 global_integrations_filter = and_(
164 Integration.repo_id==None,
165 Integration.repo_group_id==None,
166 Integration.child_repos_only==False,
167 )
168
169 if isinstance(event, events.RepoEvent):
170 root_repos_integrations_filter = and_(
171 Integration.repo_id==None,
172 Integration.repo_group_id==None,
173 Integration.child_repos_only==True,
174 )
175
176 clauses = [
177 global_integrations_filter,
178 ]
120 179
121 if isinstance(event, events.RepoEvent): # global + repo integrations
122 # + repo_group integrations
123 parent_groups = event.repo.groups_with_parents
124 query = query.filter(
125 or_(Integration.repo_id==None,
126 Integration.repo_id==event.repo.repo_id,
127 Integration.repo_group_id.in_(
128 [group.group_id for group in parent_groups]
129 )))
180 # repo integrations
181 if event.repo.repo_id: # pre create events dont have a repo_id yet
182 clauses.append(
183 Integration.repo_id==event.repo.repo_id
184 )
185
186 if event.repo.group:
187 clauses.append(
188 Integration.repo_group_id == event.repo.group.group_id
189 )
190 # repo group cascade to kids (maybe implement this sometime?)
191 # clauses.append(Integration.repo_group_id.in_(
192 # [group.group_id for group in
193 # event.repo.groups_with_parents]
194 # ))
195
196
197 if not event.repo.group: # root repo
198 clauses.append(root_repos_integrations_filter)
199
200 query = query.filter(or_(*clauses))
201
130 202 if cache:
131 203 query = query.options(FromCache(
132 204 "sql_cache_short",
133 205 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
134 206 else: # only global integrations
135 query = query.filter(Integration.repo_id==None)
207 query = query.filter(global_integrations_filter)
136 208 if cache:
137 209 query = query.options(FromCache(
138 210 "sql_cache_short", "get_enabled_global_integrations"))
139 211
140 return query.all()
212 result = query.all()
213 return result No newline at end of file
@@ -469,6 +469,8 b' class RepoGroupModel(BaseModel):'
469 469
470 470 def delete(self, repo_group, force_delete=False, fs_remove=True):
471 471 repo_group = self._get_repo_group(repo_group)
472 if not repo_group:
473 return False
472 474 try:
473 475 self.sa.delete(repo_group)
474 476 if fs_remove:
@@ -478,6 +480,7 b' class RepoGroupModel(BaseModel):'
478 480
479 481 # Trigger delete event.
480 482 events.trigger(events.RepoGroupDeleteEvent(repo_group))
483 return True
481 484
482 485 except Exception:
483 486 log.error('Error removing repo_group %s', repo_group)
@@ -38,6 +38,17 b''
38 38
39 39 .form-control {
40 40 width: 100%;
41 padding: 0.9em;
42 border: 1px solid #979797;
43 border-radius: 2px;
44 }
45 .form-control.select2-container {
46 padding: 0; /* padding already applied in .drop-menu a */
47 }
48
49 .form-control.readonly {
50 background: #eeeeee;
51 cursor: not-allowed;
41 52 }
42 53
43 54 .error-block {
@@ -1100,6 +1100,44 b' table.issuetracker {'
1100 1100 }
1101 1101 }
1102 1102
1103 table.integrations {
1104 .td-icon {
1105 width: 20px;
1106 .integration-icon {
1107 height: 20px;
1108 width: 20px;
1109 }
1110 }
1111 }
1112
1113 .integrations {
1114 a.integration-box {
1115 color: @text-color;
1116 &:hover {
1117 .panel {
1118 background: #fbfbfb;
1119 }
1120 }
1121 .integration-icon {
1122 width: 30px;
1123 height: 30px;
1124 margin-right: 20px;
1125 float: left;
1126 }
1127
1128 .panel-body {
1129 padding: 10px;
1130 }
1131 .panel {
1132 margin-bottom: 10px;
1133 }
1134 h2 {
1135 display: inline-block;
1136 margin: 0;
1137 min-width: 140px;
1138 }
1139 }
1140 }
1103 1141
1104 1142 //Permissions Settings
1105 1143 #add_perm {
@@ -270,7 +270,7 b' mark,'
270 270 text-align: right;
271 271
272 272 li:before { content: none; }
273
273 li { float: right; }
274 274 a {
275 275 display: inline-block;
276 276 margin-left: @textmargin/2;
@@ -11,6 +11,19 b''
11 11 request.route_url(route_name='repo_integrations_list',
12 12 repo_name=c.repo.repo_name,
13 13 integration=current_IntegrationType.key))}
14 %elif c.repo_group:
15 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 &raquo;
17 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
18 &raquo;
19 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
20 &raquo;
21 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
22 &raquo;
23 ${h.link_to(current_IntegrationType.display_name,
24 request.route_url(route_name='repo_group_integrations_list',
25 repo_group_name=c.repo_group.group_name,
26 integration=current_IntegrationType.key))}
14 27 %else:
15 28 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 29 &raquo;
@@ -22,18 +35,31 b''
22 35 request.route_url(route_name='global_integrations_list',
23 36 integration=current_IntegrationType.key))}
24 37 %endif
38
25 39 %if integration:
26 40 &raquo;
27 41 ${integration.name}
42 %elif current_IntegrationType:
43 &raquo;
44 ${current_IntegrationType.display_name}
28 45 %endif
29 46 </%def>
47
48 <style>
49 .control-inputs.item-options, .control-inputs.item-settings {
50 float: left;
51 width: 100%;
52 }
53 </style>
30 54 <div class="panel panel-default">
31 55 <div class="panel-heading">
32 56 <h2 class="panel-title">
33 57 %if integration:
34 58 ${current_IntegrationType.display_name} - ${integration.name}
35 59 %else:
36 ${_('Create New %(integration_type)s Integration') % {'integration_type': current_IntegrationType.display_name}}
60 ${_('Create New %(integration_type)s Integration') % {
61 'integration_type': current_IntegrationType.display_name
62 }}
37 63 %endif
38 64 </h2>
39 65 </div>
@@ -4,6 +4,12 b''
4 4 <%def name="breadcrumbs_links()">
5 5 %if c.repo:
6 6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 %elif c.repo_group:
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 &raquo;
10 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
11 &raquo;
12 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
7 13 %else:
8 14 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 15 &raquo;
@@ -15,6 +21,10 b''
15 21 ${h.link_to(_('Integrations'),
16 22 request.route_url(route_name='repo_integrations_home',
17 23 repo_name=c.repo.repo_name))}
24 %elif c.repo_group:
25 ${h.link_to(_('Integrations'),
26 request.route_url(route_name='repo_group_integrations_home',
27 repo_group_name=c.repo_group.group_name))}
18 28 %else:
19 29 ${h.link_to(_('Integrations'),
20 30 request.route_url(route_name='global_integrations_home'))}
@@ -26,54 +36,105 b''
26 36 ${_('Integrations')}
27 37 %endif
28 38 </%def>
39
29 40 <div class="panel panel-default">
30 41 <div class="panel-heading">
31 <h3 class="panel-title">${_('Create New Integration')}</h3>
42 <h3 class="panel-title">
43 %if c.repo:
44 ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
45 %elif c.repo_group:
46 ${_('Current Integrations for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
47 %else:
48 ${_('Current Integrations')}
49 %endif
50 </h3>
32 51 </div>
33 52 <div class="panel-body">
34 %if not available_integrations:
35 ${_('No integrations available.')}
36 %else:
37 %for integration in available_integrations:
53 <%
54 if c.repo:
55 home_url = request.route_path('repo_integrations_home',
56 repo_name=c.repo.repo_name)
57 elif c.repo_group:
58 home_url = request.route_path('repo_group_integrations_home',
59 repo_group_name=c.repo_group.group_name)
60 else:
61 home_url = request.route_path('global_integrations_home')
62 %>
63
64 <a href="${home_url}" class="btn ${not current_IntegrationType and 'btn-primary' or ''}">${_('All')}</a>
65
66 %for integration_key, IntegrationType in available_integrations.items():
38 67 <%
39 68 if c.repo:
40 create_url = request.route_path('repo_integrations_create',
69 list_url = request.route_path('repo_integrations_list',
41 70 repo_name=c.repo.repo_name,
42 integration=integration)
71 integration=integration_key)
43 72 elif c.repo_group:
44 create_url = request.route_path('repo_group_integrations_create',
73 list_url = request.route_path('repo_group_integrations_list',
45 74 repo_group_name=c.repo_group.group_name,
46 integration=integration)
75 integration=integration_key)
47 76 else:
48 create_url = request.route_path('global_integrations_create',
49 integration=integration)
77 list_url = request.route_path('global_integrations_list',
78 integration=integration_key)
50 79 %>
51 <a href="${create_url}" class="btn">
52 ${integration}
80 <a href="${list_url}"
81 class="btn ${current_IntegrationType and integration_key == current_IntegrationType.key and 'btn-primary' or ''}">
82 ${IntegrationType.display_name}
53 83 </a>
54 84 %endfor
55 %endif
56 </div>
57 </div>
58 <div class="panel panel-default">
59 <div class="panel-heading">
60 <h3 class="panel-title">${_('Current Integrations')}</h3>
61 </div>
62 <div class="panel-body">
63 <table class="rctable issuetracker">
85
86 <%
87 if c.repo:
88 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
89 elif c.repo_group:
90 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
91 else:
92 create_url = h.route_path('global_integrations_new')
93 %>
94 <p class="pull-right">
95 <a href="${create_url}" class="btn btn-small btn-success">${_(u'Create new integration')}</a>
96 </p>
97
98 <table class="rctable integrations">
64 99 <thead>
65 100 <tr>
66 <th>${_('Enabled')}</th>
67 <th>${_('Description')}</th>
68 <th>${_('Type')}</th>
101 <th><a href="?sort=enabled:${rev_sort_dir}">${_('Enabled')}</a></th>
102 <th><a href="?sort=name:${rev_sort_dir}">${_('Name')}</a></th>
103 <th colspan="2"><a href="?sort=integration_type:${rev_sort_dir}">${_('Type')}</a></th>
104 <th><a href="?sort=scope:${rev_sort_dir}">${_('Scope')}</a></th>
69 105 <th>${_('Actions')}</th>
70 106 <th></th>
71 107 </tr>
72 108 </thead>
73 109 <tbody>
110 %if not integrations_list:
111 <tr>
112 <td colspan="7">
113 <% integration_type = current_IntegrationType and current_IntegrationType.display_name or '' %>
114 %if c.repo:
115 ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)}
116 %elif c.repo_group:
117 ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
118 %else:
119 ${_('No {type} integrations exist yet.').format(type=integration_type)}
120 %endif
74 121
75 %for integration_type, integrations in sorted(current_integrations.items()):
76 %for integration in sorted(integrations, key=lambda x: x.name):
122 %if current_IntegrationType:
123 <%
124 if c.repo:
125 create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=current_IntegrationType.key)
126 elif c.repo_group:
127 create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key)
128 else:
129 create_url = h.route_path('global_integrations_create', integration=current_IntegrationType.key)
130 %>
131 %endif
132
133 <a href="${create_url}">${_(u'Create one')}</a>
134 </td>
135 </tr>
136 %endif
137 %for IntegrationType, integration in integrations_list:
77 138 <tr id="integration_${integration.integration_id}">
78 139 <td class="td-enabled">
79 140 %if integration.enabled:
@@ -85,11 +146,39 b''
85 146 <td class="td-description">
86 147 ${integration.name}
87 148 </td>
88 <td class="td-regex">
149 <td class="td-icon">
150 %if integration.integration_type in available_integrations:
151 <div class="integration-icon">
152 ${available_integrations[integration.integration_type].icon|n}
153 </div>
154 %else:
155 ?
156 %endif
157 </td>
158 <td class="td-type">
89 159 ${integration.integration_type}
90 160 </td>
161 <td class="td-scope">
162 %if integration.repo:
163 <a href="${h.url('summary_home', repo_name=integration.repo.repo_name)}">
164 ${_('repo')}:${integration.repo.repo_name}
165 </a>
166 %elif integration.repo_group:
167 <a href="${h.url('repo_group_home', group_name=integration.repo_group.group_name)}">
168 ${_('repogroup')}:${integration.repo_group.group_name}
169 </a>
170 %else:
171 %if integration.scope == 'root_repos':
172 ${_('top level repos only')}
173 %elif integration.scope == 'global':
174 ${_('global')}
175 %else:
176 ${_('unknown scope')}: ${integration.scope}
177 %endif
178 </td>
179 %endif
91 180 <td class="td-action">
92 %if integration_type not in available_integrations:
181 %if not IntegrationType:
93 182 ${_('unknown integration')}
94 183 %else:
95 184 <%
@@ -123,10 +212,14 b''
123 212 </td>
124 213 </tr>
125 214 %endfor
126 %endfor
127 215 <tr id="last-row"></tr>
128 216 </tbody>
129 217 </table>
218 <div class="integrations-paginator">
219 <div class="pagination-wh pagination-left">
220 ${integrations_list.pager('$link_previous ~2~ $link_next')}
221 </div>
222 </div>
130 223 </div>
131 224 </div>
132 225 <script type="text/javascript">
@@ -10,7 +10,6 b''
10 10 id="item-${oid}"
11 11 tal:omit-tag="structural"
12 12 i18n:domain="deform">
13
14 13 <label for="${oid}"
15 14 class="control-label ${required and 'required' or ''}"
16 15 tal:condition="not structural"
@@ -18,7 +17,7 b''
18 17 >
19 18 ${title}
20 19 </label>
21 <div class="control-inputs">
20 <div class="control-inputs ${field.widget.item_css_class or ''}">
22 21 <div tal:define="input_prepend field.widget.input_prepend | None;
23 22 input_append field.widget.input_append | None"
24 23 tal:omit-tag="not (input_prepend or input_append)"
@@ -1,8 +1,16 b''
1 <%def name="panel(title, class_='default')">
2 <div class="panel panel-${class_}">
1 <%def name="panel(title='', category='default', class_='')">
2 <div class="panel panel-${category} ${class_}">
3 %if title or hasattr(caller, 'title'):
3 4 <div class="panel-heading">
4 <h3 class="panel-title">${title}</h3>
5 <h3 class="panel-title">
6 %if title:
7 ${title}
8 %else:
9 ${caller.title()}
10 %endif
11 </h3>
5 12 </div>
13 %endif
6 14 <div class="panel-body">
7 15 ${caller.body()}
8 16 </div>
@@ -18,61 +18,175 b''
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 import time
21 22 import pytest
22 import requests
23 from mock import Mock, patch
24 23
25 24 from rhodecode import events
25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.model.db import Session, Integration
27 27 from rhodecode.model.integration import IntegrationModel
28 28 from rhodecode.integrations.types.base import IntegrationTypeBase
29 29
30 30
31 class TestIntegrationType(IntegrationTypeBase):
32 """ Test integration type class """
33
34 key = 'test-integration'
35 display_name = 'Test integration type'
31 class TestDeleteScopesDeletesIntegrations(object):
32 def test_delete_repo_with_integration_deletes_integration(self,
33 repo_integration_stub):
34 Session().delete(repo_integration_stub.repo)
35 Session().commit()
36 Session().expire_all()
37 integration = Integration.get(repo_integration_stub.integration_id)
38 assert integration is None
36 39
37 def __init__(self, settings):
38 super(IntegrationTypeBase, self).__init__(settings)
39 self.sent_events = [] # for testing
40 40
41 def send_event(self, event):
42 self.sent_events.append(event)
41 def test_delete_repo_group_with_integration_deletes_integration(self,
42 repogroup_integration_stub):
43 Session().delete(repogroup_integration_stub.repo_group)
44 Session().commit()
45 Session().expire_all()
46 integration = Integration.get(repogroup_integration_stub.integration_id)
47 assert integration is None
43 48
44 49
45 50 @pytest.fixture
46 def repo_integration_stub(request, repo_stub):
47 settings = {'test_key': 'test_value'}
48 integration = IntegrationModel().create(
49 TestIntegrationType, settings=settings, repo=repo_stub, enabled=True,
50 name='test repo integration')
51 def integration_repos(request, StubIntegrationType, stub_integration_settings):
52 """
53 Create repositories and integrations for testing, and destroy them after
54 """
55 fixture = Fixture()
56
57 repo_group_1_id = 'int_test_repo_group_1_%s' % time.time()
58 repo_group_1 = fixture.create_repo_group(repo_group_1_id)
59 repo_group_2_id = 'int_test_repo_group_2_%s' % time.time()
60 repo_group_2 = fixture.create_repo_group(repo_group_2_id)
61
62 repo_1_id = 'int_test_repo_1_%s' % time.time()
63 repo_1 = fixture.create_repo(repo_1_id, repo_group=repo_group_1)
64 repo_2_id = 'int_test_repo_2_%s' % time.time()
65 repo_2 = fixture.create_repo(repo_2_id, repo_group=repo_group_2)
66
67 root_repo_id = 'int_test_repo_root_%s' % time.time()
68 root_repo = fixture.create_repo(root_repo_id)
51 69
52 @request.addfinalizer
53 def cleanup():
54 IntegrationModel().delete(integration)
70 integration_global = IntegrationModel().create(
71 StubIntegrationType, settings=stub_integration_settings,
72 enabled=True, name='test global integration', scope='global')
73 integration_root_repos = IntegrationModel().create(
74 StubIntegrationType, settings=stub_integration_settings,
75 enabled=True, name='test root repos integration', scope='root_repos')
76 integration_repo_1 = IntegrationModel().create(
77 StubIntegrationType, settings=stub_integration_settings,
78 enabled=True, name='test repo 1 integration', scope=repo_1)
79 integration_repo_group_1 = IntegrationModel().create(
80 StubIntegrationType, settings=stub_integration_settings,
81 enabled=True, name='test repo group 1 integration', scope=repo_group_1)
82 integration_repo_2 = IntegrationModel().create(
83 StubIntegrationType, settings=stub_integration_settings,
84 enabled=True, name='test repo 2 integration', scope=repo_2)
85 integration_repo_group_2 = IntegrationModel().create(
86 StubIntegrationType, settings=stub_integration_settings,
87 enabled=True, name='test repo group 2 integration', scope=repo_group_2)
88
89 Session().commit()
55 90
56 return integration
91 def _cleanup():
92 Session().delete(integration_global)
93 Session().delete(integration_root_repos)
94 Session().delete(integration_repo_1)
95 Session().delete(integration_repo_group_1)
96 Session().delete(integration_repo_2)
97 Session().delete(integration_repo_group_2)
98 fixture.destroy_repo(root_repo)
99 fixture.destroy_repo(repo_1)
100 fixture.destroy_repo(repo_2)
101 fixture.destroy_repo_group(repo_group_1)
102 fixture.destroy_repo_group(repo_group_2)
103
104 request.addfinalizer(_cleanup)
105
106 return {
107 'repos': {
108 'repo_1': repo_1,
109 'repo_2': repo_2,
110 'root_repo': root_repo,
111 },
112 'repo_groups': {
113 'repo_group_1': repo_group_1,
114 'repo_group_2': repo_group_2,
115 },
116 'integrations': {
117 'global': integration_global,
118 'root_repos': integration_root_repos,
119 'repo_1': integration_repo_1,
120 'repo_2': integration_repo_2,
121 'repo_group_1': integration_repo_group_1,
122 'repo_group_2': integration_repo_group_2,
123 }
124 }
57 125
58 126
59 @pytest.fixture
60 def global_integration_stub(request):
61 settings = {'test_key': 'test_value'}
62 integration = IntegrationModel().create(
63 TestIntegrationType, settings=settings, enabled=True,
64 name='test global integration')
127 def test_enabled_integration_repo_scopes(integration_repos):
128 integrations = integration_repos['integrations']
129 repos = integration_repos['repos']
130
131 triggered_integrations = IntegrationModel().get_for_event(
132 events.RepoEvent(repos['root_repo']))
133
134 assert triggered_integrations == [
135 integrations['global'],
136 integrations['root_repos']
137 ]
138
139
140 triggered_integrations = IntegrationModel().get_for_event(
141 events.RepoEvent(repos['repo_1']))
65 142
66 @request.addfinalizer
67 def cleanup():
68 IntegrationModel().delete(integration)
143 assert triggered_integrations == [
144 integrations['global'],
145 integrations['repo_1'],
146 integrations['repo_group_1']
147 ]
148
69 149
70 return integration
150 triggered_integrations = IntegrationModel().get_for_event(
151 events.RepoEvent(repos['repo_2']))
152
153 assert triggered_integrations == [
154 integrations['global'],
155 integrations['repo_2'],
156 integrations['repo_group_2'],
157 ]
71 158
72 159
73 def test_delete_repo_with_integration_deletes_integration(repo_integration_stub):
74 Session().delete(repo_integration_stub.repo)
160 def test_disabled_integration_repo_scopes(integration_repos):
161 integrations = integration_repos['integrations']
162 repos = integration_repos['repos']
163
164 for integration in integrations.values():
165 integration.enabled = False
75 166 Session().commit()
76 Session().expire_all()
77 assert Integration.get(repo_integration_stub.integration_id) is None
167
168 triggered_integrations = IntegrationModel().get_for_event(
169 events.RepoEvent(repos['root_repo']))
170
171 assert triggered_integrations == []
172
173
174 triggered_integrations = IntegrationModel().get_for_event(
175 events.RepoEvent(repos['repo_1']))
176
177 assert triggered_integrations == []
178
78 179
180 triggered_integrations = IntegrationModel().get_for_event(
181 events.RepoEvent(repos['repo_2']))
182
183 assert triggered_integrations == []
184
185
186 def test_enabled_non_repo_integrations(integration_repos):
187 integrations = integration_repos['integrations']
188
189 triggered_integrations = IntegrationModel().get_for_event(
190 events.UserPreCreate({}))
191
192 assert triggered_integrations == [integrations['global']]
@@ -33,6 +33,7 b' import uuid'
33 33 import mock
34 34 import pyramid.testing
35 35 import pytest
36 import colander
36 37 import requests
37 38 from webtest.app import TestApp
38 39
@@ -41,7 +42,7 b' from rhodecode.model.changeset_status im'
41 42 from rhodecode.model.comment import ChangesetCommentsModel
42 43 from rhodecode.model.db import (
43 44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, Integration)
45 46 from rhodecode.model.meta import Session
46 47 from rhodecode.model.pull_request import PullRequestModel
47 48 from rhodecode.model.repo import RepoModel
@@ -49,6 +50,9 b' from rhodecode.model.repo_group import R'
49 50 from rhodecode.model.user import UserModel
50 51 from rhodecode.model.settings import VcsSettingsModel
51 52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations.types.base import IntegrationTypeBase
52 56 from rhodecode.lib.utils import repo2db_mapper
53 57 from rhodecode.lib.vcs import create_vcsserver_proxy
54 58 from rhodecode.lib.vcs.backends import get_backend
@@ -1636,3 +1640,101 b' def config_stub(request, request_stub):'
1636 1640 pyramid.testing.tearDown()
1637 1641
1638 1642 return config
1643
1644
1645 @pytest.fixture
1646 def StubIntegrationType():
1647 class _StubIntegrationType(IntegrationTypeBase):
1648 """ Test integration type class """
1649
1650 key = 'test'
1651 display_name = 'Test integration type'
1652 description = 'A test integration type for testing'
1653 icon = 'test_icon_html_image'
1654
1655 def __init__(self, settings):
1656 super(_StubIntegrationType, self).__init__(settings)
1657 self.sent_events = [] # for testing
1658
1659 def send_event(self, event):
1660 self.sent_events.append(event)
1661
1662 def settings_schema(self):
1663 class SettingsSchema(colander.Schema):
1664 test_string_field = colander.SchemaNode(
1665 colander.String(),
1666 missing=colander.required,
1667 title='test string field',
1668 )
1669 test_int_field = colander.SchemaNode(
1670 colander.Int(),
1671 title='some integer setting',
1672 )
1673 return SettingsSchema()
1674
1675
1676 integration_type_registry.register_integration_type(_StubIntegrationType)
1677 return _StubIntegrationType
1678
1679 @pytest.fixture
1680 def stub_integration_settings():
1681 return {
1682 'test_string_field': 'some data',
1683 'test_int_field': 100,
1684 }
1685
1686
1687 @pytest.fixture
1688 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1689 stub_integration_settings):
1690 integration = IntegrationModel().create(
1691 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1692 name='test repo integration', scope=repo_stub)
1693
1694 @request.addfinalizer
1695 def cleanup():
1696 IntegrationModel().delete(integration)
1697
1698 return integration
1699
1700
1701 @pytest.fixture
1702 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1703 stub_integration_settings):
1704 integration = IntegrationModel().create(
1705 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1706 name='test repogroup integration', scope=test_repo_group)
1707
1708 @request.addfinalizer
1709 def cleanup():
1710 IntegrationModel().delete(integration)
1711
1712 return integration
1713
1714
1715 @pytest.fixture
1716 def global_integration_stub(request, StubIntegrationType,
1717 stub_integration_settings):
1718 integration = IntegrationModel().create(
1719 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1720 name='test global integration', scope='global')
1721
1722 @request.addfinalizer
1723 def cleanup():
1724 IntegrationModel().delete(integration)
1725
1726 return integration
1727
1728
1729 @pytest.fixture
1730 def root_repos_integration_stub(request, StubIntegrationType,
1731 stub_integration_settings):
1732 integration = IntegrationModel().create(
1733 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1734 name='test global integration', scope='root_repos')
1735
1736 @request.addfinalizer
1737 def cleanup():
1738 IntegrationModel().delete(integration)
1739
1740 return integration
General Comments 0
You need to be logged in to leave comments. Login now