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