##// END OF EJS Templates
integrations: add integration support...
dan -
r411:df8dc98d default
parent child Browse files
Show More

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

@@ -0,0 +1,52 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 logging
22 from rhodecode.integrations.registry import IntegrationTypeRegistry
23 from rhodecode.integrations.types import slack
24
25 log = logging.getLogger(__name__)
26
27
28 # TODO: dan: This is currently global until we figure out what to do about
29 # VCS's not having a pyramid context - move it to pyramid app configuration
30 # includeme level later to allow per instance integration setup
31 integration_type_registry = IntegrationTypeRegistry()
32 integration_type_registry.register_integration_type(slack.SlackIntegrationType)
33
34 def integrations_event_handler(event):
35 """
36 Takes an event and passes it to all enabled integrations
37 """
38 from rhodecode.model.integration import IntegrationModel
39
40 integration_model = IntegrationModel()
41 integrations = integration_model.get_for_event(event)
42 for integration in integrations:
43 try:
44 integration_model.send_event(integration, event)
45 except Exception:
46 log.exception(
47 'failure occured when sending event %s to integration %s' % (
48 event, integration))
49
50
51 def includeme(config):
52 config.include('rhodecode.integrations.routes')
@@ -0,0 +1,37 b''
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012-2016 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20 import logging
21
22 log = logging.getLogger()
23
24
25 class IntegrationTypeRegistry(dict):
26 """
27 Registry Class to hold IntegrationTypes
28 """
29 def register_integration_type(self, IntegrationType):
30 key = IntegrationType.key
31 if key in self:
32 log.warning(
33 'Overriding existing integration type %s (%s) with %s' % (
34 self[key], key, IntegrationType))
35
36 self[key] = IntegrationType
37
@@ -0,0 +1,133 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 logging
22
23 from rhodecode.model.db import Repository, Integration
24 from rhodecode.config.routing import (
25 ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
26 from rhodecode.integrations import integration_type_registry
27
28 log = logging.getLogger(__name__)
29
30
31 def includeme(config):
32 config.add_route('global_integrations_home',
33 ADMIN_PREFIX + '/integrations')
34 config.add_route('global_integrations_list',
35 ADMIN_PREFIX + '/integrations/{integration}')
36 for route_name in ['global_integrations_home', 'global_integrations_list']:
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
38 attr='index',
39 renderer='rhodecode:templates/admin/integrations/list.html',
40 request_method='GET',
41 route_name=route_name)
42
43 config.add_route('global_integrations_create',
44 ADMIN_PREFIX + '/integrations/{integration}/new',
45 custom_predicates=(valid_integration,))
46 config.add_route('global_integrations_edit',
47 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
48 custom_predicates=(valid_integration,))
49 for route_name in ['global_integrations_create', 'global_integrations_edit']:
50 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
51 attr='settings_get',
52 renderer='rhodecode:templates/admin/integrations/edit.html',
53 request_method='GET',
54 route_name=route_name)
55 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
56 attr='settings_post',
57 renderer='rhodecode:templates/admin/integrations/edit.html',
58 request_method='POST',
59 route_name=route_name)
60
61 config.add_route('repo_integrations_home',
62 add_route_requirements(
63 '{repo_name}/settings/integrations',
64 URL_NAME_REQUIREMENTS
65 ),
66 custom_predicates=(valid_repo,))
67 config.add_route('repo_integrations_list',
68 add_route_requirements(
69 '{repo_name}/settings/integrations/{integration}',
70 URL_NAME_REQUIREMENTS
71 ),
72 custom_predicates=(valid_repo, valid_integration))
73 for route_name in ['repo_integrations_home', 'repo_integrations_list']:
74 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
75 attr='index',
76 request_method='GET',
77 route_name=route_name)
78
79 config.add_route('repo_integrations_create',
80 add_route_requirements(
81 '{repo_name}/settings/integrations/{integration}/new',
82 URL_NAME_REQUIREMENTS
83 ),
84 custom_predicates=(valid_repo, valid_integration))
85 config.add_route('repo_integrations_edit',
86 add_route_requirements(
87 '{repo_name}/settings/integrations/{integration}/{integration_id}',
88 URL_NAME_REQUIREMENTS
89 ),
90 custom_predicates=(valid_repo, valid_integration))
91 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
92 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
93 attr='settings_get',
94 renderer='rhodecode:templates/admin/integrations/edit.html',
95 request_method='GET',
96 route_name=route_name)
97 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
98 attr='settings_post',
99 renderer='rhodecode:templates/admin/integrations/edit.html',
100 request_method='POST',
101 route_name=route_name)
102
103
104 def valid_repo(info, request):
105 repo = Repository.get_by_repo_name(info['match']['repo_name'])
106 if repo:
107 return True
108
109
110 def valid_integration(info, request):
111 integration_type = info['match']['integration']
112 integration_id = info['match'].get('integration_id')
113 repo_name = info['match'].get('repo_name')
114
115 if integration_type not in integration_type_registry:
116 return False
117
118 repo = None
119 if repo_name:
120 repo = Repository.get_by_repo_name(info['match']['repo_name'])
121 if not repo:
122 return False
123
124 if integration_id:
125 integration = Integration.get(integration_id)
126 if not integration:
127 return False
128 if integration.integration_type != integration_type:
129 return False
130 if repo and repo.repo_id != integration.repo_id:
131 return False
132
133 return True
@@ -0,0 +1,48 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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
23 from rhodecode.translation import lazy_ugettext
24
25
26 class IntegrationSettingsSchemaBase(colander.MappingSchema):
27 """
28 This base schema is intended for use in integrations.
29 It adds a few default settings (e.g., "enabled"), so that integration
30 authors don't have to maintain a bunch of boilerplate.
31 """
32 enabled = colander.SchemaNode(
33 colander.Bool(),
34 default=True,
35 description=lazy_ugettext('Enable or disable this integration.'),
36 missing=False,
37 title=lazy_ugettext('Enabled'),
38 widget='bool',
39 )
40
41 name = colander.SchemaNode(
42 colander.String(),
43 description=lazy_ugettext('Short name for this integration.'),
44 missing=colander.required,
45 title=lazy_ugettext('Integration name'),
46 widget='string',
47 )
48
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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/
@@ -0,0 +1,43 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
22
23
24 class IntegrationTypeBase(object):
25 """ Base class for IntegrationType plugins """
26
27 def __init__(self, settings):
28 """
29 :param settings: dict of settings to be used for the integration
30 """
31 self.settings = settings
32
33
34 @classmethod
35 def settings_schema(cls):
36 """
37 A colander schema of settings for the integration type
38
39 Subclasses can return their own schema but should always
40 inherit from IntegrationSettingsSchemaBase
41 """
42 return IntegrationSettingsSchemaBase()
43
@@ -0,0 +1,199 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 from __future__ import unicode_literals
22
23 import re
24 import logging
25 import requests
26 import colander
27 from celery.task import task
28 from mako.template import Template
29
30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
37
38 log = logging.getLogger()
39
40
41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
42 service = colander.SchemaNode(
43 colander.String(),
44 title=lazy_ugettext('Slack service URL'),
45 description=h.literal(lazy_ugettext(
46 'This can be setup at the '
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
48 'slack app manager</a>')),
49 default='',
50 placeholder='https://hooks.slack.com/services/...',
51 preparer=strip_whitespace,
52 validator=colander.url,
53 widget='string'
54 )
55 username = colander.SchemaNode(
56 colander.String(),
57 title=lazy_ugettext('Username'),
58 description=lazy_ugettext('Username to show notifications coming from.'),
59 missing='Rhodecode',
60 preparer=strip_whitespace,
61 widget='string',
62 placeholder='Rhodecode'
63 )
64 channel = colander.SchemaNode(
65 colander.String(),
66 title=lazy_ugettext('Channel'),
67 description=lazy_ugettext('Channel to send notifications to.'),
68 missing='',
69 preparer=strip_whitespace,
70 widget='string',
71 placeholder='#general'
72 )
73 icon_emoji = colander.SchemaNode(
74 colander.String(),
75 title=lazy_ugettext('Emoji'),
76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
77 missing='',
78 preparer=strip_whitespace,
79 widget='string',
80 placeholder=':studio_microphone:'
81 )
82
83
84 repo_push_template = Template(r'''
85 *${data['actor']['username']}* pushed to \
86 %if data['push']['branches']:
87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
89 %else:
90 unknown branch \
91 %endif
92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
93 >>>
94 %for commit in data['push']['commits']:
95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
96 %endfor
97 ''')
98
99
100 class SlackIntegrationType(IntegrationTypeBase):
101 key = 'slack'
102 display_name = lazy_ugettext('Slack')
103 SettingsSchema = SlackSettingsSchema
104 valid_events = [
105 events.PullRequestCloseEvent,
106 events.PullRequestMergeEvent,
107 events.PullRequestUpdateEvent,
108 events.PullRequestReviewEvent,
109 events.PullRequestCreateEvent,
110 events.RepoPushEvent,
111 events.RepoCreateEvent,
112 ]
113
114 def send_event(self, event):
115 if event.__class__ not in self.valid_events:
116 log.debug('event not valid: %r' % event)
117 return
118
119 if event.name not in self.settings['events']:
120 log.debug('event ignored: %r' % event)
121 return
122
123 data = event.as_dict()
124
125 text = '*%s* caused a *%s* event' % (
126 data['actor']['username'], event.name)
127
128 if isinstance(event, events.PullRequestEvent):
129 text = self.format_pull_request_event(event, data)
130 elif isinstance(event, events.RepoPushEvent):
131 text = self.format_repo_push_event(data)
132 elif isinstance(event, events.RepoCreateEvent):
133 text = self.format_repo_create_event(data)
134 else:
135 log.error('unhandled event type: %r' % event)
136
137 run_task(post_text_to_slack, self.settings, text)
138
139 @classmethod
140 def settings_schema(cls):
141 schema = SlackSettingsSchema()
142 schema.add(colander.SchemaNode(
143 colander.Set(),
144 widget='checkbox_list',
145 choices=sorted([e.name for e in cls.valid_events]),
146 description="Events activated for this integration",
147 default=[e.name for e in cls.valid_events],
148 name='events'
149 ))
150 return schema
151
152 def format_pull_request_event(self, event, data):
153 action = {
154 events.PullRequestCloseEvent: 'closed',
155 events.PullRequestMergeEvent: 'merged',
156 events.PullRequestUpdateEvent: 'updated',
157 events.PullRequestReviewEvent: 'reviewed',
158 events.PullRequestCreateEvent: 'created',
159 }.get(event.__class__, '<unknown action>')
160
161 return ('Pull request <{url}|#{number}> ({title}) '
162 '{action} by {user}').format(
163 user=data['actor']['username'],
164 number=data['pullrequest']['pull_request_id'],
165 url=data['pullrequest']['url'],
166 title=data['pullrequest']['title'],
167 action=action
168 )
169
170 def format_repo_push_event(self, data):
171 result = repo_push_template.render(
172 data=data,
173 html_to_slack_links=html_to_slack_links,
174 )
175 return result
176
177 def format_repo_create_msg(self, data):
178 return '<{}|{}> ({}) repository created by *{}*'.format(
179 data['repo']['url'],
180 data['repo']['repo_name'],
181 data['repo']['repo_type'],
182 data['actor']['username'],
183 )
184
185
186 def html_to_slack_links(message):
187 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
188 r'<\1|\2>', message)
189
190
191 @task(ignore_result=True)
192 def post_text_to_slack(settings, text):
193 resp = requests.post(settings['service'], json={
194 "channel": settings.get('channel', ''),
195 "username": settings.get('username', 'Rhodecode'),
196 "text": text,
197 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
198 })
199 resp.raise_for_status() # raise exception on a failed request
@@ -0,0 +1,257 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 logging
23 import pylons
24
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 from pyramid.renderers import render
27 from pyramid.response import Response
28
29 from rhodecode.lib import auth
30 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
31 from rhodecode.model.db import Repository, Session, Integration
32 from rhodecode.model.scm import ScmModel
33 from rhodecode.model.integration import IntegrationModel
34 from rhodecode.admin.navigation import navigation_list
35 from rhodecode.translation import _
36 from rhodecode.integrations import integration_type_registry
37
38 log = logging.getLogger(__name__)
39
40
41 class IntegrationSettingsViewBase(object):
42 """ Base Integration settings view used by both repo / global settings """
43
44 def __init__(self, context, request):
45 self.context = context
46 self.request = request
47 self._load_general_context()
48
49 if not self.perm_check(request.user):
50 raise HTTPForbidden()
51
52 def _load_general_context(self):
53 """
54 This avoids boilerplate for repo/global+list/edit+views/templates
55 by doing all possible contexts at the same time however it should
56 be split up into separate functions once more "contexts" exist
57 """
58
59 self.IntegrationType = None
60 self.repo = None
61 self.integration = None
62 self.integrations = {}
63
64 request = self.request
65
66 if 'repo_name' in request.matchdict: # we're in a repo context
67 repo_name = request.matchdict['repo_name']
68 self.repo = Repository.get_by_repo_name(repo_name)
69
70 if 'integration' in request.matchdict: # we're in integration context
71 integration_type = request.matchdict['integration']
72 self.IntegrationType = integration_type_registry[integration_type]
73
74 if 'integration_id' in request.matchdict: # single integration context
75 integration_id = request.matchdict['integration_id']
76 self.integration = Integration.get(integration_id)
77 else: # list integrations context
78 for integration in IntegrationModel().get_integrations(self.repo):
79 self.integrations.setdefault(integration.integration_type, []
80 ).append(integration)
81
82 self.settings = self.integration and self.integration.settings or {}
83
84 def _template_c_context(self):
85 # TODO: dan: this is a stopgap in order to inherit from current pylons
86 # based admin/repo settings templates - this should be removed entirely
87 # after port to pyramid
88
89 c = pylons.tmpl_context
90 c.active = 'integrations'
91 c.rhodecode_user = self.request.user
92 c.repo = self.repo
93 c.repo_name = self.repo and self.repo.repo_name or None
94 if self.repo:
95 c.repo_info = self.repo
96 c.rhodecode_db_repo = self.repo
97 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
98 else:
99 c.navlist = navigation_list(self.request)
100
101 return c
102
103 def _form_schema(self):
104 return self.IntegrationType.settings_schema()
105
106 def settings_get(self, defaults=None, errors=None):
107 """
108 View that displays the plugin settings as a form.
109 """
110 defaults = defaults or {}
111 errors = errors or {}
112
113 schema = self._form_schema()
114
115 if not defaults:
116 if self.integration:
117 defaults['enabled'] = self.integration.enabled
118 defaults['name'] = self.integration.name
119 else:
120 if self.repo:
121 scope = self.repo.repo_name
122 else:
123 scope = _('Global')
124
125 defaults['name'] = '{} {} integration'.format(scope,
126 self.IntegrationType.display_name)
127 defaults['enabled'] = True
128
129 for node in schema:
130 setting = self.settings.get(node.name)
131 if setting is not None:
132 defaults.setdefault(node.name, setting)
133 else:
134 if node.default:
135 defaults.setdefault(node.name, node.default)
136
137 template_context = {
138 'defaults': defaults,
139 'errors': errors,
140 'schema': schema,
141 'current_IntegrationType': self.IntegrationType,
142 'integration': self.integration,
143 'settings': self.settings,
144 'resource': self.context,
145 'c': self._template_c_context(),
146 }
147
148 return template_context
149
150 @auth.CSRFRequired()
151 def settings_post(self):
152 """
153 View that validates and stores the plugin settings.
154 """
155 if self.request.params.get('delete'):
156 Session().delete(self.integration)
157 Session().commit()
158 self.request.session.flash(
159 _('Integration {integration_name} deleted successfully.').format(
160 integration_name=self.integration.name),
161 queue='success')
162 if self.repo:
163 redirect_to = self.request.route_url(
164 'repo_integrations_home', repo_name=self.repo.repo_name)
165 else:
166 redirect_to = self.request.route_url('global_integrations_home')
167 raise HTTPFound(redirect_to)
168
169 schema = self._form_schema()
170
171 params = {}
172 for node in schema.children:
173 if type(node.typ) in (colander.Set, colander.List):
174 val = self.request.params.getall(node.name)
175 else:
176 val = self.request.params.get(node.name)
177 if val:
178 params[node.name] = val
179
180 try:
181 valid_data = schema.deserialize(params)
182 except colander.Invalid, e:
183 # Display error message and display form again.
184 self.request.session.flash(
185 _('Errors exist when saving plugin settings. '
186 'Please check the form inputs.'),
187 queue='error')
188 return self.settings_get(errors=e.asdict(), defaults=params)
189
190 if not self.integration:
191 self.integration = Integration(
192 integration_type=self.IntegrationType.key)
193 if self.repo:
194 self.integration.repo = self.repo
195 Session.add(self.integration)
196
197 self.integration.enabled = valid_data.pop('enabled', False)
198 self.integration.name = valid_data.pop('name')
199 self.integration.settings = valid_data
200
201 Session.commit()
202
203 # Display success message and redirect.
204 self.request.session.flash(
205 _('Integration {integration_name} updated successfully.').format(
206 integration_name=self.IntegrationType.display_name,
207 queue='success'))
208 if self.repo:
209 redirect_to = self.request.route_url(
210 'repo_integrations_edit', repo_name=self.repo.repo_name,
211 integration=self.integration.integration_type,
212 integration_id=self.integration.integration_id)
213 else:
214 redirect_to = self.request.route_url(
215 'global_integrations_edit',
216 integration=self.integration.integration_type,
217 integration_id=self.integration.integration_id)
218
219 return HTTPFound(redirect_to)
220
221 def index(self):
222 current_integrations = self.integrations
223 if self.IntegrationType:
224 current_integrations = {
225 self.IntegrationType.key: self.integrations.get(
226 self.IntegrationType.key, [])
227 }
228
229 template_context = {
230 'current_IntegrationType': self.IntegrationType,
231 'current_integrations': current_integrations,
232 'current_integration': 'none',
233 'available_integrations': integration_type_registry,
234 'c': self._template_c_context()
235 }
236
237 if self.repo:
238 html = render('rhodecode:templates/admin/integrations/list.html',
239 template_context,
240 request=self.request)
241 else:
242 html = render('rhodecode:templates/admin/integrations/list.html',
243 template_context,
244 request=self.request)
245
246 return Response(html)
247
248
249 class GlobalIntegrationsView(IntegrationSettingsViewBase):
250 def perm_check(self, user):
251 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
252
253
254 class RepoIntegrationsView(IntegrationSettingsViewBase):
255 def perm_check(self, user):
256 return auth.HasRepoPermissionAll('repository.admin'
257 )(repo_name=self.repo.repo_name, user=user)
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 import sqlalchemy as sa
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10
11 log = logging.getLogger(__name__)
12
13
14 def upgrade(migrate_engine):
15 """
16 Upgrade operations go here.
17 Don't create your own engine; bind migrate_engine to your metadata
18 """
19 _reset_base(migrate_engine)
20 from rhodecode.lib.dbmigrate.schema import db_4_3_0_0
21
22 integrations_table = db_4_3_0_0.Integration.__table__
23 integrations_table.create()
24
25
26 def downgrade(migrate_engine):
27 pass
@@ -0,0 +1,118 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-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
22 """
23 Model for integrations
24 """
25
26
27 import logging
28 import traceback
29
30 from pylons import tmpl_context as c
31 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy import or_
33 from sqlalchemy.sql.expression import false, true
34 from mako import exceptions
35
36 import rhodecode
37 from rhodecode import events
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.lib.utils import PartialRenderer
41 from rhodecode.model import BaseModel
42 from rhodecode.model.db import Integration, User
43 from rhodecode.model.meta import Session
44 from rhodecode.integrations import integration_type_registry
45
46 log = logging.getLogger(__name__)
47
48
49 class IntegrationModel(BaseModel):
50
51 cls = Integration
52
53 def __get_integration(self, integration):
54 if isinstance(integration, Integration):
55 return integration
56 elif isinstance(integration, (int, long)):
57 return self.sa.query(Integration).get(integration)
58 else:
59 if integration:
60 raise Exception('integration must be int, long or Instance'
61 ' of Integration got %s' % type(integration))
62
63 def delete(self, integration):
64 try:
65 integration = self.__get_integration(integration)
66 if integration:
67 Session().delete(integration)
68 return True
69 except Exception:
70 log.error(traceback.format_exc())
71 raise
72 return False
73
74 def get_integration_handler(self, integration):
75 TypeClass = integration_type_registry.get(integration.integration_type)
76 if not TypeClass:
77 log.error('No class could be found for integration type: {}'.format(
78 integration.integration_type))
79 return None
80
81 return TypeClass(integration.settings)
82
83 def send_event(self, integration, event):
84 """ Send an event to an integration """
85 handler = self.get_integration_handler(integration)
86 if handler:
87 handler.send_event(event)
88
89 def get_integrations(self, repo=None):
90 if repo:
91 return self.sa.query(Integration).filter(
92 Integration.repo_id==repo.repo_id).all()
93
94 # global integrations
95 return self.sa.query(Integration).filter(
96 Integration.repo_id==None).all()
97
98 def get_for_event(self, event, cache=False):
99 """
100 Get integrations that match an event
101 """
102 query = self.sa.query(Integration).filter(Integration.enabled==True)
103
104 if isinstance(event, events.RepoEvent): # global + repo integrations
105 query = query.filter(
106 or_(Integration.repo_id==None,
107 Integration.repo_id==event.repo.repo_id))
108 if cache:
109 query = query.options(FromCache(
110 "sql_cache_short",
111 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
112 else: # only global integrations
113 query = query.filter(Integration.repo_id==None)
114 if cache:
115 query = query.options(FromCache(
116 "sql_cache_short", "get_enabled_global_integrations"))
117
118 return query.all()
@@ -0,0 +1,40 b''
1 ## -*- coding: utf-8 -*-
2 <%!
3 def inherit(context):
4 if context['c'].repo:
5 return "/admin/repos/repo_edit.html"
6 else:
7 return "/admin/settings/settings.html"
8 %>
9 <%inherit file="${inherit(context)}" />
10
11 <%def name="title()">
12 ${_('Integrations settings')}
13 %if c.rhodecode_name:
14 &middot; ${h.branding(c.rhodecode_name)}
15 %endif
16 </%def>
17
18 <%def name="breadcrumbs_links()">
19 ${h.link_to(_('Admin'),h.url('admin_home'))}
20 &raquo;
21 ${_('Integrations')}
22 </%def>
23
24 <%def name="menu_bar_nav()">
25 %if c.repo:
26 ${self.menu_items(active='repositories')}
27 %else:
28 ${self.menu_items(active='admin')}
29 %endif
30 </%def>
31
32 <%def name="menu_bar_subnav()">
33 %if c.repo:
34 ${self.repo_menu(active='options')}
35 %endif
36 </%def>
37
38 <%def name="main_content()">
39 ${next.body()}
40 </%def>
@@ -0,0 +1,108 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.html"/>
3
4 <%def name="breadcrumbs_links()">
5 %if c.repo:
6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 &raquo;
8 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
9 &raquo;
10 ${h.link_to(current_IntegrationType.display_name,
11 request.route_url(route_name='repo_integrations_list',
12 repo_name=c.repo.repo_name,
13 integration=current_IntegrationType.key))}
14 %else:
15 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 &raquo;
17 ${h.link_to(_('Settings'),h.url('admin_settings'))}
18 &raquo;
19 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
20 &raquo;
21 ${h.link_to(current_IntegrationType.display_name,
22 request.route_url(route_name='global_integrations_list',
23 integration=current_IntegrationType.key))}
24 %endif
25 %if integration:
26 &raquo;
27 ${integration.name}
28 %endif
29 </%def>
30
31
32 <div class="panel panel-default">
33 <div class="panel-heading">
34 <h2 class="panel-title">
35 %if integration:
36 ${current_IntegrationType.display_name} - ${integration.name}
37 %else:
38 ${_('Create new %(integration_type)s integration') % {'integration_type': current_IntegrationType.display_name}}
39 %endif
40 </h2>
41 </div>
42 <div class="fields panel-body">
43 ${h.secure_form(request.url)}
44 <div class="form">
45 %for node in schema:
46 <% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %>
47 <div class="field">
48 <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div>
49 <div class="input">
50 %if node.widget in ["string", "int", "unicode"]:
51 ${h.text(node.name, defaults.get(node.name), class_="medium", placeholder=hasattr(node, 'placeholder') and node.placeholder or '')}
52 %elif node.widget in ["text"]:
53 ${h.textarea(node.name, defaults.get(node.name), class_="medium", placeholder=hasattr(node, 'placeholder') and node.placeholder or '')}
54 %elif node.widget == "password":
55 ${h.password(node.name, defaults.get(node.name), class_="medium")}
56 %elif node.widget == "bool":
57 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
58 %elif node.widget == "select":
59 ${h.select(node.name, defaults.get(node.name), node.choices)}
60 %elif node.widget == "checkbox_list":
61 %for i, choice in enumerate(node.choices):
62 <%
63 name, value = choice, choice
64 if isinstance(choice, tuple):
65 choice, name = choice
66 %>
67 <div>
68 <input id="${node.name}-${choice}"
69 name="${node.name}"
70 value="${value}"
71 type="checkbox"
72 ${value in defaults.get(node.name, []) and 'checked' or ''}>
73 <label for="${node.name}-${value}">
74 ${name}
75 </label>
76 </div>
77 %endfor
78 %elif node.widget == "readonly":
79 ${node.default}
80 %else:
81 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select|password|text|checkbox_list].
82 %endif
83 %if node.name in errors:
84 <span class="error-message">${errors.get(node.name)}</span>
85 <br />
86 %endif
87 <p class="help-block">${node.description}</p>
88 </div>
89 </div>
90 %endfor
91
92 ## Allow derived templates to add something below the form
93 ## input fields
94 %if hasattr(next, 'below_form_fields'):
95 ${next.below_form_fields()}
96 %endif
97
98 <div class="buttons">
99 ${h.submit('save',_('Save'),class_="btn")}
100 %if integration:
101 ${h.submit('delete',_('Delete'),class_="btn btn-danger")}
102 %endif
103 </div>
104
105 </div>
106 ${h.end_form()}
107 </div>
108 </div> No newline at end of file
@@ -0,0 +1,147 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.html"/>
3
4 <%def name="breadcrumbs_links()">
5 %if c.repo:
6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 %else:
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 &raquo;
10 ${h.link_to(_('Settings'),h.url('admin_settings'))}
11 %endif
12 %if current_IntegrationType:
13 &raquo;
14 %if c.repo:
15 ${h.link_to(_('Integrations'),
16 request.route_url(route_name='repo_integrations_home',
17 repo_name=c.repo.repo_name))}
18 %else:
19 ${h.link_to(_('Integrations'),
20 request.route_url(route_name='global_integrations_home'))}
21 %endif
22 &raquo;
23 ${current_IntegrationType.display_name}
24 %else:
25 &raquo;
26 ${_('Integrations')}
27 %endif
28 </%def>
29 <div class="panel panel-default">
30 <div class="panel-heading">
31 <h3 class="panel-title">${_('Create new integration')}</h3>
32 </div>
33 <div class="panel-body">
34 %if not available_integrations:
35 No integrations available
36 %else:
37 %for integration in available_integrations:
38 <%
39 if c.repo:
40 create_url = request.route_url('repo_integrations_create',
41 repo_name=c.repo.repo_name,
42 integration=integration)
43 else:
44 create_url = request.route_url('global_integrations_create',
45 integration=integration)
46 %>
47 <a href="${create_url}" class="btn">
48 ${integration}
49 </a>
50 %endfor
51 %endif
52 </div>
53 </div>
54 <div class="panel panel-default">
55 <div class="panel-heading">
56 <h3 class="panel-title">${_('Current integrations')}</h3>
57 </div>
58 <div class="panel-body">
59 <table class="rctable issuetracker">
60 <thead>
61 <tr>
62 <th>${_('Enabled')}</th>
63 <th>${_('Description')}</th>
64 <th>${_('Type')}</th>
65 <th>${_('Actions')}</th>
66 <th ></th>
67 </tr>
68 </thead>
69 <tbody>
70
71 %for integration_type, integrations in sorted(current_integrations.items()):
72 %for integration in sorted(integrations, key=lambda x: x.name):
73 <tr id="integration_${integration.integration_id}">
74 <td class="td-enabled">
75 %if integration.enabled:
76 <div class="flag_status approved pull-left"></div>
77 %else:
78 <div class="flag_status rejected pull-left"></div>
79 %endif
80 </td>
81 <td class="td-description">
82 ${integration.name}
83 </td>
84 <td class="td-regex">
85 ${integration.integration_type}
86 </td>
87 <td class="td-action">
88 %if integration_type not in available_integrations:
89 ${_('unknown integration')}
90 %else:
91 <%
92 if c.repo:
93 edit_url = request.route_url('repo_integrations_edit',
94 repo_name=c.repo.repo_name,
95 integration=integration.integration_type,
96 integration_id=integration.integration_id)
97 else:
98 edit_url = request.route_url('global_integrations_edit',
99 integration=integration.integration_type,
100 integration_id=integration.integration_id)
101 %>
102 <div class="grid_edit">
103 <a href="${edit_url}">${_('Edit')}</a>
104 </div>
105 <div class="grid_delete">
106 <a href="${edit_url}"
107 class="btn btn-link btn-danger delete_integration_entry"
108 data-desc="${integration.name}"
109 data-uid="${integration.integration_id}">
110 ${_('Delete')}
111 </a>
112 </div>
113 %endif
114 </td>
115 </tr>
116 %endfor
117 %endfor
118 <tr id="last-row"></tr>
119 </tbody>
120 </table>
121 </div>
122 </div>
123 <script type="text/javascript">
124 var delete_integration = function(entry) {
125 if (confirm("Confirm to remove this integration: "+$(entry).data('desc'))) {
126 var request = $.ajax({
127 type: "POST",
128 url: $(entry).attr('href'),
129 data: {
130 'delete': 'delete',
131 'csrf_token': CSRF_TOKEN
132 },
133 success: function(){
134 location.reload();
135 },
136 error: function(data, textStatus, errorThrown){
137 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
138 }
139 });
140 };
141 }
142
143 $('.delete_integration_entry').on('click', function(e){
144 e.preventDefault();
145 delete_integration(this);
146 });
147 </script> No newline at end of file
@@ -831,19 +831,6 b''
831 831 license = [ pkgs.lib.licenses.bsdOriginal ];
832 832 };
833 833 };
834 marshmallow = super.buildPythonPackage {
835 name = "marshmallow-2.8.0";
836 buildInputs = with self; [];
837 doCheck = false;
838 propagatedBuildInputs = with self; [];
839 src = fetchurl {
840 url = "https://pypi.python.org/packages/4f/64/9393d77847d86981c84b88bbea627d30ff71b5ab1402636b366f73737817/marshmallow-2.8.0.tar.gz";
841 md5 = "204513fc123a3d9bdd7b63b9747f02e6";
842 };
843 meta = {
844 license = [ pkgs.lib.licenses.mit ];
845 };
846 };
847 834 mccabe = super.buildPythonPackage {
848 835 name = "mccabe-0.3";
849 836 buildInputs = with self; [];
@@ -1368,7 +1355,7 b''
1368 1355 name = "rhodecode-enterprise-ce-4.3.0";
1369 1356 buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner];
1370 1357 doCheck = true;
1371 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery colander decorator docutils gunicorn infrae.cache ipython iso8601 kombu marshmallow msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1358 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery colander decorator docutils gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1372 1359 src = ./.;
1373 1360 meta = {
1374 1361 license = [ { fullName = "AGPLv3, and Commercial License"; } ];
@@ -84,7 +84,6 b' iso8601==0.1.11'
84 84 itsdangerous==0.24
85 85 kombu==1.5.1
86 86 lxml==3.4.4
87 marshmallow==2.8.0
88 87 mccabe==0.3
89 88 meld3==1.0.2
90 89 mock==1.0.1
@@ -47,7 +47,7 b' CONFIG = {}'
47 47 EXTENSIONS = {}
48 48
49 49 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __dbversion__ = 54 # defines current db version for migrations
50 __dbversion__ = 55 # defines current db version for migrations
51 51 __platform__ = platform.system()
52 52 __license__ = 'AGPLv3, and Commercial License'
53 53 __author__ = 'RhodeCode GmbH'
@@ -80,6 +80,8 b' class NavigationRegistry(object):'
80 80 NavEntry('email', _('Email'), 'admin_settings_email'),
81 81 NavEntry('hooks', _('Hooks'), 'admin_settings_hooks'),
82 82 NavEntry('search', _('Full Text Search'), 'admin_settings_search'),
83 NavEntry('integrations', _('Integrations'),
84 'global_integrations_home', pyramid=True),
83 85 NavEntry('system', _('System Info'), 'admin_settings_system'),
84 86 NavEntry('open_source', _('Open Source Licenses'),
85 87 'admin_settings_open_source', pyramid=True),
@@ -213,6 +213,7 b' def includeme(config):'
213 213 config.include('pyramid_beaker')
214 214 config.include('rhodecode.admin')
215 215 config.include('rhodecode.authentication')
216 config.include('rhodecode.integrations')
216 217 config.include('rhodecode.login')
217 218 config.include('rhodecode.tweens')
218 219 config.include('rhodecode.api')
@@ -51,6 +51,19 b' URL_NAME_REQUIREMENTS = {'
51 51 }
52 52
53 53
54 def add_route_requirements(route_path, requirements):
55 """
56 Adds regex requirements to pyramid routes using a mapping dict
57
58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 '/{action}/{id:\d+}'
60
61 """
62 for key, regex in requirements.items():
63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 return route_path
65
66
54 67 class JSRoutesMapper(Mapper):
55 68 """
56 69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
@@ -19,7 +19,7 b''
19 19 from pyramid.threadlocal import get_current_registry
20 20
21 21
22 def trigger(event):
22 def trigger(event, registry=None):
23 23 """
24 24 Helper method to send an event. This wraps the pyramid logic to send an
25 25 event.
@@ -27,9 +27,18 b' def trigger(event):'
27 27 # For the first step we are using pyramids thread locals here. If the
28 28 # event mechanism works out as a good solution we should think about
29 29 # passing the registry as an argument to get rid of it.
30 registry = get_current_registry()
30 registry = registry or get_current_registry()
31 31 registry.notify(event)
32 32
33 # Until we can work around the problem that VCS operations do not have a
34 # pyramid context to work with, we send the events to integrations directly
35
36 # Later it will be possible to use regular pyramid subscribers ie:
37 # config.add_subscriber(integrations_event_handler, RhodecodeEvent)
38 from rhodecode.integrations import integrations_event_handler
39 if isinstance(event, RhodecodeEvent):
40 integrations_event_handler(event)
41
33 42
34 43 from rhodecode.events.base import RhodecodeEvent
35 44
@@ -41,8 +50,8 b' from rhodecode.events.user import ('
41 50
42 51 from rhodecode.events.repo import (
43 52 RepoEvent,
44 RepoPreCreateEvent, RepoCreatedEvent,
45 RepoPreDeleteEvent, RepoDeletedEvent,
53 RepoPreCreateEvent, RepoCreateEvent,
54 RepoPreDeleteEvent, RepoDeleteEvent,
46 55 RepoPrePushEvent, RepoPushEvent,
47 56 RepoPrePullEvent, RepoPullEvent,
48 57 )
@@ -54,4 +63,4 b' from rhodecode.events.pullrequest import'
54 63 PullRequestReviewEvent,
55 64 PullRequestMergeEvent,
56 65 PullRequestCloseEvent,
57 ) No newline at end of file
66 )
@@ -17,7 +17,6 b''
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from datetime import datetime
20 from marshmallow import Schema, fields
21 20 from pyramid.threadlocal import get_current_request
22 21 from rhodecode.lib.utils2 import AttributeDict
23 22
@@ -28,29 +27,10 b' SYSTEM_USER = AttributeDict(dict('
28 27 ))
29 28
30 29
31 class UserSchema(Schema):
32 """
33 Marshmallow schema for a user
34 """
35 username = fields.Str()
36
37
38 class RhodecodeEventSchema(Schema):
39 """
40 Marshmallow schema for a rhodecode event
41 """
42 utc_timestamp = fields.DateTime()
43 actor = fields.Nested(UserSchema)
44 actor_ip = fields.Str()
45 name = fields.Str()
46
47
48 30 class RhodecodeEvent(object):
49 31 """
50 32 Base event class for all Rhodecode events
51 33 """
52 MarshmallowSchema = RhodecodeEventSchema
53
54 34 def __init__(self):
55 35 self.request = get_current_request()
56 36 self.utc_timestamp = datetime.utcnow()
@@ -68,4 +48,12 b' class RhodecodeEvent(object):'
68 48 return '<no ip available>'
69 49
70 50 def as_dict(self):
71 return self.MarshmallowSchema().dump(self).data
51 data = {
52 'name': self.name,
53 'utc_timestamp': self.utc_timestamp,
54 'actor_ip': self.actor_ip,
55 'actor': {
56 'username': self.actor.username
57 }
58 }
59 return data No newline at end of file
@@ -16,44 +16,39 b''
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 from marshmallow import Schema, fields
20 19
20 from rhodecode.translation import lazy_ugettext
21 21 from rhodecode.events.repo import RepoEvent
22 22
23 23
24 def get_pull_request_url(pull_request):
25 from rhodecode.model.pull_request import PullRequestModel
26 return PullRequestModel().get_url(pull_request)
27
28
29 class PullRequestSchema(Schema):
30 """
31 Marshmallow schema for a pull request
32 """
33 pull_request_id = fields.Integer()
34 url = fields.Function(get_pull_request_url)
35 title = fields.Str()
36
37
38 class PullRequestEventSchema(RepoEvent.MarshmallowSchema):
39 """
40 Marshmallow schema for a pull request event
41 """
42 pullrequest = fields.Nested(PullRequestSchema)
43
44
45 24 class PullRequestEvent(RepoEvent):
46 25 """
47 26 Base class for pull request events.
48 27
49 28 :param pullrequest: a :class:`PullRequest` instance
50 29 """
51 MarshmallowSchema = PullRequestEventSchema
52 30
53 31 def __init__(self, pullrequest):
54 32 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
55 33 self.pullrequest = pullrequest
56 34
35 def as_dict(self):
36 from rhodecode.model.pull_request import PullRequestModel
37 data = super(PullRequestEvent, self).as_dict()
38
39 commits = self._commits_as_dict(self.pullrequest.revisions)
40 issues = self._issues_as_dict(commits)
41
42 data.update({
43 'pullrequest': {
44 'title': self.pullrequest.title,
45 'issues': issues,
46 'pull_request_id': self.pullrequest.pull_request_id,
47 'url': PullRequestModel().get_url(self.pullrequest)
48 }
49 })
50 return data
51
57 52
58 53 class PullRequestCreateEvent(PullRequestEvent):
59 54 """
@@ -61,6 +56,7 b' class PullRequestCreateEvent(PullRequest'
61 56 request is created.
62 57 """
63 58 name = 'pullrequest-create'
59 display_name = lazy_ugettext('pullrequest created')
64 60
65 61
66 62 class PullRequestCloseEvent(PullRequestEvent):
@@ -69,6 +65,7 b' class PullRequestCloseEvent(PullRequestE'
69 65 request is closed.
70 66 """
71 67 name = 'pullrequest-close'
68 display_name = lazy_ugettext('pullrequest closed')
72 69
73 70
74 71 class PullRequestUpdateEvent(PullRequestEvent):
@@ -77,6 +74,7 b' class PullRequestUpdateEvent(PullRequest'
77 74 request is updated.
78 75 """
79 76 name = 'pullrequest-update'
77 display_name = lazy_ugettext('pullrequest updated')
80 78
81 79
82 80 class PullRequestMergeEvent(PullRequestEvent):
@@ -85,6 +83,7 b' class PullRequestMergeEvent(PullRequestE'
85 83 request is merged.
86 84 """
87 85 name = 'pullrequest-merge'
86 display_name = lazy_ugettext('pullrequest merged')
88 87
89 88
90 89 class PullRequestReviewEvent(PullRequestEvent):
@@ -93,5 +92,6 b' class PullRequestReviewEvent(PullRequest'
93 92 request is reviewed.
94 93 """
95 94 name = 'pullrequest-review'
95 display_name = lazy_ugettext('pullrequest reviewed')
96 96
97 97
@@ -16,31 +16,13 b''
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 from marshmallow import Schema, fields
19 import logging
20 20
21 from rhodecode.translation import lazy_ugettext
21 22 from rhodecode.model.db import User, Repository, Session
22 23 from rhodecode.events.base import RhodecodeEvent
23 24
24
25 def get_repo_url(repo):
26 from rhodecode.model.repo import RepoModel
27 return RepoModel().get_url(repo)
28
29
30 class RepositorySchema(Schema):
31 """
32 Marshmallow schema for a repository
33 """
34 repo_id = fields.Integer()
35 repo_name = fields.Str()
36 url = fields.Function(get_repo_url)
37
38
39 class RepoEventSchema(RhodecodeEvent.MarshmallowSchema):
40 """
41 Marshmallow schema for a repository event
42 """
43 repo = fields.Nested(RepositorySchema)
25 log = logging.getLogger()
44 26
45 27
46 28 class RepoEvent(RhodecodeEvent):
@@ -49,12 +31,68 b' class RepoEvent(RhodecodeEvent):'
49 31
50 32 :param repo: a :class:`Repository` instance
51 33 """
52 MarshmallowSchema = RepoEventSchema
53 34
54 35 def __init__(self, repo):
55 36 super(RepoEvent, self).__init__()
56 37 self.repo = repo
57 38
39 def as_dict(self):
40 from rhodecode.model.repo import RepoModel
41 data = super(RepoEvent, self).as_dict()
42 data.update({
43 'repo': {
44 'repo_id': self.repo.repo_id,
45 'repo_name': self.repo.repo_name,
46 'url': RepoModel().get_url(self.repo)
47 }
48 })
49 return data
50
51 def _commits_as_dict(self, commit_ids):
52 """ Helper function to serialize commit_ids """
53
54 from rhodecode.lib.utils2 import extract_mentioned_users
55 from rhodecode.model.db import Repository
56 from rhodecode.lib import helpers as h
57 from rhodecode.lib.helpers import process_patterns
58 from rhodecode.lib.helpers import urlify_commit_message
59 if not commit_ids:
60 return []
61 commits = []
62 reviewers = []
63 vcs_repo = self.repo.scm_instance(cache=False)
64 try:
65 for commit_id in commit_ids:
66 cs = vcs_repo.get_changeset(commit_id)
67 cs_data = cs.__json__()
68 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
69 cs_data['reviewers'] = reviewers
70 cs_data['url'] = h.url('changeset_home',
71 repo_name=self.repo.repo_name,
72 revision=cs_data['raw_id'],
73 qualified=True
74 )
75 urlified_message, issues_data = process_patterns(
76 cs_data['message'], self.repo.repo_name)
77 cs_data['issues'] = issues_data
78 cs_data['message_html'] = urlify_commit_message(cs_data['message'],
79 self.repo.repo_name)
80 commits.append(cs_data)
81 except Exception as e:
82 log.exception(e)
83 # we don't send any commits when crash happens, only full list matters
84 # we short circuit then.
85 return []
86 return commits
87
88 def _issues_as_dict(self, commits):
89 """ Helper function to serialize issues from commits """
90 issues = {}
91 for commit in commits:
92 for issue in commit['issues']:
93 issues[issue['id']] = issue
94 return issues
95
58 96
59 97 class RepoPreCreateEvent(RepoEvent):
60 98 """
@@ -62,14 +100,16 b' class RepoPreCreateEvent(RepoEvent):'
62 100 created.
63 101 """
64 102 name = 'repo-pre-create'
103 display_name = lazy_ugettext('repository pre create')
65 104
66 105
67 class RepoCreatedEvent(RepoEvent):
106 class RepoCreateEvent(RepoEvent):
68 107 """
69 108 An instance of this class is emitted as an :term:`event` whenever a repo is
70 109 created.
71 110 """
72 name = 'repo-created'
111 name = 'repo-create'
112 display_name = lazy_ugettext('repository created')
73 113
74 114
75 115 class RepoPreDeleteEvent(RepoEvent):
@@ -78,14 +118,16 b' class RepoPreDeleteEvent(RepoEvent):'
78 118 created.
79 119 """
80 120 name = 'repo-pre-delete'
121 display_name = lazy_ugettext('repository pre delete')
81 122
82 123
83 class RepoDeletedEvent(RepoEvent):
124 class RepoDeleteEvent(RepoEvent):
84 125 """
85 126 An instance of this class is emitted as an :term:`event` whenever a repo is
86 127 created.
87 128 """
88 name = 'repo-deleted'
129 name = 'repo-delete'
130 display_name = lazy_ugettext('repository deleted')
89 131
90 132
91 133 class RepoVCSEvent(RepoEvent):
@@ -116,6 +158,7 b' class RepoPrePullEvent(RepoVCSEvent):'
116 158 are pulled from a repo.
117 159 """
118 160 name = 'repo-pre-pull'
161 display_name = lazy_ugettext('repository pre pull')
119 162
120 163
121 164 class RepoPullEvent(RepoVCSEvent):
@@ -124,6 +167,7 b' class RepoPullEvent(RepoVCSEvent):'
124 167 are pulled from a repo.
125 168 """
126 169 name = 'repo-pull'
170 display_name = lazy_ugettext('repository pull')
127 171
128 172
129 173 class RepoPrePushEvent(RepoVCSEvent):
@@ -132,6 +176,7 b' class RepoPrePushEvent(RepoVCSEvent):'
132 176 are pushed to a repo.
133 177 """
134 178 name = 'repo-pre-push'
179 display_name = lazy_ugettext('repository pre push')
135 180
136 181
137 182 class RepoPushEvent(RepoVCSEvent):
@@ -142,8 +187,33 b' class RepoPushEvent(RepoVCSEvent):'
142 187 :param extras: (optional) dict of data from proxied VCS actions
143 188 """
144 189 name = 'repo-push'
190 display_name = lazy_ugettext('repository push')
145 191
146 192 def __init__(self, repo_name, pushed_commit_ids, extras):
147 193 super(RepoPushEvent, self).__init__(repo_name, extras)
148 194 self.pushed_commit_ids = pushed_commit_ids
149 195
196 def as_dict(self):
197 data = super(RepoPushEvent, self).as_dict()
198 branch_url = repo_url = data['repo']['url']
199
200 commits = self._commits_as_dict(self.pushed_commit_ids)
201 issues = self._issues_as_dict(commits)
202
203 branches = set(
204 commit['branch'] for commit in commits if commit['branch'])
205 branches = [
206 {
207 'name': branch,
208 'url': '{}/changelog?branch={}'.format(
209 data['repo']['url'], branch)
210 }
211 for branch in branches
212 ]
213
214 data['push'] = {
215 'commits': commits,
216 'issues': issues,
217 'branches': branches,
218 }
219 return data No newline at end of file
@@ -18,6 +18,7 b''
18 18
19 19 from zope.interface import implementer
20 20
21 from rhodecode.translation import lazy_ugettext
21 22 from rhodecode.events.base import RhodecodeEvent
22 23 from rhodecode.events.interfaces import (
23 24 IUserRegistered, IUserPreCreate, IUserPreUpdate)
@@ -29,6 +30,9 b' class UserRegistered(RhodecodeEvent):'
29 30 An instance of this class is emitted as an :term:`event` whenever a user
30 31 account is registered.
31 32 """
33 name = 'user-register'
34 display_name = lazy_ugettext('user registered')
35
32 36 def __init__(self, user, session):
33 37 self.user = user
34 38 self.session = session
@@ -40,6 +44,9 b' class UserPreCreate(RhodecodeEvent):'
40 44 An instance of this class is emitted as an :term:`event` before a new user
41 45 object is created.
42 46 """
47 name = 'user-pre-create'
48 display_name = lazy_ugettext('user pre create')
49
43 50 def __init__(self, user_data):
44 51 self.user_data = user_data
45 52
@@ -50,6 +57,9 b' class UserPreUpdate(RhodecodeEvent):'
50 57 An instance of this class is emitted as an :term:`event` before a user
51 58 object is updated.
52 59 """
60 name = 'user-pre-update'
61 display_name = lazy_ugettext('user pre update')
62
53 63 def __init__(self, user, user_data):
54 64 self.user = user
55 65 self.user_data = user_data
@@ -44,7 +44,7 b' from pygments.formatters.html import Htm'
44 44 from pygments import highlight as code_highlight
45 45 from pygments.lexers import (
46 46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 from pylons import url
47 from pylons import url as pylons_url
48 48 from pylons.i18n.translation import _, ungettext
49 49 from pyramid.threadlocal import get_current_request
50 50
@@ -88,6 +88,22 b' log = logging.getLogger(__name__)'
88 88 DEFAULT_USER = User.DEFAULT_USER
89 89 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
90 90
91 def url(*args, **kw):
92 return pylons_url(*args, **kw)
93
94 def pylons_url_current(*args, **kw):
95 """
96 This function overrides pylons.url.current() which returns the current
97 path so that it will also work from a pyramid only context. This
98 should be removed once port to pyramid is complete.
99 """
100 if not args and not kw:
101 request = get_current_request()
102 return request.path
103 return pylons_url.current(*args, **kw)
104
105 url.current = pylons_url_current
106
91 107
92 108 def html_escape(text, html_escape_table=None):
93 109 """Produce entities within text."""
@@ -1614,7 +1630,7 b' def urlify_commits(text_, repository):'
1614 1630 'pref': pref,
1615 1631 'cls': 'revision-link',
1616 1632 'url': url('changeset_home', repo_name=repository,
1617 revision=commit_id),
1633 revision=commit_id, qualified=True),
1618 1634 'commit_id': commit_id,
1619 1635 'suf': suf
1620 1636 }
@@ -1624,7 +1640,8 b' def urlify_commits(text_, repository):'
1624 1640 return newtext
1625 1641
1626 1642
1627 def _process_url_func(match_obj, repo_name, uid, entry):
1643 def _process_url_func(match_obj, repo_name, uid, entry,
1644 return_raw_data=False):
1628 1645 pref = ''
1629 1646 if match_obj.group().startswith(' '):
1630 1647 pref = ' '
@@ -1650,7 +1667,7 b' def _process_url_func(match_obj, repo_na'
1650 1667 named_vars.update(match_obj.groupdict())
1651 1668 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1652 1669
1653 return tmpl % {
1670 data = {
1654 1671 'pref': pref,
1655 1672 'cls': 'issue-tracker-link',
1656 1673 'url': _url,
@@ -1658,9 +1675,15 b' def _process_url_func(match_obj, repo_na'
1658 1675 'issue-prefix': entry['pref'],
1659 1676 'serv': entry['url'],
1660 1677 }
1678 if return_raw_data:
1679 return {
1680 'id': issue_id,
1681 'url': _url
1682 }
1683 return tmpl % data
1661 1684
1662 1685
1663 def process_patterns(text_string, repo_name, config):
1686 def process_patterns(text_string, repo_name, config=None):
1664 1687 repo = None
1665 1688 if repo_name:
1666 1689 # Retrieving repo_name to avoid invalid repo_name to explode on
@@ -1670,11 +1693,9 b' def process_patterns(text_string, repo_n'
1670 1693 settings_model = IssueTrackerSettingsModel(repo=repo)
1671 1694 active_entries = settings_model.get_settings(cache=True)
1672 1695
1696 issues_data = []
1673 1697 newtext = text_string
1674 1698 for uid, entry in active_entries.items():
1675 url_func = partial(
1676 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1677
1678 1699 log.debug('found issue tracker entry with uid %s' % (uid,))
1679 1700
1680 1701 if not (entry['pat'] and entry['url']):
@@ -1692,10 +1713,20 b' def process_patterns(text_string, repo_n'
1692 1713 entry['pat'])
1693 1714 continue
1694 1715
1716 data_func = partial(
1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1718 return_raw_data=True)
1719
1720 for match_obj in pattern.finditer(text_string):
1721 issues_data.append(data_func(match_obj))
1722
1723 url_func = partial(
1724 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1725
1695 1726 newtext = pattern.sub(url_func, newtext)
1696 1727 log.debug('processed prefix:uid `%s`' % (uid,))
1697 1728
1698 return newtext
1729 return newtext, issues_data
1699 1730
1700 1731
1701 1732 def urlify_commit_message(commit_text, repository=None):
@@ -1707,22 +1738,22 b' def urlify_commit_message(commit_text, r'
1707 1738 :param repository:
1708 1739 """
1709 1740 from pylons import url # doh, we need to re-import url to mock it later
1710 from rhodecode import CONFIG
1711 1741
1712 1742 def escaper(string):
1713 1743 return string.replace('<', '&lt;').replace('>', '&gt;')
1714 1744
1715 1745 newtext = escaper(commit_text)
1746
1747 # extract http/https links and make them real urls
1748 newtext = urlify_text(newtext, safe=False)
1749
1716 1750 # urlify commits - extract commit ids and make link out of them, if we have
1717 1751 # the scope of repository present.
1718 1752 if repository:
1719 1753 newtext = urlify_commits(newtext, repository)
1720 1754
1721 # extract http/https links and make them real urls
1722 newtext = urlify_text(newtext, safe=False)
1723
1724 1755 # process issue tracker patterns
1725 newtext = process_patterns(newtext, repository or '', CONFIG)
1756 newtext, issues = process_patterns(newtext, repository or '')
1726 1757
1727 1758 return literal(newtext)
1728 1759
@@ -20,11 +20,15 b''
20 20
21 21 import json
22 22 import logging
23 import urlparse
23 24 import threading
24 25 from BaseHTTPServer import BaseHTTPRequestHandler
25 26 from SocketServer import TCPServer
27 from routes.util import URLGenerator
26 28
27 29 import Pyro4
30 import pylons
31 import rhodecode
28 32
29 33 from rhodecode.lib import hooks_base
30 34 from rhodecode.lib.utils2 import AttributeDict
@@ -236,6 +240,17 b' class Hooks(object):'
236 240
237 241 def _call_hook(self, hook, extras):
238 242 extras = AttributeDict(extras)
243 netloc = urlparse.urlparse(extras.server_url).netloc
244 environ = {
245 'SERVER_NAME': netloc.split(':')[0],
246 'SERVER_PORT': ':' in netloc and netloc.split(':')[1] or '80',
247 'SCRIPT_NAME': '',
248 'PATH_INFO': '/',
249 'HTTP_HOST': 'localhost',
250 'REQUEST_METHOD': 'GET',
251 }
252 pylons_router = URLGenerator(rhodecode.CONFIG['routes.map'], environ)
253 pylons.url._push_object(pylons_router)
239 254
240 255 try:
241 256 result = hook(extras)
@@ -248,6 +263,9 b' class Hooks(object):'
248 263 'exception': type(error).__name__,
249 264 'exception_args': error_args,
250 265 }
266 finally:
267 pylons.url._pop_object()
268
251 269 return {
252 270 'status': result.status,
253 271 'output': result.output,
@@ -3475,3 +3475,42 b' class ExternalIdentity(Base, BaseModel):'
3475 3475 query = cls.query()
3476 3476 query = query.filter(cls.local_user_id == local_user_id)
3477 3477 return query
3478
3479
3480 class Integration(Base, BaseModel):
3481 __tablename__ = 'integrations'
3482 __table_args__ = (
3483 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3484 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3485 )
3486
3487 integration_id = Column('integration_id', Integer(), primary_key=True)
3488 integration_type = Column('integration_type', String(255))
3489 enabled = Column("enabled", Boolean(), nullable=False)
3490 name = Column('name', String(255), nullable=False)
3491 settings_json = Column('settings_json',
3492 UnicodeText().with_variant(UnicodeText(16384), 'mysql'))
3493 repo_id = Column(
3494 "repo_id", Integer(), ForeignKey('repositories.repo_id'),
3495 nullable=True, unique=None, default=None)
3496 repo = relationship('Repository', lazy='joined')
3497
3498 @hybrid_property
3499 def settings(self):
3500 data = json.loads(self.settings_json or '{}')
3501 return data
3502
3503 @settings.setter
3504 def settings(self, dct):
3505 self.settings_json = json.dumps(dct, indent=2)
3506
3507 def __repr__(self):
3508 if self.repo:
3509 scope = 'repo=%r' % self.repo
3510 else:
3511 scope = 'global'
3512
3513 return '<Integration(%r, %r)>' % (self.integration_type, scope)
3514
3515 def settings_as_dict(self):
3516 return json.loads(self.settings_json)
@@ -535,7 +535,7 b' class RepoModel(BaseModel):'
535 535 # we need to flush here, in order to check if database won't
536 536 # throw any exceptions, create filesystem dirs at the very end
537 537 self.sa.flush()
538 events.trigger(events.RepoCreatedEvent(new_repo))
538 events.trigger(events.RepoCreateEvent(new_repo))
539 539 return new_repo
540 540
541 541 except Exception:
@@ -653,7 +653,7 b' class RepoModel(BaseModel):'
653 653 'deleted_on': time.time(),
654 654 })
655 655 log_delete_repository(**old_repo_dict)
656 events.trigger(events.RepoDeletedEvent(repo))
656 events.trigger(events.RepoDeleteEvent(repo))
657 657 except Exception:
658 658 log.error(traceback.format_exc())
659 659 raise
@@ -23,6 +23,10 b''
23 23 ${self.repo_menu(active='options')}
24 24 </%def>
25 25
26 <%def name="main_content()">
27 <%include file="/admin/repos/repo_edit_${c.active}.html"/>
28 </%def>
29
26 30
27 31 <%def name="main()">
28 32 <div class="box">
@@ -64,14 +68,17 b''
64 68 <li class="${'active' if c.active=='statistics' else ''}">
65 69 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
66 70 </li>
71 <li class="${'active' if c.active=='integrations' else ''}">
72 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
73 </li>
67 74 </ul>
68 75 </div>
69 76
70 77 <div class="main-content-full-width">
71 <%include file="/admin/repos/repo_edit_${c.active}.html"/>
78 ${self.main_content()}
72 79 </div>
73 80
74 81 </div>
75 82 </div>
76 83
77 </%def>
84 </%def> No newline at end of file
@@ -18,6 +18,18 b''
18 18 ${self.menu_items(active='admin')}
19 19 </%def>
20 20
21 <%def name="side_bar_nav()">
22 % for navitem in c.navlist:
23 <li class="${'active' if c.active==navitem.key else ''}">
24 <a href="${navitem.url}">${navitem.name}</a>
25 </li>
26 % endfor
27 </%def>
28
29 <%def name="main_content()">
30 <%include file="/admin/settings/settings_${c.active}.html"/>
31 </%def>
32
21 33 <%def name="main()">
22 34 <div class="box">
23 35 <div class="title">
@@ -28,18 +40,14 b''
28 40 <div class='sidebar-col-wrapper'>
29 41 <div class="sidebar">
30 42 <ul class="nav nav-pills nav-stacked">
31 % for navitem in c.navlist:
32 <li class="${'active' if c.active==navitem.key else ''}">
33 <a href="${navitem.url}">${navitem.name}</a>
34 </li>
35 % endfor
43 ${self.side_bar_nav()}
36 44 </ul>
37 45 </div>
38 46
39 47 <div class="main-content-full-width">
40 <%include file="/admin/settings/settings_${c.active}.html"/>
48 ${self.main_content()}
41 49 </div>
42 50 </div>
43 51 </div>
44 52
45 </%def>
53 </%def> No newline at end of file
@@ -16,7 +16,6 b''
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 ${self.body()}
20 19 </div>
21 20 </div>
22 21 ${self.menu_bar_subnav()}
@@ -82,6 +81,7 b''
82 81 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
83 82 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
84 83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
85 85 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
86 86 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
87 87 </ul>
@@ -27,8 +27,8 b' from rhodecode.model.repo import RepoMod'
27 27 from rhodecode.events.repo import (
28 28 RepoPrePullEvent, RepoPullEvent,
29 29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreatedEvent,
31 RepoPreDeleteEvent, RepoDeletedEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
32 32 )
33 33
34 34
@@ -51,8 +51,8 b' def scm_extras(user_regular, repo_stub):'
51 51
52 52 # TODO: dan: make the serialization tests complete json comparisons
53 53 @pytest.mark.parametrize('EventClass', [
54 RepoPreCreateEvent, RepoCreatedEvent,
55 RepoPreDeleteEvent, RepoDeletedEvent,
54 RepoPreCreateEvent, RepoCreateEvent,
55 RepoPreDeleteEvent, RepoDeleteEvent,
56 56 ])
57 57 def test_repo_events_serialized(repo_stub, EventClass):
58 58 event = EventClass(repo_stub)
@@ -88,11 +88,11 b' def test_vcs_repo_push_event_serialize(r'
88 88 def test_create_delete_repo_fires_events(backend):
89 89 with EventCatcher() as event_catcher:
90 90 repo = backend.create_repo()
91 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreatedEvent]
91 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
92 92
93 93 with EventCatcher() as event_catcher:
94 94 RepoModel().delete(repo)
95 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeletedEvent]
95 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
96 96
97 97
98 98 def test_pull_fires_events(scm_extras):
@@ -69,6 +69,40 b' def test_format_binary():'
69 69 assert helpers.format_byte_size_binary(298489462784) == '278.0 GiB'
70 70
71 71
72 @pytest.mark.parametrize('text_string, pattern, expected', [
73 ('No issue here', '(?:#)(?P<issue_id>\d+)', []),
74 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
75 [{'url': 'http://r.io/{repo}/i/42', 'id': '42'}]),
76 ('Fix #42, #53', '(?:#)(?P<issue_id>\d+)', [
77 {'url': 'http://r.io/{repo}/i/42', 'id': '42'},
78 {'url': 'http://r.io/{repo}/i/53', 'id': '53'}]),
79 ('Fix #42', '(?:#)?<issue_id>\d+)', []), # Broken regex
80 ])
81 def test_extract_issues(backend, text_string, pattern, expected):
82 repo = backend.create_repo()
83 config = {
84 '123': {
85 'uid': '123',
86 'pat': pattern,
87 'url': 'http://r.io/${repo}/i/${issue_id}',
88 'pref': '#',
89 }
90 }
91
92 def get_settings_mock(self, cache=True):
93 return config
94
95 with mock.patch.object(IssueTrackerSettingsModel,
96 'get_settings', get_settings_mock):
97 text, issues = helpers.process_patterns(text_string, repo.repo_name)
98
99 expected = copy.deepcopy(expected)
100 for item in expected:
101 item['url'] = item['url'].format(repo=repo.repo_name)
102
103 assert issues == expected
104
105
72 106 @pytest.mark.parametrize('text_string, pattern, expected_text', [
73 107 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
74 108 'Fix <a class="issue-tracker-link" href="http://r.io/{repo}/i/42">#42</a>'
@@ -90,7 +124,7 b' def test_process_patterns_repo(backend, '
90 124
91 125 with mock.patch.object(IssueTrackerSettingsModel,
92 126 'get_settings', get_settings_mock):
93 processed_text = helpers.process_patterns(
127 processed_text, issues = helpers.process_patterns(
94 128 text_string, repo.repo_name, config)
95 129
96 130 assert processed_text == expected_text.format(repo=repo.repo_name)
@@ -116,7 +150,7 b' def test_process_patterns_no_repo(text_s'
116 150
117 151 with mock.patch.object(IssueTrackerSettingsModel,
118 152 'get_global_settings', get_settings_mock):
119 processed_text = helpers.process_patterns(
153 processed_text, issues = helpers.process_patterns(
120 154 text_string, '', config)
121 155
122 156 assert processed_text == expected_text
@@ -140,7 +174,7 b' def test_process_patterns_non_existent_r'
140 174
141 175 with mock.patch.object(IssueTrackerSettingsModel,
142 176 'get_global_settings', get_settings_mock):
143 processed_text = helpers.process_patterns(
177 processed_text, issues = helpers.process_patterns(
144 178 text_string, 'do-not-exist', config)
145 179
146 180 assert processed_text == expected_text
@@ -20,3 +20,16 b' from pyramid.i18n import TranslationStri'
20 20
21 21 # Create a translation string factory for the 'rhodecode' domain.
22 22 _ = TranslationStringFactory('rhodecode')
23
24 class LazyString(object):
25 def __init__(self, *args, **kw):
26 self.args = args
27 self.kw = kw
28
29 def __str__(self):
30 return _(*self.args, **self.kw)
31
32
33 def lazy_ugettext(*args, **kw):
34 """ Lazily evaluated version of _() """
35 return LazyString(*args, **kw)
@@ -78,7 +78,6 b' requirements = ['
78 78 'ipython',
79 79 'iso8601',
80 80 'kombu',
81 'marshmallow',
82 81 'msgpack-python',
83 82 'packaging',
84 83 'psycopg2',
General Comments 0
You need to be logged in to leave comments. Login now