##// 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
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 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
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 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
@@ -42,6 +42,7 b" STATIC_FILE_PREFIX = '/_static'"
42 URL_NAME_REQUIREMENTS = {
42 URL_NAME_REQUIREMENTS = {
43 # group name can have a slash in them, but they must not end with a slash
43 # group name can have a slash in them, but they must not end with a slash
44 'group_name': r'.*?[^/]',
44 'group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
45 # repo names can have a slash in them, but they must not end with a slash
46 # repo names can have a slash in them, but they must not end with a slash
46 'repo_name': r'.*?[^/]',
47 'repo_name': r'.*?[^/]',
47 # file path eats up everything at the end
48 # file path eats up everything at the end
@@ -31,6 +31,15 b' log = logging.getLogger(__name__)'
31 def includeme(config):
31 def includeme(config):
32
32
33 # global integrations
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 config.add_route('global_integrations_home',
43 config.add_route('global_integrations_home',
35 ADMIN_PREFIX + '/integrations')
44 ADMIN_PREFIX + '/integrations')
36 config.add_route('global_integrations_list',
45 config.add_route('global_integrations_list',
@@ -48,15 +57,75 b' def includeme(config):'
48 config.add_route('global_integrations_edit',
57 config.add_route('global_integrations_edit',
49 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
50 custom_predicates=(valid_integration,))
59 custom_predicates=(valid_integration,))
60
61
51 for route_name in ['global_integrations_create', 'global_integrations_edit']:
62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
52 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
53 attr='settings_get',
64 attr='settings_get',
54 renderer='rhodecode:templates/admin/integrations/edit.html',
65 renderer='rhodecode:templates/admin/integrations/form.html',
55 request_method='GET',
66 request_method='GET',
56 route_name=route_name)
67 route_name=route_name)
57 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
58 attr='settings_post',
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 request_method='POST',
129 request_method='POST',
61 route_name=route_name)
130 route_name=route_name)
62
131
@@ -78,8 +147,21 b' def includeme(config):'
78 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
79 attr='index',
148 attr='index',
80 request_method='GET',
149 request_method='GET',
150 renderer='rhodecode:templates/admin/integrations/list.html',
81 route_name=route_name)
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 config.add_route('repo_integrations_create',
165 config.add_route('repo_integrations_create',
84 add_route_requirements(
166 add_route_requirements(
85 '{repo_name}/settings/integrations/{integration}/new',
167 '{repo_name}/settings/integrations/{integration}/new',
@@ -95,56 +177,12 b' def includeme(config):'
95 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
96 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
97 attr='settings_get',
179 attr='settings_get',
98 renderer='rhodecode:templates/admin/integrations/edit.html',
180 renderer='rhodecode:templates/admin/integrations/form.html',
99 request_method='GET',
181 request_method='GET',
100 route_name=route_name)
182 route_name=route_name)
101 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
102 attr='settings_post',
184 attr='settings_post',
103 renderer='rhodecode:templates/admin/integrations/edit.html',
185 renderer='rhodecode:templates/admin/integrations/form.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',
148 request_method='POST',
186 request_method='POST',
149 route_name=route_name)
187 route_name=route_name)
150
188
@@ -194,7 +232,7 b' def valid_integration(info, request):'
194 return False
232 return False
195 if repo and repo.repo_id != integration.repo_id:
233 if repo and repo.repo_id != integration.repo_id:
196 return False
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 return False
236 return False
199
237
200 return True
238 return True
@@ -20,26 +20,52 b''
20
20
21 import colander
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):
26 class IntegrationOptionsSchemaBase(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 """
32 enabled = colander.SchemaNode(
27 enabled = colander.SchemaNode(
33 colander.Bool(),
28 colander.Bool(),
34 default=True,
29 default=True,
35 description=lazy_ugettext('Enable or disable this integration.'),
30 description=_('Enable or disable this integration.'),
36 missing=False,
31 missing=False,
37 title=lazy_ugettext('Enabled'),
32 title=_('Enabled'),
38 )
33 )
39
34
40 name = colander.SchemaNode(
35 name = colander.SchemaNode(
41 colander.String(),
36 colander.String(),
42 description=lazy_ugettext('Short name for this integration.'),
37 description=_('Short name for this integration.'),
43 missing=colander.required,
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 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
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 class IntegrationTypeBase(object):
25 class IntegrationTypeBase(object):
25 """ Base class for IntegrationType plugins """
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 def __init__(self, settings):
91 def __init__(self, settings):
28 """
92 """
29 :param settings: dict of settings to be used for the integration
93 :param settings: dict of settings to be used for the integration
30 """
94 """
31 self.settings = settings
95 self.settings = settings
32
96
33
34 def settings_schema(self):
97 def settings_schema(self):
35 """
98 """
36 A colander schema of settings for the integration type
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()
101 return colander.Schema()
42
@@ -26,11 +26,10 b' import colander'
26 from mako.template import Template
26 from mako.template import Template
27
27
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.translation import _, lazy_ugettext
29 from rhodecode.translation import _
30 from rhodecode.lib.celerylib import run_task
30 from rhodecode.lib.celerylib import run_task
31 from rhodecode.lib.celerylib import tasks
31 from rhodecode.lib.celerylib import tasks
32 from rhodecode.integrations.types.base import IntegrationTypeBase
32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34
33
35
34
36 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
@@ -147,18 +146,79 b" repo_push_template_html = Template('''"
147 </html>
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 @colander.instantiate(validator=colander.Length(min=1))
212 @colander.instantiate(validator=colander.Length(min=1))
153 class recipients(colander.SequenceSchema):
213 class recipients(colander.SequenceSchema):
154 title = lazy_ugettext('Recipients')
214 title = _('Recipients')
155 description = lazy_ugettext('Email addresses to send push events to')
215 description = _('Email addresses to send push events to')
156 widget = deform.widget.SequenceWidget(min_len=1)
216 widget = deform.widget.SequenceWidget(min_len=1)
157
217
158 recipient = colander.SchemaNode(
218 recipient = colander.SchemaNode(
159 colander.String(),
219 colander.String(),
160 title=lazy_ugettext('Email address'),
220 title=_('Email address'),
161 description=lazy_ugettext('Email address'),
221 description=_('Email address'),
162 default='',
222 default='',
163 validator=colander.Email(),
223 validator=colander.Email(),
164 widget=deform.widget.TextInputWidget(
224 widget=deform.widget.TextInputWidget(
@@ -169,8 +229,9 b' class EmailSettingsSchema(IntegrationSet'
169
229
170 class EmailIntegrationType(IntegrationTypeBase):
230 class EmailIntegrationType(IntegrationTypeBase):
171 key = 'email'
231 key = 'email'
172 display_name = lazy_ugettext('Email')
232 display_name = _('Email')
173 SettingsSchema = EmailSettingsSchema
233 description = _('Send repo push summaries to a list of recipients via email')
234 icon = email_icon
174
235
175 def settings_schema(self):
236 def settings_schema(self):
176 schema = EmailSettingsSchema()
237 schema = EmailSettingsSchema()
@@ -29,29 +29,28 b' from celery.task import task'
29 from mako.template import Template
29 from mako.template import Template
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.celerylib import run_task
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38
37
39 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
40
39
41
40
42 class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
41 class HipchatSettingsSchema(colander.Schema):
43 color_choices = [
42 color_choices = [
44 ('yellow', lazy_ugettext('Yellow')),
43 ('yellow', _('Yellow')),
45 ('red', lazy_ugettext('Red')),
44 ('red', _('Red')),
46 ('green', lazy_ugettext('Green')),
45 ('green', _('Green')),
47 ('purple', lazy_ugettext('Purple')),
46 ('purple', _('Purple')),
48 ('gray', lazy_ugettext('Gray')),
47 ('gray', _('Gray')),
49 ]
48 ]
50
49
51 server_url = colander.SchemaNode(
50 server_url = colander.SchemaNode(
52 colander.String(),
51 colander.String(),
53 title=lazy_ugettext('Hipchat server URL'),
52 title=_('Hipchat server URL'),
54 description=lazy_ugettext('Hipchat integration url.'),
53 description=_('Hipchat integration url.'),
55 default='',
54 default='',
56 preparer=strip_whitespace,
55 preparer=strip_whitespace,
57 validator=colander.url,
56 validator=colander.url,
@@ -61,15 +60,15 b' class HipchatSettingsSchema(IntegrationS'
61 )
60 )
62 notify = colander.SchemaNode(
61 notify = colander.SchemaNode(
63 colander.Bool(),
62 colander.Bool(),
64 title=lazy_ugettext('Notify'),
63 title=_('Notify'),
65 description=lazy_ugettext('Make a notification to the users in room.'),
64 description=_('Make a notification to the users in room.'),
66 missing=False,
65 missing=False,
67 default=False,
66 default=False,
68 )
67 )
69 color = colander.SchemaNode(
68 color = colander.SchemaNode(
70 colander.String(),
69 colander.String(),
71 title=lazy_ugettext('Color'),
70 title=_('Color'),
72 description=lazy_ugettext('Background color of message.'),
71 description=_('Background color of message.'),
73 missing='',
72 missing='',
74 validator=colander.OneOf([x[0] for x in color_choices]),
73 validator=colander.OneOf([x[0] for x in color_choices]),
75 widget=deform.widget.Select2Widget(
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 class HipchatIntegrationType(IntegrationTypeBase):
100 class HipchatIntegrationType(IntegrationTypeBase):
103 key = 'hipchat'
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 valid_events = [
106 valid_events = [
106 events.PullRequestCloseEvent,
107 events.PullRequestCloseEvent,
107 events.PullRequestMergeEvent,
108 events.PullRequestMergeEvent,
@@ -29,21 +29,20 b' from celery.task import task'
29 from mako.template import Template
29 from mako.template import Template
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.celerylib import run_task
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38
37
39 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
40
39
41
40
42 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(colander.Schema):
43 service = colander.SchemaNode(
42 service = colander.SchemaNode(
44 colander.String(),
43 colander.String(),
45 title=lazy_ugettext('Slack service URL'),
44 title=_('Slack service URL'),
46 description=h.literal(lazy_ugettext(
45 description=h.literal(_(
47 'This can be setup at the '
46 'This can be setup at the '
48 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 'slack app manager</a>')),
48 'slack app manager</a>')),
@@ -56,8 +55,8 b' class SlackSettingsSchema(IntegrationSet'
56 )
55 )
57 username = colander.SchemaNode(
56 username = colander.SchemaNode(
58 colander.String(),
57 colander.String(),
59 title=lazy_ugettext('Username'),
58 title=_('Username'),
60 description=lazy_ugettext('Username to show notifications coming from.'),
59 description=_('Username to show notifications coming from.'),
61 missing='Rhodecode',
60 missing='Rhodecode',
62 preparer=strip_whitespace,
61 preparer=strip_whitespace,
63 widget=deform.widget.TextInputWidget(
62 widget=deform.widget.TextInputWidget(
@@ -66,8 +65,8 b' class SlackSettingsSchema(IntegrationSet'
66 )
65 )
67 channel = colander.SchemaNode(
66 channel = colander.SchemaNode(
68 colander.String(),
67 colander.String(),
69 title=lazy_ugettext('Channel'),
68 title=_('Channel'),
70 description=lazy_ugettext('Channel to send notifications to.'),
69 description=_('Channel to send notifications to.'),
71 missing='',
70 missing='',
72 preparer=strip_whitespace,
71 preparer=strip_whitespace,
73 widget=deform.widget.TextInputWidget(
72 widget=deform.widget.TextInputWidget(
@@ -76,8 +75,8 b' class SlackSettingsSchema(IntegrationSet'
76 )
75 )
77 icon_emoji = colander.SchemaNode(
76 icon_emoji = colander.SchemaNode(
78 colander.String(),
77 colander.String(),
79 title=lazy_ugettext('Emoji'),
78 title=_('Emoji'),
80 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
79 description=_('Emoji to use eg. :studio_microphone:'),
81 missing='',
80 missing='',
82 preparer=strip_whitespace,
81 preparer=strip_whitespace,
83 widget=deform.widget.TextInputWidget(
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 class SlackIntegrationType(IntegrationTypeBase):
106 class SlackIntegrationType(IntegrationTypeBase):
106 key = 'slack'
107 key = 'slack'
107 display_name = lazy_ugettext('Slack')
108 display_name = _('Slack')
108 SettingsSchema = SlackSettingsSchema
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 valid_events = [
112 valid_events = [
110 events.PullRequestCloseEvent,
113 events.PullRequestCloseEvent,
111 events.PullRequestMergeEvent,
114 events.PullRequestMergeEvent,
@@ -28,19 +28,19 b' from celery.task import task'
28 from mako.template import Template
28 from mako.template import Template
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import _
32 from rhodecode.integrations.types.base import IntegrationTypeBase
32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34
33
35 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
36
35
37
36
38 class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
37 class WebhookSettingsSchema(colander.Schema):
39 url = colander.SchemaNode(
38 url = colander.SchemaNode(
40 colander.String(),
39 colander.String(),
41 title=lazy_ugettext('Webhook URL'),
40 title=_('Webhook URL'),
42 description=lazy_ugettext('URL of the webhook to receive POST event.'),
41 description=_('URL of the webhook to receive POST event.'),
43 default='',
42 missing=colander.required,
43 required=True,
44 validator=colander.url,
44 validator=colander.url,
45 widget=deform.widget.TextInputWidget(
45 widget=deform.widget.TextInputWidget(
46 placeholder='https://www.example.com/webhook'
46 placeholder='https://www.example.com/webhook'
@@ -48,18 +48,24 b' class WebhookSettingsSchema(IntegrationS'
48 )
48 )
49 secret_token = colander.SchemaNode(
49 secret_token = colander.SchemaNode(
50 colander.String(),
50 colander.String(),
51 title=lazy_ugettext('Secret Token'),
51 title=_('Secret Token'),
52 description=lazy_ugettext('String used to validate received payloads.'),
52 description=_('String used to validate received payloads.'),
53 default='',
53 default='',
54 missing='',
54 widget=deform.widget.TextInputWidget(
55 widget=deform.widget.TextInputWidget(
55 placeholder='secret_token'
56 placeholder='secret_token'
56 ),
57 ),
57 )
58 )
58
59
59
60
61
62
60 class WebhookIntegrationType(IntegrationTypeBase):
63 class WebhookIntegrationType(IntegrationTypeBase):
61 key = 'webhook'
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 valid_events = [
69 valid_events = [
64 events.PullRequestCloseEvent,
70 events.PullRequestCloseEvent,
65 events.PullRequestMergeEvent,
71 events.PullRequestMergeEvent,
@@ -18,23 +18,29 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
22 import logging
23 import pylons
21 import pylons
24 import deform
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 from pyramid.renderers import render
29 from pyramid.renderers import render
28 from pyramid.response import Response
30 from pyramid.response import Response
29
31
30 from rhodecode.lib import auth
32 from rhodecode.lib import auth
31 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.helpers import Page
32 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
33 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.integration import IntegrationModel
38 from rhodecode.model.integration import IntegrationModel
35 from rhodecode.admin.navigation import navigation_list
39 from rhodecode.admin.navigation import navigation_list
36 from rhodecode.translation import _
40 from rhodecode.translation import _
37 from rhodecode.integrations import integration_type_registry
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 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
40
46
@@ -65,30 +71,45 b' class IntegrationSettingsViewBase(object'
65
71
66 request = self.request
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 repo_name = request.matchdict['repo_name']
75 repo_name = request.matchdict['repo_name']
70 self.repo = Repository.get_by_repo_name(repo_name)
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 repo_group_name = request.matchdict['repo_group_name']
79 repo_group_name = request.matchdict['repo_group_name']
74 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
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 integration_type = request.matchdict['integration']
84 integration_type = request.matchdict['integration']
78 self.IntegrationType = integration_type_registry[integration_type]
85 self.IntegrationType = integration_type_registry[integration_type]
79
86
80 if 'integration_id' in request.matchdict: # single integration context
87 if 'integration_id' in request.matchdict: # single integration context
81 integration_id = request.matchdict['integration_id']
88 integration_id = request.matchdict['integration_id']
82 self.integration = Integration.get(integration_id)
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:
91 # extra perms check just in case
88 self.integrations.setdefault(integration.integration_type, []
92 if not self._has_perms_for_integration(self.integration):
89 ).append(integration)
93 raise HTTPForbidden()
90
94
91 self.settings = self.integration and self.integration.settings or {}
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 def _template_c_context(self):
114 def _template_c_context(self):
94 # TODO: dan: this is a stopgap in order to inherit from current pylons
115 # TODO: dan: this is a stopgap in order to inherit from current pylons
@@ -102,6 +123,7 b' class IntegrationSettingsViewBase(object'
102 c.repo_group = self.repo_group
123 c.repo_group = self.repo_group
103 c.repo_name = self.repo and self.repo.repo_name or None
124 c.repo_name = self.repo and self.repo.repo_name or None
104 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126
105 if self.repo:
127 if self.repo:
106 c.repo_info = self.repo
128 c.repo_info = self.repo
107 c.rhodecode_db_repo = self.repo
129 c.rhodecode_db_repo = self.repo
@@ -112,23 +134,25 b' class IntegrationSettingsViewBase(object'
112 return c
134 return c
113
135
114 def _form_schema(self):
136 def _form_schema(self):
115 if self.integration:
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
116 settings = self.integration.settings
138 settings=self.settings)
117 else:
118 settings = {}
119 return self.IntegrationType(settings=settings).settings_schema()
120
139
121 def settings_get(self, defaults=None, errors=None, form=None):
140 # returns a clone, important if mutating the schema later
122 """
141 return schema.bind(
123 View that displays the plugin settings as a form.
142 permissions=self.request.user.permissions,
124 """
143 no_scope=not self.admin_view)
125 defaults = defaults or {}
144
126 errors = errors or {}
145
146 def _form_defaults(self):
147 defaults = {}
127
148
128 if self.integration:
149 if self.integration:
129 defaults = self.integration.settings or {}
150 defaults['settings'] = self.integration.settings or {}
130 defaults['name'] = self.integration.name
151 defaults['options'] = {
131 defaults['enabled'] = self.integration.enabled
152 'name': self.integration.name,
153 'enabled': self.integration.enabled,
154 'scope': self.integration.scope,
155 }
132 else:
156 else:
133 if self.repo:
157 if self.repo:
134 scope = _('{repo_name} repository').format(
158 scope = _('{repo_name} repository').format(
@@ -139,11 +163,44 b' class IntegrationSettingsViewBase(object'
139 else:
163 else:
140 scope = _('Global')
164 scope = _('Global')
141
165
142 defaults['name'] = '{} {} integration'.format(scope,
166 defaults['options'] = {
143 self.IntegrationType.display_name)
167 'enabled': True,
144 defaults['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 if self.integration:
205 if self.integration:
149 buttons = ('submit', 'delete')
206 buttons = ('submit', 'delete')
@@ -152,23 +209,10 b' class IntegrationSettingsViewBase(object'
152
209
153 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
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 template_context = {
212 template_context = {
164 'form': form,
213 'form': form,
165 'defaults': defaults,
166 'errors': errors,
167 'schema': schema,
168 'current_IntegrationType': self.IntegrationType,
214 'current_IntegrationType': self.IntegrationType,
169 'integration': self.integration,
215 'integration': self.integration,
170 'settings': self.settings,
171 'resource': self.context,
172 'c': self._template_c_context(),
216 'c': self._template_c_context(),
173 }
217 }
174
218
@@ -177,79 +221,90 b' class IntegrationSettingsViewBase(object'
177 @auth.CSRFRequired()
221 @auth.CSRFRequired()
178 def settings_post(self):
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'):
226 controls = self.request.POST.items()
183 Session().delete(self.integration)
227 pstruct = peppercorn.parse(controls)
184 Session().commit()
228
185 self.request.session.flash(
229 if self.integration and pstruct.get('delete'):
186 _('Integration {integration_name} deleted successfully.').format(
230 return self._delete_integration(self.integration)
187 integration_name=self.integration.name),
231
188 queue='success')
232 schema = self._form_schema()
189 if self.repo:
233
190 redirect_to = self.request.route_url(
234 skip_settings_validation = False
191 'repo_integrations_home', repo_name=self.repo.repo_name)
235 if self.integration and 'enabled' not in pstruct.get('options', {}):
192 else:
236 skip_settings_validation = True
193 redirect_to = self.request.route_url('global_integrations_home')
237 schema['settings'].validator = None
194 raise HTTPFound(redirect_to)
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 = {}
249 if not self.admin_view:
201 for node in schema.children:
250 # scope is read only field in these cases, and has to be added
202 if type(node.typ) in (colander.Set, colander.List):
251 options = pstruct.setdefault('options', {})
203 val = self.request.params.getall(node.name)
252 if 'scope' not in options:
204 else:
253 if self.repo:
205 val = self.request.params.get(node.name)
254 options['scope'] = 'repo:{}'.format(self.repo.repo_name)
206 if val:
255 elif self.repo_group:
207 params[node.name] = val
256 options['scope'] = 'repogroup:{}'.format(
257 self.repo_group.group_name)
208
258
209 controls = self.request.POST.items()
210 try:
259 try:
211 valid_data = form.validate(controls)
260 valid_data = form.validate_pstruct(pstruct)
212 except deform.ValidationFailure as e:
261 except deform.ValidationFailure as e:
213 self.request.session.flash(
262 self.request.session.flash(
214 _('Errors exist when saving integration settings. '
263 _('Errors exist when saving integration settings. '
215 'Please check the form inputs.'),
264 'Please check the form inputs.'),
216 queue='error')
265 queue='error')
217 return self.settings_get(errors={}, defaults=params, form=e)
266 return self.settings_get(form=e)
218
267
219 if not self.integration:
268 if not self.integration:
220 self.integration = Integration()
269 self.integration = Integration()
221 self.integration.integration_type = self.IntegrationType.key
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 Session().add(self.integration)
271 Session().add(self.integration)
227
272
228 self.integration.enabled = valid_data.pop('enabled', False)
273 scope = valid_data['options']['scope']
229 self.integration.name = valid_data.pop('name')
230 self.integration.settings = valid_data
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 Session().commit()
282 Session().commit()
233
234 # Display success message and redirect.
283 # Display success message and redirect.
235 self.request.session.flash(
284 self.request.session.flash(
236 _('Integration {integration_name} updated successfully.').format(
285 _('Integration {integration_name} updated successfully.').format(
237 integration_name=self.IntegrationType.display_name),
286 integration_name=self.IntegrationType.display_name),
238 queue='success')
287 queue='success')
239
288
240 if self.repo:
289
241 redirect_to = self.request.route_url(
290 # if integration scope changes, we must redirect to the right place
242 'repo_integrations_edit', repo_name=self.repo.repo_name,
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 integration=self.integration.integration_type,
298 integration=self.integration.integration_type,
244 integration_id=self.integration.integration_id)
299 integration_id=self.integration.integration_id)
245 elif self.repo:
300 elif isinstance(self.integration.scope, RepoGroup) and not admin_view:
246 redirect_to = self.request.route_url(
301 redirect_to = self.request.route_path(
247 'repo_group_integrations_edit',
302 'repo_group_integrations_edit',
248 repo_group_name=self.repo_group.group_name,
303 repo_group_name=self.integration.scope.group_name,
249 integration=self.integration.integration_type,
304 integration=self.integration.integration_type,
250 integration_id=self.integration.integration_id)
305 integration_id=self.integration.integration_id)
251 else:
306 else:
252 redirect_to = self.request.route_url(
307 redirect_to = self.request.route_path(
253 'global_integrations_edit',
308 'global_integrations_edit',
254 integration=self.integration.integration_type,
309 integration=self.integration.integration_type,
255 integration_id=self.integration.integration_id)
310 integration_id=self.integration.integration_id)
@@ -257,31 +312,60 b' class IntegrationSettingsViewBase(object'
257 return HTTPFound(redirect_to)
312 return HTTPFound(redirect_to)
258
313
259 def index(self):
314 def index(self):
260 current_integrations = self.integrations
315 """ List integrations """
261 if self.IntegrationType:
316 if self.repo:
262 current_integrations = {
317 scope = self.repo
263 self.IntegrationType.key: self.integrations.get(
318 elif self.repo_group:
264 self.IntegrationType.key, [])
319 scope = self.repo_group
265 }
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 template_context = {
352 template_context = {
353 'sort_field': sort_field,
354 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
268 'current_IntegrationType': self.IntegrationType,
355 'current_IntegrationType': self.IntegrationType,
269 'current_integrations': current_integrations,
356 'integrations_list': integrations,
270 'available_integrations': integration_type_registry,
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:
363 def new_integration(self):
275 html = render('rhodecode:templates/admin/integrations/list.html',
364 template_context = {
276 template_context,
365 'available_integrations': integration_type_registry,
277 request=self.request)
366 'c': self._template_c_context(),
278 else:
367 }
279 html = render('rhodecode:templates/admin/integrations/list.html',
368 return template_context
280 template_context,
281 request=self.request)
282
283 return Response(html)
284
285
369
286 class GlobalIntegrationsView(IntegrationSettingsViewBase):
370 class GlobalIntegrationsView(IntegrationSettingsViewBase):
287 def perm_check(self, user):
371 def perm_check(self, user):
@@ -293,7 +377,9 b' class RepoIntegrationsView(IntegrationSe'
293 return auth.HasRepoPermissionAll('repository.admin'
377 return auth.HasRepoPermissionAll('repository.admin'
294 )(repo_name=self.repo.repo_name, user=user)
378 )(repo_name=self.repo.repo_name, user=user)
295
379
380
296 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
381 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
297 def perm_check(self, user):
382 def perm_check(self, user):
298 return auth.HasRepoGroupPermissionAll('group.admin'
383 return auth.HasRepoGroupPermissionAll('group.admin'
299 )(group_name=self.repo_group.group_name, user=user)
384 )(group_name=self.repo_group.group_name, user=user)
385
@@ -3481,7 +3481,6 b' class Integration(Base, BaseModel):'
3481 integration_type = Column('integration_type', String(255))
3481 integration_type = Column('integration_type', String(255))
3482 enabled = Column('enabled', Boolean(), nullable=False)
3482 enabled = Column('enabled', Boolean(), nullable=False)
3483 name = Column('name', String(255), nullable=False)
3483 name = Column('name', String(255), nullable=False)
3484
3485 settings = Column(
3484 settings = Column(
3486 'settings_json', MutationObj.as_mutable(
3485 'settings_json', MutationObj.as_mutable(
3487 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3486 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
@@ -2036,6 +2036,8 b' class RepoGroup(Base, BaseModel):'
2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 user = relationship('User')
2038 user = relationship('User')
2039 integrations = relationship('Integration',
2040 cascade="all, delete, delete-orphan")
2039
2041
2040 def __init__(self, group_name='', parent_group=None):
2042 def __init__(self, group_name='', parent_group=None):
2041 self.group_name = group_name
2043 self.group_name = group_name
@@ -3481,6 +3483,8 b' class Integration(Base, BaseModel):'
3481 integration_type = Column('integration_type', String(255))
3483 integration_type = Column('integration_type', String(255))
3482 enabled = Column('enabled', Boolean(), nullable=False)
3484 enabled = Column('enabled', Boolean(), nullable=False)
3483 name = Column('name', String(255), nullable=False)
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 settings = Column(
3489 settings = Column(
3486 'settings_json', MutationObj.as_mutable(
3490 'settings_json', MutationObj.as_mutable(
@@ -3495,12 +3499,36 b' class Integration(Base, BaseModel):'
3495 nullable=True, unique=None, default=None)
3499 nullable=True, unique=None, default=None)
3496 repo_group = relationship('RepoGroup', lazy='joined')
3500 repo_group = relationship('RepoGroup', lazy='joined')
3497
3501
3498 def __repr__(self):
3502 @hybrid_property
3503 def scope(self):
3499 if self.repo:
3504 if self.repo:
3500 scope = 'repo=%r' % self.repo
3505 return self.repo
3501 elif self.repo_group:
3506 if self.repo_group:
3502 scope = 'repo_group=%r' % 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 else:
3529 else:
3504 scope = 'global'
3530 raise Exception("invalid scope: %s, must be one of "
3505
3531 "['global', 'root_repos', <RepoGroup>. <Repository>]" % value)
3506 return '<Integration(%r, %r)>' % (self.integration_type, scope)
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 from pylons import tmpl_context as c
30 from pylons import tmpl_context as c
31 from pylons.i18n.translation import _, ungettext
31 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy import or_
32 from sqlalchemy import or_, and_
33 from sqlalchemy.sql.expression import false, true
33 from sqlalchemy.sql.expression import false, true
34 from mako import exceptions
34 from mako import exceptions
35
35
@@ -39,7 +39,7 b' from rhodecode.lib import helpers as h'
39 from rhodecode.lib.caching_query import FromCache
39 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.lib.utils import PartialRenderer
40 from rhodecode.lib.utils import PartialRenderer
41 from rhodecode.model import BaseModel
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 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.integrations import integration_type_registry
44 from rhodecode.integrations import integration_type_registry
45 from rhodecode.integrations.types.base import IntegrationTypeBase
45 from rhodecode.integrations.types.base import IntegrationTypeBase
@@ -61,28 +61,34 b' class IntegrationModel(BaseModel):'
61 raise Exception('integration must be int, long or Instance'
61 raise Exception('integration must be int, long or Instance'
62 ' of Integration got %s' % type(integration))
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 """ Create an IntegrationType integration """
65 """ Create an IntegrationType integration """
66 integration = Integration()
66 integration = Integration()
67 integration.integration_type = IntegrationType.key
67 integration.integration_type = IntegrationType.key
68 integration.settings = {}
69 integration.repo = repo
70 integration.enabled = enabled
71 integration.name = name
72
73 self.sa.add(integration)
68 self.sa.add(integration)
69 self.update_integration(integration, name, enabled, scope, settings)
74 self.sa.commit()
70 self.sa.commit()
75 return integration
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 def delete(self, integration):
87 def delete(self, integration):
78 try:
88 integration = self.__get_integration(integration)
79 integration = self.__get_integration(integration)
89 if integration:
80 if integration:
90 self.sa.delete(integration)
81 self.sa.delete(integration)
91 return True
82 return True
83 except Exception:
84 log.error(traceback.format_exc())
85 raise
86 return False
92 return False
87
93
88 def get_integration_handler(self, integration):
94 def get_integration_handler(self, integration):
@@ -100,41 +106,108 b' class IntegrationModel(BaseModel):'
100 if handler:
106 if handler:
101 handler.send_event(event)
107 handler.send_event(event)
102
108
103 def get_integrations(self, repo=None, repo_group=None):
109 def get_integrations(self, scope, IntegrationType=None):
104 if repo:
110 """
105 return self.sa.query(Integration).filter(
111 Return integrations for a scope, which must be one of:
106 Integration.repo_id==repo.repo_id).all()
112
107 elif repo_group:
113 'all' - every integration, global/repogroup/repo
108 return self.sa.query(Integration).filter(
114 'global' - global integrations only
109 Integration.repo_group_id==repo_group.group_id).all()
115 <Repository> instance - integrations for this repo only
116 <RepoGroup> instance - integrations for this repogroup only
117 """
110
118
111 # global integrations
119 if isinstance(scope, Repository):
112 return self.sa.query(Integration).filter(
120 query = self.sa.query(Integration).filter(
113 Integration.repo_id==None).all()
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':
126 # global integrations
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 def get_for_event(self, event, cache=False):
153 def get_for_event(self, event, cache=False):
116 """
154 """
117 Get integrations that match an event
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
180 # repo integrations
122 # + repo_group integrations
181 if event.repo.repo_id: # pre create events dont have a repo_id yet
123 parent_groups = event.repo.groups_with_parents
182 clauses.append(
124 query = query.filter(
183 Integration.repo_id==event.repo.repo_id
125 or_(Integration.repo_id==None,
184 )
126 Integration.repo_id==event.repo.repo_id,
185
127 Integration.repo_group_id.in_(
186 if event.repo.group:
128 [group.group_id for group in parent_groups]
187 clauses.append(
129 )))
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 if cache:
202 if cache:
131 query = query.options(FromCache(
203 query = query.options(FromCache(
132 "sql_cache_short",
204 "sql_cache_short",
133 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
205 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
134 else: # only global integrations
206 else: # only global integrations
135 query = query.filter(Integration.repo_id==None)
207 query = query.filter(global_integrations_filter)
136 if cache:
208 if cache:
137 query = query.options(FromCache(
209 query = query.options(FromCache(
138 "sql_cache_short", "get_enabled_global_integrations"))
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 def delete(self, repo_group, force_delete=False, fs_remove=True):
470 def delete(self, repo_group, force_delete=False, fs_remove=True):
471 repo_group = self._get_repo_group(repo_group)
471 repo_group = self._get_repo_group(repo_group)
472 if not repo_group:
473 return False
472 try:
474 try:
473 self.sa.delete(repo_group)
475 self.sa.delete(repo_group)
474 if fs_remove:
476 if fs_remove:
@@ -478,6 +480,7 b' class RepoGroupModel(BaseModel):'
478
480
479 # Trigger delete event.
481 # Trigger delete event.
480 events.trigger(events.RepoGroupDeleteEvent(repo_group))
482 events.trigger(events.RepoGroupDeleteEvent(repo_group))
483 return True
481
484
482 except Exception:
485 except Exception:
483 log.error('Error removing repo_group %s', repo_group)
486 log.error('Error removing repo_group %s', repo_group)
@@ -38,6 +38,17 b''
38
38
39 .form-control {
39 .form-control {
40 width: 100%;
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 .error-block {
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 //Permissions Settings
1142 //Permissions Settings
1105 #add_perm {
1143 #add_perm {
@@ -261,7 +261,7 b' mark,'
261 margin-bottom: 0;
261 margin-bottom: 0;
262 }
262 }
263
263
264 .links{
264 .links {
265 float: right;
265 float: right;
266 display: inline;
266 display: inline;
267 margin: 0;
267 margin: 0;
@@ -270,7 +270,7 b' mark,'
270 text-align: right;
270 text-align: right;
271
271
272 li:before { content: none; }
272 li:before { content: none; }
273
273 li { float: right; }
274 a {
274 a {
275 display: inline-block;
275 display: inline-block;
276 margin-left: @textmargin/2;
276 margin-left: @textmargin/2;
@@ -11,6 +11,19 b''
11 request.route_url(route_name='repo_integrations_list',
11 request.route_url(route_name='repo_integrations_list',
12 repo_name=c.repo.repo_name,
12 repo_name=c.repo.repo_name,
13 integration=current_IntegrationType.key))}
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 %else:
27 %else:
15 ${h.link_to(_('Admin'),h.url('admin_home'))}
28 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 &raquo;
29 &raquo;
@@ -22,18 +35,31 b''
22 request.route_url(route_name='global_integrations_list',
35 request.route_url(route_name='global_integrations_list',
23 integration=current_IntegrationType.key))}
36 integration=current_IntegrationType.key))}
24 %endif
37 %endif
38
25 %if integration:
39 %if integration:
26 &raquo;
40 &raquo;
27 ${integration.name}
41 ${integration.name}
42 %elif current_IntegrationType:
43 &raquo;
44 ${current_IntegrationType.display_name}
28 %endif
45 %endif
29 </%def>
46 </%def>
47
48 <style>
49 .control-inputs.item-options, .control-inputs.item-settings {
50 float: left;
51 width: 100%;
52 }
53 </style>
30 <div class="panel panel-default">
54 <div class="panel panel-default">
31 <div class="panel-heading">
55 <div class="panel-heading">
32 <h2 class="panel-title">
56 <h2 class="panel-title">
33 %if integration:
57 %if integration:
34 ${current_IntegrationType.display_name} - ${integration.name}
58 ${current_IntegrationType.display_name} - ${integration.name}
35 %else:
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 %endif
63 %endif
38 </h2>
64 </h2>
39 </div>
65 </div>
@@ -4,6 +4,12 b''
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 %if c.repo:
5 %if c.repo:
6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
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 %else:
13 %else:
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
14 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 &raquo;
15 &raquo;
@@ -15,6 +21,10 b''
15 ${h.link_to(_('Integrations'),
21 ${h.link_to(_('Integrations'),
16 request.route_url(route_name='repo_integrations_home',
22 request.route_url(route_name='repo_integrations_home',
17 repo_name=c.repo.repo_name))}
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 %else:
28 %else:
19 ${h.link_to(_('Integrations'),
29 ${h.link_to(_('Integrations'),
20 request.route_url(route_name='global_integrations_home'))}
30 request.route_url(route_name='global_integrations_home'))}
@@ -26,54 +36,105 b''
26 ${_('Integrations')}
36 ${_('Integrations')}
27 %endif
37 %endif
28 </%def>
38 </%def>
39
29 <div class="panel panel-default">
40 <div class="panel panel-default">
30 <div class="panel-heading">
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 </div>
51 </div>
33 <div class="panel-body">
52 <div class="panel-body">
34 %if not available_integrations:
53 <%
35 ${_('No integrations available.')}
54 if c.repo:
36 %else:
55 home_url = request.route_path('repo_integrations_home',
37 %for integration in available_integrations:
56 repo_name=c.repo.repo_name)
38 <%
57 elif c.repo_group:
39 if c.repo:
58 home_url = request.route_path('repo_group_integrations_home',
40 create_url = request.route_path('repo_integrations_create',
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():
67 <%
68 if c.repo:
69 list_url = request.route_path('repo_integrations_list',
41 repo_name=c.repo.repo_name,
70 repo_name=c.repo.repo_name,
42 integration=integration)
71 integration=integration_key)
43 elif c.repo_group:
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 repo_group_name=c.repo_group.group_name,
74 repo_group_name=c.repo_group.group_name,
46 integration=integration)
75 integration=integration_key)
47 else:
76 else:
48 create_url = request.route_path('global_integrations_create',
77 list_url = request.route_path('global_integrations_list',
49 integration=integration)
78 integration=integration_key)
50 %>
79 %>
51 <a href="${create_url}" class="btn">
80 <a href="${list_url}"
52 ${integration}
81 class="btn ${current_IntegrationType and integration_key == current_IntegrationType.key and 'btn-primary' or ''}">
82 ${IntegrationType.display_name}
53 </a>
83 </a>
54 %endfor
84 %endfor
55 %endif
85
56 </div>
86 <%
57 </div>
87 if c.repo:
58 <div class="panel panel-default">
88 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
59 <div class="panel-heading">
89 elif c.repo_group:
60 <h3 class="panel-title">${_('Current Integrations')}</h3>
90 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
61 </div>
91 else:
62 <div class="panel-body">
92 create_url = h.route_path('global_integrations_new')
63 <table class="rctable issuetracker">
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 <thead>
99 <thead>
65 <tr>
100 <tr>
66 <th>${_('Enabled')}</th>
101 <th><a href="?sort=enabled:${rev_sort_dir}">${_('Enabled')}</a></th>
67 <th>${_('Description')}</th>
102 <th><a href="?sort=name:${rev_sort_dir}">${_('Name')}</a></th>
68 <th>${_('Type')}</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 <th>${_('Actions')}</th>
105 <th>${_('Actions')}</th>
70 <th></th>
106 <th></th>
71 </tr>
107 </tr>
72 </thead>
108 </thead>
73 <tbody>
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()):
122 %if current_IntegrationType:
76 %for integration in sorted(integrations, key=lambda x: x.name):
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 <tr id="integration_${integration.integration_id}">
138 <tr id="integration_${integration.integration_id}">
78 <td class="td-enabled">
139 <td class="td-enabled">
79 %if integration.enabled:
140 %if integration.enabled:
@@ -85,11 +146,39 b''
85 <td class="td-description">
146 <td class="td-description">
86 ${integration.name}
147 ${integration.name}
87 </td>
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 ${integration.integration_type}
159 ${integration.integration_type}
90 </td>
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 <td class="td-action">
180 <td class="td-action">
92 %if integration_type not in available_integrations:
181 %if not IntegrationType:
93 ${_('unknown integration')}
182 ${_('unknown integration')}
94 %else:
183 %else:
95 <%
184 <%
@@ -122,11 +211,15 b''
122 %endif
211 %endif
123 </td>
212 </td>
124 </tr>
213 </tr>
125 %endfor
126 %endfor
214 %endfor
127 <tr id="last-row"></tr>
215 <tr id="last-row"></tr>
128 </tbody>
216 </tbody>
129 </table>
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 </div>
223 </div>
131 </div>
224 </div>
132 <script type="text/javascript">
225 <script type="text/javascript">
@@ -10,7 +10,6 b''
10 id="item-${oid}"
10 id="item-${oid}"
11 tal:omit-tag="structural"
11 tal:omit-tag="structural"
12 i18n:domain="deform">
12 i18n:domain="deform">
13
14 <label for="${oid}"
13 <label for="${oid}"
15 class="control-label ${required and 'required' or ''}"
14 class="control-label ${required and 'required' or ''}"
16 tal:condition="not structural"
15 tal:condition="not structural"
@@ -18,7 +17,7 b''
18 >
17 >
19 ${title}
18 ${title}
20 </label>
19 </label>
21 <div class="control-inputs">
20 <div class="control-inputs ${field.widget.item_css_class or ''}">
22 <div tal:define="input_prepend field.widget.input_prepend | None;
21 <div tal:define="input_prepend field.widget.input_prepend | None;
23 input_append field.widget.input_append | None"
22 input_append field.widget.input_append | None"
24 tal:omit-tag="not (input_prepend or input_append)"
23 tal:omit-tag="not (input_prepend or input_append)"
@@ -1,8 +1,16 b''
1 <%def name="panel(title, class_='default')">
1 <%def name="panel(title='', category='default', class_='')">
2 <div class="panel panel-${class_}">
2 <div class="panel panel-${category} ${class_}">
3 %if title or hasattr(caller, 'title'):
3 <div class="panel-heading">
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 </div>
12 </div>
13 %endif
6 <div class="panel-body">
14 <div class="panel-body">
7 ${caller.body()}
15 ${caller.body()}
8 </div>
16 </div>
@@ -18,61 +18,175 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import pytest
22 import pytest
22 import requests
23 from mock import Mock, patch
24
23
25 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Session, Integration
26 from rhodecode.model.db import Session, Integration
27 from rhodecode.model.integration import IntegrationModel
27 from rhodecode.model.integration import IntegrationModel
28 from rhodecode.integrations.types.base import IntegrationTypeBase
28 from rhodecode.integrations.types.base import IntegrationTypeBase
29
29
30
30
31 class TestIntegrationType(IntegrationTypeBase):
31 class TestDeleteScopesDeletesIntegrations(object):
32 """ Test integration type class """
32 def test_delete_repo_with_integration_deletes_integration(self,
33
33 repo_integration_stub):
34 key = 'test-integration'
34 Session().delete(repo_integration_stub.repo)
35 display_name = 'Test integration type'
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):
41 def test_delete_repo_group_with_integration_deletes_integration(self,
42 self.sent_events.append(event)
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 @pytest.fixture
50 @pytest.fixture
46 def repo_integration_stub(request, repo_stub):
51 def integration_repos(request, StubIntegrationType, stub_integration_settings):
47 settings = {'test_key': 'test_value'}
52 """
48 integration = IntegrationModel().create(
53 Create repositories and integrations for testing, and destroy them after
49 TestIntegrationType, settings=settings, repo=repo_stub, enabled=True,
54 """
50 name='test repo integration')
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
70 integration_global = IntegrationModel().create(
53 def cleanup():
71 StubIntegrationType, settings=stub_integration_settings,
54 IntegrationModel().delete(integration)
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
127 def test_enabled_integration_repo_scopes(integration_repos):
60 def global_integration_stub(request):
128 integrations = integration_repos['integrations']
61 settings = {'test_key': 'test_value'}
129 repos = integration_repos['repos']
62 integration = IntegrationModel().create(
130
63 TestIntegrationType, settings=settings, enabled=True,
131 triggered_integrations = IntegrationModel().get_for_event(
64 name='test global integration')
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
143 assert triggered_integrations == [
67 def cleanup():
144 integrations['global'],
68 IntegrationModel().delete(integration)
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):
160 def test_disabled_integration_repo_scopes(integration_repos):
74 Session().delete(repo_integration_stub.repo)
161 integrations = integration_repos['integrations']
162 repos = integration_repos['repos']
163
164 for integration in integrations.values():
165 integration.enabled = False
75 Session().commit()
166 Session().commit()
76 Session().expire_all()
167
77 assert Integration.get(repo_integration_stub.integration_id) is None
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 import mock
33 import mock
34 import pyramid.testing
34 import pyramid.testing
35 import pytest
35 import pytest
36 import colander
36 import requests
37 import requests
37 from webtest.app import TestApp
38 from webtest.app import TestApp
38
39
@@ -41,7 +42,7 b' from rhodecode.model.changeset_status im'
41 from rhodecode.model.comment import ChangesetCommentsModel
42 from rhodecode.model.comment import ChangesetCommentsModel
42 from rhodecode.model.db import (
43 from rhodecode.model.db import (
43 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, Integration)
45 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
46 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.repo import RepoModel
@@ -49,6 +50,9 b' from rhodecode.model.repo_group import R'
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.user_group import UserGroupModel
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 from rhodecode.lib.utils import repo2db_mapper
56 from rhodecode.lib.utils import repo2db_mapper
53 from rhodecode.lib.vcs import create_vcsserver_proxy
57 from rhodecode.lib.vcs import create_vcsserver_proxy
54 from rhodecode.lib.vcs.backends import get_backend
58 from rhodecode.lib.vcs.backends import get_backend
@@ -1636,3 +1640,101 b' def config_stub(request, request_stub):'
1636 pyramid.testing.tearDown()
1640 pyramid.testing.tearDown()
1637
1641
1638 return config
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