Show More
@@ -0,0 +1,55 b'' | |||||
|
1 | |RCE| 4.1.0 |RNS| | |||
|
2 | ----------------- | |||
|
3 | ||||
|
4 | Release Date | |||
|
5 | ^^^^^^^^^^^^ | |||
|
6 | ||||
|
7 | - 2016-06-XX | |||
|
8 | ||||
|
9 | General | |||
|
10 | ^^^^^^^ | |||
|
11 | ||||
|
12 | - Migrated more views to Pyramid. Those now include login, social plugins, search | |||
|
13 | - Started Implementing Pyramid Events system in exchange to rcextensions callbacks | |||
|
14 | - JS routes assets are now generated in development mode automatically | |||
|
15 | - ini: Add fallback authentication plugin setting. In case only one | |||
|
16 | authentication backend is enabled users can now enable fallback auth if | |||
|
17 | they cannot log-in due to external servers being down | |||
|
18 | - Bumped Mercurial to 3.8.3 version | |||
|
19 | - Bumped RhodeCode Tools to 0.8.3 version | |||
|
20 | ||||
|
21 | New Features | |||
|
22 | ^^^^^^^^^^^^ | |||
|
23 | ||||
|
24 | - search: add syntax highlighting, line numbers and line context to file | |||
|
25 | content search results | |||
|
26 | - Go To switcher now searches commit hashes as well | |||
|
27 | - Token based authentication is now in CE edition as well | |||
|
28 | - User groups: added autocomplete widget to be able to select members of | |||
|
29 | other group as part of current group. | |||
|
30 | ||||
|
31 | Security | |||
|
32 | ^^^^^^^^ | |||
|
33 | ||||
|
34 | - Added new action loggers for actions like adding/revoking permissions. | |||
|
35 | - permissions: show origin of permissions in permissions summary. Allows users | |||
|
36 | to see where and how permissions are inherited | |||
|
37 | ||||
|
38 | Performance | |||
|
39 | ^^^^^^^^^^^ | |||
|
40 | ||||
|
41 | ||||
|
42 | ||||
|
43 | Fixes | |||
|
44 | ^^^^^ | |||
|
45 | ||||
|
46 | - api: gracefully handle errors on repos that are damaged or missing | |||
|
47 | from filesystem. | |||
|
48 | - logging: log the original error when a merge failure occurs | |||
|
49 | - #3965 Cannot change the owner of a user's group by using the API | |||
|
50 | - database is now initialized inside pyramid context | |||
|
51 | - fixed wrong check on LDAP plugin about missing ldap server | |||
|
52 | - Bring back multi-threaded workers to gunicorn for backward compatibility with | |||
|
53 | previous RhodeCode versions | |||
|
54 | - Commit dates are now properly handled as UTC. This fixes some issues | |||
|
55 | with displaying age of commits No newline at end of file |
@@ -0,0 +1,225 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 | ||||
|
24 | from sqlalchemy.ext.hybrid import hybrid_property | |||
|
25 | ||||
|
26 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |||
|
27 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |||
|
28 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |||
|
29 | from rhodecode.lib.colander_utils import strip_whitespace | |||
|
30 | from rhodecode.lib.utils2 import str2bool, safe_unicode | |||
|
31 | from rhodecode.model.db import User | |||
|
32 | from rhodecode.translation import _ | |||
|
33 | ||||
|
34 | ||||
|
35 | log = logging.getLogger(__name__) | |||
|
36 | ||||
|
37 | ||||
|
38 | def plugin_factory(plugin_id, *args, **kwds): | |||
|
39 | """ | |||
|
40 | Factory function that is called during plugin discovery. | |||
|
41 | It returns the plugin instance. | |||
|
42 | """ | |||
|
43 | plugin = RhodeCodeAuthPlugin(plugin_id) | |||
|
44 | return plugin | |||
|
45 | ||||
|
46 | ||||
|
47 | class HeadersAuthnResource(AuthnPluginResourceBase): | |||
|
48 | pass | |||
|
49 | ||||
|
50 | ||||
|
51 | class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase): | |||
|
52 | header = colander.SchemaNode( | |||
|
53 | colander.String(), | |||
|
54 | default='REMOTE_USER', | |||
|
55 | description=_('Header to extract the user from'), | |||
|
56 | preparer=strip_whitespace, | |||
|
57 | title=_('Header'), | |||
|
58 | widget='string') | |||
|
59 | fallback_header = colander.SchemaNode( | |||
|
60 | colander.String(), | |||
|
61 | default='HTTP_X_FORWARDED_USER', | |||
|
62 | description=_('Header to extract the user from when main one fails'), | |||
|
63 | preparer=strip_whitespace, | |||
|
64 | title=_('Fallback header'), | |||
|
65 | widget='string') | |||
|
66 | clean_username = colander.SchemaNode( | |||
|
67 | colander.Boolean(), | |||
|
68 | default=True, | |||
|
69 | description=_('Perform cleaning of user, if passed user has @ in ' | |||
|
70 | 'username then first part before @ is taken. ' | |||
|
71 | 'If there\'s \\ in the username only the part after ' | |||
|
72 | ' \\ is taken'), | |||
|
73 | missing=False, | |||
|
74 | title=_('Clean username'), | |||
|
75 | widget='bool') | |||
|
76 | ||||
|
77 | ||||
|
78 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): | |||
|
79 | ||||
|
80 | def includeme(self, config): | |||
|
81 | config.add_authn_plugin(self) | |||
|
82 | config.add_authn_resource(self.get_id(), HeadersAuthnResource(self)) | |||
|
83 | config.add_view( | |||
|
84 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |||
|
85 | attr='settings_get', | |||
|
86 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
|
87 | request_method='GET', | |||
|
88 | route_name='auth_home', | |||
|
89 | context=HeadersAuthnResource) | |||
|
90 | config.add_view( | |||
|
91 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |||
|
92 | attr='settings_post', | |||
|
93 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
|
94 | request_method='POST', | |||
|
95 | route_name='auth_home', | |||
|
96 | context=HeadersAuthnResource) | |||
|
97 | ||||
|
98 | def get_display_name(self): | |||
|
99 | return _('Headers') | |||
|
100 | ||||
|
101 | def get_settings_schema(self): | |||
|
102 | return HeadersSettingsSchema() | |||
|
103 | ||||
|
104 | @hybrid_property | |||
|
105 | def name(self): | |||
|
106 | return 'headers' | |||
|
107 | ||||
|
108 | @property | |||
|
109 | def is_headers_auth(self): | |||
|
110 | return True | |||
|
111 | ||||
|
112 | def use_fake_password(self): | |||
|
113 | return True | |||
|
114 | ||||
|
115 | def user_activation_state(self): | |||
|
116 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] | |||
|
117 | return 'hg.extern_activate.auto' in def_user_perms | |||
|
118 | ||||
|
119 | def _clean_username(self, username): | |||
|
120 | # Removing realm and domain from username | |||
|
121 | username = username.split('@')[0] | |||
|
122 | username = username.rsplit('\\')[-1] | |||
|
123 | return username | |||
|
124 | ||||
|
125 | def _get_username(self, environ, settings): | |||
|
126 | username = None | |||
|
127 | environ = environ or {} | |||
|
128 | if not environ: | |||
|
129 | log.debug('got empty environ: %s' % environ) | |||
|
130 | ||||
|
131 | settings = settings or {} | |||
|
132 | if settings.get('header'): | |||
|
133 | header = settings.get('header') | |||
|
134 | username = environ.get(header) | |||
|
135 | log.debug('extracted %s:%s' % (header, username)) | |||
|
136 | ||||
|
137 | # fallback mode | |||
|
138 | if not username and settings.get('fallback_header'): | |||
|
139 | header = settings.get('fallback_header') | |||
|
140 | username = environ.get(header) | |||
|
141 | log.debug('extracted %s:%s' % (header, username)) | |||
|
142 | ||||
|
143 | if username and str2bool(settings.get('clean_username')): | |||
|
144 | log.debug('Received username `%s` from headers' % username) | |||
|
145 | username = self._clean_username(username) | |||
|
146 | log.debug('New cleanup user is:%s' % username) | |||
|
147 | return username | |||
|
148 | ||||
|
149 | def get_user(self, username=None, **kwargs): | |||
|
150 | """ | |||
|
151 | Helper method for user fetching in plugins, by default it's using | |||
|
152 | simple fetch by username, but this method can be custimized in plugins | |||
|
153 | eg. headers auth plugin to fetch user by environ params | |||
|
154 | :param username: username if given to fetch | |||
|
155 | :param kwargs: extra arguments needed for user fetching. | |||
|
156 | """ | |||
|
157 | environ = kwargs.get('environ') or {} | |||
|
158 | settings = kwargs.get('settings') or {} | |||
|
159 | username = self._get_username(environ, settings) | |||
|
160 | # we got the username, so use default method now | |||
|
161 | return super(RhodeCodeAuthPlugin, self).get_user(username) | |||
|
162 | ||||
|
163 | def auth(self, userobj, username, password, settings, **kwargs): | |||
|
164 | """ | |||
|
165 | Get's the headers_auth username (or email). It tries to get username | |||
|
166 | from REMOTE_USER if this plugin is enabled, if that fails | |||
|
167 | it tries to get username from HTTP_X_FORWARDED_USER if fallback header | |||
|
168 | is set. clean_username extracts the username from this data if it's | |||
|
169 | having @ in it. | |||
|
170 | Return None on failure. On success, return a dictionary of the form: | |||
|
171 | ||||
|
172 | see: RhodeCodeAuthPluginBase.auth_func_attrs | |||
|
173 | ||||
|
174 | :param userobj: | |||
|
175 | :param username: | |||
|
176 | :param password: | |||
|
177 | :param settings: | |||
|
178 | :param kwargs: | |||
|
179 | """ | |||
|
180 | environ = kwargs.get('environ') | |||
|
181 | if not environ: | |||
|
182 | log.debug('Empty environ data skipping...') | |||
|
183 | return None | |||
|
184 | ||||
|
185 | if not userobj: | |||
|
186 | userobj = self.get_user('', environ=environ, settings=settings) | |||
|
187 | ||||
|
188 | # we don't care passed username/password for headers auth plugins. | |||
|
189 | # only way to log in is using environ | |||
|
190 | username = None | |||
|
191 | if userobj: | |||
|
192 | username = getattr(userobj, 'username') | |||
|
193 | ||||
|
194 | if not username: | |||
|
195 | # we don't have any objects in DB user doesn't exist extract | |||
|
196 | # username from environ based on the settings | |||
|
197 | username = self._get_username(environ, settings) | |||
|
198 | ||||
|
199 | # if cannot fetch username, it's a no-go for this plugin to proceed | |||
|
200 | if not username: | |||
|
201 | return None | |||
|
202 | ||||
|
203 | # old attrs fetched from RhodeCode database | |||
|
204 | admin = getattr(userobj, 'admin', False) | |||
|
205 | active = getattr(userobj, 'active', True) | |||
|
206 | email = getattr(userobj, 'email', '') | |||
|
207 | firstname = getattr(userobj, 'firstname', '') | |||
|
208 | lastname = getattr(userobj, 'lastname', '') | |||
|
209 | extern_type = getattr(userobj, 'extern_type', '') | |||
|
210 | ||||
|
211 | user_attrs = { | |||
|
212 | 'username': username, | |||
|
213 | 'firstname': safe_unicode(firstname or username), | |||
|
214 | 'lastname': safe_unicode(lastname or ''), | |||
|
215 | 'groups': [], | |||
|
216 | 'email': email or '', | |||
|
217 | 'admin': admin or False, | |||
|
218 | 'active': active, | |||
|
219 | 'active_from_extern': True, | |||
|
220 | 'extern_name': username, | |||
|
221 | 'extern_type': extern_type, | |||
|
222 | } | |||
|
223 | ||||
|
224 | log.info('user `%s` authenticated correctly' % user_attrs['username']) | |||
|
225 | return user_attrs |
@@ -0,0 +1,136 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 | """ | |||
|
22 | RhodeCode authentication token plugin for built in internal auth | |||
|
23 | """ | |||
|
24 | ||||
|
25 | import logging | |||
|
26 | ||||
|
27 | from sqlalchemy.ext.hybrid import hybrid_property | |||
|
28 | ||||
|
29 | from rhodecode.translation import _ | |||
|
30 | from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE | |||
|
31 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |||
|
32 | from rhodecode.model.db import User, UserApiKeys | |||
|
33 | ||||
|
34 | ||||
|
35 | log = logging.getLogger(__name__) | |||
|
36 | ||||
|
37 | ||||
|
38 | def plugin_factory(plugin_id, *args, **kwds): | |||
|
39 | plugin = RhodeCodeAuthPlugin(plugin_id) | |||
|
40 | return plugin | |||
|
41 | ||||
|
42 | ||||
|
43 | class RhodecodeAuthnResource(AuthnPluginResourceBase): | |||
|
44 | pass | |||
|
45 | ||||
|
46 | ||||
|
47 | class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): | |||
|
48 | """ | |||
|
49 | Enables usage of authentication tokens for vcs operations. | |||
|
50 | """ | |||
|
51 | ||||
|
52 | def includeme(self, config): | |||
|
53 | config.add_authn_plugin(self) | |||
|
54 | config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self)) | |||
|
55 | config.add_view( | |||
|
56 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |||
|
57 | attr='settings_get', | |||
|
58 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
|
59 | request_method='GET', | |||
|
60 | route_name='auth_home', | |||
|
61 | context=RhodecodeAuthnResource) | |||
|
62 | config.add_view( | |||
|
63 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |||
|
64 | attr='settings_post', | |||
|
65 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
|
66 | request_method='POST', | |||
|
67 | route_name='auth_home', | |||
|
68 | context=RhodecodeAuthnResource) | |||
|
69 | ||||
|
70 | def get_display_name(self): | |||
|
71 | return _('Rhodecode Token Auth') | |||
|
72 | ||||
|
73 | @hybrid_property | |||
|
74 | def name(self): | |||
|
75 | return "authtoken" | |||
|
76 | ||||
|
77 | def user_activation_state(self): | |||
|
78 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] | |||
|
79 | return 'hg.register.auto_activate' in def_user_perms | |||
|
80 | ||||
|
81 | def allows_authentication_from( | |||
|
82 | self, user, allows_non_existing_user=True, | |||
|
83 | allowed_auth_plugins=None, allowed_auth_sources=None): | |||
|
84 | """ | |||
|
85 | Custom method for this auth that doesn't accept empty users. And also | |||
|
86 | allows rhodecode and authtoken extern_type to auth with this. But only | |||
|
87 | via vcs mode | |||
|
88 | """ | |||
|
89 | # only this and rhodecode plugins can use this type | |||
|
90 | from rhodecode.authentication.plugins import auth_rhodecode | |||
|
91 | allowed_auth_plugins = [ | |||
|
92 | self.name, auth_rhodecode.RhodeCodeAuthPlugin.name] | |||
|
93 | # only for vcs operations | |||
|
94 | allowed_auth_sources = [VCS_TYPE] | |||
|
95 | ||||
|
96 | return super(RhodeCodeAuthPlugin, self).allows_authentication_from( | |||
|
97 | user, allows_non_existing_user=False, | |||
|
98 | allowed_auth_plugins=allowed_auth_plugins, | |||
|
99 | allowed_auth_sources=allowed_auth_sources) | |||
|
100 | ||||
|
101 | def auth(self, userobj, username, password, settings, **kwargs): | |||
|
102 | if not userobj: | |||
|
103 | log.debug('userobj was:%s skipping' % (userobj, )) | |||
|
104 | return None | |||
|
105 | ||||
|
106 | user_attrs = { | |||
|
107 | "username": userobj.username, | |||
|
108 | "firstname": userobj.firstname, | |||
|
109 | "lastname": userobj.lastname, | |||
|
110 | "groups": [], | |||
|
111 | "email": userobj.email, | |||
|
112 | "admin": userobj.admin, | |||
|
113 | "active": userobj.active, | |||
|
114 | "active_from_extern": userobj.active, | |||
|
115 | "extern_name": userobj.user_id, | |||
|
116 | "extern_type": userobj.extern_type, | |||
|
117 | } | |||
|
118 | ||||
|
119 | log.debug('Authenticating user with args %s', user_attrs) | |||
|
120 | if userobj.active: | |||
|
121 | role = UserApiKeys.ROLE_VCS | |||
|
122 | active_tokens = [x.api_key for x in | |||
|
123 | User.extra_valid_auth_tokens(userobj, role=role)] | |||
|
124 | if userobj.username == username and password in active_tokens: | |||
|
125 | log.info( | |||
|
126 | 'user `%s` successfully authenticated via %s', | |||
|
127 | user_attrs['username'], self.name) | |||
|
128 | return user_attrs | |||
|
129 | log.error( | |||
|
130 | 'user `%s` failed to authenticate via %s, reason: bad or ' | |||
|
131 | 'inactive token.', username, self.name) | |||
|
132 | else: | |||
|
133 | log.warning( | |||
|
134 | 'user `%s` failed to authenticate via %s, reason: account not ' | |||
|
135 | 'active.', username, self.name) | |||
|
136 | return None |
@@ -0,0 +1,42 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 | def generate_jsroutes_content(jsroutes): | |||
|
22 | statements = [] | |||
|
23 | for url_name, url, fields in jsroutes: | |||
|
24 | statements.append( | |||
|
25 | "pyroutes.register('%s', '%s', %s);" % (url_name, url, fields)) | |||
|
26 | return u''' | |||
|
27 | /****************************************************************************** | |||
|
28 | * * | |||
|
29 | * DO NOT CHANGE THIS FILE MANUALLY * | |||
|
30 | * * | |||
|
31 | * * | |||
|
32 | * This file is automatically generated when the app starts up. * | |||
|
33 | * * | |||
|
34 | * To add a route here pass jsroute=True to the route definition in the app * | |||
|
35 | * * | |||
|
36 | ******************************************************************************/ | |||
|
37 | function registerRCRoutes() { | |||
|
38 | // routes registration | |||
|
39 | %s | |||
|
40 | } | |||
|
41 | ''' % '\n '.join(statements) | |||
|
42 |
@@ -0,0 +1,31 b'' | |||||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | ||||
|
19 | from zope.interface import implementer | |||
|
20 | from rhodecode.interfaces import IUserRegistered | |||
|
21 | ||||
|
22 | ||||
|
23 | @implementer(IUserRegistered) | |||
|
24 | class UserRegistered(object): | |||
|
25 | """ | |||
|
26 | An instance of this class is emitted as an :term:`event` whenever a user | |||
|
27 | account is registered. | |||
|
28 | """ | |||
|
29 | def __init__(self, user, session): | |||
|
30 | self.user = user | |||
|
31 | self.session = session |
@@ -0,0 +1,28 b'' | |||||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | ||||
|
19 | from zope.interface import Attribute, Interface | |||
|
20 | ||||
|
21 | ||||
|
22 | class IUserRegistered(Interface): | |||
|
23 | """ | |||
|
24 | An event type that is emitted whenever a new user registers a user | |||
|
25 | account. | |||
|
26 | """ | |||
|
27 | user = Attribute('The user object.') | |||
|
28 | session = Attribute('The session while processing the register form post.') |
@@ -0,0 +1,30 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 | ||||
|
22 | def strip_whitespace(value): | |||
|
23 | """ | |||
|
24 | Removes leading/trailing whitespace, newlines, and tabs from the value. | |||
|
25 | Implements the `colander.interface.Preparer` interface. | |||
|
26 | """ | |||
|
27 | if isinstance(value, basestring): | |||
|
28 | return value.strip(' \t\n\r') | |||
|
29 | else: | |||
|
30 | return value |
@@ -0,0 +1,80 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | import logging | |||
|
4 | ||||
|
5 | from sqlalchemy.orm.attributes import flag_modified | |||
|
6 | ||||
|
7 | from rhodecode.lib.dbmigrate.versions import _reset_base | |||
|
8 | from rhodecode.model import init_model_encryption, meta | |||
|
9 | ||||
|
10 | log = logging.getLogger(__name__) | |||
|
11 | ||||
|
12 | ||||
|
13 | def upgrade(migrate_engine): | |||
|
14 | """ | |||
|
15 | Upgrade operations go here. | |||
|
16 | Don't create your own engine; bind migrate_engine to your metadata | |||
|
17 | """ | |||
|
18 | _reset_base(migrate_engine) | |||
|
19 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |||
|
20 | init_model_encryption(db_3_7_0_0) | |||
|
21 | fixups(db_3_7_0_0, meta.Session) | |||
|
22 | ||||
|
23 | ||||
|
24 | def downgrade(migrate_engine): | |||
|
25 | pass | |||
|
26 | ||||
|
27 | ||||
|
28 | AUTH_PLUGINS_SETTING = "auth_plugins" | |||
|
29 | ||||
|
30 | PLUGIN_RENAME_MAP = { | |||
|
31 | 'egg:rhodecode-enterprise-ce#container': 'egg:rhodecode-enterprise-ce#headers', | |||
|
32 | } | |||
|
33 | ||||
|
34 | SETTINGS_RENAME_MAP = { | |||
|
35 | 'auth_container_cache_ttl': 'auth_headers_cache_ttl', | |||
|
36 | 'auth_container_clean_username': 'auth_headers_clean_username', | |||
|
37 | 'auth_container_enabled': 'auth_headers_enabled', | |||
|
38 | 'auth_container_fallback_header': 'auth_headers_fallback_header', | |||
|
39 | 'auth_container_header': 'auth_headers_header', | |||
|
40 | } | |||
|
41 | ||||
|
42 | ||||
|
43 | def rename_plugins(models, Session): | |||
|
44 | query = models.RhodeCodeSetting.query().filter( | |||
|
45 | models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING) | |||
|
46 | plugin_setting = query.scalar() | |||
|
47 | plugins = plugin_setting.app_settings_value | |||
|
48 | ||||
|
49 | new_plugins = [] | |||
|
50 | ||||
|
51 | for plugin_id in plugins: | |||
|
52 | new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None) | |||
|
53 | if new_plugin_id: | |||
|
54 | new_plugins.append(new_plugin_id) | |||
|
55 | else: | |||
|
56 | new_plugins.append(plugin_id) | |||
|
57 | ||||
|
58 | plugin_setting.app_settings_value = ','.join(new_plugins) | |||
|
59 | ||||
|
60 | log.info("Rename of auth plugin IDs") | |||
|
61 | log.info("Original setting value: %s", plugins) | |||
|
62 | log.info("New setting value: %s", new_plugins) | |||
|
63 | ||||
|
64 | ||||
|
65 | def rename_plugin_settings(models, Session): | |||
|
66 | for old_name, new_name in SETTINGS_RENAME_MAP.items(): | |||
|
67 | query = models.RhodeCodeSetting.query().filter( | |||
|
68 | models.RhodeCodeSetting.app_settings_name == old_name) | |||
|
69 | setting = query.scalar() | |||
|
70 | if setting: | |||
|
71 | setting.app_settings_name = new_name | |||
|
72 | log.info( | |||
|
73 | 'Rename of plugin setting "%s" to "%s"', old_name, new_name) | |||
|
74 | ||||
|
75 | ||||
|
76 | def fixups(models, Session): | |||
|
77 | rename_plugins(models, Session) | |||
|
78 | rename_plugin_settings(models, Session) | |||
|
79 | ||||
|
80 | Session().commit() |
@@ -0,0 +1,57 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | import logging | |||
|
4 | ||||
|
5 | from rhodecode.lib.dbmigrate.versions import _reset_base | |||
|
6 | from rhodecode.model import init_model_encryption, meta | |||
|
7 | ||||
|
8 | log = logging.getLogger(__name__) | |||
|
9 | ||||
|
10 | ||||
|
11 | def upgrade(migrate_engine): | |||
|
12 | """ | |||
|
13 | Upgrade operations go here. | |||
|
14 | Don't create your own engine; bind migrate_engine to your metadata | |||
|
15 | """ | |||
|
16 | _reset_base(migrate_engine) | |||
|
17 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |||
|
18 | init_model_encryption(db_3_7_0_0) | |||
|
19 | fixups(db_3_7_0_0, meta.Session) | |||
|
20 | ||||
|
21 | ||||
|
22 | def downgrade(migrate_engine): | |||
|
23 | pass | |||
|
24 | ||||
|
25 | ||||
|
26 | AUTH_PLUGINS_SETTING = "auth_plugins" | |||
|
27 | ||||
|
28 | PLUGIN_RENAME_MAP = { | |||
|
29 | 'egg:rhodecode-enterprise-ee#token': 'egg:rhodecode-enterprise-ce#token', | |||
|
30 | } | |||
|
31 | ||||
|
32 | ||||
|
33 | def rename_plugins(models, Session): | |||
|
34 | query = models.RhodeCodeSetting.query().filter( | |||
|
35 | models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING) | |||
|
36 | plugin_setting = query.scalar() | |||
|
37 | plugins = plugin_setting.app_settings_value | |||
|
38 | ||||
|
39 | new_plugins = [] | |||
|
40 | ||||
|
41 | for plugin_id in plugins: | |||
|
42 | new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None) | |||
|
43 | if new_plugin_id: | |||
|
44 | new_plugins.append(new_plugin_id) | |||
|
45 | else: | |||
|
46 | new_plugins.append(plugin_id) | |||
|
47 | ||||
|
48 | plugin_setting.app_settings_value = ','.join(new_plugins) | |||
|
49 | ||||
|
50 | log.info("Rename of auth plugin IDs") | |||
|
51 | log.info("Original setting value: %s", plugins) | |||
|
52 | log.info("New setting value: %s", new_plugins) | |||
|
53 | ||||
|
54 | ||||
|
55 | def fixups(models, Session): | |||
|
56 | rename_plugins(models, Session) | |||
|
57 | Session().commit() |
@@ -0,0 +1,55 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | import logging | |||
|
4 | from collections import namedtuple | |||
|
5 | ||||
|
6 | from rhodecode.lib.dbmigrate.versions import _reset_base | |||
|
7 | from rhodecode.model import init_model_encryption, meta | |||
|
8 | ||||
|
9 | log = logging.getLogger(__name__) | |||
|
10 | ||||
|
11 | ||||
|
12 | def upgrade(migrate_engine): | |||
|
13 | """ | |||
|
14 | Upgrade operations go here. | |||
|
15 | Don't create your own engine; bind migrate_engine to your metadata | |||
|
16 | """ | |||
|
17 | _reset_base(migrate_engine) | |||
|
18 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |||
|
19 | init_model_encryption(db_3_7_0_0) | |||
|
20 | fixups(db_3_7_0_0, meta.Session) | |||
|
21 | ||||
|
22 | ||||
|
23 | def downgrade(migrate_engine): | |||
|
24 | pass | |||
|
25 | ||||
|
26 | ||||
|
27 | AUTH_PLUGINS_SETTING = "auth_plugins" | |||
|
28 | ||||
|
29 | EXTERN_TYPE_RENAME_MAP = { | |||
|
30 | 'container': 'headers', | |||
|
31 | } | |||
|
32 | ||||
|
33 | # Only used for logging purposes. | |||
|
34 | RenameExternTypeOperation = namedtuple( | |||
|
35 | 'RenameExternTypeOperation', ['user', 'old', 'new']) | |||
|
36 | ||||
|
37 | ||||
|
38 | def fixups(models, Session): | |||
|
39 | operations = [] | |||
|
40 | ||||
|
41 | # Rename the extern_type attribute | |||
|
42 | query = models.User.query().filter( | |||
|
43 | models.User.extern_type.in_(EXTERN_TYPE_RENAME_MAP.keys())) | |||
|
44 | for user in query: | |||
|
45 | old = user.extern_type | |||
|
46 | new = EXTERN_TYPE_RENAME_MAP[old] | |||
|
47 | user.extern_type = new | |||
|
48 | Session.add(user) | |||
|
49 | operations.append(RenameExternTypeOperation(user, old, new)) | |||
|
50 | ||||
|
51 | log.info("Migration of users 'extern_type' attribute.") | |||
|
52 | for op in operations: | |||
|
53 | log.info("%s", op) | |||
|
54 | ||||
|
55 | Session().commit() |
@@ -0,0 +1,44 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 | ||||
|
22 | from rhodecode.config.routing import ADMIN_PREFIX | |||
|
23 | ||||
|
24 | ||||
|
25 | def includeme(config): | |||
|
26 | ||||
|
27 | config.add_route( | |||
|
28 | name='login', | |||
|
29 | pattern=ADMIN_PREFIX + '/login') | |||
|
30 | config.add_route( | |||
|
31 | name='logout', | |||
|
32 | pattern=ADMIN_PREFIX + '/logout') | |||
|
33 | config.add_route( | |||
|
34 | name='register', | |||
|
35 | pattern=ADMIN_PREFIX + '/register') | |||
|
36 | config.add_route( | |||
|
37 | name='reset_password', | |||
|
38 | pattern=ADMIN_PREFIX + '/password_reset') | |||
|
39 | config.add_route( | |||
|
40 | name='reset_password_confirmation', | |||
|
41 | pattern=ADMIN_PREFIX + '/password_reset_confirmation') | |||
|
42 | ||||
|
43 | # Scan module for configuration decorators. | |||
|
44 | config.scan() |
@@ -0,0 +1,337 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 datetime | |||
|
22 | import formencode | |||
|
23 | import logging | |||
|
24 | import urlparse | |||
|
25 | ||||
|
26 | from pylons import url | |||
|
27 | from pyramid.httpexceptions import HTTPFound | |||
|
28 | from pyramid.view import view_config | |||
|
29 | from recaptcha.client.captcha import submit | |||
|
30 | ||||
|
31 | from rhodecode.authentication.base import authenticate, HTTP_TYPE | |||
|
32 | from rhodecode.events import UserRegistered | |||
|
33 | from rhodecode.lib.auth import ( | |||
|
34 | AuthUser, HasPermissionAnyDecorator, CSRFRequired) | |||
|
35 | from rhodecode.lib.base import get_ip_addr | |||
|
36 | from rhodecode.lib.exceptions import UserCreationError | |||
|
37 | from rhodecode.lib.utils2 import safe_str | |||
|
38 | from rhodecode.model.db import User | |||
|
39 | from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm | |||
|
40 | from rhodecode.model.login_session import LoginSession | |||
|
41 | from rhodecode.model.meta import Session | |||
|
42 | from rhodecode.model.settings import SettingsModel | |||
|
43 | from rhodecode.model.user import UserModel | |||
|
44 | from rhodecode.translation import _ | |||
|
45 | ||||
|
46 | ||||
|
47 | log = logging.getLogger(__name__) | |||
|
48 | ||||
|
49 | ||||
|
50 | def _store_user_in_session(session, username, remember=False): | |||
|
51 | user = User.get_by_username(username, case_insensitive=True) | |||
|
52 | auth_user = AuthUser(user.user_id) | |||
|
53 | auth_user.set_authenticated() | |||
|
54 | cs = auth_user.get_cookie_store() | |||
|
55 | session['rhodecode_user'] = cs | |||
|
56 | user.update_lastlogin() | |||
|
57 | Session().commit() | |||
|
58 | ||||
|
59 | # If they want to be remembered, update the cookie | |||
|
60 | if remember: | |||
|
61 | _year = (datetime.datetime.now() + | |||
|
62 | datetime.timedelta(seconds=60 * 60 * 24 * 365)) | |||
|
63 | session._set_cookie_expires(_year) | |||
|
64 | ||||
|
65 | session.save() | |||
|
66 | ||||
|
67 | log.info('user %s is now authenticated and stored in ' | |||
|
68 | 'session, session attrs %s', username, cs) | |||
|
69 | ||||
|
70 | # dumps session attrs back to cookie | |||
|
71 | session._update_cookie_out() | |||
|
72 | # we set new cookie | |||
|
73 | headers = None | |||
|
74 | if session.request['set_cookie']: | |||
|
75 | # send set-cookie headers back to response to update cookie | |||
|
76 | headers = [('Set-Cookie', session.request['cookie_out'])] | |||
|
77 | return headers | |||
|
78 | ||||
|
79 | ||||
|
80 | def get_came_from(request): | |||
|
81 | came_from = safe_str(request.GET.get('came_from', '')) | |||
|
82 | parsed = urlparse.urlparse(came_from) | |||
|
83 | allowed_schemes = ['http', 'https'] | |||
|
84 | if parsed.scheme and parsed.scheme not in allowed_schemes: | |||
|
85 | log.error('Suspicious URL scheme detected %s for url %s' % | |||
|
86 | (parsed.scheme, parsed)) | |||
|
87 | came_from = url('home') | |||
|
88 | elif parsed.netloc and request.host != parsed.netloc: | |||
|
89 | log.error('Suspicious NETLOC detected %s for url %s server url ' | |||
|
90 | 'is: %s' % (parsed.netloc, parsed, request.host)) | |||
|
91 | came_from = url('home') | |||
|
92 | elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): | |||
|
93 | log.error('Header injection detected `%s` for url %s server url ' % | |||
|
94 | (parsed.path, parsed)) | |||
|
95 | came_from = url('home') | |||
|
96 | ||||
|
97 | return came_from or url('home') | |||
|
98 | ||||
|
99 | ||||
|
100 | class LoginView(object): | |||
|
101 | ||||
|
102 | def __init__(self, context, request): | |||
|
103 | self.request = request | |||
|
104 | self.context = context | |||
|
105 | self.session = request.session | |||
|
106 | self._rhodecode_user = request.user | |||
|
107 | ||||
|
108 | def _get_template_context(self): | |||
|
109 | return { | |||
|
110 | 'came_from': get_came_from(self.request), | |||
|
111 | 'defaults': {}, | |||
|
112 | 'errors': {}, | |||
|
113 | } | |||
|
114 | ||||
|
115 | @view_config( | |||
|
116 | route_name='login', request_method='GET', | |||
|
117 | renderer='rhodecode:templates/login.html') | |||
|
118 | def login(self): | |||
|
119 | came_from = get_came_from(self.request) | |||
|
120 | user = self.request.user | |||
|
121 | ||||
|
122 | # redirect if already logged in | |||
|
123 | if user.is_authenticated and not user.is_default and user.ip_allowed: | |||
|
124 | raise HTTPFound(came_from) | |||
|
125 | ||||
|
126 | # check if we use headers plugin, and try to login using it. | |||
|
127 | try: | |||
|
128 | log.debug('Running PRE-AUTH for headers based authentication') | |||
|
129 | auth_info = authenticate( | |||
|
130 | '', '', self.request.environ, HTTP_TYPE, skip_missing=True) | |||
|
131 | if auth_info: | |||
|
132 | headers = _store_user_in_session( | |||
|
133 | self.session, auth_info.get('username')) | |||
|
134 | raise HTTPFound(came_from, headers=headers) | |||
|
135 | except UserCreationError as e: | |||
|
136 | log.error(e) | |||
|
137 | self.session.flash(e, queue='error') | |||
|
138 | ||||
|
139 | return self._get_template_context() | |||
|
140 | ||||
|
141 | @view_config( | |||
|
142 | route_name='login', request_method='POST', | |||
|
143 | renderer='rhodecode:templates/login.html') | |||
|
144 | def login_post(self): | |||
|
145 | came_from = get_came_from(self.request) | |||
|
146 | session = self.request.session | |||
|
147 | login_form = LoginForm()() | |||
|
148 | ||||
|
149 | try: | |||
|
150 | session.invalidate() | |||
|
151 | form_result = login_form.to_python(self.request.params) | |||
|
152 | # form checks for username/password, now we're authenticated | |||
|
153 | headers = _store_user_in_session( | |||
|
154 | self.session, | |||
|
155 | username=form_result['username'], | |||
|
156 | remember=form_result['remember']) | |||
|
157 | raise HTTPFound(came_from, headers=headers) | |||
|
158 | except formencode.Invalid as errors: | |||
|
159 | defaults = errors.value | |||
|
160 | # remove password from filling in form again | |||
|
161 | del defaults['password'] | |||
|
162 | render_ctx = self._get_template_context() | |||
|
163 | render_ctx.update({ | |||
|
164 | 'errors': errors.error_dict, | |||
|
165 | 'defaults': defaults, | |||
|
166 | }) | |||
|
167 | return render_ctx | |||
|
168 | ||||
|
169 | except UserCreationError as e: | |||
|
170 | # headers auth or other auth functions that create users on | |||
|
171 | # the fly can throw this exception signaling that there's issue | |||
|
172 | # with user creation, explanation should be provided in | |||
|
173 | # Exception itself | |||
|
174 | session.flash(e, queue='error') | |||
|
175 | return self._get_template_context() | |||
|
176 | ||||
|
177 | @CSRFRequired() | |||
|
178 | @view_config(route_name='logout', request_method='POST') | |||
|
179 | def logout(self): | |||
|
180 | LoginSession().destroy_user_session() | |||
|
181 | return HTTPFound(url('home')) | |||
|
182 | ||||
|
183 | @HasPermissionAnyDecorator( | |||
|
184 | 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') | |||
|
185 | @view_config( | |||
|
186 | route_name='register', request_method='GET', | |||
|
187 | renderer='rhodecode:templates/register.html',) | |||
|
188 | def register(self, defaults=None, errors=None): | |||
|
189 | defaults = defaults or {} | |||
|
190 | errors = errors or {} | |||
|
191 | ||||
|
192 | settings = SettingsModel().get_all_settings() | |||
|
193 | captcha_public_key = settings.get('rhodecode_captcha_public_key') | |||
|
194 | captcha_private_key = settings.get('rhodecode_captcha_private_key') | |||
|
195 | captcha_active = bool(captcha_private_key) | |||
|
196 | register_message = settings.get('rhodecode_register_message') or '' | |||
|
197 | auto_active = 'hg.register.auto_activate' in User.get_default_user()\ | |||
|
198 | .AuthUser.permissions['global'] | |||
|
199 | ||||
|
200 | render_ctx = self._get_template_context() | |||
|
201 | render_ctx.update({ | |||
|
202 | 'defaults': defaults, | |||
|
203 | 'errors': errors, | |||
|
204 | 'auto_active': auto_active, | |||
|
205 | 'captcha_active': captcha_active, | |||
|
206 | 'captcha_public_key': captcha_public_key, | |||
|
207 | 'register_message': register_message, | |||
|
208 | }) | |||
|
209 | return render_ctx | |||
|
210 | ||||
|
211 | @view_config( | |||
|
212 | route_name='register', request_method='POST', | |||
|
213 | renderer='rhodecode:templates/register.html') | |||
|
214 | def register_post(self): | |||
|
215 | captcha_private_key = SettingsModel().get_setting_by_name( | |||
|
216 | 'rhodecode_captcha_private_key') | |||
|
217 | captcha_active = bool(captcha_private_key) | |||
|
218 | auto_active = 'hg.register.auto_activate' in User.get_default_user()\ | |||
|
219 | .AuthUser.permissions['global'] | |||
|
220 | ||||
|
221 | register_form = RegisterForm()() | |||
|
222 | try: | |||
|
223 | form_result = register_form.to_python(self.request.params) | |||
|
224 | form_result['active'] = auto_active | |||
|
225 | ||||
|
226 | if captcha_active: | |||
|
227 | response = submit( | |||
|
228 | self.request.params.get('recaptcha_challenge_field'), | |||
|
229 | self.request.params.get('recaptcha_response_field'), | |||
|
230 | private_key=captcha_private_key, | |||
|
231 | remoteip=get_ip_addr(self.request.environ)) | |||
|
232 | if captcha_active and not response.is_valid: | |||
|
233 | _value = form_result | |||
|
234 | _msg = _('bad captcha') | |||
|
235 | error_dict = {'recaptcha_field': _msg} | |||
|
236 | raise formencode.Invalid(_msg, _value, None, | |||
|
237 | error_dict=error_dict) | |||
|
238 | ||||
|
239 | new_user = UserModel().create_registration(form_result) | |||
|
240 | event = UserRegistered(user=new_user, session=self.session) | |||
|
241 | self.request.registry.notify(event) | |||
|
242 | self.session.flash( | |||
|
243 | _('You have successfully registered with RhodeCode'), | |||
|
244 | queue='success') | |||
|
245 | Session().commit() | |||
|
246 | ||||
|
247 | redirect_ro = self.request.route_path('login') | |||
|
248 | raise HTTPFound(redirect_ro) | |||
|
249 | ||||
|
250 | except formencode.Invalid as errors: | |||
|
251 | del errors.value['password'] | |||
|
252 | del errors.value['password_confirmation'] | |||
|
253 | return self.register( | |||
|
254 | defaults=errors.value, errors=errors.error_dict) | |||
|
255 | ||||
|
256 | except UserCreationError as e: | |||
|
257 | # container auth or other auth functions that create users on | |||
|
258 | # the fly can throw this exception signaling that there's issue | |||
|
259 | # with user creation, explanation should be provided in | |||
|
260 | # Exception itself | |||
|
261 | self.session.flash(e, queue='error') | |||
|
262 | return self.register() | |||
|
263 | ||||
|
264 | @view_config( | |||
|
265 | route_name='reset_password', request_method=('GET', 'POST'), | |||
|
266 | renderer='rhodecode:templates/password_reset.html') | |||
|
267 | def password_reset(self): | |||
|
268 | settings = SettingsModel().get_all_settings() | |||
|
269 | captcha_private_key = settings.get('rhodecode_captcha_private_key') | |||
|
270 | captcha_active = bool(captcha_private_key) | |||
|
271 | captcha_public_key = settings.get('rhodecode_captcha_public_key') | |||
|
272 | ||||
|
273 | render_ctx = { | |||
|
274 | 'captcha_active': captcha_active, | |||
|
275 | 'captcha_public_key': captcha_public_key, | |||
|
276 | 'defaults': {}, | |||
|
277 | 'errors': {}, | |||
|
278 | } | |||
|
279 | ||||
|
280 | if self.request.POST: | |||
|
281 | password_reset_form = PasswordResetForm()() | |||
|
282 | try: | |||
|
283 | form_result = password_reset_form.to_python( | |||
|
284 | self.request.params) | |||
|
285 | if captcha_active: | |||
|
286 | response = submit( | |||
|
287 | self.request.params.get('recaptcha_challenge_field'), | |||
|
288 | self.request.params.get('recaptcha_response_field'), | |||
|
289 | private_key=captcha_private_key, | |||
|
290 | remoteip=get_ip_addr(self.request.environ)) | |||
|
291 | if captcha_active and not response.is_valid: | |||
|
292 | _value = form_result | |||
|
293 | _msg = _('bad captcha') | |||
|
294 | error_dict = {'recaptcha_field': _msg} | |||
|
295 | raise formencode.Invalid(_msg, _value, None, | |||
|
296 | error_dict=error_dict) | |||
|
297 | ||||
|
298 | # Generate reset URL and send mail. | |||
|
299 | user_email = form_result['email'] | |||
|
300 | user = User.get_by_email(user_email) | |||
|
301 | password_reset_url = self.request.route_url( | |||
|
302 | 'reset_password_confirmation', | |||
|
303 | _query={'key': user.api_key}) | |||
|
304 | UserModel().reset_password_link( | |||
|
305 | form_result, password_reset_url) | |||
|
306 | ||||
|
307 | # Display success message and redirect. | |||
|
308 | self.session.flash( | |||
|
309 | _('Your password reset link was sent'), | |||
|
310 | queue='success') | |||
|
311 | return HTTPFound(self.request.route_path('login')) | |||
|
312 | ||||
|
313 | except formencode.Invalid as errors: | |||
|
314 | render_ctx.update({ | |||
|
315 | 'defaults': errors.value, | |||
|
316 | 'errors': errors.error_dict, | |||
|
317 | }) | |||
|
318 | ||||
|
319 | return render_ctx | |||
|
320 | ||||
|
321 | @view_config(route_name='reset_password_confirmation', | |||
|
322 | request_method='GET') | |||
|
323 | def password_reset_confirmation(self): | |||
|
324 | if self.request.GET and self.request.GET.get('key'): | |||
|
325 | try: | |||
|
326 | user = User.get_by_auth_token(self.request.GET.get('key')) | |||
|
327 | data = {'email': user.email} | |||
|
328 | UserModel().reset_password(data) | |||
|
329 | self.session.flash( | |||
|
330 | _('Your password reset was successful, ' | |||
|
331 | 'a new password has been sent to your email'), | |||
|
332 | queue='success') | |||
|
333 | except Exception as e: | |||
|
334 | log.error(e) | |||
|
335 | return HTTPFound(self.request.route_path('reset_password')) | |||
|
336 | ||||
|
337 | return HTTPFound(self.request.route_path('login')) |
@@ -0,0 +1,490 b'' | |||||
|
1 | /*!*************************************************** | |||
|
2 | * mark.js v6.1.0 | |||
|
3 | * https://github.com/julmot/mark.js | |||
|
4 | * Copyright (c) 2014–2016, Julian Motz | |||
|
5 | * Released under the MIT license https://git.io/vwTVl | |||
|
6 | *****************************************************/ | |||
|
7 | ||||
|
8 | "use strict"; | |||
|
9 | ||||
|
10 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | |||
|
11 | ||||
|
12 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | |||
|
13 | ||||
|
14 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; | |||
|
15 | ||||
|
16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | |||
|
17 | ||||
|
18 | (function (factory, window, document) { | |||
|
19 | if (typeof define === "function" && define.amd) { | |||
|
20 | define(["jquery"], function (jQuery) { | |||
|
21 | return factory(window, document, jQuery); | |||
|
22 | }); | |||
|
23 | } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") { | |||
|
24 | factory(window, document, require("jquery")); | |||
|
25 | } else { | |||
|
26 | factory(window, document, jQuery); | |||
|
27 | } | |||
|
28 | })(function (window, document, $) { | |||
|
29 | var Mark = function () { | |||
|
30 | function Mark(ctx) { | |||
|
31 | _classCallCheck(this, Mark); | |||
|
32 | ||||
|
33 | this.ctx = ctx; | |||
|
34 | } | |||
|
35 | ||||
|
36 | _createClass(Mark, [{ | |||
|
37 | key: "log", | |||
|
38 | value: function log(msg) { | |||
|
39 | var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1]; | |||
|
40 | ||||
|
41 | var log = this.opt.log; | |||
|
42 | if (!this.opt.debug) { | |||
|
43 | return; | |||
|
44 | } | |||
|
45 | if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") { | |||
|
46 | log[level]("mark.js: " + msg); | |||
|
47 | } | |||
|
48 | } | |||
|
49 | }, { | |||
|
50 | key: "escapeStr", | |||
|
51 | value: function escapeStr(str) { | |||
|
52 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); | |||
|
53 | } | |||
|
54 | }, { | |||
|
55 | key: "createRegExp", | |||
|
56 | value: function createRegExp(str) { | |||
|
57 | str = this.escapeStr(str); | |||
|
58 | if (Object.keys(this.opt.synonyms).length) { | |||
|
59 | str = this.createSynonymsRegExp(str); | |||
|
60 | } | |||
|
61 | if (this.opt.diacritics) { | |||
|
62 | str = this.createDiacriticsRegExp(str); | |||
|
63 | } | |||
|
64 | str = this.createAccuracyRegExp(str); | |||
|
65 | return str; | |||
|
66 | } | |||
|
67 | }, { | |||
|
68 | key: "createSynonymsRegExp", | |||
|
69 | value: function createSynonymsRegExp(str) { | |||
|
70 | var syn = this.opt.synonyms; | |||
|
71 | for (var index in syn) { | |||
|
72 | if (syn.hasOwnProperty(index)) { | |||
|
73 | var value = syn[index], | |||
|
74 | k1 = this.escapeStr(index), | |||
|
75 | k2 = this.escapeStr(value); | |||
|
76 | str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")"); | |||
|
77 | } | |||
|
78 | } | |||
|
79 | return str; | |||
|
80 | } | |||
|
81 | }, { | |||
|
82 | key: "createDiacriticsRegExp", | |||
|
83 | value: function createDiacriticsRegExp(str) { | |||
|
84 | var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"]; | |||
|
85 | var handled = []; | |||
|
86 | str.split("").forEach(function (ch) { | |||
|
87 | dct.every(function (dct) { | |||
|
88 | if (dct.indexOf(ch) !== -1) { | |||
|
89 | if (handled.indexOf(dct) > -1) { | |||
|
90 | return false; | |||
|
91 | } | |||
|
92 | ||||
|
93 | str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]"); | |||
|
94 | handled.push(dct); | |||
|
95 | } | |||
|
96 | return true; | |||
|
97 | }); | |||
|
98 | }); | |||
|
99 | return str; | |||
|
100 | } | |||
|
101 | }, { | |||
|
102 | key: "createAccuracyRegExp", | |||
|
103 | value: function createAccuracyRegExp(str) { | |||
|
104 | switch (this.opt.accuracy) { | |||
|
105 | case "partially": | |||
|
106 | return "()(" + str + ")"; | |||
|
107 | case "complementary": | |||
|
108 | return "()(\\S*" + str + "\\S*)"; | |||
|
109 | case "exactly": | |||
|
110 | return "(^|\\s)(" + str + ")(?=\\s|$)"; | |||
|
111 | } | |||
|
112 | } | |||
|
113 | }, { | |||
|
114 | key: "getSeparatedKeywords", | |||
|
115 | value: function getSeparatedKeywords(sv) { | |||
|
116 | var _this = this; | |||
|
117 | ||||
|
118 | var stack = []; | |||
|
119 | sv.forEach(function (kw) { | |||
|
120 | if (!_this.opt.separateWordSearch) { | |||
|
121 | if (kw.trim()) { | |||
|
122 | stack.push(kw); | |||
|
123 | } | |||
|
124 | } else { | |||
|
125 | kw.split(" ").forEach(function (kwSplitted) { | |||
|
126 | if (kwSplitted.trim()) { | |||
|
127 | stack.push(kwSplitted); | |||
|
128 | } | |||
|
129 | }); | |||
|
130 | } | |||
|
131 | }); | |||
|
132 | return { | |||
|
133 | "keywords": stack, | |||
|
134 | "length": stack.length | |||
|
135 | }; | |||
|
136 | } | |||
|
137 | }, { | |||
|
138 | key: "getElements", | |||
|
139 | value: function getElements() { | |||
|
140 | var ctx = void 0, | |||
|
141 | stack = []; | |||
|
142 | if (typeof this.ctx === "undefined") { | |||
|
143 | ctx = []; | |||
|
144 | } else if (this.ctx instanceof HTMLElement) { | |||
|
145 | ctx = [this.ctx]; | |||
|
146 | } else if (Array.isArray(this.ctx)) { | |||
|
147 | ctx = this.ctx; | |||
|
148 | } else { | |||
|
149 | ctx = Array.prototype.slice.call(this.ctx); | |||
|
150 | } | |||
|
151 | ctx.forEach(function (ctx) { | |||
|
152 | stack.push(ctx); | |||
|
153 | var childs = ctx.querySelectorAll("*"); | |||
|
154 | if (childs.length) { | |||
|
155 | stack = stack.concat(Array.prototype.slice.call(childs)); | |||
|
156 | } | |||
|
157 | }); | |||
|
158 | if (!ctx.length) { | |||
|
159 | this.log("Empty context", "warn"); | |||
|
160 | } | |||
|
161 | return { | |||
|
162 | "elements": stack, | |||
|
163 | "length": stack.length | |||
|
164 | }; | |||
|
165 | } | |||
|
166 | }, { | |||
|
167 | key: "matches", | |||
|
168 | value: function matches(el, selector) { | |||
|
169 | return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); | |||
|
170 | } | |||
|
171 | }, { | |||
|
172 | key: "matchesFilter", | |||
|
173 | value: function matchesFilter(el, exclM) { | |||
|
174 | var _this2 = this; | |||
|
175 | ||||
|
176 | var remain = true; | |||
|
177 | var fltr = this.opt.filter.concat(["script", "style", "title"]); | |||
|
178 | if (!this.opt.iframes) { | |||
|
179 | fltr = fltr.concat(["iframe"]); | |||
|
180 | } | |||
|
181 | if (exclM) { | |||
|
182 | fltr = fltr.concat(["*[data-markjs='true']"]); | |||
|
183 | } | |||
|
184 | fltr.every(function (filter) { | |||
|
185 | if (_this2.matches(el, filter)) { | |||
|
186 | return remain = false; | |||
|
187 | } | |||
|
188 | return true; | |||
|
189 | }); | |||
|
190 | return !remain; | |||
|
191 | } | |||
|
192 | }, { | |||
|
193 | key: "onIframeReady", | |||
|
194 | value: function onIframeReady(ifr, successFn, errorFn) { | |||
|
195 | try { | |||
|
196 | (function () { | |||
|
197 | var ifrWin = ifr.contentWindow, | |||
|
198 | bl = "about:blank", | |||
|
199 | compl = "complete"; | |||
|
200 | var callCallback = function callCallback() { | |||
|
201 | try { | |||
|
202 | if (ifrWin.document === null) { | |||
|
203 | throw new Error("iframe inaccessible"); | |||
|
204 | } | |||
|
205 | successFn(ifrWin.document); | |||
|
206 | } catch (e) { | |||
|
207 | errorFn(); | |||
|
208 | } | |||
|
209 | }; | |||
|
210 | var isBlank = function isBlank() { | |||
|
211 | var src = ifr.getAttribute("src").trim(), | |||
|
212 | href = ifrWin.location.href; | |||
|
213 | return href === bl && src !== bl && src; | |||
|
214 | }; | |||
|
215 | var observeOnload = function observeOnload() { | |||
|
216 | var listener = function listener() { | |||
|
217 | try { | |||
|
218 | if (!isBlank()) { | |||
|
219 | ifr.removeEventListener("load", listener); | |||
|
220 | callCallback(); | |||
|
221 | } | |||
|
222 | } catch (e) { | |||
|
223 | errorFn(); | |||
|
224 | } | |||
|
225 | }; | |||
|
226 | ifr.addEventListener("load", listener); | |||
|
227 | }; | |||
|
228 | if (ifrWin.document.readyState === compl) { | |||
|
229 | if (isBlank()) { | |||
|
230 | observeOnload(); | |||
|
231 | } else { | |||
|
232 | callCallback(); | |||
|
233 | } | |||
|
234 | } else { | |||
|
235 | observeOnload(); | |||
|
236 | } | |||
|
237 | })(); | |||
|
238 | } catch (e) { | |||
|
239 | errorFn(); | |||
|
240 | } | |||
|
241 | } | |||
|
242 | }, { | |||
|
243 | key: "forEachElementInIframe", | |||
|
244 | value: function forEachElementInIframe(ifr, cb) { | |||
|
245 | var _this3 = this; | |||
|
246 | ||||
|
247 | var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2]; | |||
|
248 | ||||
|
249 | var open = 0; | |||
|
250 | var checkEnd = function checkEnd() { | |||
|
251 | if (--open < 1) { | |||
|
252 | end(); | |||
|
253 | } | |||
|
254 | }; | |||
|
255 | this.onIframeReady(ifr, function (con) { | |||
|
256 | var stack = Array.prototype.slice.call(con.querySelectorAll("*")); | |||
|
257 | if ((open = stack.length) === 0) { | |||
|
258 | checkEnd(); | |||
|
259 | } | |||
|
260 | stack.forEach(function (el) { | |||
|
261 | if (el.tagName.toLowerCase() === "iframe") { | |||
|
262 | (function () { | |||
|
263 | var j = 0; | |||
|
264 | _this3.forEachElementInIframe(el, function (iel, len) { | |||
|
265 | cb(iel, len); | |||
|
266 | if (len - 1 === j) { | |||
|
267 | checkEnd(); | |||
|
268 | } | |||
|
269 | j++; | |||
|
270 | }, checkEnd); | |||
|
271 | })(); | |||
|
272 | } else { | |||
|
273 | cb(el, stack.length); | |||
|
274 | checkEnd(); | |||
|
275 | } | |||
|
276 | }); | |||
|
277 | }, function () { | |||
|
278 | var src = ifr.getAttribute("src"); | |||
|
279 | _this3.log("iframe '" + src + "' could not be accessed", "warn"); | |||
|
280 | checkEnd(); | |||
|
281 | }); | |||
|
282 | } | |||
|
283 | }, { | |||
|
284 | key: "forEachElement", | |||
|
285 | value: function forEachElement(cb) { | |||
|
286 | var _this4 = this; | |||
|
287 | ||||
|
288 | var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; | |||
|
289 | var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; | |||
|
290 | ||||
|
291 | var _getElements = this.getElements(); | |||
|
292 | ||||
|
293 | var stack = _getElements.elements; | |||
|
294 | var open = _getElements.length; | |||
|
295 | ||||
|
296 | var checkEnd = function checkEnd() { | |||
|
297 | if (--open === 0) { | |||
|
298 | end(); | |||
|
299 | } | |||
|
300 | }; | |||
|
301 | checkEnd(++open); | |||
|
302 | stack.forEach(function (el) { | |||
|
303 | if (!_this4.matchesFilter(el, exclM)) { | |||
|
304 | if (el.tagName.toLowerCase() === "iframe") { | |||
|
305 | _this4.forEachElementInIframe(el, function (iel) { | |||
|
306 | if (!_this4.matchesFilter(iel, exclM)) { | |||
|
307 | cb(iel); | |||
|
308 | } | |||
|
309 | }, checkEnd); | |||
|
310 | return; | |||
|
311 | } else { | |||
|
312 | cb(el); | |||
|
313 | } | |||
|
314 | } | |||
|
315 | checkEnd(); | |||
|
316 | }); | |||
|
317 | } | |||
|
318 | }, { | |||
|
319 | key: "forEachNode", | |||
|
320 | value: function forEachNode(cb) { | |||
|
321 | var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; | |||
|
322 | ||||
|
323 | this.forEachElement(function (n) { | |||
|
324 | for (n = n.firstChild; n; n = n.nextSibling) { | |||
|
325 | if (n.nodeType === 3 && n.textContent.trim()) { | |||
|
326 | cb(n); | |||
|
327 | } | |||
|
328 | } | |||
|
329 | }, end); | |||
|
330 | } | |||
|
331 | }, { | |||
|
332 | key: "wrapMatches", | |||
|
333 | value: function wrapMatches(node, regex, custom, cb) { | |||
|
334 | var hEl = !this.opt.element ? "mark" : this.opt.element, | |||
|
335 | index = custom ? 0 : 2; | |||
|
336 | var match = void 0; | |||
|
337 | while ((match = regex.exec(node.textContent)) !== null) { | |||
|
338 | var pos = match.index; | |||
|
339 | if (!custom) { | |||
|
340 | pos += match[index - 1].length; | |||
|
341 | } | |||
|
342 | var startNode = node.splitText(pos); | |||
|
343 | ||||
|
344 | node = startNode.splitText(match[index].length); | |||
|
345 | if (startNode.parentNode !== null) { | |||
|
346 | var repl = document.createElement(hEl); | |||
|
347 | repl.setAttribute("data-markjs", "true"); | |||
|
348 | if (this.opt.className) { | |||
|
349 | repl.setAttribute("class", this.opt.className); | |||
|
350 | } | |||
|
351 | repl.textContent = match[index]; | |||
|
352 | startNode.parentNode.replaceChild(repl, startNode); | |||
|
353 | cb(repl); | |||
|
354 | } | |||
|
355 | regex.lastIndex = 0; | |||
|
356 | } | |||
|
357 | } | |||
|
358 | }, { | |||
|
359 | key: "unwrapMatches", | |||
|
360 | value: function unwrapMatches(node) { | |||
|
361 | var parent = node.parentNode; | |||
|
362 | var docFrag = document.createDocumentFragment(); | |||
|
363 | while (node.firstChild) { | |||
|
364 | docFrag.appendChild(node.removeChild(node.firstChild)); | |||
|
365 | } | |||
|
366 | parent.replaceChild(docFrag, node); | |||
|
367 | parent.normalize(); | |||
|
368 | } | |||
|
369 | }, { | |||
|
370 | key: "markRegExp", | |||
|
371 | value: function markRegExp(regexp, opt) { | |||
|
372 | var _this5 = this; | |||
|
373 | ||||
|
374 | this.opt = opt; | |||
|
375 | this.log("Searching with expression \"" + regexp + "\""); | |||
|
376 | var found = false; | |||
|
377 | var eachCb = function eachCb(element) { | |||
|
378 | found = true; | |||
|
379 | _this5.opt.each(element); | |||
|
380 | }; | |||
|
381 | this.forEachNode(function (node) { | |||
|
382 | _this5.wrapMatches(node, regexp, true, eachCb); | |||
|
383 | }, function () { | |||
|
384 | if (!found) { | |||
|
385 | _this5.opt.noMatch(regexp); | |||
|
386 | } | |||
|
387 | _this5.opt.complete(); | |||
|
388 | _this5.opt.done(); | |||
|
389 | }); | |||
|
390 | } | |||
|
391 | }, { | |||
|
392 | key: "mark", | |||
|
393 | value: function mark(sv, opt) { | |||
|
394 | var _this6 = this; | |||
|
395 | ||||
|
396 | this.opt = opt; | |||
|
397 | sv = typeof sv === "string" ? [sv] : sv; | |||
|
398 | ||||
|
399 | var _getSeparatedKeywords = this.getSeparatedKeywords(sv); | |||
|
400 | ||||
|
401 | var kwArr = _getSeparatedKeywords.keywords; | |||
|
402 | var kwArrLen = _getSeparatedKeywords.length; | |||
|
403 | ||||
|
404 | if (kwArrLen === 0) { | |||
|
405 | this.opt.complete(); | |||
|
406 | this.opt.done(); | |||
|
407 | } | |||
|
408 | kwArr.forEach(function (kw) { | |||
|
409 | var regex = new RegExp(_this6.createRegExp(kw), "gmi"), | |||
|
410 | found = false; | |||
|
411 | var eachCb = function eachCb(element) { | |||
|
412 | found = true; | |||
|
413 | _this6.opt.each(element); | |||
|
414 | }; | |||
|
415 | _this6.log("Searching with expression \"" + regex + "\""); | |||
|
416 | _this6.forEachNode(function (node) { | |||
|
417 | _this6.wrapMatches(node, regex, false, eachCb); | |||
|
418 | }, function () { | |||
|
419 | if (!found) { | |||
|
420 | _this6.opt.noMatch(kw); | |||
|
421 | } | |||
|
422 | if (kwArr[kwArrLen - 1] === kw) { | |||
|
423 | _this6.opt.complete(); | |||
|
424 | _this6.opt.done(); | |||
|
425 | } | |||
|
426 | }); | |||
|
427 | }); | |||
|
428 | } | |||
|
429 | }, { | |||
|
430 | key: "unmark", | |||
|
431 | value: function unmark(opt) { | |||
|
432 | var _this7 = this; | |||
|
433 | ||||
|
434 | this.opt = opt; | |||
|
435 | var sel = this.opt.element ? this.opt.element : "*"; | |||
|
436 | sel += "[data-markjs]"; | |||
|
437 | if (this.opt.className) { | |||
|
438 | sel += "." + this.opt.className; | |||
|
439 | } | |||
|
440 | this.log("Removal selector \"" + sel + "\""); | |||
|
441 | this.forEachElement(function (el) { | |||
|
442 | if (_this7.matches(el, sel)) { | |||
|
443 | _this7.unwrapMatches(el); | |||
|
444 | } | |||
|
445 | }, function () { | |||
|
446 | _this7.opt.complete(); | |||
|
447 | _this7.opt.done(); | |||
|
448 | }, false); | |||
|
449 | } | |||
|
450 | }, { | |||
|
451 | key: "opt", | |||
|
452 | set: function set(val) { | |||
|
453 | this._opt = _extends({}, { | |||
|
454 | "element": "", | |||
|
455 | "className": "", | |||
|
456 | "filter": [], | |||
|
457 | "iframes": false, | |||
|
458 | "separateWordSearch": true, | |||
|
459 | "diacritics": true, | |||
|
460 | "synonyms": {}, | |||
|
461 | "accuracy": "partially", | |||
|
462 | "each": function each() {}, | |||
|
463 | "noMatch": function noMatch() {}, | |||
|
464 | "done": function done() {}, | |||
|
465 | "complete": function complete() {}, | |||
|
466 | "debug": false, | |||
|
467 | "log": window.console | |||
|
468 | }, val); | |||
|
469 | }, | |||
|
470 | get: function get() { | |||
|
471 | return this._opt; | |||
|
472 | } | |||
|
473 | }]); | |||
|
474 | ||||
|
475 | return Mark; | |||
|
476 | }(); | |||
|
477 | ||||
|
478 | $.fn.mark = function (sv, opt) { | |||
|
479 | new Mark(this).mark(sv, opt); | |||
|
480 | return this; | |||
|
481 | }; | |||
|
482 | $.fn.markRegExp = function (regexp, opt) { | |||
|
483 | new Mark(this).markRegExp(regexp, opt); | |||
|
484 | return this; | |||
|
485 | }; | |||
|
486 | $.fn.unmark = function (opt) { | |||
|
487 | new Mark(this).unmark(opt); | |||
|
488 | return this; | |||
|
489 | }; | |||
|
490 | }, window, document); |
@@ -0,0 +1,26 b'' | |||||
|
1 | // # Copyright (C) 2010-2016 RhodeCode GmbH | |||
|
2 | // # | |||
|
3 | // # This program is free software: you can redistribute it and/or modify | |||
|
4 | // # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | // # (only), as published by the Free Software Foundation. | |||
|
6 | // # | |||
|
7 | // # This program is distributed in the hope that it will be useful, | |||
|
8 | // # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | // # GNU General Public License for more details. | |||
|
11 | // # | |||
|
12 | // # You should have received a copy of the GNU Affero General Public License | |||
|
13 | // # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | // # | |||
|
15 | // # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | // # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | // # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | ||||
|
19 | ||||
|
20 | /* | |||
|
21 | * Deferred functions that must run before any rhodecode javascript go here | |||
|
22 | */ | |||
|
23 | ||||
|
24 | registerRCRoutes(); | |||
|
25 | ||||
|
26 | // TODO: move i18n here |
@@ -0,0 +1,22 b'' | |||||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | ||||
|
19 | from pyramid.i18n import TranslationStringFactory | |||
|
20 | ||||
|
21 | # Create a translation string factory for the 'rhodecode' domain. | |||
|
22 | _ = TranslationStringFactory('rhodecode') |
@@ -1,5 +1,5 b'' | |||||
1 | [bumpversion] |
|
1 | [bumpversion] | |
2 |
current_version = 4. |
|
2 | current_version = 4.1.0 | |
3 | message = release: Bump version {current_version} to {new_version} |
|
3 | message = release: Bump version {current_version} to {new_version} | |
4 |
|
4 | |||
5 | [bumpversion:file:rhodecode/VERSION] |
|
5 | [bumpversion:file:rhodecode/VERSION] |
@@ -27,6 +27,7 b' module.exports = function(grunt) {' | |||||
27 | '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js', |
|
27 | '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js', | |
28 | '<%= dirs.js.src %>/plugins/jquery.autocomplete.js', |
|
28 | '<%= dirs.js.src %>/plugins/jquery.autocomplete.js', | |
29 | '<%= dirs.js.src %>/plugins/jquery.debounce.js', |
|
29 | '<%= dirs.js.src %>/plugins/jquery.debounce.js', | |
|
30 | '<%= dirs.js.src %>/plugins/jquery.mark.js', | |||
30 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', |
|
31 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', | |
31 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', |
|
32 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', | |
32 |
|
33 | |||
@@ -59,7 +60,7 b' module.exports = function(grunt) {' | |||||
59 | '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js', |
|
60 | '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js', | |
60 |
|
61 | |||
61 | // Rhodecode components |
|
62 | // Rhodecode components | |
62 |
'<%= dirs.js.src %>/rhodecode/ |
|
63 | '<%= dirs.js.src %>/rhodecode/init.js', | |
63 | '<%= dirs.js.src %>/rhodecode/codemirror.js', |
|
64 | '<%= dirs.js.src %>/rhodecode/codemirror.js', | |
64 | '<%= dirs.js.src %>/rhodecode/comments.js', |
|
65 | '<%= dirs.js.src %>/rhodecode/comments.js', | |
65 | '<%= dirs.js.src %>/rhodecode/constants.js', |
|
66 | '<%= dirs.js.src %>/rhodecode/constants.js', |
@@ -34,9 +34,10 b' pdebug = false' | |||||
34 | host = 127.0.0.1 |
|
34 | host = 127.0.0.1 | |
35 | port = 5000 |
|
35 | port = 5000 | |
36 |
|
36 | |||
37 | ########################## |
|
37 | ################################## | |
38 | ## WAITRESS WSGI SERVER ## |
|
38 | ## WAITRESS WSGI SERVER ## | |
39 | ########################## |
|
39 | ## Recommended for Development ## | |
|
40 | ################################## | |||
40 | use = egg:waitress#main |
|
41 | use = egg:waitress#main | |
41 | ## number of worker threads |
|
42 | ## number of worker threads | |
42 | threads = 5 |
|
43 | threads = 5 | |
@@ -56,7 +57,7 b' asyncore_use_poll = true' | |||||
56 | ## when this option is set to more than one worker, recommended |
|
57 | ## when this option is set to more than one worker, recommended | |
57 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers |
|
58 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers | |
58 | ## The `instance_id = *` must be set in the [app:main] section below |
|
59 | ## The `instance_id = *` must be set in the [app:main] section below | |
59 |
#workers = |
|
60 | #workers = 2 | |
60 | ## number of threads for each of the worker, must be set to 1 for gevent |
|
61 | ## number of threads for each of the worker, must be set to 1 for gevent | |
61 | ## generally recommened to be at 1 |
|
62 | ## generally recommened to be at 1 | |
62 | #threads = 1 |
|
63 | #threads = 1 | |
@@ -71,7 +72,7 b' asyncore_use_poll = true' | |||||
71 | ## restarted, could prevent memory leaks |
|
72 | ## restarted, could prevent memory leaks | |
72 | #max_requests = 1000 |
|
73 | #max_requests = 1000 | |
73 | #max_requests_jitter = 30 |
|
74 | #max_requests_jitter = 30 | |
74 |
## am |
|
75 | ## amount of time a worker can spend with handling a request before it | |
75 | ## gets killed and restarted. Set to 6hrs |
|
76 | ## gets killed and restarted. Set to 6hrs | |
76 | #timeout = 21600 |
|
77 | #timeout = 21600 | |
77 |
|
78 | |||
@@ -199,6 +200,21 b' default_encoding = UTF-8' | |||||
199 | ## all running rhodecode instances. Leave empty if you don't use it |
|
200 | ## all running rhodecode instances. Leave empty if you don't use it | |
200 | instance_id = |
|
201 | instance_id = | |
201 |
|
202 | |||
|
203 | ## Fallback authentication plugin. Set this to a plugin ID to force the usage | |||
|
204 | ## of an authentication plugin also if it is disabled by it's settings. | |||
|
205 | ## This could be useful if you are unable to log in to the system due to broken | |||
|
206 | ## authentication settings. Then you can enable e.g. the internal rhodecode auth | |||
|
207 | ## module to log in again and fix the settings. | |||
|
208 | ## | |||
|
209 | ## Available builtin plugin IDs (hash is part of the ID): | |||
|
210 | ## egg:rhodecode-enterprise-ce#rhodecode | |||
|
211 | ## egg:rhodecode-enterprise-ce#pam | |||
|
212 | ## egg:rhodecode-enterprise-ce#ldap | |||
|
213 | ## egg:rhodecode-enterprise-ce#jasig_cas | |||
|
214 | ## egg:rhodecode-enterprise-ce#headers | |||
|
215 | ## egg:rhodecode-enterprise-ce#crowd | |||
|
216 | #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode | |||
|
217 | ||||
202 | ## alternative return HTTP header for failed authentication. Default HTTP |
|
218 | ## alternative return HTTP header for failed authentication. Default HTTP | |
203 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with |
|
219 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with | |
204 | ## handling that causing a series of failed authentication calls. |
|
220 | ## handling that causing a series of failed authentication calls. | |
@@ -356,12 +372,17 b' beaker.session.auto = false' | |||||
356 | ################################### |
|
372 | ################################### | |
357 | ## SEARCH INDEXING CONFIGURATION ## |
|
373 | ## SEARCH INDEXING CONFIGURATION ## | |
358 | ################################### |
|
374 | ################################### | |
|
375 | ## Full text search indexer is available in rhodecode-tools under | |||
|
376 | ## `rhodecode-tools index` command | |||
359 |
|
377 | |||
|
378 | # WHOOSH Backend, doesn't require additional services to run | |||
|
379 | # it works good with few dozen repos | |||
360 | search.module = rhodecode.lib.index.whoosh |
|
380 | search.module = rhodecode.lib.index.whoosh | |
361 | search.location = %(here)s/data/index |
|
381 | search.location = %(here)s/data/index | |
362 |
|
382 | |||
|
383 | ||||
363 | ################################### |
|
384 | ################################### | |
364 | ## ERROR AND LOG HANDLING SYSTEM ## |
|
385 | ## APPENLIGHT CONFIG ## | |
365 | ################################### |
|
386 | ################################### | |
366 |
|
387 | |||
367 | ## Appenlight is tailored to work with RhodeCode, see |
|
388 | ## Appenlight is tailored to work with RhodeCode, see | |
@@ -372,7 +393,7 b' appenlight = false' | |||||
372 |
|
393 | |||
373 | appenlight.server_url = https://api.appenlight.com |
|
394 | appenlight.server_url = https://api.appenlight.com | |
374 | appenlight.api_key = YOUR_API_KEY |
|
395 | appenlight.api_key = YOUR_API_KEY | |
375 |
|
|
396 | #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 | |
376 |
|
397 | |||
377 | # used for JS client |
|
398 | # used for JS client | |
378 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY |
|
399 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY | |
@@ -462,16 +483,26 b' sqlalchemy.db1.convert_unicode = true' | |||||
462 | ################## |
|
483 | ################## | |
463 | vcs.server.enable = true |
|
484 | vcs.server.enable = true | |
464 | vcs.server = localhost:9900 |
|
485 | vcs.server = localhost:9900 | |
465 | # Available protocols: pyro4, http |
|
|||
466 | vcs.server.protocol = pyro4 |
|
|||
467 |
|
486 | |||
468 | # available impl: |
|
487 | ## Web server connectivity protocol, responsible for web based VCS operatations | |
469 | # vcsserver.scm_app (EE only, for testing), |
|
488 | ## Available protocols are: | |
470 | # rhodecode.lib.middleware.utils.scm_app_http |
|
489 | ## `pyro4` - using pyro4 server | |
471 | # pyro4 |
|
490 | ## `http` - using http-rpc backend | |
|
491 | #vcs.server.protocol = http | |||
|
492 | ||||
|
493 | ## Push/Pull operations protocol, available options are: | |||
|
494 | ## `pyro4` - using pyro4 server | |||
|
495 | ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended | |||
|
496 | ## `vcsserver.scm_app` - internal app (EE only) | |||
472 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http |
|
497 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http | |
473 |
|
498 | |||
|
499 | ## Push/Pull operations hooks protocol, available options are: | |||
|
500 | ## `pyro4` - using pyro4 server | |||
|
501 | ## `http` - using http-rpc backend | |||
|
502 | #vcs.hooks.protocol = http | |||
|
503 | ||||
474 | vcs.server.log_level = debug |
|
504 | vcs.server.log_level = debug | |
|
505 | ## Start VCSServer with this instance as a subprocess, usefull for development | |||
475 | vcs.start_server = true |
|
506 | vcs.start_server = true | |
476 | vcs.backends = hg, git, svn |
|
507 | vcs.backends = hg, git, svn | |
477 | vcs.connection_timeout = 3600 |
|
508 | vcs.connection_timeout = 3600 |
@@ -34,46 +34,47 b' pdebug = false' | |||||
34 | host = 127.0.0.1 |
|
34 | host = 127.0.0.1 | |
35 | port = 5000 |
|
35 | port = 5000 | |
36 |
|
36 | |||
37 | ########################## |
|
37 | ################################## | |
38 | ## WAITRESS WSGI SERVER ## |
|
38 | ## WAITRESS WSGI SERVER ## | |
39 | ########################## |
|
39 | ## Recommended for Development ## | |
40 | use = egg:waitress#main |
|
40 | ################################## | |
|
41 | #use = egg:waitress#main | |||
41 | ## number of worker threads |
|
42 | ## number of worker threads | |
42 | threads = 5 |
|
43 | #threads = 5 | |
43 | ## MAX BODY SIZE 100GB |
|
44 | ## MAX BODY SIZE 100GB | |
44 | max_request_body_size = 107374182400 |
|
45 | #max_request_body_size = 107374182400 | |
45 | ## Use poll instead of select, fixes file descriptors limits problems. |
|
46 | ## Use poll instead of select, fixes file descriptors limits problems. | |
46 | ## May not work on old windows systems. |
|
47 | ## May not work on old windows systems. | |
47 | asyncore_use_poll = true |
|
48 | #asyncore_use_poll = true | |
48 |
|
49 | |||
49 |
|
50 | |||
50 | ########################## |
|
51 | ########################## | |
51 | ## GUNICORN WSGI SERVER ## |
|
52 | ## GUNICORN WSGI SERVER ## | |
52 | ########################## |
|
53 | ########################## | |
53 | ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini> |
|
54 | ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini> | |
54 |
|
|
55 | use = egg:gunicorn#main | |
55 | ## Sets the number of process workers. You must set `instance_id = *` |
|
56 | ## Sets the number of process workers. You must set `instance_id = *` | |
56 | ## when this option is set to more than one worker, recommended |
|
57 | ## when this option is set to more than one worker, recommended | |
57 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers |
|
58 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers | |
58 | ## The `instance_id = *` must be set in the [app:main] section below |
|
59 | ## The `instance_id = *` must be set in the [app:main] section below | |
59 |
|
|
60 | workers = 2 | |
60 | ## number of threads for each of the worker, must be set to 1 for gevent |
|
61 | ## number of threads for each of the worker, must be set to 1 for gevent | |
61 | ## generally recommened to be at 1 |
|
62 | ## generally recommened to be at 1 | |
62 | #threads = 1 |
|
63 | #threads = 1 | |
63 | ## process name |
|
64 | ## process name | |
64 |
|
|
65 | proc_name = rhodecode | |
65 | ## type of worker class, one of sync, gevent |
|
66 | ## type of worker class, one of sync, gevent | |
66 | ## recommended for bigger setup is using of of other than sync one |
|
67 | ## recommended for bigger setup is using of of other than sync one | |
67 |
|
|
68 | worker_class = sync | |
68 | ## The maximum number of simultaneous clients. Valid only for Gevent |
|
69 | ## The maximum number of simultaneous clients. Valid only for Gevent | |
69 | #worker_connections = 10 |
|
70 | #worker_connections = 10 | |
70 | ## max number of requests that worker will handle before being gracefully |
|
71 | ## max number of requests that worker will handle before being gracefully | |
71 | ## restarted, could prevent memory leaks |
|
72 | ## restarted, could prevent memory leaks | |
72 |
|
|
73 | max_requests = 1000 | |
73 |
|
|
74 | max_requests_jitter = 30 | |
74 |
## am |
|
75 | ## amount of time a worker can spend with handling a request before it | |
75 | ## gets killed and restarted. Set to 6hrs |
|
76 | ## gets killed and restarted. Set to 6hrs | |
76 |
|
|
77 | timeout = 21600 | |
77 |
|
78 | |||
78 |
|
79 | |||
79 | ## prefix middleware for RhodeCode, disables force_https flag. |
|
80 | ## prefix middleware for RhodeCode, disables force_https flag. | |
@@ -173,6 +174,21 b' default_encoding = UTF-8' | |||||
173 | ## all running rhodecode instances. Leave empty if you don't use it |
|
174 | ## all running rhodecode instances. Leave empty if you don't use it | |
174 | instance_id = |
|
175 | instance_id = | |
175 |
|
176 | |||
|
177 | ## Fallback authentication plugin. Set this to a plugin ID to force the usage | |||
|
178 | ## of an authentication plugin also if it is disabled by it's settings. | |||
|
179 | ## This could be useful if you are unable to log in to the system due to broken | |||
|
180 | ## authentication settings. Then you can enable e.g. the internal rhodecode auth | |||
|
181 | ## module to log in again and fix the settings. | |||
|
182 | ## | |||
|
183 | ## Available builtin plugin IDs (hash is part of the ID): | |||
|
184 | ## egg:rhodecode-enterprise-ce#rhodecode | |||
|
185 | ## egg:rhodecode-enterprise-ce#pam | |||
|
186 | ## egg:rhodecode-enterprise-ce#ldap | |||
|
187 | ## egg:rhodecode-enterprise-ce#jasig_cas | |||
|
188 | ## egg:rhodecode-enterprise-ce#headers | |||
|
189 | ## egg:rhodecode-enterprise-ce#crowd | |||
|
190 | #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode | |||
|
191 | ||||
176 | ## alternative return HTTP header for failed authentication. Default HTTP |
|
192 | ## alternative return HTTP header for failed authentication. Default HTTP | |
177 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with |
|
193 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with | |
178 | ## handling that causing a series of failed authentication calls. |
|
194 | ## handling that causing a series of failed authentication calls. | |
@@ -304,7 +320,7 b' beaker.session.data_dir = %(here)s/data/' | |||||
304 |
|
320 | |||
305 | beaker.session.key = rhodecode |
|
321 | beaker.session.key = rhodecode | |
306 | beaker.session.secret = production-rc-uytcxaz |
|
322 | beaker.session.secret = production-rc-uytcxaz | |
307 |
|
|
323 | beaker.session.lock_dir = %(here)s/data/sessions/lock | |
308 |
|
324 | |||
309 | ## Secure encrypted cookie. Requires AES and AES python libraries |
|
325 | ## Secure encrypted cookie. Requires AES and AES python libraries | |
310 | ## you must disable beaker.session.secret to use this |
|
326 | ## you must disable beaker.session.secret to use this | |
@@ -330,12 +346,17 b' beaker.session.auto = false' | |||||
330 | ################################### |
|
346 | ################################### | |
331 | ## SEARCH INDEXING CONFIGURATION ## |
|
347 | ## SEARCH INDEXING CONFIGURATION ## | |
332 | ################################### |
|
348 | ################################### | |
|
349 | ## Full text search indexer is available in rhodecode-tools under | |||
|
350 | ## `rhodecode-tools index` command | |||
333 |
|
351 | |||
|
352 | # WHOOSH Backend, doesn't require additional services to run | |||
|
353 | # it works good with few dozen repos | |||
334 | search.module = rhodecode.lib.index.whoosh |
|
354 | search.module = rhodecode.lib.index.whoosh | |
335 | search.location = %(here)s/data/index |
|
355 | search.location = %(here)s/data/index | |
336 |
|
356 | |||
|
357 | ||||
337 | ################################### |
|
358 | ################################### | |
338 | ## ERROR AND LOG HANDLING SYSTEM ## |
|
359 | ## APPENLIGHT CONFIG ## | |
339 | ################################### |
|
360 | ################################### | |
340 |
|
361 | |||
341 | ## Appenlight is tailored to work with RhodeCode, see |
|
362 | ## Appenlight is tailored to work with RhodeCode, see | |
@@ -346,7 +367,7 b' appenlight = false' | |||||
346 |
|
367 | |||
347 | appenlight.server_url = https://api.appenlight.com |
|
368 | appenlight.server_url = https://api.appenlight.com | |
348 | appenlight.api_key = YOUR_API_KEY |
|
369 | appenlight.api_key = YOUR_API_KEY | |
349 |
|
|
370 | #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 | |
350 |
|
371 | |||
351 | # used for JS client |
|
372 | # used for JS client | |
352 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY |
|
373 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY | |
@@ -401,11 +422,6 b' appenlight.log_namespace_blacklist =' | |||||
401 | set debug = false |
|
422 | set debug = false | |
402 |
|
423 | |||
403 |
|
424 | |||
404 | ############## |
|
|||
405 | ## STYLING ## |
|
|||
406 | ############## |
|
|||
407 | debug_style = false |
|
|||
408 |
|
||||
409 | ######################################################### |
|
425 | ######################################################### | |
410 | ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ### |
|
426 | ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ### | |
411 | ######################################################### |
|
427 | ######################################################### | |
@@ -436,16 +452,26 b' sqlalchemy.db1.convert_unicode = true' | |||||
436 | ################## |
|
452 | ################## | |
437 | vcs.server.enable = true |
|
453 | vcs.server.enable = true | |
438 | vcs.server = localhost:9900 |
|
454 | vcs.server = localhost:9900 | |
439 | # Available protocols: pyro4, http |
|
|||
440 | vcs.server.protocol = pyro4 |
|
|||
441 |
|
455 | |||
442 | # available impl: |
|
456 | ## Web server connectivity protocol, responsible for web based VCS operatations | |
443 | # vcsserver.scm_app (EE only, for testing), |
|
457 | ## Available protocols are: | |
444 | # rhodecode.lib.middleware.utils.scm_app_http |
|
458 | ## `pyro4` - using pyro4 server | |
445 | # pyro4 |
|
459 | ## `http` - using http-rpc backend | |
|
460 | #vcs.server.protocol = http | |||
|
461 | ||||
|
462 | ## Push/Pull operations protocol, available options are: | |||
|
463 | ## `pyro4` - using pyro4 server | |||
|
464 | ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended | |||
|
465 | ## `vcsserver.scm_app` - internal app (EE only) | |||
446 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http |
|
466 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http | |
447 |
|
467 | |||
|
468 | ## Push/Pull operations hooks protocol, available options are: | |||
|
469 | ## `pyro4` - using pyro4 server | |||
|
470 | ## `http` - using http-rpc backend | |||
|
471 | #vcs.hooks.protocol = http | |||
|
472 | ||||
448 | vcs.server.log_level = info |
|
473 | vcs.server.log_level = info | |
|
474 | ## Start VCSServer with this instance as a subprocess, usefull for development | |||
449 | vcs.start_server = false |
|
475 | vcs.start_server = false | |
450 | vcs.backends = hg, git, svn |
|
476 | vcs.backends = hg, git, svn | |
451 | vcs.connection_timeout = 3600 |
|
477 | vcs.connection_timeout = 3600 |
@@ -85,7 +85,7 b' let' | |||||
85 | pythonLocalOverrides = self: super: { |
|
85 | pythonLocalOverrides = self: super: { | |
86 | rhodecode-enterprise-ce = |
|
86 | rhodecode-enterprise-ce = | |
87 | let |
|
87 | let | |
88 |
version = |
|
88 | version = builtins.readFile ./rhodecode/VERSION; | |
89 | linkNodeModules = '' |
|
89 | linkNodeModules = '' | |
90 | echo "Link node packages" |
|
90 | echo "Link node packages" | |
91 | # TODO: check if this adds stuff as a dependency, closure size |
|
91 | # TODO: check if this adds stuff as a dependency, closure size | |
@@ -119,7 +119,9 b' let' | |||||
119 | # TODO: johbo: Make a nicer way to expose the parts. Maybe |
|
119 | # TODO: johbo: Make a nicer way to expose the parts. Maybe | |
120 | # pkgs/default.nix? |
|
120 | # pkgs/default.nix? | |
121 | passthru = { |
|
121 | passthru = { | |
122 | inherit myPythonPackagesUnfix; |
|
122 | inherit | |
|
123 | pythonLocalOverrides | |||
|
124 | myPythonPackagesUnfix; | |||
123 | pythonPackages = self; |
|
125 | pythonPackages = self; | |
124 | }; |
|
126 | }; | |
125 |
|
127 | |||
@@ -160,6 +162,7 b' let' | |||||
160 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ |
|
162 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ | |
161 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ |
|
163 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ | |
162 | ln -s ${self.PasteScript}/bin/paster $out/bin/ |
|
164 | ln -s ${self.PasteScript}/bin/paster $out/bin/ | |
|
165 | ln -s ${self.pyramid}/bin/* $out/bin/ #*/ | |||
163 |
|
166 | |||
164 | # rhodecode-tools |
|
167 | # rhodecode-tools | |
165 | # TODO: johbo: re-think this. Do the tools import anything from enterprise? |
|
168 | # TODO: johbo: re-think this. Do the tools import anything from enterprise? | |
@@ -169,6 +172,7 b' let' | |||||
169 | for file in $out/bin/*; do #*/ |
|
172 | for file in $out/bin/*; do #*/ | |
170 | wrapProgram $file \ |
|
173 | wrapProgram $file \ | |
171 | --prefix PYTHONPATH : $PYTHONPATH \ |
|
174 | --prefix PYTHONPATH : $PYTHONPATH \ | |
|
175 | --prefix PATH : $PATH \ | |||
172 | --set PYTHONHASHSEED random |
|
176 | --set PYTHONHASHSEED random | |
173 | done |
|
177 | done | |
174 |
|
178 |
@@ -9,24 +9,24 b' Here is a sample configuration file for ' | |||||
9 | ServerName hg.myserver.com |
|
9 | ServerName hg.myserver.com | |
10 | ServerAlias hg.myserver.com |
|
10 | ServerAlias hg.myserver.com | |
11 |
|
11 | |||
12 |
## uncomment root directive if you want to serve static files by |
|
12 | ## uncomment root directive if you want to serve static files by | |
13 | ## requires static_files = false in .ini file |
|
13 | ## Apache requires static_files = false in .ini file | |
14 |
DocumentRoot /path/to/installation |
|
14 | #DocumentRoot /path/to/rhodecode/installation/public | |
15 |
|
15 | |||
16 | <Proxy *> |
|
16 | <Proxy *> | |
17 | Order allow,deny |
|
17 | Order allow,deny | |
18 | Allow from all |
|
18 | Allow from all | |
19 | </Proxy> |
|
19 | </Proxy> | |
20 |
|
20 | |||
21 |
# |
|
21 | ## Important ! | |
22 | #Directive to properly generate url (clone url) for pylons |
|
22 | ## Directive to properly generate url (clone url) for pylons | |
23 | ProxyPreserveHost On |
|
23 | ProxyPreserveHost On | |
24 |
|
24 | |||
25 |
# |
|
25 | ## RhodeCode instance running | |
26 |
ProxyPass / http://127.0.0.1: |
|
26 | ProxyPass / http://127.0.0.1:10002/ | |
27 |
ProxyPassReverse / http://127.0.0.1: |
|
27 | ProxyPassReverse / http://127.0.0.1:10002/ | |
28 |
|
28 | |||
29 | #to enable https use line below |
|
29 | ## to enable https use line below | |
30 | #SetEnvIf X-Url-Scheme https HTTPS=1 |
|
30 | #SetEnvIf X-Url-Scheme https HTTPS=1 | |
31 |
|
31 | |||
32 | </VirtualHost> |
|
32 | </VirtualHost> |
@@ -3,7 +3,15 b'' | |||||
3 | Full-text Search |
|
3 | Full-text Search | |
4 | ---------------- |
|
4 | ---------------- | |
5 |
|
5 | |||
6 |
By default |RC |
|
6 | By default |RC| is configured to use `Whoosh`_ to index |repos| and | |
|
7 | provide full-text search. | |||
|
8 | ||||
|
9 | |RCE| also provides support for `Elasticsearch`_ as a backend for scalable | |||
|
10 | search. See :ref:`enable-elasticsearch` for details. | |||
|
11 | ||||
|
12 | Indexing | |||
|
13 | ^^^^^^^^ | |||
|
14 | ||||
7 | To run the indexer you need to use an |authtoken| with admin rights to all |
|
15 | To run the indexer you need to use an |authtoken| with admin rights to all | |
8 | |repos|. |
|
16 | |repos|. | |
9 |
|
17 | |||
@@ -232,4 +240,33 b' use the following example :file:`mapping' | |||||
232 | max_filesize = 800MB |
|
240 | max_filesize = 800MB | |
233 | commit_parse_limit = 20000 |
|
241 | commit_parse_limit = 20000 | |
234 |
|
242 | |||
|
243 | .. _enable-elasticsearch: | |||
|
244 | ||||
|
245 | Enabling Elasticsearch | |||
|
246 | ^^^^^^^^^^^^^^^^^^^^^^ | |||
|
247 | ||||
|
248 | 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The | |||
|
249 | default location is | |||
|
250 | :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` | |||
|
251 | 2. Find the search configuration section: | |||
|
252 | ||||
|
253 | .. code-block:: ini | |||
|
254 | ||||
|
255 | ################################### | |||
|
256 | ## SEARCH INDEXING CONFIGURATION ## | |||
|
257 | ################################### | |||
|
258 | ||||
|
259 | search.module = rhodecode.lib.index.whoosh | |||
|
260 | search.location = %(here)s/data/index | |||
|
261 | ||||
|
262 | and change it to: | |||
|
263 | ||||
|
264 | .. code-block:: ini | |||
|
265 | ||||
|
266 | search.module = rc_elasticsearch | |||
|
267 | search.location = http://localhost:9200/ | |||
|
268 | ||||
|
269 | where ``search.location`` points to the elasticsearch server. | |||
|
270 | ||||
235 | .. _Whoosh: https://pypi.python.org/pypi/Whoosh/ |
|
271 | .. _Whoosh: https://pypi.python.org/pypi/Whoosh/ | |
|
272 | .. _Elasticsearch: https://www.elastic.co/ No newline at end of file |
@@ -7,11 +7,11 b' Use the following example to configure N' | |||||
7 |
|
7 | |||
8 | upstream rc { |
|
8 | upstream rc { | |
9 |
|
9 | |||
10 |
server 127.0.0.1: |
|
10 | server 127.0.0.1:10002; | |
11 |
|
11 | |||
12 | # add more instances for load balancing |
|
12 | # add more instances for load balancing | |
13 |
# server 127.0.0.1: |
|
13 | # server 127.0.0.1:10003; | |
14 |
# server 127.0.0.1: |
|
14 | # server 127.0.0.1:10004; | |
15 | } |
|
15 | } | |
16 |
|
16 | |||
17 | ## gist alias |
|
17 | ## gist alias | |
@@ -58,9 +58,10 b' Use the following example to configure N' | |||||
58 |
|
58 | |||
59 | ## uncomment root directive if you want to serve static files by nginx |
|
59 | ## uncomment root directive if you want to serve static files by nginx | |
60 | ## requires static_files = false in .ini file |
|
60 | ## requires static_files = false in .ini file | |
61 |
# root /path/to/installation |
|
61 | # root /path/to/rhodecode/installation/public; | |
62 |
|
62 | |||
63 | include /etc/nginx/proxy.conf; |
|
63 | include /etc/nginx/proxy.conf; | |
|
64 | ||||
64 |
|
|
65 | location / { | |
65 |
|
|
66 | try_files $uri @rhode; | |
66 |
|
|
67 | } |
@@ -64,6 +64,14 b' performance is more important than CPU p' | |||||
64 | environment handling 1000s of users and |repos| you should deploy on a 12+ |
|
64 | environment handling 1000s of users and |repos| you should deploy on a 12+ | |
65 | core 64GB RAM server. In short, the more RAM the better. |
|
65 | core 64GB RAM server. In short, the more RAM the better. | |
66 |
|
66 | |||
|
67 | ||||
|
68 | For example: | |||
|
69 | ||||
|
70 | - for team of 1 - 5 active users you can run on 1GB RAM machine with 1CPU | |||
|
71 | - above 250 active users, |RCM| needs at least 8GB of memory. | |||
|
72 | Number of CPUs is less important, but recommended to have at least 2-3 CPUs | |||
|
73 | ||||
|
74 | ||||
67 | .. _config-rce-files: |
|
75 | .. _config-rce-files: | |
68 |
|
76 | |||
69 | Configuration Files |
|
77 | Configuration Files |
@@ -23,6 +23,8 b" rst_epilog = '''" | |||||
23 | .. |RCV| replace:: RhodeCode Enterprise |
|
23 | .. |RCV| replace:: RhodeCode Enterprise | |
24 | .. |RCM| replace:: RhodeCode Enterprise |
|
24 | .. |RCM| replace:: RhodeCode Enterprise | |
25 | .. |RCE| replace:: RhodeCode Enterprise |
|
25 | .. |RCE| replace:: RhodeCode Enterprise | |
|
26 | .. |RCCE| replace:: RhodeCode Community | |||
|
27 | .. |RCEE| replace:: RhodeCode Enterprise | |||
26 | .. |RCX| replace:: RhodeCode Extensions |
|
28 | .. |RCX| replace:: RhodeCode Extensions | |
27 | .. |RCT| replace:: RhodeCode Tools |
|
29 | .. |RCT| replace:: RhodeCode Tools | |
28 | .. |RCEBOLD| replace:: **RhodeCode Enterprise** |
|
30 | .. |RCEBOLD| replace:: **RhodeCode Enterprise** |
@@ -5,12 +5,12 b' Make Database Changes' | |||||
5 |
|
5 | |||
6 | .. important:: |
|
6 | .. important:: | |
7 |
|
7 | |||
8 |
If you do change the |repo| database that |RC |
|
8 | If you do change the |repo| database that |RCEE| uses, then you will need to | |
9 | upgrade the database, and also remap and rescan the |repos|. More detailed |
|
9 | upgrade the database, and also remap and rescan the |repos|. More detailed | |
10 | information is available in the |
|
10 | information is available in the | |
11 | :ref:`Alternative upgrade documentation <control:install-port>`. |
|
11 | :ref:`Alternative upgrade documentation <control:install-port>`. | |
12 |
|
12 | |||
13 |
If you need to change database connection details for a |RC |
|
13 | If you need to change database connection details for a |RCEE| instance, | |
14 | use the following steps: |
|
14 | use the following steps: | |
15 |
|
15 | |||
16 | 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The |
|
16 | 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The |
@@ -17,10 +17,12 b' Quick Start Guide' | |||||
17 | credentials during |RCE| installation. See the relevant database |
|
17 | credentials during |RCE| installation. See the relevant database | |
18 | documentation for more details. |
|
18 | documentation for more details. | |
19 |
|
19 | |||
20 |
To get |RC |
|
20 | To get |RCE| up and running, run through the below steps: | |
21 |
|
21 | |||
22 | 1. Download the latest |RCC| installer from your `rhodecode.com`_ profile |
|
22 | 1. Download the latest |RCC| installer from your `rhodecode.com`_ profile | |
23 | page. If you don't have an account, sign up at `rhodecode.com/register`_. |
|
23 | or main page. | |
|
24 | If you don't have an account, sign up at `rhodecode.com/register`_. | |||
|
25 | ||||
24 | 2. Run the |RCC| installer and accept the End User Licence using the |
|
26 | 2. Run the |RCC| installer and accept the End User Licence using the | |
25 | following example: |
|
27 | following example: | |
26 |
|
28 | |||
@@ -45,13 +47,18 b' 3. Install a VCS Server, and configure i' | |||||
45 | Added process group vcsserver-1 |
|
47 | Added process group vcsserver-1 | |
46 |
|
48 | |||
47 |
|
49 | |||
48 |
4. Install |RCE|. If using MySQL or PostgreSQL, during |
|
50 | 4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during | |
49 |
asked for your database credentials, so have them at hand. |
|
51 | installation you'll be asked for your database credentials, so have them at hand. | |
50 | any for SQLite. |
|
52 | Mysql or Postgres needs to be running and a new database needs to be created. | |
|
53 | You don't need any credentials or to create a database for SQLite. | |||
51 |
|
54 | |||
52 | .. code-block:: bash |
|
55 | .. code-block:: bash | |
53 | :emphasize-lines: 11-16 |
|
56 | :emphasize-lines: 11-16 | |
54 |
|
57 | |||
|
58 | $ rccontrol install Community | |||
|
59 | ||||
|
60 | or | |||
|
61 | ||||
55 | $ rccontrol install Enterprise |
|
62 | $ rccontrol install Enterprise | |
56 |
|
63 | |||
57 | Username [admin]: username |
|
64 | Username [admin]: username | |
@@ -69,8 +76,8 b' 4. Install |RCE|. If using MySQL or Post' | |||||
69 | Database password: somepassword |
|
76 | Database password: somepassword | |
70 | Database name: example-db-name |
|
77 | Database name: example-db-name | |
71 |
|
78 | |||
72 |
5. Check the status of your installation. You |RCE| instance runs |
|
79 | 5. Check the status of your installation. You |RCEE|/|RCCE| instance runs | |
73 | displayed in the status message. |
|
80 | on the URL displayed in the status message. | |
74 |
|
81 | |||
75 | .. code-block:: bash |
|
82 | .. code-block:: bash | |
76 |
|
83 | |||
@@ -79,13 +86,13 b' 5. Check the status of your installation' | |||||
79 | - NAME: enterprise-1 |
|
86 | - NAME: enterprise-1 | |
80 | - STATUS: RUNNING |
|
87 | - STATUS: RUNNING | |
81 | - TYPE: Enterprise |
|
88 | - TYPE: Enterprise | |
82 |
- VERSION: |
|
89 | - VERSION: 4.1.0 | |
83 | - URL: http://127.0.0.1:10003 |
|
90 | - URL: http://127.0.0.1:10003 | |
84 |
|
91 | |||
85 | - NAME: vcsserver-1 |
|
92 | - NAME: vcsserver-1 | |
86 | - STATUS: RUNNING |
|
93 | - STATUS: RUNNING | |
87 | - TYPE: VCSServer |
|
94 | - TYPE: VCSServer | |
88 |
- VERSION: |
|
95 | - VERSION: 4.1.0 | |
89 | - URL: http://127.0.0.1:10001 |
|
96 | - URL: http://127.0.0.1:10001 | |
90 |
|
97 | |||
91 | .. note:: |
|
98 | .. note:: |
@@ -37,6 +37,10 b' New Features' | |||||
37 | Github, Twitter, Bitbucket and Google. It's possible now to use your |
|
37 | Github, Twitter, Bitbucket and Google. It's possible now to use your | |
38 | Google account to log in to RhodeCode and take advantage of things like 2FA. |
|
38 | Google account to log in to RhodeCode and take advantage of things like 2FA. | |
39 |
|
39 | |||
|
40 | - Search: full text search now properly orders commits by date, and shows line | |||
|
41 | numbers for file content search. | |||
|
42 | ||||
|
43 | ||||
40 | Security |
|
44 | Security | |
41 | ^^^^^^^^ |
|
45 | ^^^^^^^^ | |
42 |
|
46 | |||
@@ -46,8 +50,10 b' Security' | |||||
46 | Performance |
|
50 | Performance | |
47 | ^^^^^^^^^^^ |
|
51 | ^^^^^^^^^^^ | |
48 |
|
52 | |||
49 |
- Optimized admin pan |
|
53 | - Optimized admin panels to faster load large amount of data | |
50 | - Improved file tree loading speed |
|
54 | - Improved file tree loading speed | |
|
55 | - New HTTP backend is ~10% faster, and doesn't require so many threads | |||
|
56 | for vcsserver | |||
51 |
|
57 | |||
52 |
|
58 | |||
53 | Fixes |
|
59 | Fixes |
@@ -6,6 +6,10 b' Release Notes' | |||||
6 | |RCE| 4.x Versions |
|
6 | |RCE| 4.x Versions | |
7 | ------------------ |
|
7 | ------------------ | |
8 |
|
8 | |||
|
9 | .. toctree:: | |||
|
10 | :maxdepth: 1 | |||
|
11 | ||||
|
12 | release-notes-4.1.0.rst | |||
9 | release-notes-4.0.1.rst |
|
13 | release-notes-4.0.1.rst | |
10 | release-notes-4.0.0.rst |
|
14 | release-notes-4.0.0.rst | |
11 |
|
15 |
@@ -1,13 +1,12 b'' | |||||
1 | diff --git a/requirements.txt b/requirements.txt |
|
1 | diff --git a/requirements.txt b/requirements.txt | |
2 | --- a/requirements.txt |
|
2 | --- a/requirements.txt | |
3 | +++ b/requirements.txt |
|
3 | +++ b/requirements.txt | |
4 | @@ -1,8 +1,8 @@ |
|
4 | @@ -3,7 +3,7 @@future==0.14.3 | |
5 | click==5.1 |
|
|||
6 | future==0.14.3 |
|
|||
7 | six==1.9.0 |
|
5 | six==1.9.0 | |
8 | mako==1.0.1 |
|
6 | mako==1.0.1 | |
9 | markupsafe==0.23 |
|
7 | markupsafe==0.23 | |
10 | -requests==2.5.1 |
|
8 | -requests==2.5.1 | |
11 | +requests |
|
9 | +requests | |
|
10 | #responses | |||
12 | whoosh==2.7.0 |
|
11 | whoosh==2.7.0 | |
13 |
|
|
12 | elasticsearch==2.3.0 No newline at end of file |
@@ -21,6 +21,20 b' self: super: {' | |||||
21 | ''; |
|
21 | ''; | |
22 | }); |
|
22 | }); | |
23 |
|
23 | |||
|
24 | gunicorn = super.gunicorn.override (attrs: { | |||
|
25 | propagatedBuildInputs = attrs.propagatedBuildInputs ++ [ | |||
|
26 | # johbo: futures is needed as long as we are on Python 2, otherwise | |||
|
27 | # gunicorn explodes if used with multiple threads per worker. | |||
|
28 | self.futures | |||
|
29 | ]; | |||
|
30 | }); | |||
|
31 | ||||
|
32 | ipython = super.ipython.override (attrs: { | |||
|
33 | propagatedBuildInputs = attrs.propagatedBuildInputs ++ [ | |||
|
34 | self.gnureadline | |||
|
35 | ]; | |||
|
36 | }); | |||
|
37 | ||||
24 | kombu = super.kombu.override (attrs: { |
|
38 | kombu = super.kombu.override (attrs: { | |
25 | # The current version of kombu needs some patching to work with the |
|
39 | # The current version of kombu needs some patching to work with the | |
26 | # other libs. Should be removed once we update celery and kombu. |
|
40 | # other libs. Should be removed once we update celery and kombu. |
@@ -359,16 +359,6 b'' | |||||
359 | md5 = "898bc87e54f278055b561316ba73e222"; |
|
359 | md5 = "898bc87e54f278055b561316ba73e222"; | |
360 | }; |
|
360 | }; | |
361 | }; |
|
361 | }; | |
362 | certifi = super.buildPythonPackage { |
|
|||
363 | name = "certifi-2016.2.28"; |
|
|||
364 | buildInputs = with self; []; |
|
|||
365 | doCheck = false; |
|
|||
366 | propagatedBuildInputs = with self; []; |
|
|||
367 | src = fetchurl { |
|
|||
368 | url = "https://pypi.python.org/packages/5c/f8/f6c54727c74579c6bbe5926f5deb9677c5810a33e11da58d1a4e2d09d041/certifi-2016.2.28.tar.gz"; |
|
|||
369 | md5 = "5d672aa766e1f773c75cfeccd02d3650"; |
|
|||
370 | }; |
|
|||
371 | }; |
|
|||
372 | click = super.buildPythonPackage { |
|
362 | click = super.buildPythonPackage { | |
373 | name = "click-5.1"; |
|
363 | name = "click-5.1"; | |
374 | buildInputs = with self; []; |
|
364 | buildInputs = with self; []; | |
@@ -490,13 +480,23 b'' | |||||
490 | }; |
|
480 | }; | |
491 | }; |
|
481 | }; | |
492 | elasticsearch = super.buildPythonPackage { |
|
482 | elasticsearch = super.buildPythonPackage { | |
493 |
name = "elasticsearch- |
|
483 | name = "elasticsearch-2.3.0"; | |
494 | buildInputs = with self; []; |
|
484 | buildInputs = with self; []; | |
495 | doCheck = false; |
|
485 | doCheck = false; | |
496 | propagatedBuildInputs = with self; [urllib3]; |
|
486 | propagatedBuildInputs = with self; [urllib3]; | |
497 | src = fetchurl { |
|
487 | src = fetchurl { | |
498 |
url = "https://pypi.python.org/packages/13 |
|
488 | url = "https://pypi.python.org/packages/10/35/5fd52c5f0b0ee405ed4b5195e8bce44c5e041787680dc7b94b8071cac600/elasticsearch-2.3.0.tar.gz"; | |
499 | md5 = "3550390baea1639479f79758d66ab032"; |
|
489 | md5 = "2550f3b51629cf1ef9636608af92c340"; | |
|
490 | }; | |||
|
491 | }; | |||
|
492 | elasticsearch-dsl = super.buildPythonPackage { | |||
|
493 | name = "elasticsearch-dsl-2.0.0"; | |||
|
494 | buildInputs = with self; []; | |||
|
495 | doCheck = false; | |||
|
496 | propagatedBuildInputs = with self; [six python-dateutil elasticsearch]; | |||
|
497 | src = fetchurl { | |||
|
498 | url = "https://pypi.python.org/packages/4e/5d/e788ae8dbe2ff4d13426db0a027533386a5c276c77a2654dc0e2007ce04a/elasticsearch-dsl-2.0.0.tar.gz"; | |||
|
499 | md5 = "4cdfec81bb35383dd3b7d02d7dc5ee68"; | |||
500 | }; |
|
500 | }; | |
501 | }; |
|
501 | }; | |
502 | flake8 = super.buildPythonPackage { |
|
502 | flake8 = super.buildPythonPackage { | |
@@ -540,7 +540,7 b'' | |||||
540 | }; |
|
540 | }; | |
541 | }; |
|
541 | }; | |
542 | gprof2dot = super.buildPythonPackage { |
|
542 | gprof2dot = super.buildPythonPackage { | |
543 | name = "gprof2dot-2015.12.1"; |
|
543 | name = "gprof2dot-2015.12.01"; | |
544 | buildInputs = with self; []; |
|
544 | buildInputs = with self; []; | |
545 | doCheck = false; |
|
545 | doCheck = false; | |
546 | propagatedBuildInputs = with self; []; |
|
546 | propagatedBuildInputs = with self; []; | |
@@ -550,13 +550,13 b'' | |||||
550 | }; |
|
550 | }; | |
551 | }; |
|
551 | }; | |
552 | greenlet = super.buildPythonPackage { |
|
552 | greenlet = super.buildPythonPackage { | |
553 |
name = "greenlet-0.4. |
|
553 | name = "greenlet-0.4.9"; | |
554 | buildInputs = with self; []; |
|
554 | buildInputs = with self; []; | |
555 | doCheck = false; |
|
555 | doCheck = false; | |
556 | propagatedBuildInputs = with self; []; |
|
556 | propagatedBuildInputs = with self; []; | |
557 | src = fetchurl { |
|
557 | src = fetchurl { | |
558 | url = "https://pypi.python.org/packages/7a/9f/a1a0d9bdf3203ae1502c5a8434fe89d323599d78a106985bc327351a69d4/greenlet-0.4.7.zip"; |
|
558 | url = "https://pypi.python.org/packages/4e/3d/9d421539b74e33608b245092870156b2e171fb49f2b51390aa4641eecb4a/greenlet-0.4.9.zip"; | |
559 | md5 = "c2333a8ff30fa75c5d5ec0e67b461086"; |
|
559 | md5 = "c6659cdb2a5e591723e629d2eef22e82"; | |
560 | }; |
|
560 | }; | |
561 | }; |
|
561 | }; | |
562 | gunicorn = super.buildPythonPackage { |
|
562 | gunicorn = super.buildPythonPackage { | |
@@ -603,7 +603,7 b'' | |||||
603 | name = "ipython-3.1.0"; |
|
603 | name = "ipython-3.1.0"; | |
604 | buildInputs = with self; []; |
|
604 | buildInputs = with self; []; | |
605 | doCheck = false; |
|
605 | doCheck = false; | |
606 |
propagatedBuildInputs = with self; [ |
|
606 | propagatedBuildInputs = with self; []; | |
607 | src = fetchurl { |
|
607 | src = fetchurl { | |
608 | url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz"; |
|
608 | url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz"; | |
609 | md5 = "a749d90c16068687b0ec45a27e72ef8f"; |
|
609 | md5 = "a749d90c16068687b0ec45a27e72ef8f"; | |
@@ -799,16 +799,6 b'' | |||||
799 | md5 = "47b4eac84118e2606658122104e62072"; |
|
799 | md5 = "47b4eac84118e2606658122104e62072"; | |
800 | }; |
|
800 | }; | |
801 | }; |
|
801 | }; | |
802 | pyelasticsearch = super.buildPythonPackage { |
|
|||
803 | name = "pyelasticsearch-1.4"; |
|
|||
804 | buildInputs = with self; []; |
|
|||
805 | doCheck = false; |
|
|||
806 | propagatedBuildInputs = with self; [certifi elasticsearch urllib3 simplejson six]; |
|
|||
807 | src = fetchurl { |
|
|||
808 | url = "https://pypi.python.org/packages/2f/3a/7643cfcfc4cbdbb20ada800bbd54ac9705d0c047d7b8f8d5eeeb3047b4eb/pyelasticsearch-1.4.tar.gz"; |
|
|||
809 | md5 = "ed61ebb7b253364e55b4923d11e17049"; |
|
|||
810 | }; |
|
|||
811 | }; |
|
|||
812 | pyflakes = super.buildPythonPackage { |
|
802 | pyflakes = super.buildPythonPackage { | |
813 | name = "pyflakes-0.8.1"; |
|
803 | name = "pyflakes-0.8.1"; | |
814 | buildInputs = with self; []; |
|
804 | buildInputs = with self; []; | |
@@ -1050,20 +1040,20 b'' | |||||
1050 | }; |
|
1040 | }; | |
1051 | }; |
|
1041 | }; | |
1052 | rhodecode-enterprise-ce = super.buildPythonPackage { |
|
1042 | rhodecode-enterprise-ce = super.buildPythonPackage { | |
1053 |
name = "rhodecode-enterprise-ce-4.0 |
|
1043 | name = "rhodecode-enterprise-ce-4.1.0"; | |
1054 | buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner]; |
|
1044 | buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner]; | |
1055 | doCheck = true; |
|
1045 | doCheck = true; | |
1056 | 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 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 psutil py-bcrypt]; |
|
1046 | 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 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 psutil py-bcrypt]; | |
1057 | src = ./.; |
|
1047 | src = ./.; | |
1058 | }; |
|
1048 | }; | |
1059 | rhodecode-tools = super.buildPythonPackage { |
|
1049 | rhodecode-tools = super.buildPythonPackage { | |
1060 |
name = "rhodecode-tools-0. |
|
1050 | name = "rhodecode-tools-0.8.3"; | |
1061 | buildInputs = with self; []; |
|
1051 | buildInputs = with self; []; | |
1062 | doCheck = false; |
|
1052 | doCheck = false; | |
1063 |
propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh |
|
1053 | propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh elasticsearch elasticsearch-dsl]; | |
1064 | src = fetchurl { |
|
1054 | src = fetchurl { | |
1065 |
url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0. |
|
1055 | url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip"; | |
1066 | md5 = "91daea803aaa264ce7a8213bc2220d4c"; |
|
1056 | md5 = "9acdfd71b8ddf4056057065f37ab9ccb"; | |
1067 | }; |
|
1057 | }; | |
1068 | }; |
|
1058 | }; | |
1069 | serpent = super.buildPythonPackage { |
|
1059 | serpent = super.buildPythonPackage { |
@@ -11,7 +11,6 b' MySQL-python==1.2.5' | |||||
11 | Paste==2.0.2 |
|
11 | Paste==2.0.2 | |
12 | PasteDeploy==1.5.2 |
|
12 | PasteDeploy==1.5.2 | |
13 | PasteScript==1.7.5 |
|
13 | PasteScript==1.7.5 | |
14 | pyelasticsearch==1.4 |
|
|||
15 | Pygments==2.0.2 |
|
14 | Pygments==2.0.2 | |
16 |
|
15 | |||
17 | # TODO: This version is not available on PyPI |
|
16 | # TODO: This version is not available on PyPI | |
@@ -70,13 +69,14 b' flake8==2.4.1' | |||||
70 | future==0.14.3 |
|
69 | future==0.14.3 | |
71 | futures==3.0.2 |
|
70 | futures==3.0.2 | |
72 | gprof2dot==2015.12.1 |
|
71 | gprof2dot==2015.12.1 | |
73 |
greenlet==0.4. |
|
72 | greenlet==0.4.9 | |
74 | gunicorn==19.6.0 |
|
73 | gunicorn==19.6.0 | |
75 |
|
74 | |||
76 | # TODO: Needs subvertpy and blows up without Subversion headers, |
|
75 | # TODO: Needs subvertpy and blows up without Subversion headers, | |
77 | # actually we should not need this for Enterprise at all. |
|
76 | # actually we should not need this for Enterprise at all. | |
78 | # hgsubversion==1.8.2 |
|
77 | # hgsubversion==1.8.2 | |
79 |
|
78 | |||
|
79 | gnureadline==6.3.3 | |||
80 | infrae.cache==1.0.1 |
|
80 | infrae.cache==1.0.1 | |
81 | invoke==0.11.1 |
|
81 | invoke==0.11.1 | |
82 | ipdb==0.8 |
|
82 | ipdb==0.8 | |
@@ -124,7 +124,7 b' pyzmq==14.6.0' | |||||
124 | # TODO: This is not available in public |
|
124 | # TODO: This is not available in public | |
125 | # rc-testdata==0.2.0 |
|
125 | # rc-testdata==0.2.0 | |
126 |
|
126 | |||
127 |
https://code.rhodecode.com/rhodecode-tools-ce/archive/v0. |
|
127 | https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip#md5=9acdfd71b8ddf4056057065f37ab9ccb | |
128 |
|
128 | |||
129 |
|
129 | |||
130 | recaptcha-client==1.0.6 |
|
130 | recaptcha-client==1.0.6 |
@@ -47,7 +47,7 b' CONFIG = {}' | |||||
47 | EXTENSIONS = {} |
|
47 | EXTENSIONS = {} | |
48 |
|
48 | |||
49 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
49 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
50 |
__dbversion__ = 5 |
|
50 | __dbversion__ = 54 # defines current db version for migrations | |
51 | __platform__ = platform.system() |
|
51 | __platform__ = platform.system() | |
52 | __license__ = 'AGPLv3, and Commercial License' |
|
52 | __license__ = 'AGPLv3, and Commercial License' | |
53 | __author__ = 'RhodeCode GmbH' |
|
53 | __author__ = 'RhodeCode GmbH' |
@@ -24,67 +24,81 b' import pytest' | |||||
24 | from rhodecode.model.repo import RepoModel |
|
24 | from rhodecode.model.repo import RepoModel | |
25 | from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN |
|
25 | from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN | |
26 | from rhodecode.api.tests.utils import ( |
|
26 | from rhodecode.api.tests.utils import ( | |
27 | build_data, api_call, assert_error, assert_ok, crash) |
|
27 | build_data, api_call, assert_error, assert_ok, crash, jsonify) | |
28 | from rhodecode.tests.fixture import Fixture |
|
28 | from rhodecode.tests.fixture import Fixture | |
29 |
|
29 | |||
30 |
|
30 | |||
31 | fixture = Fixture() |
|
31 | fixture = Fixture() | |
32 |
|
32 | |||
|
33 | UPDATE_REPO_NAME = 'api_update_me' | |||
|
34 | ||||
|
35 | class SAME_AS_UPDATES(object): """ Constant used for tests below """ | |||
33 |
|
36 | |||
34 | @pytest.mark.usefixtures("testuser_api", "app") |
|
37 | @pytest.mark.usefixtures("testuser_api", "app") | |
35 | class TestApiUpdateRepo(object): |
|
38 | class TestApiUpdateRepo(object): | |
36 | @pytest.mark.parametrize("changing_attr, updates", [ |
|
39 | ||
37 | ('owner', {'owner': TEST_USER_REGULAR_LOGIN}), |
|
40 | @pytest.mark.parametrize("updates, expected", [ | |
38 | ('description', {'description': 'new description'}), |
|
41 | ({'owner': TEST_USER_REGULAR_LOGIN}, SAME_AS_UPDATES), | |
39 | ('active', {'active': True}), |
|
42 | ({'description': 'new description'}, SAME_AS_UPDATES), | |
40 | ('active', {'active': False}), |
|
43 | ({'clone_uri': 'http://foo.com/repo'}, SAME_AS_UPDATES), | |
41 |
('clone_uri', {'clone_uri': ' |
|
44 | ({'clone_uri': None}, {'clone_uri': ''}), | |
42 |
('clone_uri', {'clone_uri': |
|
45 | ({'clone_uri': ''}, {'clone_uri': ''}), | |
43 |
( |
|
46 | ({'landing_rev': 'branch:master'}, {'landing_rev': ['branch','master']}), | |
44 |
( |
|
47 | ({'enable_statistics': True}, SAME_AS_UPDATES), | |
45 |
( |
|
48 | ({'enable_locking': True}, SAME_AS_UPDATES), | |
46 |
( |
|
49 | ({'enable_downloads': True}, SAME_AS_UPDATES), | |
47 |
( |
|
50 | ({'name': 'new_repo_name'}, {'repo_name': 'new_repo_name'}), | |
48 |
( |
|
51 | ({'group': 'test_group_for_update'}, | |
|
52 | {'repo_name': 'test_group_for_update/%s' % UPDATE_REPO_NAME}), | |||
49 | ]) |
|
53 | ]) | |
50 |
def test_api_update_repo(self, |
|
54 | def test_api_update_repo(self, updates, expected, backend): | |
51 |
repo_name = |
|
55 | repo_name = UPDATE_REPO_NAME | |
52 | repo = fixture.create_repo(repo_name, repo_type=backend.alias) |
|
56 | repo = fixture.create_repo(repo_name, repo_type=backend.alias) | |
53 |
if |
|
57 | if updates.get('group'): | |
54 | fixture.create_repo_group(updates['group']) |
|
58 | fixture.create_repo_group(updates['group']) | |
55 |
|
59 | |||
|
60 | expected_api_data = repo.get_api_data(include_secrets=True) | |||
|
61 | if expected is SAME_AS_UPDATES: | |||
|
62 | expected_api_data.update(updates) | |||
|
63 | else: | |||
|
64 | expected_api_data.update(expected) | |||
|
65 | ||||
|
66 | ||||
56 | id_, params = build_data( |
|
67 | id_, params = build_data( | |
57 | self.apikey, 'update_repo', repoid=repo_name, **updates) |
|
68 | self.apikey, 'update_repo', repoid=repo_name, **updates) | |
58 | response = api_call(self.app, params) |
|
69 | response = api_call(self.app, params) | |
59 | if changing_attr == 'name': |
|
70 | ||
|
71 | if updates.get('name'): | |||
60 | repo_name = updates['name'] |
|
72 | repo_name = updates['name'] | |
61 |
if |
|
73 | if updates.get('group'): | |
62 | repo_name = '/'.join([updates['group'], repo_name]) |
|
74 | repo_name = '/'.join([updates['group'], repo_name]) | |
|
75 | ||||
63 | try: |
|
76 | try: | |
64 | expected = { |
|
77 | expected = { | |
65 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name), |
|
78 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name), | |
66 |
'repository': |
|
79 | 'repository': jsonify(expected_api_data) | |
67 | } |
|
80 | } | |
68 | assert_ok(id_, expected, given=response.body) |
|
81 | assert_ok(id_, expected, given=response.body) | |
69 | finally: |
|
82 | finally: | |
70 | fixture.destroy_repo(repo_name) |
|
83 | fixture.destroy_repo(repo_name) | |
71 |
if |
|
84 | if updates.get('group'): | |
72 |
|
||||
73 | fixture.destroy_repo_group(updates['group']) |
|
85 | fixture.destroy_repo_group(updates['group']) | |
74 |
|
86 | |||
75 | def test_api_update_repo_fork_of_field(self, backend): |
|
87 | def test_api_update_repo_fork_of_field(self, backend): | |
76 | master_repo = backend.create_repo() |
|
88 | master_repo = backend.create_repo() | |
77 | repo = backend.create_repo() |
|
89 | repo = backend.create_repo() | |
78 |
|
||||
79 | updates = { |
|
90 | updates = { | |
80 | 'fork_of': master_repo.repo_name |
|
91 | 'fork_of': master_repo.repo_name | |
81 | } |
|
92 | } | |
|
93 | expected_api_data = repo.get_api_data(include_secrets=True) | |||
|
94 | expected_api_data.update(updates) | |||
|
95 | ||||
82 | id_, params = build_data( |
|
96 | id_, params = build_data( | |
83 | self.apikey, 'update_repo', repoid=repo.repo_name, **updates) |
|
97 | self.apikey, 'update_repo', repoid=repo.repo_name, **updates) | |
84 | response = api_call(self.app, params) |
|
98 | response = api_call(self.app, params) | |
85 | expected = { |
|
99 | expected = { | |
86 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), |
|
100 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), | |
87 |
'repository': |
|
101 | 'repository': jsonify(expected_api_data) | |
88 | } |
|
102 | } | |
89 | assert_ok(id_, expected, given=response.body) |
|
103 | assert_ok(id_, expected, given=response.body) | |
90 | result = response.json['result']['repository'] |
|
104 | result = response.json['result']['repository'] | |
@@ -131,7 +145,7 b' class TestApiUpdateRepo(object):' | |||||
131 |
|
145 | |||
132 | @mock.patch.object(RepoModel, 'update', crash) |
|
146 | @mock.patch.object(RepoModel, 'update', crash) | |
133 | def test_api_update_repo_exception_occurred(self, backend): |
|
147 | def test_api_update_repo_exception_occurred(self, backend): | |
134 |
repo_name = |
|
148 | repo_name = UPDATE_REPO_NAME | |
135 | fixture.create_repo(repo_name, repo_type=backend.alias) |
|
149 | fixture.create_repo(repo_name, repo_type=backend.alias) | |
136 | id_, params = build_data( |
|
150 | id_, params = build_data( | |
137 | self.apikey, 'update_repo', repoid=repo_name, |
|
151 | self.apikey, 'update_repo', repoid=repo_name, |
@@ -25,7 +25,7 b' from rhodecode.model.user import UserMod' | |||||
25 | from rhodecode.model.user_group import UserGroupModel |
|
25 | from rhodecode.model.user_group import UserGroupModel | |
26 | from rhodecode.tests import TEST_USER_REGULAR_LOGIN |
|
26 | from rhodecode.tests import TEST_USER_REGULAR_LOGIN | |
27 | from rhodecode.api.tests.utils import ( |
|
27 | from rhodecode.api.tests.utils import ( | |
28 | build_data, api_call, assert_error, assert_ok, crash) |
|
28 | build_data, api_call, assert_error, assert_ok, crash, jsonify) | |
29 |
|
29 | |||
30 |
|
30 | |||
31 | @pytest.mark.usefixtures("testuser_api", "app") |
|
31 | @pytest.mark.usefixtures("testuser_api", "app") | |
@@ -40,14 +40,18 b' class TestUpdateUserGroup(object):' | |||||
40 | def test_api_update_user_group(self, changing_attr, updates, user_util): |
|
40 | def test_api_update_user_group(self, changing_attr, updates, user_util): | |
41 | user_group = user_util.create_user_group() |
|
41 | user_group = user_util.create_user_group() | |
42 | group_name = user_group.users_group_name |
|
42 | group_name = user_group.users_group_name | |
|
43 | expected_api_data = user_group.get_api_data() | |||
|
44 | expected_api_data.update(updates) | |||
|
45 | ||||
43 | id_, params = build_data( |
|
46 | id_, params = build_data( | |
44 | self.apikey, 'update_user_group', usergroupid=group_name, |
|
47 | self.apikey, 'update_user_group', usergroupid=group_name, | |
45 | **updates) |
|
48 | **updates) | |
46 | response = api_call(self.app, params) |
|
49 | response = api_call(self.app, params) | |
|
50 | ||||
47 | expected = { |
|
51 | expected = { | |
48 | 'msg': 'updated user group ID:%s %s' % ( |
|
52 | 'msg': 'updated user group ID:%s %s' % ( | |
49 | user_group.users_group_id, user_group.users_group_name), |
|
53 | user_group.users_group_id, user_group.users_group_name), | |
50 |
'user_group': |
|
54 | 'user_group': jsonify(expected_api_data) | |
51 | } |
|
55 | } | |
52 | assert_ok(id_, expected, given=response.body) |
|
56 | assert_ok(id_, expected, given=response.body) | |
53 |
|
57 | |||
@@ -63,6 +67,10 b' class TestUpdateUserGroup(object):' | |||||
63 | self, changing_attr, updates, user_util): |
|
67 | self, changing_attr, updates, user_util): | |
64 | user_group = user_util.create_user_group() |
|
68 | user_group = user_util.create_user_group() | |
65 | group_name = user_group.users_group_name |
|
69 | group_name = user_group.users_group_name | |
|
70 | expected_api_data = user_group.get_api_data() | |||
|
71 | expected_api_data.update(updates) | |||
|
72 | ||||
|
73 | ||||
66 | # grant permission to this user |
|
74 | # grant permission to this user | |
67 | user = UserModel().get_by_username(self.TEST_USER_LOGIN) |
|
75 | user = UserModel().get_by_username(self.TEST_USER_LOGIN) | |
68 |
|
76 | |||
@@ -75,7 +83,7 b' class TestUpdateUserGroup(object):' | |||||
75 | expected = { |
|
83 | expected = { | |
76 | 'msg': 'updated user group ID:%s %s' % ( |
|
84 | 'msg': 'updated user group ID:%s %s' % ( | |
77 | user_group.users_group_id, user_group.users_group_name), |
|
85 | user_group.users_group_id, user_group.users_group_name), | |
78 |
'user_group': |
|
86 | 'user_group': jsonify(expected_api_data) | |
79 | } |
|
87 | } | |
80 | assert_ok(id_, expected, given=response.body) |
|
88 | assert_ok(id_, expected, given=response.body) | |
81 |
|
89 |
@@ -323,7 +323,7 b' def get_repo_changeset(request, apiuser,' | |||||
323 | def get_repo_changesets(request, apiuser, repoid, start_rev, limit, |
|
323 | def get_repo_changesets(request, apiuser, repoid, start_rev, limit, | |
324 | details=Optional('basic')): |
|
324 | details=Optional('basic')): | |
325 | """ |
|
325 | """ | |
326 |
Returns a set of c |
|
326 | Returns a set of commits limited by the number starting | |
327 | from the `start_rev` option. |
|
327 | from the `start_rev` option. | |
328 |
|
328 | |||
329 | Additional parameters define the amount of details returned by this |
|
329 | Additional parameters define the amount of details returned by this | |
@@ -338,7 +338,7 b' def get_repo_changesets(request, apiuser' | |||||
338 | :type repoid: str or int |
|
338 | :type repoid: str or int | |
339 | :param start_rev: The starting revision from where to get changesets. |
|
339 | :param start_rev: The starting revision from where to get changesets. | |
340 | :type start_rev: str |
|
340 | :type start_rev: str | |
341 |
:param limit: Limit the number of c |
|
341 | :param limit: Limit the number of commits to this amount | |
342 | :type limit: str or int |
|
342 | :type limit: str or int | |
343 | :param details: Set the level of detail returned. Valid option are: |
|
343 | :param details: Set the level of detail returned. Valid option are: | |
344 | ``basic``, ``extended`` and ``full``. |
|
344 | ``basic``, ``extended`` and ``full``. | |
@@ -370,14 +370,17 b' def get_repo_changesets(request, apiuser' | |||||
370 |
|
370 | |||
371 | vcs_repo = repo.scm_instance() |
|
371 | vcs_repo = repo.scm_instance() | |
372 | # SVN needs a special case to distinguish its index and commit id |
|
372 | # SVN needs a special case to distinguish its index and commit id | |
373 | if vcs_repo.alias == 'svn' and (start_rev == '0'): |
|
373 | if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'): | |
374 | start_rev = vcs_repo.commit_ids[0] |
|
374 | start_rev = vcs_repo.commit_ids[0] | |
375 |
|
375 | |||
376 | try: |
|
376 | try: | |
377 |
commits = repo |
|
377 | commits = vcs_repo.get_commits( | |
378 | start_id=start_rev, pre_load=pre_load) |
|
378 | start_id=start_rev, pre_load=pre_load) | |
379 | except TypeError as e: |
|
379 | except TypeError as e: | |
380 | raise JSONRPCError(e.message) |
|
380 | raise JSONRPCError(e.message) | |
|
381 | except Exception: | |||
|
382 | log.exception('Fetching of commits failed') | |||
|
383 | raise JSONRPCError('Error occurred during commit fetching') | |||
381 |
|
384 | |||
382 | ret = [] |
|
385 | ret = [] | |
383 | for cnt, commit in enumerate(commits): |
|
386 | for cnt, commit in enumerate(commits): |
@@ -19,6 +19,7 b'' | |||||
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
21 | import logging |
|
21 | import logging | |
|
22 | import importlib | |||
22 |
|
23 | |||
23 | from pkg_resources import iter_entry_points |
|
24 | from pkg_resources import iter_entry_points | |
24 | from pyramid.authentication import SessionAuthenticationPolicy |
|
25 | from pyramid.authentication import SessionAuthenticationPolicy | |
@@ -27,9 +28,15 b' from rhodecode.authentication.registry i' | |||||
27 | from rhodecode.authentication.routes import root_factory |
|
28 | from rhodecode.authentication.routes import root_factory | |
28 | from rhodecode.authentication.routes import AuthnRootResource |
|
29 | from rhodecode.authentication.routes import AuthnRootResource | |
29 | from rhodecode.config.routing import ADMIN_PREFIX |
|
30 | from rhodecode.config.routing import ADMIN_PREFIX | |
|
31 | from rhodecode.model.settings import SettingsModel | |||
|
32 | ||||
30 |
|
33 | |||
31 | log = logging.getLogger(__name__) |
|
34 | log = logging.getLogger(__name__) | |
32 |
|
35 | |||
|
36 | # Plugin ID prefixes to distinct between normal and legacy plugins. | |||
|
37 | plugin_prefix = 'egg:' | |||
|
38 | legacy_plugin_prefix = 'py:' | |||
|
39 | ||||
33 |
|
40 | |||
34 | # TODO: Currently this is only used to discover the authentication plugins. |
|
41 | # TODO: Currently this is only used to discover the authentication plugins. | |
35 | # Later on this may be used in a generic way to look up and include all kinds |
|
42 | # Later on this may be used in a generic way to look up and include all kinds | |
@@ -38,16 +45,45 b' log = logging.getLogger(__name__)' | |||||
38 | # TODO: When refactoring this think about splitting it up into distinct |
|
45 | # TODO: When refactoring this think about splitting it up into distinct | |
39 | # discover, load and include phases. |
|
46 | # discover, load and include phases. | |
40 | def _discover_plugins(config, entry_point='enterprise.plugins1'): |
|
47 | def _discover_plugins(config, entry_point='enterprise.plugins1'): | |
41 | _discovered_plugins = {} |
|
|||
42 |
|
||||
43 | for ep in iter_entry_points(entry_point): |
|
48 | for ep in iter_entry_points(entry_point): | |
44 |
plugin_id = ' |
|
49 | plugin_id = '{}{}#{}'.format( | |
|
50 | plugin_prefix, ep.dist.project_name, ep.name) | |||
45 | log.debug('Plugin discovered: "%s"', plugin_id) |
|
51 | log.debug('Plugin discovered: "%s"', plugin_id) | |
|
52 | try: | |||
46 | module = ep.load() |
|
53 | module = ep.load() | |
47 | plugin = module(plugin_id=plugin_id) |
|
54 | plugin = module(plugin_id=plugin_id) | |
48 | config.include(plugin.includeme) |
|
55 | config.include(plugin.includeme) | |
|
56 | except Exception as e: | |||
|
57 | log.exception( | |||
|
58 | 'Exception while loading authentication plugin ' | |||
|
59 | '"{}": {}'.format(plugin_id, e.message)) | |||
49 |
|
60 | |||
50 | return _discovered_plugins |
|
61 | ||
|
62 | def _import_legacy_plugin(plugin_id): | |||
|
63 | module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1] | |||
|
64 | module = importlib.import_module(module_name) | |||
|
65 | return module.plugin_factory(plugin_id=plugin_id) | |||
|
66 | ||||
|
67 | ||||
|
68 | def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix): | |||
|
69 | """ | |||
|
70 | Function that imports the legacy plugins stored in the 'auth_plugins' | |||
|
71 | setting in database which are using the specified prefix. Normally 'py:' is | |||
|
72 | used for the legacy plugins. | |||
|
73 | """ | |||
|
74 | auth_plugins = SettingsModel().get_setting_by_name('auth_plugins') | |||
|
75 | enabled_plugins = auth_plugins.app_settings_value | |||
|
76 | legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)] | |||
|
77 | ||||
|
78 | for plugin_id in legacy_plugins: | |||
|
79 | log.debug('Legacy plugin discovered: "%s"', plugin_id) | |||
|
80 | try: | |||
|
81 | plugin = _import_legacy_plugin(plugin_id) | |||
|
82 | config.include(plugin.includeme) | |||
|
83 | except Exception as e: | |||
|
84 | log.exception( | |||
|
85 | 'Exception while loading legacy authentication plugin ' | |||
|
86 | '"{}": {}'.format(plugin_id, e.message)) | |||
51 |
|
87 | |||
52 |
|
88 | |||
53 | def includeme(config): |
|
89 | def includeme(config): | |
@@ -56,7 +92,7 b' def includeme(config):' | |||||
56 | config.set_authentication_policy(authn_policy) |
|
92 | config.set_authentication_policy(authn_policy) | |
57 |
|
93 | |||
58 | # Create authentication plugin registry and add it to the pyramid registry. |
|
94 | # Create authentication plugin registry and add it to the pyramid registry. | |
59 | authn_registry = AuthenticationPluginRegistry() |
|
95 | authn_registry = AuthenticationPluginRegistry(config.get_settings()) | |
60 | config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin) |
|
96 | config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin) | |
61 | config.registry.registerUtility(authn_registry) |
|
97 | config.registry.registerUtility(authn_registry) | |
62 |
|
98 | |||
@@ -83,3 +119,4 b' def includeme(config):' | |||||
83 |
|
119 | |||
84 | # Auto discover authentication plugins and include their configuration. |
|
120 | # Auto discover authentication plugins and include their configuration. | |
85 | _discover_plugins(config) |
|
121 | _discover_plugins(config) | |
|
122 | _discover_legacy_plugins(config) |
@@ -25,24 +25,18 b' Authentication modules' | |||||
25 | import logging |
|
25 | import logging | |
26 | import time |
|
26 | import time | |
27 | import traceback |
|
27 | import traceback | |
|
28 | import warnings | |||
28 |
|
29 | |||
29 | from authomatic import Authomatic |
|
|||
30 | from authomatic.adapters import WebObAdapter |
|
|||
31 | from authomatic.providers import oauth2, oauth1 |
|
|||
32 | from pylons import url |
|
|||
33 | from pylons.controllers.util import Response |
|
|||
34 | from pylons.i18n.translation import _ |
|
|||
35 | from pyramid.threadlocal import get_current_registry |
|
30 | from pyramid.threadlocal import get_current_registry | |
36 | from sqlalchemy.ext.hybrid import hybrid_property |
|
31 | from sqlalchemy.ext.hybrid import hybrid_property | |
37 |
|
32 | |||
38 | import rhodecode.lib.helpers as h |
|
|||
39 | from rhodecode.authentication.interface import IAuthnPluginRegistry |
|
33 | from rhodecode.authentication.interface import IAuthnPluginRegistry | |
40 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
41 | from rhodecode.lib import caches |
|
35 | from rhodecode.lib import caches | |
42 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt |
|
36 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt | |
43 | from rhodecode.lib.utils2 import md5_safe, safe_int |
|
37 | from rhodecode.lib.utils2 import md5_safe, safe_int | |
44 | from rhodecode.lib.utils2 import safe_str |
|
38 | from rhodecode.lib.utils2 import safe_str | |
45 |
from rhodecode.model.db import User |
|
39 | from rhodecode.model.db import User | |
46 | from rhodecode.model.meta import Session |
|
40 | from rhodecode.model.meta import Session | |
47 | from rhodecode.model.settings import SettingsModel |
|
41 | from rhodecode.model.settings import SettingsModel | |
48 | from rhodecode.model.user import UserModel |
|
42 | from rhodecode.model.user import UserModel | |
@@ -226,17 +220,23 b' class RhodeCodeAuthPluginBase(object):' | |||||
226 | """ |
|
220 | """ | |
227 | raise NotImplementedError("Not implemented in base class") |
|
221 | raise NotImplementedError("Not implemented in base class") | |
228 |
|
222 | |||
|
223 | @property | |||
|
224 | def is_headers_auth(self): | |||
|
225 | """ | |||
|
226 | Returns True if this authentication plugin uses HTTP headers as | |||
|
227 | authentication method. | |||
|
228 | """ | |||
|
229 | return False | |||
|
230 | ||||
229 | @hybrid_property |
|
231 | @hybrid_property | |
230 | def is_container_auth(self): |
|
232 | def is_container_auth(self): | |
231 | """ |
|
233 | """ | |
232 | Returns bool if this module uses container auth. |
|
234 | Deprecated method that indicates if this authentication plugin uses | |
233 |
|
235 | HTTP headers as authentication method. | ||
234 | This property will trigger an automatic call to authenticate on |
|
|||
235 | a visit to the website or during a push/pull. |
|
|||
236 |
|
||||
237 | :returns: bool |
|
|||
238 | """ |
|
236 | """ | |
239 | return False |
|
237 | warnings.warn( | |
|
238 | 'Use is_headers_auth instead.', category=DeprecationWarning) | |||
|
239 | return self.is_headers_auth | |||
240 |
|
240 | |||
241 | @hybrid_property |
|
241 | @hybrid_property | |
242 | def allows_creating_users(self): |
|
242 | def allows_creating_users(self): | |
@@ -299,7 +299,7 b' class RhodeCodeAuthPluginBase(object):' | |||||
299 | """ |
|
299 | """ | |
300 | Helper method for user fetching in plugins, by default it's using |
|
300 | Helper method for user fetching in plugins, by default it's using | |
301 | simple fetch by username, but this method can be custimized in plugins |
|
301 | simple fetch by username, but this method can be custimized in plugins | |
302 |
eg. |
|
302 | eg. headers auth plugin to fetch user by environ params | |
303 |
|
303 | |||
304 | :param username: username if given to fetch from database |
|
304 | :param username: username if given to fetch from database | |
305 | :param kwargs: extra arguments needed for user fetching. |
|
305 | :param kwargs: extra arguments needed for user fetching. | |
@@ -477,131 +477,11 b' class RhodeCodeExternalAuthPlugin(RhodeC' | |||||
477 | return auth |
|
477 | return auth | |
478 |
|
478 | |||
479 |
|
479 | |||
480 | class AuthomaticBase(RhodeCodeExternalAuthPlugin): |
|
|||
481 |
|
||||
482 | # TODO: Think about how to create and store this secret string. |
|
|||
483 | # We need the secret for the authomatic library. It needs to be the same |
|
|||
484 | # across requests. |
|
|||
485 | def _get_authomatic_secret(self, length=40): |
|
|||
486 | secret = self.get_setting_by_name('secret') |
|
|||
487 | if secret is None or secret == 'None' or secret == '': |
|
|||
488 | from Crypto import Random, Hash |
|
|||
489 | secret_bytes = Random.new().read(length) |
|
|||
490 | secret_hash = Hash.SHA256.new() |
|
|||
491 | secret_hash.update(secret_bytes) |
|
|||
492 | secret = secret_hash.hexdigest() |
|
|||
493 | self.create_or_update_setting('secret', secret) |
|
|||
494 | Session.commit() |
|
|||
495 | secret = self.get_setting_by_name('secret') |
|
|||
496 | return secret |
|
|||
497 |
|
||||
498 | def get_authomatic(self): |
|
|||
499 | scope = [] |
|
|||
500 | if self.name == 'bitbucket': |
|
|||
501 | provider_class = oauth1.Bitbucket |
|
|||
502 | scope = ['account', 'email', 'repository', 'issue', 'issue:write'] |
|
|||
503 | elif self.name == 'github': |
|
|||
504 | provider_class = oauth2.GitHub |
|
|||
505 | scope = ['repo', 'public_repo', 'user:email'] |
|
|||
506 | elif self.name == 'google': |
|
|||
507 | provider_class = oauth2.Google |
|
|||
508 | scope = ['profile', 'email'] |
|
|||
509 | elif self.name == 'twitter': |
|
|||
510 | provider_class = oauth1.Twitter |
|
|||
511 |
|
||||
512 | authomatic_conf = { |
|
|||
513 | self.name: { |
|
|||
514 | 'class_': provider_class, |
|
|||
515 | 'consumer_key': self.get_setting_by_name('consumer_key'), |
|
|||
516 | 'consumer_secret': self.get_setting_by_name('consumer_secret'), |
|
|||
517 | 'scope': scope, |
|
|||
518 | 'access_headers': {'User-Agent': 'TestAppAgent'}, |
|
|||
519 | } |
|
|||
520 | } |
|
|||
521 | secret = self._get_authomatic_secret() |
|
|||
522 | return Authomatic(config=authomatic_conf, |
|
|||
523 | secret=secret) |
|
|||
524 |
|
||||
525 | def get_provider_result(self, request): |
|
|||
526 | """ |
|
|||
527 | Provides `authomatic.core.LoginResult` for provider and request |
|
|||
528 |
|
||||
529 | :param provider_name: |
|
|||
530 | :param request: |
|
|||
531 | :param config: |
|
|||
532 | :return: |
|
|||
533 | """ |
|
|||
534 | response = Response() |
|
|||
535 | adapter = WebObAdapter(request, response) |
|
|||
536 | authomatic_inst = self.get_authomatic() |
|
|||
537 | return authomatic_inst.login(adapter, self.name), response |
|
|||
538 |
|
||||
539 | def handle_social_data(self, session, user_id, social_data): |
|
|||
540 | """ |
|
|||
541 | Updates user tokens in database whenever necessary |
|
|||
542 | :param request: |
|
|||
543 | :param user: |
|
|||
544 | :param social_data: |
|
|||
545 | :return: |
|
|||
546 | """ |
|
|||
547 | if not self.is_active(): |
|
|||
548 | h.flash(_('This provider is currently disabled'), |
|
|||
549 | category='warning') |
|
|||
550 | return False |
|
|||
551 |
|
||||
552 | social_data = social_data |
|
|||
553 | update_identity = False |
|
|||
554 |
|
||||
555 | existing_row = ExternalIdentity.by_external_id_and_provider( |
|
|||
556 | social_data['user']['id'], |
|
|||
557 | social_data['credentials.provider'] |
|
|||
558 | ) |
|
|||
559 |
|
||||
560 | if existing_row: |
|
|||
561 | Session().delete(existing_row) |
|
|||
562 | update_identity = True |
|
|||
563 |
|
||||
564 | if not existing_row or update_identity: |
|
|||
565 | if not update_identity: |
|
|||
566 | h.flash(_('Your external identity is now ' |
|
|||
567 | 'connected with your account'), category='success') |
|
|||
568 |
|
||||
569 | if not social_data['user']['id']: |
|
|||
570 | h.flash(_('No external user id found? Perhaps permissions' |
|
|||
571 | 'for authentication are set incorrectly'), |
|
|||
572 | category='error') |
|
|||
573 | return False |
|
|||
574 |
|
||||
575 | ex_identity = ExternalIdentity() |
|
|||
576 | ex_identity.external_id = social_data['user']['id'] |
|
|||
577 | ex_identity.external_username = social_data['user']['user_name'] |
|
|||
578 | ex_identity.provider_name = social_data['credentials.provider'] |
|
|||
579 | ex_identity.access_token = social_data['credentials.token'] |
|
|||
580 | ex_identity.token_secret = social_data['credentials.token_secret'] |
|
|||
581 | ex_identity.alt_token = social_data['credentials.refresh_token'] |
|
|||
582 | ex_identity.local_user_id = user_id |
|
|||
583 | Session().add(ex_identity) |
|
|||
584 | session.pop('rhodecode.social_auth', None) |
|
|||
585 | return ex_identity |
|
|||
586 |
|
||||
587 | def callback_url(self): |
|
|||
588 | try: |
|
|||
589 | return url('social_auth', provider_name=self.name, qualified=True) |
|
|||
590 | except TypeError: |
|
|||
591 | pass |
|
|||
592 | return '' |
|
|||
593 |
|
||||
594 |
|
||||
595 | def loadplugin(plugin_id): |
|
480 | def loadplugin(plugin_id): | |
596 | """ |
|
481 | """ | |
597 | Loads and returns an instantiated authentication plugin. |
|
482 | Loads and returns an instantiated authentication plugin. | |
598 | Returns the RhodeCodeAuthPluginBase subclass on success, |
|
483 | Returns the RhodeCodeAuthPluginBase subclass on success, | |
599 |
|
|
484 | or None on failure. | |
600 |
|
||||
601 | raises: |
|
|||
602 | KeyError -- if no plugin available with given name |
|
|||
603 | TypeError -- if the RhodeCodeAuthPlugin is not a subclass of |
|
|||
604 | ours RhodeCodeAuthPluginBase |
|
|||
605 | """ |
|
485 | """ | |
606 | # TODO: Disusing pyramids thread locals to retrieve the registry. |
|
486 | # TODO: Disusing pyramids thread locals to retrieve the registry. | |
607 | authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) |
|
487 | authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) | |
@@ -622,9 +502,9 b' def authenticate(username, password, env' | |||||
622 | Authentication function used for access control, |
|
502 | Authentication function used for access control, | |
623 | It tries to authenticate based on enabled authentication modules. |
|
503 | It tries to authenticate based on enabled authentication modules. | |
624 |
|
504 | |||
625 |
:param username: username can be empty for |
|
505 | :param username: username can be empty for headers auth | |
626 |
:param password: password can be empty for |
|
506 | :param password: password can be empty for headers auth | |
627 |
:param environ: environ headers passed for |
|
507 | :param environ: environ headers passed for headers auth | |
628 | :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE` |
|
508 | :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE` | |
629 | :param skip_missing: ignores plugins that are in db but not in environment |
|
509 | :param skip_missing: ignores plugins that are in db but not in environment | |
630 | :returns: None if auth failed, plugin_user dict if auth is correct |
|
510 | :returns: None if auth failed, plugin_user dict if auth is correct | |
@@ -632,51 +512,41 b' def authenticate(username, password, env' | |||||
632 | if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]: |
|
512 | if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]: | |
633 | raise ValueError('auth type must be on of http, vcs got "%s" instead' |
|
513 | raise ValueError('auth type must be on of http, vcs got "%s" instead' | |
634 | % auth_type) |
|
514 | % auth_type) | |
635 |
|
|
515 | headers_only = environ and not (username and password) | |
636 | auth_plugins = SettingsModel().get_auth_plugins() |
|
|||
637 | for plugin_id in auth_plugins: |
|
|||
638 | plugin = loadplugin(plugin_id) |
|
|||
639 |
|
516 | |||
640 | if plugin is None: |
|
517 | authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) | |
641 | log.warning('Authentication plugin missing: "{}"'.format( |
|
518 | for plugin in authn_registry.get_plugins_for_authentication(): | |
642 | plugin_id)) |
|
|||
643 | continue |
|
|||
644 |
|
||||
645 | if not plugin.is_active(): |
|
|||
646 | log.info('Authentication plugin is inactive: "{}"'.format( |
|
|||
647 | plugin_id)) |
|
|||
648 | continue |
|
|||
649 |
|
||||
650 | plugin.set_auth_type(auth_type) |
|
519 | plugin.set_auth_type(auth_type) | |
651 | user = plugin.get_user(username) |
|
520 | user = plugin.get_user(username) | |
652 | display_user = user.username if user else username |
|
521 | display_user = user.username if user else username | |
653 |
|
522 | |||
654 |
if |
|
523 | if headers_only and not plugin.is_headers_auth: | |
655 |
log.debug('Auth type is for |
|
524 | log.debug('Auth type is for headers only and plugin `%s` is not ' | |
656 |
' |
|
525 | 'headers plugin, skipping...', plugin.get_id()) | |
657 | continue |
|
526 | continue | |
658 |
|
527 | |||
659 | # load plugin settings from RhodeCode database |
|
528 | # load plugin settings from RhodeCode database | |
660 | plugin_settings = plugin.get_settings() |
|
529 | plugin_settings = plugin.get_settings() | |
661 | log.debug('Plugin settings:%s', plugin_settings) |
|
530 | log.debug('Plugin settings:%s', plugin_settings) | |
662 |
|
531 | |||
663 | log.debug('Trying authentication using ** %s **', plugin_id) |
|
532 | log.debug('Trying authentication using ** %s **', plugin.get_id()) | |
664 | # use plugin's method of user extraction. |
|
533 | # use plugin's method of user extraction. | |
665 | user = plugin.get_user(username, environ=environ, |
|
534 | user = plugin.get_user(username, environ=environ, | |
666 | settings=plugin_settings) |
|
535 | settings=plugin_settings) | |
667 | display_user = user.username if user else username |
|
536 | display_user = user.username if user else username | |
668 | log.debug('Plugin %s extracted user is `%s`', plugin_id, display_user) |
|
537 | log.debug( | |
|
538 | 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user) | |||
669 |
|
539 | |||
670 | if not plugin.allows_authentication_from(user): |
|
540 | if not plugin.allows_authentication_from(user): | |
671 | log.debug('Plugin %s does not accept user `%s` for authentication', |
|
541 | log.debug('Plugin %s does not accept user `%s` for authentication', | |
672 | plugin_id, display_user) |
|
542 | plugin.get_id(), display_user) | |
673 | continue |
|
543 | continue | |
674 | else: |
|
544 | else: | |
675 | log.debug('Plugin %s accepted user `%s` for authentication', |
|
545 | log.debug('Plugin %s accepted user `%s` for authentication', | |
676 | plugin_id, display_user) |
|
546 | plugin.get_id(), display_user) | |
677 |
|
547 | |||
678 | log.info('Authenticating user `%s` using %s plugin', |
|
548 | log.info('Authenticating user `%s` using %s plugin', | |
679 | display_user, plugin_id) |
|
549 | display_user, plugin.get_id()) | |
680 |
|
550 | |||
681 | _cache_ttl = 0 |
|
551 | _cache_ttl = 0 | |
682 |
|
552 | |||
@@ -691,7 +561,7 b' def authenticate(username, password, env' | |||||
691 | # get instance of cache manager configured for a namespace |
|
561 | # get instance of cache manager configured for a namespace | |
692 | cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl) |
|
562 | cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl) | |
693 |
|
563 | |||
694 | log.debug('Cache for plugin `%s` active: %s', plugin_id, |
|
564 | log.debug('Cache for plugin `%s` active: %s', plugin.get_id(), | |
695 | plugin_cache_active) |
|
565 | plugin_cache_active) | |
696 |
|
566 | |||
697 | # for environ based password can be empty, but then the validation is |
|
567 | # for environ based password can be empty, but then the validation is | |
@@ -706,7 +576,7 b' def authenticate(username, password, env' | |||||
706 | # then auth is correct. |
|
576 | # then auth is correct. | |
707 | start = time.time() |
|
577 | start = time.time() | |
708 | log.debug('Running plugin `%s` _authenticate method', |
|
578 | log.debug('Running plugin `%s` _authenticate method', | |
709 | plugin_id) |
|
579 | plugin.get_id()) | |
710 |
|
580 | |||
711 | def auth_func(): |
|
581 | def auth_func(): | |
712 | """ |
|
582 | """ | |
@@ -726,7 +596,7 b' def authenticate(username, password, env' | |||||
726 | auth_time = time.time() - start |
|
596 | auth_time = time.time() - start | |
727 | log.debug('Authentication for plugin `%s` completed in %.3fs, ' |
|
597 | log.debug('Authentication for plugin `%s` completed in %.3fs, ' | |
728 | 'expiration time of fetched cache %.1fs.', |
|
598 | 'expiration time of fetched cache %.1fs.', | |
729 | plugin_id, auth_time, _cache_ttl) |
|
599 | plugin.get_id(), auth_time, _cache_ttl) | |
730 |
|
600 | |||
731 | log.debug('PLUGIN USER DATA: %s', plugin_user) |
|
601 | log.debug('PLUGIN USER DATA: %s', plugin_user) | |
732 |
|
602 | |||
@@ -735,5 +605,5 b' def authenticate(username, password, env' | |||||
735 | return plugin_user |
|
605 | return plugin_user | |
736 | # we failed to Auth because .auth() method didn't return proper user |
|
606 | # we failed to Auth because .auth() method didn't return proper user | |
737 | log.debug("User `%s` failed to authenticate against %s", |
|
607 | log.debug("User `%s` failed to authenticate against %s", | |
738 | display_user, plugin_id) |
|
608 | display_user, plugin.get_id()) | |
739 | return None |
|
609 | return None |
@@ -34,6 +34,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||||
34 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
34 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |
35 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
35 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
36 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
36 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
37 | from rhodecode.lib.colander_utils import strip_whitespace | |||
37 | from rhodecode.lib.ext_json import json, formatted_json |
|
38 | from rhodecode.lib.ext_json import json, formatted_json | |
38 | from rhodecode.model.db import User |
|
39 | from rhodecode.model.db import User | |
39 |
|
40 | |||
@@ -58,12 +59,14 b' class CrowdSettingsSchema(AuthnPluginSet' | |||||
58 | colander.String(), |
|
59 | colander.String(), | |
59 | default='127.0.0.1', |
|
60 | default='127.0.0.1', | |
60 | description=_('The FQDN or IP of the Atlassian CROWD Server'), |
|
61 | description=_('The FQDN or IP of the Atlassian CROWD Server'), | |
|
62 | preparer=strip_whitespace, | |||
61 | title=_('Host'), |
|
63 | title=_('Host'), | |
62 | widget='string') |
|
64 | widget='string') | |
63 | port = colander.SchemaNode( |
|
65 | port = colander.SchemaNode( | |
64 | colander.Int(), |
|
66 | colander.Int(), | |
65 | default=8095, |
|
67 | default=8095, | |
66 | description=_('The Port in use by the Atlassian CROWD Server'), |
|
68 | description=_('The Port in use by the Atlassian CROWD Server'), | |
|
69 | preparer=strip_whitespace, | |||
67 | title=_('Port'), |
|
70 | title=_('Port'), | |
68 | validator=colander.Range(min=0, max=65536), |
|
71 | validator=colander.Range(min=0, max=65536), | |
69 | widget='int') |
|
72 | widget='int') | |
@@ -71,12 +74,14 b' class CrowdSettingsSchema(AuthnPluginSet' | |||||
71 | colander.String(), |
|
74 | colander.String(), | |
72 | default='', |
|
75 | default='', | |
73 | description=_('The Application Name to authenticate to CROWD'), |
|
76 | description=_('The Application Name to authenticate to CROWD'), | |
|
77 | preparer=strip_whitespace, | |||
74 | title=_('Application Name'), |
|
78 | title=_('Application Name'), | |
75 | widget='string') |
|
79 | widget='string') | |
76 | app_password = colander.SchemaNode( |
|
80 | app_password = colander.SchemaNode( | |
77 | colander.String(), |
|
81 | colander.String(), | |
78 | default='', |
|
82 | default='', | |
79 | description=_('The password to authenticate to CROWD'), |
|
83 | description=_('The password to authenticate to CROWD'), | |
|
84 | preparer=strip_whitespace, | |||
80 | title=_('Application Password'), |
|
85 | title=_('Application Password'), | |
81 | widget='password') |
|
86 | widget='password') | |
82 | admin_groups = colander.SchemaNode( |
|
87 | admin_groups = colander.SchemaNode( | |
@@ -85,6 +90,7 b' class CrowdSettingsSchema(AuthnPluginSet' | |||||
85 | description=_('A comma separated list of group names that identify ' |
|
90 | description=_('A comma separated list of group names that identify ' | |
86 | 'users as RhodeCode Administrators'), |
|
91 | 'users as RhodeCode Administrators'), | |
87 | missing='', |
|
92 | missing='', | |
|
93 | preparer=strip_whitespace, | |||
88 | title=_('Admin Groups'), |
|
94 | title=_('Admin Groups'), | |
89 | widget='string') |
|
95 | widget='string') | |
90 |
|
96 | |||
@@ -191,12 +197,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||||
191 | config.add_view( |
|
197 | config.add_view( | |
192 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
198 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
193 | attr='settings_get', |
|
199 | attr='settings_get', | |
|
200 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
194 | request_method='GET', |
|
201 | request_method='GET', | |
195 | route_name='auth_home', |
|
202 | route_name='auth_home', | |
196 | context=CrowdAuthnResource) |
|
203 | context=CrowdAuthnResource) | |
197 | config.add_view( |
|
204 | config.add_view( | |
198 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
205 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
199 | attr='settings_post', |
|
206 | attr='settings_post', | |
|
207 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
200 | request_method='POST', |
|
208 | request_method='POST', | |
201 | route_name='auth_home', |
|
209 | route_name='auth_home', | |
202 | context=CrowdAuthnResource) |
|
210 | context=CrowdAuthnResource) |
@@ -36,6 +36,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||||
36 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
36 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |
37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
38 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
38 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
39 | from rhodecode.lib.colander_utils import strip_whitespace | |||
39 | from rhodecode.lib.utils2 import safe_unicode |
|
40 | from rhodecode.lib.utils2 import safe_unicode | |
40 | from rhodecode.model.db import User |
|
41 | from rhodecode.model.db import User | |
41 |
|
42 | |||
@@ -60,6 +61,7 b' class JasigCasSettingsSchema(AuthnPlugin' | |||||
60 | colander.String(), |
|
61 | colander.String(), | |
61 | default='https://domain.com/cas/v1/tickets', |
|
62 | default='https://domain.com/cas/v1/tickets', | |
62 | description=_('The url of the Jasig CAS REST service'), |
|
63 | description=_('The url of the Jasig CAS REST service'), | |
|
64 | preparer=strip_whitespace, | |||
63 | title=_('URL'), |
|
65 | title=_('URL'), | |
64 | widget='string') |
|
66 | widget='string') | |
65 |
|
67 | |||
@@ -72,12 +74,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||||
72 | config.add_view( |
|
74 | config.add_view( | |
73 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
75 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
74 | attr='settings_get', |
|
76 | attr='settings_get', | |
|
77 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
75 | request_method='GET', |
|
78 | request_method='GET', | |
76 | route_name='auth_home', |
|
79 | route_name='auth_home', | |
77 | context=JasigCasAuthnResource) |
|
80 | context=JasigCasAuthnResource) | |
78 | config.add_view( |
|
81 | config.add_view( | |
79 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
82 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
80 | attr='settings_post', |
|
83 | attr='settings_post', | |
|
84 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
81 | request_method='POST', |
|
85 | request_method='POST', | |
82 | route_name='auth_home', |
|
86 | route_name='auth_home', | |
83 | context=JasigCasAuthnResource) |
|
87 | context=JasigCasAuthnResource) | |
@@ -92,8 +96,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||||
92 | def name(self): |
|
96 | def name(self): | |
93 | return "jasig-cas" |
|
97 | return "jasig-cas" | |
94 |
|
98 | |||
95 |
@ |
|
99 | @property | |
96 |
def is_ |
|
100 | def is_headers_auth(self): | |
97 | return True |
|
101 | return True | |
98 |
|
102 | |||
99 | def use_fake_password(self): |
|
103 | def use_fake_password(self): |
@@ -33,6 +33,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||||
33 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
33 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |
34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
35 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
35 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
36 | from rhodecode.lib.colander_utils import strip_whitespace | |||
36 | from rhodecode.lib.exceptions import ( |
|
37 | from rhodecode.lib.exceptions import ( | |
37 | LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError |
|
38 | LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError | |
38 | ) |
|
39 | ) | |
@@ -45,8 +46,9 b' log = logging.getLogger(__name__)' | |||||
45 | try: |
|
46 | try: | |
46 | import ldap |
|
47 | import ldap | |
47 | except ImportError: |
|
48 | except ImportError: | |
48 | # means that python-ldap is not installed |
|
49 | # means that python-ldap is not installed, we use Missing object to mark | |
49 |
|
|
50 | # ldap lib is Missing | |
|
51 | ldap = Missing | |||
50 |
|
52 | |||
51 |
|
53 | |||
52 | def plugin_factory(plugin_id, *args, **kwds): |
|
54 | def plugin_factory(plugin_id, *args, **kwds): | |
@@ -71,12 +73,14 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
71 | colander.String(), |
|
73 | colander.String(), | |
72 | default='', |
|
74 | default='', | |
73 | description=_('Host of the LDAP Server'), |
|
75 | description=_('Host of the LDAP Server'), | |
|
76 | preparer=strip_whitespace, | |||
74 | title=_('LDAP Host'), |
|
77 | title=_('LDAP Host'), | |
75 | widget='string') |
|
78 | widget='string') | |
76 | port = colander.SchemaNode( |
|
79 | port = colander.SchemaNode( | |
77 | colander.Int(), |
|
80 | colander.Int(), | |
78 | default=389, |
|
81 | default=389, | |
79 | description=_('Port that the LDAP server is listening on'), |
|
82 | description=_('Port that the LDAP server is listening on'), | |
|
83 | preparer=strip_whitespace, | |||
80 | title=_('Port'), |
|
84 | title=_('Port'), | |
81 | validator=colander.Range(min=0, max=65536), |
|
85 | validator=colander.Range(min=0, max=65536), | |
82 | widget='int') |
|
86 | widget='int') | |
@@ -85,6 +89,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
85 | default='', |
|
89 | default='', | |
86 | description=_('User to connect to LDAP'), |
|
90 | description=_('User to connect to LDAP'), | |
87 | missing='', |
|
91 | missing='', | |
|
92 | preparer=strip_whitespace, | |||
88 | title=_('Account'), |
|
93 | title=_('Account'), | |
89 | widget='string') |
|
94 | widget='string') | |
90 | dn_pass = colander.SchemaNode( |
|
95 | dn_pass = colander.SchemaNode( | |
@@ -92,6 +97,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
92 | default='', |
|
97 | default='', | |
93 | description=_('Password to connect to LDAP'), |
|
98 | description=_('Password to connect to LDAP'), | |
94 | missing='', |
|
99 | missing='', | |
|
100 | preparer=strip_whitespace, | |||
95 | title=_('Password'), |
|
101 | title=_('Password'), | |
96 | widget='password') |
|
102 | widget='password') | |
97 | tls_kind = colander.SchemaNode( |
|
103 | tls_kind = colander.SchemaNode( | |
@@ -113,6 +119,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
113 | default='', |
|
119 | default='', | |
114 | description=_('Base DN to search (e.g., dc=mydomain,dc=com)'), |
|
120 | description=_('Base DN to search (e.g., dc=mydomain,dc=com)'), | |
115 | missing='', |
|
121 | missing='', | |
|
122 | preparer=strip_whitespace, | |||
116 | title=_('Base DN'), |
|
123 | title=_('Base DN'), | |
117 | widget='string') |
|
124 | widget='string') | |
118 | filter = colander.SchemaNode( |
|
125 | filter = colander.SchemaNode( | |
@@ -120,6 +127,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
120 | default='', |
|
127 | default='', | |
121 | description=_('Filter to narrow results (e.g., ou=Users, etc)'), |
|
128 | description=_('Filter to narrow results (e.g., ou=Users, etc)'), | |
122 | missing='', |
|
129 | missing='', | |
|
130 | preparer=strip_whitespace, | |||
123 | title=_('LDAP Search Filter'), |
|
131 | title=_('LDAP Search Filter'), | |
124 | widget='string') |
|
132 | widget='string') | |
125 | search_scope = colander.SchemaNode( |
|
133 | search_scope = colander.SchemaNode( | |
@@ -133,14 +141,16 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
133 | colander.String(), |
|
141 | colander.String(), | |
134 | default='', |
|
142 | default='', | |
135 | description=_('LDAP Attribute to map to user name'), |
|
143 | description=_('LDAP Attribute to map to user name'), | |
|
144 | missing_msg=_('The LDAP Login attribute of the CN must be specified'), | |||
|
145 | preparer=strip_whitespace, | |||
136 | title=_('Login Attribute'), |
|
146 | title=_('Login Attribute'), | |
137 | missing_msg=_('The LDAP Login attribute of the CN must be specified'), |
|
|||
138 | widget='string') |
|
147 | widget='string') | |
139 | attr_firstname = colander.SchemaNode( |
|
148 | attr_firstname = colander.SchemaNode( | |
140 | colander.String(), |
|
149 | colander.String(), | |
141 | default='', |
|
150 | default='', | |
142 | description=_('LDAP Attribute to map to first name'), |
|
151 | description=_('LDAP Attribute to map to first name'), | |
143 | missing='', |
|
152 | missing='', | |
|
153 | preparer=strip_whitespace, | |||
144 | title=_('First Name Attribute'), |
|
154 | title=_('First Name Attribute'), | |
145 | widget='string') |
|
155 | widget='string') | |
146 | attr_lastname = colander.SchemaNode( |
|
156 | attr_lastname = colander.SchemaNode( | |
@@ -148,6 +158,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
148 | default='', |
|
158 | default='', | |
149 | description=_('LDAP Attribute to map to last name'), |
|
159 | description=_('LDAP Attribute to map to last name'), | |
150 | missing='', |
|
160 | missing='', | |
|
161 | preparer=strip_whitespace, | |||
151 | title=_('Last Name Attribute'), |
|
162 | title=_('Last Name Attribute'), | |
152 | widget='string') |
|
163 | widget='string') | |
153 | attr_email = colander.SchemaNode( |
|
164 | attr_email = colander.SchemaNode( | |
@@ -155,6 +166,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
155 | default='', |
|
166 | default='', | |
156 | description=_('LDAP Attribute to map to email address'), |
|
167 | description=_('LDAP Attribute to map to email address'), | |
157 | missing='', |
|
168 | missing='', | |
|
169 | preparer=strip_whitespace, | |||
158 | title=_('Email Attribute'), |
|
170 | title=_('Email Attribute'), | |
159 | widget='string') |
|
171 | widget='string') | |
160 |
|
172 | |||
@@ -171,7 +183,7 b' class AuthLdap(object):' | |||||
171 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, |
|
183 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, | |
172 | search_scope='SUBTREE', attr_login='uid', |
|
184 | search_scope='SUBTREE', attr_login='uid', | |
173 | ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'): |
|
185 | ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'): | |
174 |
if |
|
186 | if ldap == Missing: | |
175 | raise LdapImportError("Missing or incompatible ldap library") |
|
187 | raise LdapImportError("Missing or incompatible ldap library") | |
176 |
|
188 | |||
177 | self.ldap_version = ldap_version |
|
189 | self.ldap_version = ldap_version | |
@@ -317,12 +329,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||||
317 | config.add_view( |
|
329 | config.add_view( | |
318 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
330 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
319 | attr='settings_get', |
|
331 | attr='settings_get', | |
|
332 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
320 | request_method='GET', |
|
333 | request_method='GET', | |
321 | route_name='auth_home', |
|
334 | route_name='auth_home', | |
322 | context=LdapAuthnResource) |
|
335 | context=LdapAuthnResource) | |
323 | config.add_view( |
|
336 | config.add_view( | |
324 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
337 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
325 | attr='settings_post', |
|
338 | attr='settings_post', | |
|
339 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
326 | request_method='POST', |
|
340 | request_method='POST', | |
327 | route_name='auth_home', |
|
341 | route_name='auth_home', | |
328 | context=LdapAuthnResource) |
|
342 | context=LdapAuthnResource) |
@@ -35,6 +35,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||||
35 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
35 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |
36 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
36 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
37 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
37 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
38 | from rhodecode.lib.colander_utils import strip_whitespace | |||
38 |
|
39 | |||
39 | log = logging.getLogger(__name__) |
|
40 | log = logging.getLogger(__name__) | |
40 |
|
41 | |||
@@ -57,6 +58,7 b' class PamSettingsSchema(AuthnPluginSetti' | |||||
57 | colander.String(), |
|
58 | colander.String(), | |
58 | default='login', |
|
59 | default='login', | |
59 | description=_('PAM service name to use for authentication.'), |
|
60 | description=_('PAM service name to use for authentication.'), | |
|
61 | preparer=strip_whitespace, | |||
60 | title=_('PAM service name'), |
|
62 | title=_('PAM service name'), | |
61 | widget='string') |
|
63 | widget='string') | |
62 | gecos = colander.SchemaNode( |
|
64 | gecos = colander.SchemaNode( | |
@@ -64,6 +66,7 b' class PamSettingsSchema(AuthnPluginSetti' | |||||
64 | default='(?P<last_name>.+),\s*(?P<first_name>\w+)', |
|
66 | default='(?P<last_name>.+),\s*(?P<first_name>\w+)', | |
65 | description=_('Regular expression for extracting user name/email etc. ' |
|
67 | description=_('Regular expression for extracting user name/email etc. ' | |
66 | 'from Unix userinfo.'), |
|
68 | 'from Unix userinfo.'), | |
|
69 | preparer=strip_whitespace, | |||
67 | title=_('Gecos Regex'), |
|
70 | title=_('Gecos Regex'), | |
68 | widget='string') |
|
71 | widget='string') | |
69 |
|
72 | |||
@@ -79,12 +82,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||||
79 | config.add_view( |
|
82 | config.add_view( | |
80 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
83 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
81 | attr='settings_get', |
|
84 | attr='settings_get', | |
|
85 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
82 | request_method='GET', |
|
86 | request_method='GET', | |
83 | route_name='auth_home', |
|
87 | route_name='auth_home', | |
84 | context=PamAuthnResource) |
|
88 | context=PamAuthnResource) | |
85 | config.add_view( |
|
89 | config.add_view( | |
86 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
90 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
87 | attr='settings_post', |
|
91 | attr='settings_post', | |
|
92 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
88 | request_method='POST', |
|
93 | request_method='POST', | |
89 | route_name='auth_home', |
|
94 | route_name='auth_home', | |
90 | context=PamAuthnResource) |
|
95 | context=PamAuthnResource) |
@@ -52,12 +52,14 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP' | |||||
52 | config.add_view( |
|
52 | config.add_view( | |
53 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
53 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
54 | attr='settings_get', |
|
54 | attr='settings_get', | |
|
55 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
55 | request_method='GET', |
|
56 | request_method='GET', | |
56 | route_name='auth_home', |
|
57 | route_name='auth_home', | |
57 | context=RhodecodeAuthnResource) |
|
58 | context=RhodecodeAuthnResource) | |
58 | config.add_view( |
|
59 | config.add_view( | |
59 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
60 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
60 | attr='settings_post', |
|
61 | attr='settings_post', | |
|
62 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |||
61 | request_method='POST', |
|
63 | request_method='POST', | |
62 | route_name='auth_home', |
|
64 | route_name='auth_home', | |
63 | context=RhodecodeAuthnResource) |
|
65 | context=RhodecodeAuthnResource) |
@@ -25,14 +25,20 b' from zope.interface import implementer' | |||||
25 |
|
25 | |||
26 | from rhodecode.authentication.interface import IAuthnPluginRegistry |
|
26 | from rhodecode.authentication.interface import IAuthnPluginRegistry | |
27 | from rhodecode.lib.utils2 import safe_str |
|
27 | from rhodecode.lib.utils2 import safe_str | |
|
28 | from rhodecode.model.settings import SettingsModel | |||
28 |
|
29 | |||
29 | log = logging.getLogger(__name__) |
|
30 | log = logging.getLogger(__name__) | |
30 |
|
31 | |||
31 |
|
32 | |||
32 | @implementer(IAuthnPluginRegistry) |
|
33 | @implementer(IAuthnPluginRegistry) | |
33 | class AuthenticationPluginRegistry(object): |
|
34 | class AuthenticationPluginRegistry(object): | |
34 | def __init__(self): |
|
35 | ||
|
36 | # INI settings key to set a fallback authentication plugin. | |||
|
37 | fallback_plugin_key = 'rhodecode.auth_plugin_fallback' | |||
|
38 | ||||
|
39 | def __init__(self, settings): | |||
35 | self._plugins = {} |
|
40 | self._plugins = {} | |
|
41 | self._fallback_plugin = settings.get(self.fallback_plugin_key, None) | |||
36 |
|
42 | |||
37 | def add_authn_plugin(self, config, plugin): |
|
43 | def add_authn_plugin(self, config, plugin): | |
38 | plugin_id = plugin.get_id() |
|
44 | plugin_id = plugin.get_id() | |
@@ -51,3 +57,31 b' class AuthenticationPluginRegistry(objec' | |||||
51 |
|
57 | |||
52 | def get_plugin(self, plugin_id): |
|
58 | def get_plugin(self, plugin_id): | |
53 | return self._plugins.get(plugin_id, None) |
|
59 | return self._plugins.get(plugin_id, None) | |
|
60 | ||||
|
61 | def get_plugins_for_authentication(self): | |||
|
62 | """ | |||
|
63 | Returns a list of plugins which should be consulted when authenticating | |||
|
64 | a user. It only returns plugins which are enabled and active. | |||
|
65 | Additionally it includes the fallback plugin from the INI file, if | |||
|
66 | `rhodecode.auth_plugin_fallback` is set to a plugin ID. | |||
|
67 | """ | |||
|
68 | plugins = [] | |||
|
69 | ||||
|
70 | # Add all enabled and active plugins to the list. We iterate over the | |||
|
71 | # auth_plugins setting from DB beacuse it also represents the ordering. | |||
|
72 | enabled_plugins = SettingsModel().get_auth_plugins() | |||
|
73 | for plugin_id in enabled_plugins: | |||
|
74 | plugin = self.get_plugin(plugin_id) | |||
|
75 | if plugin is not None and plugin.is_active(): | |||
|
76 | plugins.append(plugin) | |||
|
77 | ||||
|
78 | # Add the fallback plugin from ini file. | |||
|
79 | if self._fallback_plugin: | |||
|
80 | log.warn( | |||
|
81 | 'Using fallback authentication plugin from INI file: "%s"', | |||
|
82 | self._fallback_plugin) | |||
|
83 | plugin = self.get_plugin(self._fallback_plugin) | |||
|
84 | if plugin is not None and plugin not in plugins: | |||
|
85 | plugins.append(plugin) | |||
|
86 | ||||
|
87 | return plugins |
@@ -21,12 +21,11 b'' | |||||
21 | import logging |
|
21 | import logging | |
22 |
|
22 | |||
23 | from pyramid.exceptions import ConfigurationError |
|
23 | from pyramid.exceptions import ConfigurationError | |
24 | from pyramid.i18n import TranslationStringFactory |
|
|||
25 |
|
24 | |||
26 | from rhodecode.lib.utils2 import safe_str |
|
25 | from rhodecode.lib.utils2 import safe_str | |
27 | from rhodecode.model.settings import SettingsModel |
|
26 | from rhodecode.model.settings import SettingsModel | |
|
27 | from rhodecode.translation import _ | |||
28 |
|
28 | |||
29 | _ = TranslationStringFactory('rhodecode-enterprise') |
|
|||
30 |
|
29 | |||
31 | log = logging.getLogger(__name__) |
|
30 | log = logging.getLogger(__name__) | |
32 |
|
31 | |||
@@ -128,7 +127,7 b' class AuthnRootResource(AuthnResourceBas' | |||||
128 | # Allow plugin resources with identical names by rename duplicates. |
|
127 | # Allow plugin resources with identical names by rename duplicates. | |
129 | unique_name = _ensure_unique_name(resource.__name__) |
|
128 | unique_name = _ensure_unique_name(resource.__name__) | |
130 | if unique_name != resource.__name__: |
|
129 | if unique_name != resource.__name__: | |
131 |
log.warn('Name collision for traversal resource "%s" registered' |
|
130 | log.warn('Name collision for traversal resource "%s" registered ' | |
132 | 'by authentication plugin "%s"', resource.__name__, |
|
131 | 'by authentication plugin "%s"', resource.__name__, | |
133 | plugin_id) |
|
132 | plugin_id) | |
134 | resource.__name__ = unique_name |
|
133 | resource.__name__ = unique_name |
@@ -20,9 +20,7 b'' | |||||
20 |
|
20 | |||
21 | import colander |
|
21 | import colander | |
22 |
|
22 | |||
23 | from pyramid.i18n import TranslationStringFactory |
|
23 | from rhodecode.translation import _ | |
24 |
|
||||
25 | _ = TranslationStringFactory('rhodecode-enterprise') |
|
|||
26 |
|
24 | |||
27 |
|
25 | |||
28 | class AuthnPluginSettingsSchemaBase(colander.MappingSchema): |
|
26 | class AuthnPluginSettingsSchemaBase(colander.MappingSchema): |
@@ -23,7 +23,6 b' import formencode.htmlfill' | |||||
23 | import logging |
|
23 | import logging | |
24 |
|
24 | |||
25 | from pyramid.httpexceptions import HTTPFound |
|
25 | from pyramid.httpexceptions import HTTPFound | |
26 | from pyramid.i18n import TranslationStringFactory |
|
|||
27 | from pyramid.renderers import render |
|
26 | from pyramid.renderers import render | |
28 | from pyramid.response import Response |
|
27 | from pyramid.response import Response | |
29 |
|
28 | |||
@@ -34,11 +33,10 b' from rhodecode.lib.auth import LoginRequ' | |||||
34 | from rhodecode.model.forms import AuthSettingsForm |
|
33 | from rhodecode.model.forms import AuthSettingsForm | |
35 | from rhodecode.model.meta import Session |
|
34 | from rhodecode.model.meta import Session | |
36 | from rhodecode.model.settings import SettingsModel |
|
35 | from rhodecode.model.settings import SettingsModel | |
|
36 | from rhodecode.translation import _ | |||
37 |
|
37 | |||
38 | log = logging.getLogger(__name__) |
|
38 | log = logging.getLogger(__name__) | |
39 |
|
39 | |||
40 | _ = TranslationStringFactory('rhodecode-enterprise') |
|
|||
41 |
|
||||
42 |
|
40 | |||
43 | class AuthnPluginViewBase(object): |
|
41 | class AuthnPluginViewBase(object): | |
44 |
|
42 | |||
@@ -47,51 +45,27 b' class AuthnPluginViewBase(object):' | |||||
47 | self.context = context |
|
45 | self.context = context | |
48 | self.plugin = context.plugin |
|
46 | self.plugin = context.plugin | |
49 |
|
47 | |||
50 | # TODO: Think about replacing the htmlfill stuff. |
|
48 | def settings_get(self, defaults=None, errors=None): | |
51 | def _render_and_fill(self, template, template_context, request, |
|
|||
52 | form_defaults, validation_errors): |
|
|||
53 | """ |
|
|||
54 | Helper to render a template and fill the HTML form fields with |
|
|||
55 | defaults. Also displays the form errors. |
|
|||
56 | """ |
|
|||
57 | # Render template to string. |
|
|||
58 | html = render(template, template_context, request=request) |
|
|||
59 |
|
||||
60 | # Fill the HTML form fields with default values and add error messages. |
|
|||
61 | html = formencode.htmlfill.render( |
|
|||
62 | html, |
|
|||
63 | defaults=form_defaults, |
|
|||
64 | errors=validation_errors, |
|
|||
65 | prefix_error=False, |
|
|||
66 | encoding="UTF-8", |
|
|||
67 | force_defaults=False) |
|
|||
68 |
|
||||
69 | return html |
|
|||
70 |
|
||||
71 | def settings_get(self): |
|
|||
72 | """ |
|
49 | """ | |
73 | View that displays the plugin settings as a form. |
|
50 | View that displays the plugin settings as a form. | |
74 | """ |
|
51 | """ | |
75 |
|
|
52 | defaults = defaults or {} | |
76 | validation_errors = None |
|
53 | errors = errors or {} | |
77 | schema = self.plugin.get_settings_schema() |
|
54 | schema = self.plugin.get_settings_schema() | |
78 |
|
55 | |||
79 | # Get default values for the form. |
|
56 | # Get default values for the form. | |
80 |
for node in schema |
|
57 | for node in schema: | |
81 |
value = self.plugin.get_setting_by_name(node.name) |
|
58 | db_value = self.plugin.get_setting_by_name(node.name) | |
82 |
|
|
59 | defaults.setdefault(node.name, db_value) | |
83 |
|
60 | |||
84 | template_context = { |
|
61 | template_context = { | |
|
62 | 'defaults': defaults, | |||
|
63 | 'errors': errors, | |||
|
64 | 'plugin': self.context.plugin, | |||
85 | 'resource': self.context, |
|
65 | 'resource': self.context, | |
86 | 'plugin': self.context.plugin |
|
|||
87 | } |
|
66 | } | |
88 |
|
67 | |||
89 | return Response(self._render_and_fill( |
|
68 | return template_context | |
90 | 'rhodecode:templates/admin/auth/plugin_settings.html', |
|
|||
91 | template_context, |
|
|||
92 | self.request, |
|
|||
93 | form_defaults, |
|
|||
94 | validation_errors)) |
|
|||
95 |
|
69 | |||
96 | def settings_post(self): |
|
70 | def settings_post(self): | |
97 | """ |
|
71 | """ | |
@@ -102,24 +76,12 b' class AuthnPluginViewBase(object):' | |||||
102 | valid_data = schema.deserialize(self.request.params) |
|
76 | valid_data = schema.deserialize(self.request.params) | |
103 | except colander.Invalid, e: |
|
77 | except colander.Invalid, e: | |
104 | # Display error message and display form again. |
|
78 | # Display error message and display form again. | |
105 | form_defaults = self.request.params |
|
|||
106 | validation_errors = e.asdict() |
|
|||
107 | self.request.session.flash( |
|
79 | self.request.session.flash( | |
108 | _('Errors exist when saving plugin settings. ' |
|
80 | _('Errors exist when saving plugin settings. ' | |
109 |
|
|
81 | 'Please check the form inputs.'), | |
110 | queue='error') |
|
82 | queue='error') | |
111 |
|
83 | defaults = schema.flatten(self.request.params) | ||
112 | template_context = { |
|
84 | return self.settings_get(errors=e.asdict(), defaults=defaults) | |
113 | 'resource': self.context, |
|
|||
114 | 'plugin': self.context.plugin |
|
|||
115 | } |
|
|||
116 |
|
||||
117 | return Response(self._render_and_fill( |
|
|||
118 | 'rhodecode:templates/admin/auth/plugin_settings.html', |
|
|||
119 | template_context, |
|
|||
120 | self.request, |
|
|||
121 | form_defaults, |
|
|||
122 | validation_errors)) |
|
|||
123 |
|
85 | |||
124 | # Store validated data. |
|
86 | # Store validated data. | |
125 | for name, value in valid_data.items(): |
|
87 | for name, value in valid_data.items(): | |
@@ -151,10 +113,10 b' class AuthSettingsView(object):' | |||||
151 |
|
113 | |||
152 | @LoginRequired() |
|
114 | @LoginRequired() | |
153 | @HasPermissionAllDecorator('hg.admin') |
|
115 | @HasPermissionAllDecorator('hg.admin') | |
154 |
def index(self, defaults= |
|
116 | def index(self, defaults=None, errors=None, prefix_error=False): | |
|
117 | defaults = defaults or {} | |||
155 | authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry) |
|
118 | authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry) | |
156 | default_plugins = ['egg:rhodecode-enterprise-ce#rhodecode'] |
|
119 | enabled_plugins = SettingsModel().get_auth_plugins() | |
157 | enabled_plugins = SettingsModel().get_auth_plugins() or default_plugins |
|
|||
158 |
|
120 | |||
159 | # Create template context and render it. |
|
121 | # Create template context and render it. | |
160 | template_context = { |
|
122 | template_context = { |
@@ -27,10 +27,12 b' import logging' | |||||
27 | import rhodecode |
|
27 | import rhodecode | |
28 | import platform |
|
28 | import platform | |
29 | import re |
|
29 | import re | |
|
30 | import io | |||
30 |
|
31 | |||
31 | from mako.lookup import TemplateLookup |
|
32 | from mako.lookup import TemplateLookup | |
32 | from pylons.configuration import PylonsConfig |
|
33 | from pylons.configuration import PylonsConfig | |
33 | from pylons.error import handle_mako_error |
|
34 | from pylons.error import handle_mako_error | |
|
35 | from pyramid.settings import asbool | |||
34 |
|
36 | |||
35 | # don't remove this import it does magic for celery |
|
37 | # don't remove this import it does magic for celery | |
36 | from rhodecode.lib import celerypylons # noqa |
|
38 | from rhodecode.lib import celerypylons # noqa | |
@@ -39,6 +41,7 b' import rhodecode.lib.app_globals as app_' | |||||
39 |
|
41 | |||
40 | from rhodecode.config import utils |
|
42 | from rhodecode.config import utils | |
41 | from rhodecode.config.routing import make_map |
|
43 | from rhodecode.config.routing import make_map | |
|
44 | from rhodecode.config.jsroutes import generate_jsroutes_content | |||
42 |
|
45 | |||
43 | from rhodecode.lib import helpers |
|
46 | from rhodecode.lib import helpers | |
44 | from rhodecode.lib.auth import set_available_permissions |
|
47 | from rhodecode.lib.auth import set_available_permissions | |
@@ -51,7 +54,6 b' from rhodecode.model.scm import ScmModel' | |||||
51 |
|
54 | |||
52 | log = logging.getLogger(__name__) |
|
55 | log = logging.getLogger(__name__) | |
53 |
|
56 | |||
54 |
|
||||
55 | def load_environment(global_conf, app_conf, initial=False, |
|
57 | def load_environment(global_conf, app_conf, initial=False, | |
56 | test_env=None, test_index=None): |
|
58 | test_env=None, test_index=None): | |
57 | """ |
|
59 | """ | |
@@ -60,7 +62,6 b' def load_environment(global_conf, app_co' | |||||
60 | """ |
|
62 | """ | |
61 | config = PylonsConfig() |
|
63 | config = PylonsConfig() | |
62 |
|
64 | |||
63 | rhodecode.is_test = str2bool(app_conf.get('is_test', 'False')) |
|
|||
64 |
|
65 | |||
65 | # Pylons paths |
|
66 | # Pylons paths | |
66 | root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
67 | root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
@@ -80,6 +81,16 b' def load_environment(global_conf, app_co' | |||||
80 | config['app_conf'].get('celery.always.eager')) |
|
81 | config['app_conf'].get('celery.always.eager')) | |
81 |
|
82 | |||
82 | config['routes.map'] = make_map(config) |
|
83 | config['routes.map'] = make_map(config) | |
|
84 | ||||
|
85 | if asbool(config['debug']): | |||
|
86 | jsroutes = config['routes.map'].jsroutes() | |||
|
87 | jsroutes_file_content = generate_jsroutes_content(jsroutes) | |||
|
88 | jsroutes_file_path = os.path.join( | |||
|
89 | paths['static_files'], 'js', 'rhodecode', 'routes.js') | |||
|
90 | ||||
|
91 | with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f: | |||
|
92 | f.write(jsroutes_file_content) | |||
|
93 | ||||
83 | config['pylons.app_globals'] = app_globals.Globals(config) |
|
94 | config['pylons.app_globals'] = app_globals.Globals(config) | |
84 | config['pylons.h'] = helpers |
|
95 | config['pylons.h'] = helpers | |
85 | rhodecode.CONFIG = config |
|
96 | rhodecode.CONFIG = config | |
@@ -100,18 +111,6 b' def load_environment(global_conf, app_co' | |||||
100 |
|
111 | |||
101 | # sets the c attribute access when don't existing attribute are accessed |
|
112 | # sets the c attribute access when don't existing attribute are accessed | |
102 | config['pylons.strict_tmpl_context'] = True |
|
113 | config['pylons.strict_tmpl_context'] = True | |
103 | config_file_name = os.path.split(config['__file__'])[-1] |
|
|||
104 | test = re.match('^test[\w_]*\.ini$', config_file_name) is not None |
|
|||
105 | if test: |
|
|||
106 | if test_env is None: |
|
|||
107 | test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0)) |
|
|||
108 |
|
||||
109 | from rhodecode.lib.utils import create_test_env, create_test_index |
|
|||
110 | from rhodecode.tests import TESTS_TMP_PATH |
|
|||
111 | # test repos |
|
|||
112 | if test_env: |
|
|||
113 | create_test_env(TESTS_TMP_PATH, config) |
|
|||
114 | create_test_index(TESTS_TMP_PATH, config, True) |
|
|||
115 |
|
114 | |||
116 | # Limit backends to "vcs.backends" from configuration |
|
115 | # Limit backends to "vcs.backends" from configuration | |
117 | backends = config['vcs.backends'] = aslist( |
|
116 | backends = config['vcs.backends'] = aslist( | |
@@ -133,10 +132,6 b' def load_environment(global_conf, app_co' | |||||
133 | protocol=utils.get_vcs_server_protocol(config), |
|
132 | protocol=utils.get_vcs_server_protocol(config), | |
134 | log_level=config['vcs.server.log_level']) |
|
133 | log_level=config['vcs.server.log_level']) | |
135 |
|
134 | |||
136 | # MULTIPLE DB configs |
|
|||
137 | # Setup the SQLAlchemy database engine |
|
|||
138 | utils.initialize_database(config) |
|
|||
139 |
|
||||
140 | set_available_permissions(config) |
|
135 | set_available_permissions(config) | |
141 | db_cfg = make_db_config(clear_session=True) |
|
136 | db_cfg = make_db_config(clear_session=True) | |
142 |
|
137 | |||
@@ -179,3 +174,19 b' def _use_direct_hook_calls(config):' | |||||
179 | def _get_vcs_hooks_protocol(config): |
|
174 | def _get_vcs_hooks_protocol(config): | |
180 | protocol = config.get('vcs.hooks.protocol', 'pyro4').lower() |
|
175 | protocol = config.get('vcs.hooks.protocol', 'pyro4').lower() | |
181 | return protocol |
|
176 | return protocol | |
|
177 | ||||
|
178 | ||||
|
179 | def load_pyramid_environment(global_config, settings): | |||
|
180 | # Some parts of the code expect a merge of global and app settings. | |||
|
181 | settings_merged = global_config.copy() | |||
|
182 | settings_merged.update(settings) | |||
|
183 | ||||
|
184 | # If this is a test run we prepare the test environment like | |||
|
185 | # creating a test database, test search index and test repositories. | |||
|
186 | # This has to be done before the database connection is initialized. | |||
|
187 | if settings['is_test']: | |||
|
188 | rhodecode.is_test = True | |||
|
189 | utils.initialize_test_environment(settings_merged) | |||
|
190 | ||||
|
191 | # Initialize the database connection. | |||
|
192 | utils.initialize_database(settings_merged) |
@@ -37,7 +37,8 b' import routes.util' | |||||
37 |
|
37 | |||
38 | import rhodecode |
|
38 | import rhodecode | |
39 | from rhodecode.config import patches |
|
39 | from rhodecode.config import patches | |
40 |
from rhodecode.config.environment import |
|
40 | from rhodecode.config.environment import ( | |
|
41 | load_environment, load_pyramid_environment) | |||
41 | from rhodecode.lib.middleware import csrf |
|
42 | from rhodecode.lib.middleware import csrf | |
42 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled |
|
43 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled | |
43 | from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper |
|
44 | from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper | |
@@ -160,6 +161,9 b' def make_pyramid_app(global_config, **se' | |||||
160 | sanitize_settings_and_apply_defaults(settings) |
|
161 | sanitize_settings_and_apply_defaults(settings) | |
161 | config = Configurator(settings=settings) |
|
162 | config = Configurator(settings=settings) | |
162 | add_pylons_compat_data(config.registry, global_config, settings_pylons) |
|
163 | add_pylons_compat_data(config.registry, global_config, settings_pylons) | |
|
164 | ||||
|
165 | load_pyramid_environment(global_config, settings) | |||
|
166 | ||||
163 | includeme(config) |
|
167 | includeme(config) | |
164 | includeme_last(config) |
|
168 | includeme_last(config) | |
165 | pyramid_app = config.make_wsgi_app() |
|
169 | pyramid_app = config.make_wsgi_app() | |
@@ -182,6 +186,7 b' def includeme(config):' | |||||
182 | config.include('pyramid_mako') |
|
186 | config.include('pyramid_mako') | |
183 | config.include('pyramid_beaker') |
|
187 | config.include('pyramid_beaker') | |
184 | config.include('rhodecode.authentication') |
|
188 | config.include('rhodecode.authentication') | |
|
189 | config.include('rhodecode.login') | |||
185 | config.include('rhodecode.tweens') |
|
190 | config.include('rhodecode.tweens') | |
186 | config.include('rhodecode.api') |
|
191 | config.include('rhodecode.api') | |
187 |
|
192 | |||
@@ -301,6 +306,7 b' def sanitize_settings_and_apply_defaults' | |||||
301 |
|
306 | |||
302 | _bool_setting(settings, 'vcs.server.enable', 'true') |
|
307 | _bool_setting(settings, 'vcs.server.enable', 'true') | |
303 | _bool_setting(settings, 'static_files', 'true') |
|
308 | _bool_setting(settings, 'static_files', 'true') | |
|
309 | _bool_setting(settings, 'is_test', 'false') | |||
304 |
|
310 | |||
305 | return settings |
|
311 | return settings | |
306 |
|
312 |
@@ -29,6 +29,7 b' IMPORTANT: if you change any routing her' | |||||
29 | and _route_name variable which uses some of stored naming here to do redirects. |
|
29 | and _route_name variable which uses some of stored naming here to do redirects. | |
30 | """ |
|
30 | """ | |
31 | import os |
|
31 | import os | |
|
32 | import re | |||
32 | from routes import Mapper |
|
33 | from routes import Mapper | |
33 |
|
34 | |||
34 | from rhodecode.config import routing_links |
|
35 | from rhodecode.config import routing_links | |
@@ -50,9 +51,60 b' URL_NAME_REQUIREMENTS = {' | |||||
50 | } |
|
51 | } | |
51 |
|
52 | |||
52 |
|
53 | |||
|
54 | class JSRoutesMapper(Mapper): | |||
|
55 | """ | |||
|
56 | Wrapper for routes.Mapper to make pyroutes compatible url definitions | |||
|
57 | """ | |||
|
58 | _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$') | |||
|
59 | _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)') | |||
|
60 | def __init__(self, *args, **kw): | |||
|
61 | super(JSRoutesMapper, self).__init__(*args, **kw) | |||
|
62 | self._jsroutes = [] | |||
|
63 | ||||
|
64 | def connect(self, *args, **kw): | |||
|
65 | """ | |||
|
66 | Wrapper for connect to take an extra argument jsroute=True | |||
|
67 | ||||
|
68 | :param jsroute: boolean, if True will add the route to the pyroutes list | |||
|
69 | """ | |||
|
70 | if kw.pop('jsroute', False): | |||
|
71 | if not self._named_route_regex.match(args[0]): | |||
|
72 | raise Exception('only named routes can be added to pyroutes') | |||
|
73 | self._jsroutes.append(args[0]) | |||
|
74 | ||||
|
75 | super(JSRoutesMapper, self).connect(*args, **kw) | |||
|
76 | ||||
|
77 | def _extract_route_information(self, route): | |||
|
78 | """ | |||
|
79 | Convert a route into tuple(name, path, args), eg: | |||
|
80 | ('user_profile', '/profile/%(username)s', ['username']) | |||
|
81 | """ | |||
|
82 | routepath = route.routepath | |||
|
83 | def replace(matchobj): | |||
|
84 | if matchobj.group(1): | |||
|
85 | return "%%(%s)s" % matchobj.group(1).split(':')[0] | |||
|
86 | else: | |||
|
87 | return "%%(%s)s" % matchobj.group(2) | |||
|
88 | ||||
|
89 | routepath = self._argument_prog.sub(replace, routepath) | |||
|
90 | return ( | |||
|
91 | route.name, | |||
|
92 | routepath, | |||
|
93 | [(arg[0].split(':')[0] if arg[0] != '' else arg[1]) | |||
|
94 | for arg in self._argument_prog.findall(route.routepath)] | |||
|
95 | ) | |||
|
96 | ||||
|
97 | def jsroutes(self): | |||
|
98 | """ | |||
|
99 | Return a list of pyroutes.js compatible routes | |||
|
100 | """ | |||
|
101 | for route_name in self._jsroutes: | |||
|
102 | yield self._extract_route_information(self._routenames[route_name]) | |||
|
103 | ||||
|
104 | ||||
53 | def make_map(config): |
|
105 | def make_map(config): | |
54 | """Create, configure and return the routes Mapper""" |
|
106 | """Create, configure and return the routes Mapper""" | |
55 | rmap = Mapper(directory=config['pylons.paths']['controllers'], |
|
107 | rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'], | |
56 | always_scan=config['debug']) |
|
108 | always_scan=config['debug']) | |
57 | rmap.minimization = False |
|
109 | rmap.minimization = False | |
58 | rmap.explicit = False |
|
110 | rmap.explicit = False | |
@@ -124,14 +176,14 b' def make_map(config):' | |||||
124 | #========================================================================== |
|
176 | #========================================================================== | |
125 |
|
177 | |||
126 | # MAIN PAGE |
|
178 | # MAIN PAGE | |
127 | rmap.connect('home', '/', controller='home', action='index') |
|
179 | rmap.connect('home', '/', controller='home', action='index', jsroute=True) | |
128 |
rmap.connect(' |
|
180 | rmap.connect('goto_switcher_data', '/_goto_data', controller='home', | |
129 |
action=' |
|
181 | action='goto_switcher_data') | |
130 | rmap.connect('repo_list_data', '/_repos', controller='home', |
|
182 | rmap.connect('repo_list_data', '/_repos', controller='home', | |
131 | action='repo_list_data') |
|
183 | action='repo_list_data') | |
132 |
|
184 | |||
133 | rmap.connect('user_autocomplete_data', '/_users', controller='home', |
|
185 | rmap.connect('user_autocomplete_data', '/_users', controller='home', | |
134 | action='user_autocomplete_data') |
|
186 | action='user_autocomplete_data', jsroute=True) | |
135 | rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home', |
|
187 | rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home', | |
136 | action='user_group_autocomplete_data') |
|
188 | action='user_group_autocomplete_data') | |
137 |
|
189 | |||
@@ -167,7 +219,7 b' def make_map(config):' | |||||
167 | action='create', conditions={'method': ['POST']}) |
|
219 | action='create', conditions={'method': ['POST']}) | |
168 | m.connect('repos', '/repos', |
|
220 | m.connect('repos', '/repos', | |
169 | action='index', conditions={'method': ['GET']}) |
|
221 | action='index', conditions={'method': ['GET']}) | |
170 | m.connect('new_repo', '/create_repository', |
|
222 | m.connect('new_repo', '/create_repository', jsroute=True, | |
171 | action='create_repository', conditions={'method': ['GET']}) |
|
223 | action='create_repository', conditions={'method': ['GET']}) | |
172 | m.connect('/repos/{repo_name}', |
|
224 | m.connect('/repos/{repo_name}', | |
173 | action='update', conditions={'method': ['PUT'], |
|
225 | action='update', conditions={'method': ['PUT'], | |
@@ -303,22 +355,29 b' def make_map(config):' | |||||
303 | function=check_user_group) |
|
355 | function=check_user_group) | |
304 |
|
356 | |||
305 | # EXTRAS USER GROUP ROUTES |
|
357 | # EXTRAS USER GROUP ROUTES | |
306 |
m.connect('edit_user_group_global_perms', |
|
358 | m.connect('edit_user_group_global_perms', | |
|
359 | '/user_groups/{user_group_id}/edit/global_permissions', | |||
307 | action='edit_global_perms', conditions={'method': ['GET']}) |
|
360 | action='edit_global_perms', conditions={'method': ['GET']}) | |
308 |
m.connect('edit_user_group_global_perms', |
|
361 | m.connect('edit_user_group_global_perms', | |
|
362 | '/user_groups/{user_group_id}/edit/global_permissions', | |||
309 | action='update_global_perms', conditions={'method': ['PUT']}) |
|
363 | action='update_global_perms', conditions={'method': ['PUT']}) | |
310 |
m.connect('edit_user_group_perms_summary', |
|
364 | m.connect('edit_user_group_perms_summary', | |
|
365 | '/user_groups/{user_group_id}/edit/permissions_summary', | |||
311 | action='edit_perms_summary', conditions={'method': ['GET']}) |
|
366 | action='edit_perms_summary', conditions={'method': ['GET']}) | |
312 |
|
367 | |||
313 |
m.connect('edit_user_group_perms', |
|
368 | m.connect('edit_user_group_perms', | |
|
369 | '/user_groups/{user_group_id}/edit/permissions', | |||
314 | action='edit_perms', conditions={'method': ['GET']}) |
|
370 | action='edit_perms', conditions={'method': ['GET']}) | |
315 |
m.connect('edit_user_group_perms', |
|
371 | m.connect('edit_user_group_perms', | |
|
372 | '/user_groups/{user_group_id}/edit/permissions', | |||
316 | action='update_perms', conditions={'method': ['PUT']}) |
|
373 | action='update_perms', conditions={'method': ['PUT']}) | |
317 |
|
374 | |||
318 |
m.connect('edit_user_group_advanced', |
|
375 | m.connect('edit_user_group_advanced', | |
|
376 | '/user_groups/{user_group_id}/edit/advanced', | |||
319 | action='edit_advanced', conditions={'method': ['GET']}) |
|
377 | action='edit_advanced', conditions={'method': ['GET']}) | |
320 |
|
378 | |||
321 |
m.connect('edit_user_group_members', |
|
379 | m.connect('edit_user_group_members', | |
|
380 | '/user_groups/{user_group_id}/edit/members', jsroute=True, | |||
322 | action='edit_members', conditions={'method': ['GET']}) |
|
381 | action='edit_members', conditions={'method': ['GET']}) | |
323 |
|
382 | |||
324 | # ADMIN PERMISSIONS ROUTES |
|
383 | # ADMIN PERMISSIONS ROUTES | |
@@ -496,12 +555,6 b' def make_map(config):' | |||||
496 | m.connect('my_account_auth_tokens', '/my_account/auth_tokens', |
|
555 | m.connect('my_account_auth_tokens', '/my_account/auth_tokens', | |
497 | action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) |
|
556 | action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) | |
498 |
|
557 | |||
499 | m.connect('my_account_oauth', '/my_account/oauth', |
|
|||
500 | action='my_account_oauth', conditions={'method': ['GET']}) |
|
|||
501 | m.connect('my_account_oauth', '/my_account/oauth', |
|
|||
502 | action='my_account_oauth_delete', |
|
|||
503 | conditions={'method': ['DELETE']}) |
|
|||
504 |
|
||||
505 | # NOTIFICATION REST ROUTES |
|
558 | # NOTIFICATION REST ROUTES | |
506 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
559 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
507 | controller='admin/notifications') as m: |
|
560 | controller='admin/notifications') as m: | |
@@ -522,9 +575,9 b' def make_map(config):' | |||||
522 | controller='admin/gists') as m: |
|
575 | controller='admin/gists') as m: | |
523 | m.connect('gists', '/gists', |
|
576 | m.connect('gists', '/gists', | |
524 | action='create', conditions={'method': ['POST']}) |
|
577 | action='create', conditions={'method': ['POST']}) | |
525 | m.connect('gists', '/gists', |
|
578 | m.connect('gists', '/gists', jsroute=True, | |
526 | action='index', conditions={'method': ['GET']}) |
|
579 | action='index', conditions={'method': ['GET']}) | |
527 | m.connect('new_gist', '/gists/new', |
|
580 | m.connect('new_gist', '/gists/new', jsroute=True, | |
528 | action='new', conditions={'method': ['GET']}) |
|
581 | action='new', conditions={'method': ['GET']}) | |
529 |
|
582 | |||
530 | m.connect('/gists/{gist_id}', |
|
583 | m.connect('/gists/{gist_id}', | |
@@ -557,8 +610,12 b' def make_map(config):' | |||||
557 | m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', |
|
610 | m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', | |
558 | action='add_repo') |
|
611 | action='add_repo') | |
559 | m.connect( |
|
612 | m.connect( | |
560 | 'pull_requests_global', '/pull_requests/{pull_request_id:[0-9]+}', |
|
613 | 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}', | |
561 | action='pull_requests') |
|
614 | action='pull_requests') | |
|
615 | m.connect( | |||
|
616 | 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}', | |||
|
617 | action='pull_requests') | |||
|
618 | ||||
562 |
|
619 | |||
563 | # USER JOURNAL |
|
620 | # USER JOURNAL | |
564 | rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), |
|
621 | rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), | |
@@ -586,7 +643,7 b' def make_map(config):' | |||||
586 | action='public_journal_atom') |
|
643 | action='public_journal_atom') | |
587 |
|
644 | |||
588 | rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,), |
|
645 | rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,), | |
589 | controller='journal', action='toggle_following', |
|
646 | controller='journal', action='toggle_following', jsroute=True, | |
590 | conditions={'method': ['POST']}) |
|
647 | conditions={'method': ['POST']}) | |
591 |
|
648 | |||
592 | # FULL TEXT SEARCH |
|
649 | # FULL TEXT SEARCH | |
@@ -598,27 +655,6 b' def make_map(config):' | |||||
598 | conditions={'function': check_repo}, |
|
655 | conditions={'function': check_repo}, | |
599 | requirements=URL_NAME_REQUIREMENTS) |
|
656 | requirements=URL_NAME_REQUIREMENTS) | |
600 |
|
657 | |||
601 | # LOGIN/LOGOUT/REGISTER/SIGN IN |
|
|||
602 | rmap.connect('login_home', '%s/login' % (ADMIN_PREFIX,), controller='login', |
|
|||
603 | action='index') |
|
|||
604 |
|
||||
605 | rmap.connect('logout_home', '%s/logout' % (ADMIN_PREFIX,), controller='login', |
|
|||
606 | action='logout', conditions={'method': ['POST']}) |
|
|||
607 |
|
||||
608 | rmap.connect('register', '%s/register' % (ADMIN_PREFIX,), controller='login', |
|
|||
609 | action='register') |
|
|||
610 |
|
||||
611 | rmap.connect('reset_password', '%s/password_reset' % (ADMIN_PREFIX,), |
|
|||
612 | controller='login', action='password_reset') |
|
|||
613 |
|
||||
614 | rmap.connect('reset_password_confirmation', |
|
|||
615 | '%s/password_reset_confirmation' % (ADMIN_PREFIX,), |
|
|||
616 | controller='login', action='password_reset_confirmation') |
|
|||
617 |
|
||||
618 | rmap.connect('social_auth', |
|
|||
619 | '%s/social_auth/{provider_name}' % (ADMIN_PREFIX,), |
|
|||
620 | controller='login', action='social_auth') |
|
|||
621 |
|
||||
622 | # FEEDS |
|
658 | # FEEDS | |
623 | rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', |
|
659 | rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', | |
624 | controller='feed', action='rss', |
|
660 | controller='feed', action='rss', | |
@@ -644,17 +680,17 b' def make_map(config):' | |||||
644 | rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}', |
|
680 | rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}', | |
645 | controller='summary', action='repo_stats', |
|
681 | controller='summary', action='repo_stats', | |
646 | conditions={'function': check_repo}, |
|
682 | conditions={'function': check_repo}, | |
647 | requirements=URL_NAME_REQUIREMENTS) |
|
683 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
648 |
|
684 | |||
649 | rmap.connect('repo_refs_data', '/{repo_name}/refs-data', |
|
685 | rmap.connect('repo_refs_data', '/{repo_name}/refs-data', | |
650 | controller='summary', action='repo_refs_data', |
|
686 | controller='summary', action='repo_refs_data', jsroute=True, | |
651 | requirements=URL_NAME_REQUIREMENTS) |
|
687 | requirements=URL_NAME_REQUIREMENTS) | |
652 | rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog', |
|
688 | rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog', | |
653 | controller='summary', action='repo_refs_changelog_data', |
|
689 | controller='summary', action='repo_refs_changelog_data', | |
654 | requirements=URL_NAME_REQUIREMENTS) |
|
690 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
655 |
|
691 | |||
656 | rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', |
|
692 | rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', | |
657 | controller='changeset', revision='tip', |
|
693 | controller='changeset', revision='tip', jsroute=True, | |
658 | conditions={'function': check_repo}, |
|
694 | conditions={'function': check_repo}, | |
659 | requirements=URL_NAME_REQUIREMENTS) |
|
695 | requirements=URL_NAME_REQUIREMENTS) | |
660 | rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}', |
|
696 | rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}', | |
@@ -667,12 +703,13 b' def make_map(config):' | |||||
667 | requirements=URL_NAME_REQUIREMENTS) |
|
703 | requirements=URL_NAME_REQUIREMENTS) | |
668 |
|
704 | |||
669 | # repo edit options |
|
705 | # repo edit options | |
670 | rmap.connect('edit_repo', '/{repo_name}/settings', |
|
706 | rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True, | |
671 | controller='admin/repos', action='edit', |
|
707 | controller='admin/repos', action='edit', | |
672 | conditions={'method': ['GET'], 'function': check_repo}, |
|
708 | conditions={'method': ['GET'], 'function': check_repo}, | |
673 | requirements=URL_NAME_REQUIREMENTS) |
|
709 | requirements=URL_NAME_REQUIREMENTS) | |
674 |
|
710 | |||
675 | rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions', |
|
711 | rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions', | |
|
712 | jsroute=True, | |||
676 | controller='admin/repos', action='edit_permissions', |
|
713 | controller='admin/repos', action='edit_permissions', | |
677 | conditions={'method': ['GET'], 'function': check_repo}, |
|
714 | conditions={'method': ['GET'], 'function': check_repo}, | |
678 | requirements=URL_NAME_REQUIREMENTS) |
|
715 | requirements=URL_NAME_REQUIREMENTS) | |
@@ -804,13 +841,13 b' def make_map(config):' | |||||
804 | requirements=URL_NAME_REQUIREMENTS) |
|
841 | requirements=URL_NAME_REQUIREMENTS) | |
805 |
|
842 | |||
806 | rmap.connect('changeset_comment', |
|
843 | rmap.connect('changeset_comment', | |
807 | '/{repo_name}/changeset/{revision}/comment', |
|
844 | '/{repo_name}/changeset/{revision}/comment', jsroute=True, | |
808 | controller='changeset', revision='tip', action='comment', |
|
845 | controller='changeset', revision='tip', action='comment', | |
809 | conditions={'function': check_repo}, |
|
846 | conditions={'function': check_repo}, | |
810 | requirements=URL_NAME_REQUIREMENTS) |
|
847 | requirements=URL_NAME_REQUIREMENTS) | |
811 |
|
848 | |||
812 | rmap.connect('changeset_comment_preview', |
|
849 | rmap.connect('changeset_comment_preview', | |
813 | '/{repo_name}/changeset/comment/preview', |
|
850 | '/{repo_name}/changeset/comment/preview', jsroute=True, | |
814 | controller='changeset', action='preview_comment', |
|
851 | controller='changeset', action='preview_comment', | |
815 | conditions={'function': check_repo, 'method': ['POST']}, |
|
852 | conditions={'function': check_repo, 'method': ['POST']}, | |
816 | requirements=URL_NAME_REQUIREMENTS) |
|
853 | requirements=URL_NAME_REQUIREMENTS) | |
@@ -819,11 +856,11 b' def make_map(config):' | |||||
819 | '/{repo_name}/changeset/comment/{comment_id}/delete', |
|
856 | '/{repo_name}/changeset/comment/{comment_id}/delete', | |
820 | controller='changeset', action='delete_comment', |
|
857 | controller='changeset', action='delete_comment', | |
821 | conditions={'function': check_repo, 'method': ['DELETE']}, |
|
858 | conditions={'function': check_repo, 'method': ['DELETE']}, | |
822 | requirements=URL_NAME_REQUIREMENTS) |
|
859 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
823 |
|
860 | |||
824 | rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}', |
|
861 | rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}', | |
825 | controller='changeset', action='changeset_info', |
|
862 | controller='changeset', action='changeset_info', | |
826 | requirements=URL_NAME_REQUIREMENTS) |
|
863 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
827 |
|
864 | |||
828 | rmap.connect('compare_home', |
|
865 | rmap.connect('compare_home', | |
829 | '/{repo_name}/compare', |
|
866 | '/{repo_name}/compare', | |
@@ -835,33 +872,33 b' def make_map(config):' | |||||
835 | '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', |
|
872 | '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', | |
836 | controller='compare', action='compare', |
|
873 | controller='compare', action='compare', | |
837 | conditions={'function': check_repo}, |
|
874 | conditions={'function': check_repo}, | |
838 | requirements=URL_NAME_REQUIREMENTS) |
|
875 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
839 |
|
876 | |||
840 | rmap.connect('pullrequest_home', |
|
877 | rmap.connect('pullrequest_home', | |
841 | '/{repo_name}/pull-request/new', controller='pullrequests', |
|
878 | '/{repo_name}/pull-request/new', controller='pullrequests', | |
842 | action='index', conditions={'function': check_repo, |
|
879 | action='index', conditions={'function': check_repo, | |
843 | 'method': ['GET']}, |
|
880 | 'method': ['GET']}, | |
844 | requirements=URL_NAME_REQUIREMENTS) |
|
881 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
845 |
|
882 | |||
846 | rmap.connect('pullrequest', |
|
883 | rmap.connect('pullrequest', | |
847 | '/{repo_name}/pull-request/new', controller='pullrequests', |
|
884 | '/{repo_name}/pull-request/new', controller='pullrequests', | |
848 | action='create', conditions={'function': check_repo, |
|
885 | action='create', conditions={'function': check_repo, | |
849 | 'method': ['POST']}, |
|
886 | 'method': ['POST']}, | |
850 | requirements=URL_NAME_REQUIREMENTS) |
|
887 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
851 |
|
888 | |||
852 | rmap.connect('pullrequest_repo_refs', |
|
889 | rmap.connect('pullrequest_repo_refs', | |
853 | '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}', |
|
890 | '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}', | |
854 | controller='pullrequests', |
|
891 | controller='pullrequests', | |
855 | action='get_repo_refs', |
|
892 | action='get_repo_refs', | |
856 | conditions={'function': check_repo, 'method': ['GET']}, |
|
893 | conditions={'function': check_repo, 'method': ['GET']}, | |
857 | requirements=URL_NAME_REQUIREMENTS) |
|
894 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
858 |
|
895 | |||
859 | rmap.connect('pullrequest_repo_destinations', |
|
896 | rmap.connect('pullrequest_repo_destinations', | |
860 | '/{repo_name}/pull-request/repo-destinations', |
|
897 | '/{repo_name}/pull-request/repo-destinations', | |
861 | controller='pullrequests', |
|
898 | controller='pullrequests', | |
862 | action='get_repo_destinations', |
|
899 | action='get_repo_destinations', | |
863 | conditions={'function': check_repo, 'method': ['GET']}, |
|
900 | conditions={'function': check_repo, 'method': ['GET']}, | |
864 | requirements=URL_NAME_REQUIREMENTS) |
|
901 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
865 |
|
902 | |||
866 | rmap.connect('pullrequest_show', |
|
903 | rmap.connect('pullrequest_show', | |
867 | '/{repo_name}/pull-request/{pull_request_id}', |
|
904 | '/{repo_name}/pull-request/{pull_request_id}', | |
@@ -875,7 +912,7 b' def make_map(config):' | |||||
875 | controller='pullrequests', |
|
912 | controller='pullrequests', | |
876 | action='update', conditions={'function': check_repo, |
|
913 | action='update', conditions={'function': check_repo, | |
877 | 'method': ['PUT']}, |
|
914 | 'method': ['PUT']}, | |
878 | requirements=URL_NAME_REQUIREMENTS) |
|
915 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
879 |
|
916 | |||
880 | rmap.connect('pullrequest_merge', |
|
917 | rmap.connect('pullrequest_merge', | |
881 | '/{repo_name}/pull-request/{pull_request_id}', |
|
918 | '/{repo_name}/pull-request/{pull_request_id}', | |
@@ -896,20 +933,20 b' def make_map(config):' | |||||
896 | controller='pullrequests', |
|
933 | controller='pullrequests', | |
897 | action='show_all', conditions={'function': check_repo, |
|
934 | action='show_all', conditions={'function': check_repo, | |
898 | 'method': ['GET']}, |
|
935 | 'method': ['GET']}, | |
899 | requirements=URL_NAME_REQUIREMENTS) |
|
936 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
900 |
|
937 | |||
901 | rmap.connect('pullrequest_comment', |
|
938 | rmap.connect('pullrequest_comment', | |
902 | '/{repo_name}/pull-request-comment/{pull_request_id}', |
|
939 | '/{repo_name}/pull-request-comment/{pull_request_id}', | |
903 | controller='pullrequests', |
|
940 | controller='pullrequests', | |
904 | action='comment', conditions={'function': check_repo, |
|
941 | action='comment', conditions={'function': check_repo, | |
905 | 'method': ['POST']}, |
|
942 | 'method': ['POST']}, | |
906 | requirements=URL_NAME_REQUIREMENTS) |
|
943 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
907 |
|
944 | |||
908 | rmap.connect('pullrequest_comment_delete', |
|
945 | rmap.connect('pullrequest_comment_delete', | |
909 | '/{repo_name}/pull-request-comment/{comment_id}/delete', |
|
946 | '/{repo_name}/pull-request-comment/{comment_id}/delete', | |
910 | controller='pullrequests', action='delete_comment', |
|
947 | controller='pullrequests', action='delete_comment', | |
911 | conditions={'function': check_repo, 'method': ['DELETE']}, |
|
948 | conditions={'function': check_repo, 'method': ['DELETE']}, | |
912 | requirements=URL_NAME_REQUIREMENTS) |
|
949 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
913 |
|
950 | |||
914 | rmap.connect('summary_home_explicit', '/{repo_name}/summary', |
|
951 | rmap.connect('summary_home_explicit', '/{repo_name}/summary', | |
915 | controller='summary', conditions={'function': check_repo}, |
|
952 | controller='summary', conditions={'function': check_repo}, | |
@@ -927,7 +964,7 b' def make_map(config):' | |||||
927 | controller='bookmarks', conditions={'function': check_repo}, |
|
964 | controller='bookmarks', conditions={'function': check_repo}, | |
928 | requirements=URL_NAME_REQUIREMENTS) |
|
965 | requirements=URL_NAME_REQUIREMENTS) | |
929 |
|
966 | |||
930 | rmap.connect('changelog_home', '/{repo_name}/changelog', |
|
967 | rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True, | |
931 | controller='changelog', conditions={'function': check_repo}, |
|
968 | controller='changelog', conditions={'function': check_repo}, | |
932 | requirements=URL_NAME_REQUIREMENTS) |
|
969 | requirements=URL_NAME_REQUIREMENTS) | |
933 |
|
970 | |||
@@ -936,21 +973,21 b' def make_map(config):' | |||||
936 | conditions={'function': check_repo}, |
|
973 | conditions={'function': check_repo}, | |
937 | requirements=URL_NAME_REQUIREMENTS) |
|
974 | requirements=URL_NAME_REQUIREMENTS) | |
938 |
|
975 | |||
939 |
rmap.connect('changelog_file_home', |
|
976 | rmap.connect('changelog_file_home', | |
|
977 | '/{repo_name}/changelog/{revision}/{f_path}', | |||
940 | controller='changelog', f_path=None, |
|
978 | controller='changelog', f_path=None, | |
941 | conditions={'function': check_repo}, |
|
979 | conditions={'function': check_repo}, | |
942 | requirements=URL_NAME_REQUIREMENTS) |
|
980 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
943 |
|
981 | |||
944 | rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}', |
|
982 | rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}', | |
945 | controller='changelog', action='changelog_details', |
|
983 | controller='changelog', action='changelog_details', | |
946 | conditions={'function': check_repo}, |
|
984 | conditions={'function': check_repo}, | |
947 | requirements=URL_NAME_REQUIREMENTS) |
|
985 | requirements=URL_NAME_REQUIREMENTS) | |
948 |
|
986 | |||
949 | rmap.connect('files_home', |
|
987 | rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}', | |
950 | '/{repo_name}/files/{revision}/{f_path}', |
|
|||
951 | controller='files', revision='tip', f_path='', |
|
988 | controller='files', revision='tip', f_path='', | |
952 | conditions={'function': check_repo}, |
|
989 | conditions={'function': check_repo}, | |
953 | requirements=URL_NAME_REQUIREMENTS) |
|
990 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
954 |
|
991 | |||
955 | rmap.connect('files_home_simple_catchrev', |
|
992 | rmap.connect('files_home_simple_catchrev', | |
956 | '/{repo_name}/files/{revision}', |
|
993 | '/{repo_name}/files/{revision}', | |
@@ -968,13 +1005,13 b' def make_map(config):' | |||||
968 | '/{repo_name}/history/{revision}/{f_path}', |
|
1005 | '/{repo_name}/history/{revision}/{f_path}', | |
969 | controller='files', action='history', revision='tip', f_path='', |
|
1006 | controller='files', action='history', revision='tip', f_path='', | |
970 | conditions={'function': check_repo}, |
|
1007 | conditions={'function': check_repo}, | |
971 | requirements=URL_NAME_REQUIREMENTS) |
|
1008 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
972 |
|
1009 | |||
973 | rmap.connect('files_authors_home', |
|
1010 | rmap.connect('files_authors_home', | |
974 | '/{repo_name}/authors/{revision}/{f_path}', |
|
1011 | '/{repo_name}/authors/{revision}/{f_path}', | |
975 | controller='files', action='authors', revision='tip', f_path='', |
|
1012 | controller='files', action='authors', revision='tip', f_path='', | |
976 | conditions={'function': check_repo}, |
|
1013 | conditions={'function': check_repo}, | |
977 | requirements=URL_NAME_REQUIREMENTS) |
|
1014 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
978 |
|
1015 | |||
979 | rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}', |
|
1016 | rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}', | |
980 | controller='files', action='diff', f_path='', |
|
1017 | controller='files', action='diff', f_path='', | |
@@ -1053,19 +1090,19 b' def make_map(config):' | |||||
1053 | rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}', |
|
1090 | rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}', | |
1054 | controller='files', action='archivefile', |
|
1091 | controller='files', action='archivefile', | |
1055 | conditions={'function': check_repo}, |
|
1092 | conditions={'function': check_repo}, | |
1056 | requirements=URL_NAME_REQUIREMENTS) |
|
1093 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
1057 |
|
1094 | |||
1058 | rmap.connect('files_nodelist_home', |
|
1095 | rmap.connect('files_nodelist_home', | |
1059 | '/{repo_name}/nodelist/{revision}/{f_path}', |
|
1096 | '/{repo_name}/nodelist/{revision}/{f_path}', | |
1060 | controller='files', action='nodelist', |
|
1097 | controller='files', action='nodelist', | |
1061 | conditions={'function': check_repo}, |
|
1098 | conditions={'function': check_repo}, | |
1062 | requirements=URL_NAME_REQUIREMENTS) |
|
1099 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
1063 |
|
1100 | |||
1064 | rmap.connect('files_metadata_list_home', |
|
1101 | rmap.connect('files_metadata_list_home', | |
1065 | '/{repo_name}/metadata_list/{revision}/{f_path}', |
|
1102 | '/{repo_name}/metadata_list/{revision}/{f_path}', | |
1066 | controller='files', action='metadata_list', |
|
1103 | controller='files', action='metadata_list', | |
1067 | conditions={'function': check_repo}, |
|
1104 | conditions={'function': check_repo}, | |
1068 | requirements=URL_NAME_REQUIREMENTS) |
|
1105 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
1069 |
|
1106 | |||
1070 | rmap.connect('repo_fork_create_home', '/{repo_name}/fork', |
|
1107 | rmap.connect('repo_fork_create_home', '/{repo_name}/fork', | |
1071 | controller='forks', action='fork_create', |
|
1108 | controller='forks', action='fork_create', | |
@@ -1096,7 +1133,7 b' def make_map(config):' | |||||
1096 |
|
1133 | |||
1097 | # catch all, at the end |
|
1134 | # catch all, at the end | |
1098 | _connect_with_slash( |
|
1135 | _connect_with_slash( | |
1099 | rmap, 'summary_home', '/{repo_name}', |
|
1136 | rmap, 'summary_home', '/{repo_name}', jsroute=True, | |
1100 | controller='summary', action='index', |
|
1137 | controller='summary', action='index', | |
1101 | conditions={'function': check_repo}, |
|
1138 | conditions={'function': check_repo}, | |
1102 | requirements=URL_NAME_REQUIREMENTS) |
|
1139 | requirements=URL_NAME_REQUIREMENTS) |
@@ -73,6 +73,18 b' def initialize_database(config):' | |||||
73 | init_model(engine, encryption_key=config['beaker.session.secret']) |
|
73 | init_model(engine, encryption_key=config['beaker.session.secret']) | |
74 |
|
74 | |||
75 |
|
75 | |||
|
76 | def initialize_test_environment(settings, test_env=None): | |||
|
77 | if test_env is None: | |||
|
78 | test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0)) | |||
|
79 | ||||
|
80 | from rhodecode.lib.utils import create_test_env, create_test_index | |||
|
81 | from rhodecode.tests import TESTS_TMP_PATH | |||
|
82 | # test repos | |||
|
83 | if test_env: | |||
|
84 | create_test_env(TESTS_TMP_PATH, settings) | |||
|
85 | create_test_index(TESTS_TMP_PATH, settings, True) | |||
|
86 | ||||
|
87 | ||||
76 | def get_vcs_server_protocol(config): |
|
88 | def get_vcs_server_protocol(config): | |
77 | protocol = config.get('vcs.server.protocol', 'pyro4') |
|
89 | protocol = config.get('vcs.server.protocol', 'pyro4') | |
78 | return protocol |
|
90 | return protocol |
@@ -39,16 +39,15 b' from rhodecode.lib.auth import (' | |||||
39 | from rhodecode.lib.base import BaseController, render |
|
39 | from rhodecode.lib.base import BaseController, render | |
40 | from rhodecode.lib.utils2 import safe_int, md5 |
|
40 | from rhodecode.lib.utils2 import safe_int, md5 | |
41 | from rhodecode.lib.ext_json import json |
|
41 | from rhodecode.lib.ext_json import json | |
42 | from rhodecode.model.db import (Repository, PullRequest, PullRequestReviewers, |
|
42 | from rhodecode.model.db import ( | |
43 | UserEmailMap, User, UserFollowing, |
|
43 | Repository, PullRequest, PullRequestReviewers, UserEmailMap, User, | |
44 | ExternalIdentity) |
|
44 | UserFollowing) | |
45 | from rhodecode.model.forms import UserForm, PasswordChangeForm |
|
45 | from rhodecode.model.forms import UserForm, PasswordChangeForm | |
46 | from rhodecode.model.scm import RepoList |
|
46 | from rhodecode.model.scm import RepoList | |
47 | from rhodecode.model.user import UserModel |
|
47 | from rhodecode.model.user import UserModel | |
48 | from rhodecode.model.repo import RepoModel |
|
48 | from rhodecode.model.repo import RepoModel | |
49 | from rhodecode.model.auth_token import AuthTokenModel |
|
49 | from rhodecode.model.auth_token import AuthTokenModel | |
50 | from rhodecode.model.meta import Session |
|
50 | from rhodecode.model.meta import Session | |
51 | from rhodecode.model.settings import SettingsModel |
|
|||
52 |
|
51 | |||
53 | log = logging.getLogger(__name__) |
|
52 | log = logging.getLogger(__name__) | |
54 |
|
53 | |||
@@ -347,27 +346,3 b' class MyAccountController(BaseController' | |||||
347 | h.flash(_("Auth token successfully deleted"), category='success') |
|
346 | h.flash(_("Auth token successfully deleted"), category='success') | |
348 |
|
347 | |||
349 | return redirect(url('my_account_auth_tokens')) |
|
348 | return redirect(url('my_account_auth_tokens')) | |
350 |
|
||||
351 | def my_account_oauth(self): |
|
|||
352 | c.active = 'oauth' |
|
|||
353 | self.__load_data() |
|
|||
354 | c.user_oauth_tokens = ExternalIdentity().by_local_user_id( |
|
|||
355 | c.rhodecode_user.user_id).all() |
|
|||
356 | settings = SettingsModel().get_all_settings() |
|
|||
357 | c.social_plugins = SettingsModel().list_enabled_social_plugins( |
|
|||
358 | settings) |
|
|||
359 | return render('admin/my_account/my_account.html') |
|
|||
360 |
|
||||
361 | @auth.CSRFRequired() |
|
|||
362 | def my_account_oauth_delete(self): |
|
|||
363 | token = ExternalIdentity.by_external_id_and_provider( |
|
|||
364 | request.params.get('external_id'), |
|
|||
365 | request.params.get('provider_name'), |
|
|||
366 | local_user_id=c.rhodecode_user.user_id |
|
|||
367 | ) |
|
|||
368 | if token: |
|
|||
369 | Session().delete(token) |
|
|||
370 | Session().commit() |
|
|||
371 | h.flash(_("OAuth token successfully deleted"), category='success') |
|
|||
372 |
|
||||
373 | return redirect(url('my_account_oauth')) |
|
@@ -36,6 +36,7 b' from rhodecode.lib import auth' | |||||
36 | from rhodecode.lib import helpers as h |
|
36 | from rhodecode.lib import helpers as h | |
37 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ |
|
37 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ | |
38 | RepoGroupAssignmentError |
|
38 | RepoGroupAssignmentError | |
|
39 | from rhodecode.lib.utils import jsonify, action_logger | |||
39 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
40 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int | |
40 | from rhodecode.lib.auth import ( |
|
41 | from rhodecode.lib.auth import ( | |
41 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, |
|
42 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, | |
@@ -181,7 +182,8 b' class UserGroupsController(BaseControlle' | |||||
181 | h.flash(_('Error occurred during creation of user group %s') \ |
|
182 | h.flash(_('Error occurred during creation of user group %s') \ | |
182 | % request.POST.get('users_group_name'), category='error') |
|
183 | % request.POST.get('users_group_name'), category='error') | |
183 |
|
184 | |||
184 |
return redirect( |
|
185 | return redirect( | |
|
186 | url('edit_users_group', user_group_id=user_group.users_group_id)) | |||
185 |
|
187 | |||
186 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
188 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') | |
187 | def new(self): |
|
189 | def new(self): | |
@@ -467,5 +469,12 b' class UserGroupsController(BaseControlle' | |||||
467 | c.group_members_obj = sorted((x.user for x in c.user_group.members), |
|
469 | c.group_members_obj = sorted((x.user for x in c.user_group.members), | |
468 | key=lambda u: u.username.lower()) |
|
470 | key=lambda u: u.username.lower()) | |
469 |
|
471 | |||
470 |
|
|
472 | group_members = [(x.user_id, x.username) for x in c.group_members_obj] | |
|
473 | ||||
|
474 | if request.is_xhr: | |||
|
475 | return jsonify(lambda *a, **k: { | |||
|
476 | 'members': group_members | |||
|
477 | }) | |||
|
478 | ||||
|
479 | c.group_members = group_members | |||
471 | return render('admin/user_groups/user_group_edit.html') |
|
480 | return render('admin/user_groups/user_group_edit.html') |
@@ -198,7 +198,9 b' class CompareController(BaseRepoControll' | |||||
198 | c.statuses = c.rhodecode_db_repo.statuses( |
|
198 | c.statuses = c.rhodecode_db_repo.statuses( | |
199 | [x.raw_id for x in c.commit_ranges]) |
|
199 | [x.raw_id for x in c.commit_ranges]) | |
200 |
|
200 | |||
201 | if partial: |
|
201 | if partial: # for PR ajax commits loader | |
|
202 | if not c.ancestor: | |||
|
203 | return '' # cannot merge if there is no ancestor | |||
202 | return render('compare/compare_commits.html') |
|
204 | return render('compare/compare_commits.html') | |
203 |
|
205 | |||
204 | if c.ancestor: |
|
206 | if c.ancestor: |
@@ -24,16 +24,17 b' Home controller for RhodeCode Enterprise' | |||||
24 |
|
24 | |||
25 | import logging |
|
25 | import logging | |
26 | import time |
|
26 | import time | |
27 |
|
27 | import re | ||
28 |
|
28 | |||
29 | from pylons import tmpl_context as c, request |
|
29 | from pylons import tmpl_context as c, request, url, config | |
30 | from pylons.i18n.translation import _ |
|
30 | from pylons.i18n.translation import _ | |
31 | from sqlalchemy.sql import func |
|
31 | from sqlalchemy.sql import func | |
32 |
|
32 | |||
33 | from rhodecode.lib.auth import ( |
|
33 | from rhodecode.lib.auth import ( | |
34 | LoginRequired, HasPermissionAllDecorator, |
|
34 | LoginRequired, HasPermissionAllDecorator, AuthUser, | |
35 | HasRepoGroupPermissionAnyDecorator, XHRRequired) |
|
35 | HasRepoGroupPermissionAnyDecorator, XHRRequired) | |
36 | from rhodecode.lib.base import BaseController, render |
|
36 | from rhodecode.lib.base import BaseController, render | |
|
37 | from rhodecode.lib.index import searcher_from_config | |||
37 | from rhodecode.lib.ext_json import json |
|
38 | from rhodecode.lib.ext_json import json | |
38 | from rhodecode.lib.utils import jsonify |
|
39 | from rhodecode.lib.utils import jsonify | |
39 | from rhodecode.lib.utils2 import safe_unicode |
|
40 | from rhodecode.lib.utils2 import safe_unicode | |
@@ -134,7 +135,8 b' class HomeController(BaseController):' | |||||
134 | 'id': obj['name'], |
|
135 | 'id': obj['name'], | |
135 | 'text': obj['name'], |
|
136 | 'text': obj['name'], | |
136 | 'type': 'repo', |
|
137 | 'type': 'repo', | |
137 | 'obj': obj['dbrepo'] |
|
138 | 'obj': obj['dbrepo'], | |
|
139 | 'url': url('summary_home', repo_name=obj['name']) | |||
138 | } |
|
140 | } | |
139 | for obj in repo_iter] |
|
141 | for obj in repo_iter] | |
140 |
|
142 | |||
@@ -156,16 +158,45 b' class HomeController(BaseController):' | |||||
156 | 'id': obj.group_name, |
|
158 | 'id': obj.group_name, | |
157 | 'text': obj.group_name, |
|
159 | 'text': obj.group_name, | |
158 | 'type': 'group', |
|
160 | 'type': 'group', | |
159 | 'obj': {} |
|
161 | 'obj': {}, | |
|
162 | 'url': url('repo_group_home', group_name=obj.group_name) | |||
160 | } |
|
163 | } | |
161 | for obj in repo_groups_iter] |
|
164 | for obj in repo_groups_iter] | |
162 |
|
165 | |||
|
166 | def _get_hash_commit_list(self, hash_starts_with=None, limit=20): | |||
|
167 | if not hash_starts_with or len(hash_starts_with) < 3: | |||
|
168 | return [] | |||
|
169 | ||||
|
170 | commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) | |||
|
171 | ||||
|
172 | if len(commit_hashes) != 1: | |||
|
173 | return [] | |||
|
174 | ||||
|
175 | commit_hash_prefix = commit_hashes[0] | |||
|
176 | ||||
|
177 | auth_user = AuthUser( | |||
|
178 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) | |||
|
179 | searcher = searcher_from_config(config) | |||
|
180 | result = searcher.search( | |||
|
181 | 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user) | |||
|
182 | ||||
|
183 | return [ | |||
|
184 | { | |||
|
185 | 'id': entry['commit_id'], | |||
|
186 | 'text': entry['commit_id'], | |||
|
187 | 'type': 'commit', | |||
|
188 | 'obj': {'repo': entry['repository']}, | |||
|
189 | 'url': url('changeset_home', | |||
|
190 | repo_name=entry['repository'], revision=entry['commit_id']) | |||
|
191 | } | |||
|
192 | for entry in result['results']] | |||
|
193 | ||||
163 | @LoginRequired() |
|
194 | @LoginRequired() | |
164 | @XHRRequired() |
|
195 | @XHRRequired() | |
165 | @jsonify |
|
196 | @jsonify | |
166 |
def |
|
197 | def goto_switcher_data(self): | |
167 | query = request.GET.get('query') |
|
198 | query = request.GET.get('query') | |
168 |
log.debug('generating switcher |
|
199 | log.debug('generating goto switcher list, query %s', query) | |
169 |
|
200 | |||
170 | res = [] |
|
201 | res = [] | |
171 | repo_groups = self._get_repo_group_list(query) |
|
202 | repo_groups = self._get_repo_group_list(query) | |
@@ -182,6 +213,19 b' class HomeController(BaseController):' | |||||
182 | 'children': repos |
|
213 | 'children': repos | |
183 | }) |
|
214 | }) | |
184 |
|
215 | |||
|
216 | commits = self._get_hash_commit_list(query) | |||
|
217 | if commits: | |||
|
218 | unique_repos = {} | |||
|
219 | for commit in commits: | |||
|
220 | unique_repos.setdefault(commit['obj']['repo'], [] | |||
|
221 | ).append(commit) | |||
|
222 | ||||
|
223 | for repo in unique_repos: | |||
|
224 | res.append({ | |||
|
225 | 'text': _('Commits in %(repo)s') % {'repo': repo}, | |||
|
226 | 'children': unique_repos[repo] | |||
|
227 | }) | |||
|
228 | ||||
185 | data = { |
|
229 | data = { | |
186 | 'more': False, |
|
230 | 'more': False, | |
187 | 'results': res |
|
231 | 'results': res | |
@@ -203,6 +247,7 b' class HomeController(BaseController):' | |||||
203 | 'text': _('Repositories'), |
|
247 | 'text': _('Repositories'), | |
204 | 'children': repos |
|
248 | 'children': repos | |
205 | }) |
|
249 | }) | |
|
250 | ||||
206 | data = { |
|
251 | data = { | |
207 | 'more': False, |
|
252 | 'more': False, | |
208 | 'results': res |
|
253 | 'results': res |
@@ -590,6 +590,8 b' class PullrequestsController(BaseRepoCon' | |||||
590 | PullRequestModel().close_pull_request( |
|
590 | PullRequestModel().close_pull_request( | |
591 | pull_request.pull_request_id, user) |
|
591 | pull_request.pull_request_id, user) | |
592 | Session().commit() |
|
592 | Session().commit() | |
|
593 | msg = _('Pull request was successfully merged and closed.') | |||
|
594 | h.flash(msg, category='success') | |||
593 | else: |
|
595 | else: | |
594 | log.debug( |
|
596 | log.debug( | |
595 | "The merge was not successful. Merge response: %s", |
|
597 | "The merge was not successful. Merge response: %s", |
@@ -56,30 +56,33 b' class SearchController(BaseRepoControlle' | |||||
56 | search_params = schema.deserialize( |
|
56 | search_params = schema.deserialize( | |
57 | dict(search_query=request.GET.get('q'), |
|
57 | dict(search_query=request.GET.get('q'), | |
58 | search_type=request.GET.get('type'), |
|
58 | search_type=request.GET.get('type'), | |
|
59 | search_sort=request.GET.get('sort'), | |||
59 | page_limit=request.GET.get('page_limit'), |
|
60 | page_limit=request.GET.get('page_limit'), | |
60 | requested_page=request.GET.get('page')) |
|
61 | requested_page=request.GET.get('page')) | |
61 | ) |
|
62 | ) | |
62 | except validation_schema.Invalid as e: |
|
63 | except validation_schema.Invalid as e: | |
63 | errors = e.children |
|
64 | errors = e.children | |
64 |
|
65 | |||
|
66 | def url_generator(**kw): | |||
|
67 | q = urllib.quote(safe_str(search_query)) | |||
|
68 | return update_params( | |||
|
69 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) | |||
|
70 | ||||
65 | search_query = search_params.get('search_query') |
|
71 | search_query = search_params.get('search_query') | |
66 | search_type = search_params.get('search_type') |
|
72 | search_type = search_params.get('search_type') | |
67 |
|
73 | search_sort = search_params.get('search_sort') | ||
68 | if search_params.get('search_query'): |
|
74 | if search_params.get('search_query'): | |
69 | page_limit = search_params['page_limit'] |
|
75 | page_limit = search_params['page_limit'] | |
70 | requested_page = search_params['requested_page'] |
|
76 | requested_page = search_params['requested_page'] | |
71 |
|
77 | |||
72 | def url_generator(**kw): |
|
|||
73 | q = urllib.quote(safe_str(search_query)) |
|
|||
74 | return update_params( |
|
|||
75 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) |
|
|||
76 |
|
78 | |||
77 | c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, |
|
79 | c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, | |
78 | ip_addr=self.ip_addr) |
|
80 | ip_addr=self.ip_addr) | |
79 |
|
81 | |||
80 | try: |
|
82 | try: | |
81 | search_result = searcher.search( |
|
83 | search_result = searcher.search( | |
82 |
search_query, search_type, c.perm_user, repo_name |
|
84 | search_query, search_type, c.perm_user, repo_name, | |
|
85 | requested_page, page_limit, search_sort) | |||
83 |
|
86 | |||
84 | formatted_results = Page( |
|
87 | formatted_results = Page( | |
85 | search_result['results'], page=requested_page, |
|
88 | search_result['results'], page=requested_page, | |
@@ -97,6 +100,8 b' class SearchController(BaseRepoControlle' | |||||
97 | errors = [ |
|
100 | errors = [ | |
98 | validation_schema.Invalid(node, search_result['error'])] |
|
101 | validation_schema.Invalid(node, search_result['error'])] | |
99 |
|
102 | |||
|
103 | c.sort = search_sort | |||
|
104 | c.url_generator = url_generator | |||
100 | c.errors = errors |
|
105 | c.errors = errors | |
101 | c.formatted_results = formatted_results |
|
106 | c.formatted_results = formatted_results | |
102 | c.runtime = execution_time |
|
107 | c.runtime = execution_time |
@@ -299,6 +299,54 b' def _cached_perms_data(user_id, scope, u' | |||||
299 | explicit, algo) |
|
299 | explicit, algo) | |
300 | return permissions.calculate() |
|
300 | return permissions.calculate() | |
301 |
|
301 | |||
|
302 | class PermOrigin: | |||
|
303 | ADMIN = 'superadmin' | |||
|
304 | ||||
|
305 | REPO_USER = 'user:%s' | |||
|
306 | REPO_USERGROUP = 'usergroup:%s' | |||
|
307 | REPO_OWNER = 'repo.owner' | |||
|
308 | REPO_DEFAULT = 'repo.default' | |||
|
309 | REPO_PRIVATE = 'repo.private' | |||
|
310 | ||||
|
311 | REPOGROUP_USER = 'user:%s' | |||
|
312 | REPOGROUP_USERGROUP = 'usergroup:%s' | |||
|
313 | REPOGROUP_OWNER = 'group.owner' | |||
|
314 | REPOGROUP_DEFAULT = 'group.default' | |||
|
315 | ||||
|
316 | USERGROUP_USER = 'user:%s' | |||
|
317 | USERGROUP_USERGROUP = 'usergroup:%s' | |||
|
318 | USERGROUP_OWNER = 'usergroup.owner' | |||
|
319 | USERGROUP_DEFAULT = 'usergroup.default' | |||
|
320 | ||||
|
321 | ||||
|
322 | class PermOriginDict(dict): | |||
|
323 | """ | |||
|
324 | A special dict used for tracking permissions along with their origins. | |||
|
325 | ||||
|
326 | `__setitem__` has been overridden to expect a tuple(perm, origin) | |||
|
327 | `__getitem__` will return only the perm | |||
|
328 | `.perm_origin_stack` will return the stack of (perm, origin) set per key | |||
|
329 | ||||
|
330 | >>> perms = PermOriginDict() | |||
|
331 | >>> perms['resource'] = 'read', 'default' | |||
|
332 | >>> perms['resource'] | |||
|
333 | 'read' | |||
|
334 | >>> perms['resource'] = 'write', 'admin' | |||
|
335 | >>> perms['resource'] | |||
|
336 | 'write' | |||
|
337 | >>> perms.perm_origin_stack | |||
|
338 | {'resource': [('read', 'default'), ('write', 'admin')]} | |||
|
339 | """ | |||
|
340 | ||||
|
341 | ||||
|
342 | def __init__(self, *args, **kw): | |||
|
343 | dict.__init__(self, *args, **kw) | |||
|
344 | self.perm_origin_stack = {} | |||
|
345 | ||||
|
346 | def __setitem__(self, key, (perm, origin)): | |||
|
347 | self.perm_origin_stack.setdefault(key, []).append((perm, origin)) | |||
|
348 | dict.__setitem__(self, key, perm) | |||
|
349 | ||||
302 |
|
350 | |||
303 | class PermissionCalculator(object): |
|
351 | class PermissionCalculator(object): | |
304 |
|
352 | |||
@@ -318,9 +366,9 b' class PermissionCalculator(object):' | |||||
318 |
|
366 | |||
319 | self.default_user_id = User.get_default_user(cache=True).user_id |
|
367 | self.default_user_id = User.get_default_user(cache=True).user_id | |
320 |
|
368 | |||
321 |
self.permissions_repositories = |
|
369 | self.permissions_repositories = PermOriginDict() | |
322 |
self.permissions_repository_groups = |
|
370 | self.permissions_repository_groups = PermOriginDict() | |
323 |
self.permissions_user_groups = |
|
371 | self.permissions_user_groups = PermOriginDict() | |
324 | self.permissions_global = set() |
|
372 | self.permissions_global = set() | |
325 |
|
373 | |||
326 | self.default_repo_perms = Permission.get_default_repo_perms( |
|
374 | self.default_repo_perms = Permission.get_default_repo_perms( | |
@@ -355,19 +403,19 b' class PermissionCalculator(object):' | |||||
355 | for perm in self.default_repo_perms: |
|
403 | for perm in self.default_repo_perms: | |
356 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
404 | r_k = perm.UserRepoToPerm.repository.repo_name | |
357 | p = 'repository.admin' |
|
405 | p = 'repository.admin' | |
358 | self.permissions_repositories[r_k] = p |
|
406 | self.permissions_repositories[r_k] = p, PermOrigin.ADMIN | |
359 |
|
407 | |||
360 | # repository groups |
|
408 | # repository groups | |
361 | for perm in self.default_repo_groups_perms: |
|
409 | for perm in self.default_repo_groups_perms: | |
362 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
410 | rg_k = perm.UserRepoGroupToPerm.group.group_name | |
363 | p = 'group.admin' |
|
411 | p = 'group.admin' | |
364 | self.permissions_repository_groups[rg_k] = p |
|
412 | self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN | |
365 |
|
413 | |||
366 | # user groups |
|
414 | # user groups | |
367 | for perm in self.default_user_group_perms: |
|
415 | for perm in self.default_user_group_perms: | |
368 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name |
|
416 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name | |
369 | p = 'usergroup.admin' |
|
417 | p = 'usergroup.admin' | |
370 | self.permissions_user_groups[u_k] = p |
|
418 | self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN | |
371 |
|
419 | |||
372 | return self._permission_structure() |
|
420 | return self._permission_structure() | |
373 |
|
421 | |||
@@ -438,8 +486,7 b' class PermissionCalculator(object):' | |||||
438 | self.permissions_global = self.permissions_global.difference( |
|
486 | self.permissions_global = self.permissions_global.difference( | |
439 | _configurable) |
|
487 | _configurable) | |
440 | for perm in perms: |
|
488 | for perm in perms: | |
441 | self.permissions_global.add( |
|
489 | self.permissions_global.add(perm.permission.permission_name) | |
442 | perm.permission.permission_name) |
|
|||
443 |
|
490 | |||
444 | # user explicit global permissions |
|
491 | # user explicit global permissions | |
445 | user_perms = Session().query(UserToPerm)\ |
|
492 | user_perms = Session().query(UserToPerm)\ | |
@@ -478,13 +525,16 b' class PermissionCalculator(object):' | |||||
478 | # on given repo |
|
525 | # on given repo | |
479 | for perm in self.default_repo_perms: |
|
526 | for perm in self.default_repo_perms: | |
480 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
527 | r_k = perm.UserRepoToPerm.repository.repo_name | |
|
528 | o = PermOrigin.REPO_DEFAULT | |||
481 | if perm.Repository.private and not ( |
|
529 | if perm.Repository.private and not ( | |
482 | perm.Repository.user_id == self.user_id): |
|
530 | perm.Repository.user_id == self.user_id): | |
483 | # disable defaults for private repos, |
|
531 | # disable defaults for private repos, | |
484 | p = 'repository.none' |
|
532 | p = 'repository.none' | |
|
533 | o = PermOrigin.REPO_PRIVATE | |||
485 | elif perm.Repository.user_id == self.user_id: |
|
534 | elif perm.Repository.user_id == self.user_id: | |
486 | # set admin if owner |
|
535 | # set admin if owner | |
487 | p = 'repository.admin' |
|
536 | p = 'repository.admin' | |
|
537 | o = PermOrigin.REPO_OWNER | |||
488 | else: |
|
538 | else: | |
489 | p = perm.Permission.permission_name |
|
539 | p = perm.Permission.permission_name | |
490 | # if we decide this user isn't inheriting permissions from |
|
540 | # if we decide this user isn't inheriting permissions from | |
@@ -492,15 +542,17 b' class PermissionCalculator(object):' | |||||
492 | # permissions work |
|
542 | # permissions work | |
493 | if not user_inherit_object_permissions: |
|
543 | if not user_inherit_object_permissions: | |
494 | p = 'repository.none' |
|
544 | p = 'repository.none' | |
495 | self.permissions_repositories[r_k] = p |
|
545 | self.permissions_repositories[r_k] = p, o | |
496 |
|
546 | |||
497 | # defaults for repository groups taken from `default` user permission |
|
547 | # defaults for repository groups taken from `default` user permission | |
498 | # on given group |
|
548 | # on given group | |
499 | for perm in self.default_repo_groups_perms: |
|
549 | for perm in self.default_repo_groups_perms: | |
500 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
550 | rg_k = perm.UserRepoGroupToPerm.group.group_name | |
|
551 | o = PermOrigin.REPOGROUP_DEFAULT | |||
501 | if perm.RepoGroup.user_id == self.user_id: |
|
552 | if perm.RepoGroup.user_id == self.user_id: | |
502 | # set admin if owner |
|
553 | # set admin if owner | |
503 | p = 'group.admin' |
|
554 | p = 'group.admin' | |
|
555 | o = PermOrigin.REPOGROUP_OWNER | |||
504 | else: |
|
556 | else: | |
505 | p = perm.Permission.permission_name |
|
557 | p = perm.Permission.permission_name | |
506 |
|
558 | |||
@@ -508,18 +560,19 b' class PermissionCalculator(object):' | |||||
508 | # user we set him to .none so only explicit permissions work |
|
560 | # user we set him to .none so only explicit permissions work | |
509 | if not user_inherit_object_permissions: |
|
561 | if not user_inherit_object_permissions: | |
510 | p = 'group.none' |
|
562 | p = 'group.none' | |
511 | self.permissions_repository_groups[rg_k] = p |
|
563 | self.permissions_repository_groups[rg_k] = p, o | |
512 |
|
564 | |||
513 | # defaults for user groups taken from `default` user permission |
|
565 | # defaults for user groups taken from `default` user permission | |
514 | # on given user group |
|
566 | # on given user group | |
515 | for perm in self.default_user_group_perms: |
|
567 | for perm in self.default_user_group_perms: | |
516 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name |
|
568 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name | |
517 | p = perm.Permission.permission_name |
|
569 | p = perm.Permission.permission_name | |
|
570 | o = PermOrigin.USERGROUP_DEFAULT | |||
518 | # if we decide this user isn't inheriting permissions from default |
|
571 | # if we decide this user isn't inheriting permissions from default | |
519 | # user we set him to .none so only explicit permissions work |
|
572 | # user we set him to .none so only explicit permissions work | |
520 | if not user_inherit_object_permissions: |
|
573 | if not user_inherit_object_permissions: | |
521 | p = 'usergroup.none' |
|
574 | p = 'usergroup.none' | |
522 | self.permissions_user_groups[u_k] = p |
|
575 | self.permissions_user_groups[u_k] = p, o | |
523 |
|
576 | |||
524 | def _calculate_repository_permissions(self): |
|
577 | def _calculate_repository_permissions(self): | |
525 | """ |
|
578 | """ | |
@@ -538,17 +591,20 b' class PermissionCalculator(object):' | |||||
538 | multiple_counter = collections.defaultdict(int) |
|
591 | multiple_counter = collections.defaultdict(int) | |
539 | for perm in user_repo_perms_from_user_group: |
|
592 | for perm in user_repo_perms_from_user_group: | |
540 | r_k = perm.UserGroupRepoToPerm.repository.repo_name |
|
593 | r_k = perm.UserGroupRepoToPerm.repository.repo_name | |
|
594 | ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name | |||
541 | multiple_counter[r_k] += 1 |
|
595 | multiple_counter[r_k] += 1 | |
542 | p = perm.Permission.permission_name |
|
596 | p = perm.Permission.permission_name | |
|
597 | o = PermOrigin.REPO_USERGROUP % ug_k | |||
543 |
|
598 | |||
544 | if perm.Repository.user_id == self.user_id: |
|
599 | if perm.Repository.user_id == self.user_id: | |
545 | # set admin if owner |
|
600 | # set admin if owner | |
546 | p = 'repository.admin' |
|
601 | p = 'repository.admin' | |
|
602 | o = PermOrigin.REPO_OWNER | |||
547 | else: |
|
603 | else: | |
548 | if multiple_counter[r_k] > 1: |
|
604 | if multiple_counter[r_k] > 1: | |
549 | cur_perm = self.permissions_repositories[r_k] |
|
605 | cur_perm = self.permissions_repositories[r_k] | |
550 | p = self._choose_permission(p, cur_perm) |
|
606 | p = self._choose_permission(p, cur_perm) | |
551 | self.permissions_repositories[r_k] = p |
|
607 | self.permissions_repositories[r_k] = p, o | |
552 |
|
608 | |||
553 | # user explicit permissions for repositories, overrides any specified |
|
609 | # user explicit permissions for repositories, overrides any specified | |
554 | # by the group permission |
|
610 | # by the group permission | |
@@ -556,16 +612,18 b' class PermissionCalculator(object):' | |||||
556 | self.user_id, self.scope_repo_id) |
|
612 | self.user_id, self.scope_repo_id) | |
557 | for perm in user_repo_perms: |
|
613 | for perm in user_repo_perms: | |
558 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
614 | r_k = perm.UserRepoToPerm.repository.repo_name | |
|
615 | o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username | |||
559 | # set admin if owner |
|
616 | # set admin if owner | |
560 | if perm.Repository.user_id == self.user_id: |
|
617 | if perm.Repository.user_id == self.user_id: | |
561 | p = 'repository.admin' |
|
618 | p = 'repository.admin' | |
|
619 | o = PermOrigin.REPO_OWNER | |||
562 | else: |
|
620 | else: | |
563 | p = perm.Permission.permission_name |
|
621 | p = perm.Permission.permission_name | |
564 | if not self.explicit: |
|
622 | if not self.explicit: | |
565 | cur_perm = self.permissions_repositories.get( |
|
623 | cur_perm = self.permissions_repositories.get( | |
566 | r_k, 'repository.none') |
|
624 | r_k, 'repository.none') | |
567 | p = self._choose_permission(p, cur_perm) |
|
625 | p = self._choose_permission(p, cur_perm) | |
568 | self.permissions_repositories[r_k] = p |
|
626 | self.permissions_repositories[r_k] = p, o | |
569 |
|
627 | |||
570 | def _calculate_repository_group_permissions(self): |
|
628 | def _calculate_repository_group_permissions(self): | |
571 | """ |
|
629 | """ | |
@@ -583,32 +641,39 b' class PermissionCalculator(object):' | |||||
583 | multiple_counter = collections.defaultdict(int) |
|
641 | multiple_counter = collections.defaultdict(int) | |
584 | for perm in user_repo_group_perms_from_user_group: |
|
642 | for perm in user_repo_group_perms_from_user_group: | |
585 | g_k = perm.UserGroupRepoGroupToPerm.group.group_name |
|
643 | g_k = perm.UserGroupRepoGroupToPerm.group.group_name | |
|
644 | ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name | |||
|
645 | o = PermOrigin.REPOGROUP_USERGROUP % ug_k | |||
586 | multiple_counter[g_k] += 1 |
|
646 | multiple_counter[g_k] += 1 | |
587 | p = perm.Permission.permission_name |
|
647 | p = perm.Permission.permission_name | |
588 | if perm.RepoGroup.user_id == self.user_id: |
|
648 | if perm.RepoGroup.user_id == self.user_id: | |
589 | # set admin if owner |
|
649 | # set admin if owner | |
590 | p = 'group.admin' |
|
650 | p = 'group.admin' | |
|
651 | o = PermOrigin.REPOGROUP_OWNER | |||
591 | else: |
|
652 | else: | |
592 | if multiple_counter[g_k] > 1: |
|
653 | if multiple_counter[g_k] > 1: | |
593 | cur_perm = self.permissions_repository_groups[g_k] |
|
654 | cur_perm = self.permissions_repository_groups[g_k] | |
594 | p = self._choose_permission(p, cur_perm) |
|
655 | p = self._choose_permission(p, cur_perm) | |
595 | self.permissions_repository_groups[g_k] = p |
|
656 | self.permissions_repository_groups[g_k] = p, o | |
596 |
|
657 | |||
597 | # user explicit permissions for repository groups |
|
658 | # user explicit permissions for repository groups | |
598 | user_repo_groups_perms = Permission.get_default_group_perms( |
|
659 | user_repo_groups_perms = Permission.get_default_group_perms( | |
599 | self.user_id, self.scope_repo_group_id) |
|
660 | self.user_id, self.scope_repo_group_id) | |
600 | for perm in user_repo_groups_perms: |
|
661 | for perm in user_repo_groups_perms: | |
601 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
662 | rg_k = perm.UserRepoGroupToPerm.group.group_name | |
|
663 | u_k = perm.UserRepoGroupToPerm.user.username | |||
|
664 | o = PermOrigin.REPOGROUP_USER % u_k | |||
|
665 | ||||
602 | if perm.RepoGroup.user_id == self.user_id: |
|
666 | if perm.RepoGroup.user_id == self.user_id: | |
603 | # set admin if owner |
|
667 | # set admin if owner | |
604 | p = 'group.admin' |
|
668 | p = 'group.admin' | |
|
669 | o = PermOrigin.REPOGROUP_OWNER | |||
605 | else: |
|
670 | else: | |
606 | p = perm.Permission.permission_name |
|
671 | p = perm.Permission.permission_name | |
607 | if not self.explicit: |
|
672 | if not self.explicit: | |
608 | cur_perm = self.permissions_repository_groups.get( |
|
673 | cur_perm = self.permissions_repository_groups.get( | |
609 | rg_k, 'group.none') |
|
674 | rg_k, 'group.none') | |
610 | p = self._choose_permission(p, cur_perm) |
|
675 | p = self._choose_permission(p, cur_perm) | |
611 | self.permissions_repository_groups[rg_k] = p |
|
676 | self.permissions_repository_groups[rg_k] = p, o | |
612 |
|
677 | |||
613 | def _calculate_user_group_permissions(self): |
|
678 | def _calculate_user_group_permissions(self): | |
614 | """ |
|
679 | """ | |
@@ -623,24 +688,29 b' class PermissionCalculator(object):' | |||||
623 | for perm in user_group_from_user_group: |
|
688 | for perm in user_group_from_user_group: | |
624 | g_k = perm.UserGroupUserGroupToPerm\ |
|
689 | g_k = perm.UserGroupUserGroupToPerm\ | |
625 | .target_user_group.users_group_name |
|
690 | .target_user_group.users_group_name | |
|
691 | u_k = perm.UserGroupUserGroupToPerm\ | |||
|
692 | .user_group.users_group_name | |||
|
693 | o = PermOrigin.USERGROUP_USERGROUP % u_k | |||
626 | multiple_counter[g_k] += 1 |
|
694 | multiple_counter[g_k] += 1 | |
627 | p = perm.Permission.permission_name |
|
695 | p = perm.Permission.permission_name | |
628 | if multiple_counter[g_k] > 1: |
|
696 | if multiple_counter[g_k] > 1: | |
629 | cur_perm = self.permissions_user_groups[g_k] |
|
697 | cur_perm = self.permissions_user_groups[g_k] | |
630 | p = self._choose_permission(p, cur_perm) |
|
698 | p = self._choose_permission(p, cur_perm) | |
631 | self.permissions_user_groups[g_k] = p |
|
699 | self.permissions_user_groups[g_k] = p, o | |
632 |
|
700 | |||
633 | # user explicit permission for user groups |
|
701 | # user explicit permission for user groups | |
634 | user_user_groups_perms = Permission.get_default_user_group_perms( |
|
702 | user_user_groups_perms = Permission.get_default_user_group_perms( | |
635 | self.user_id, self.scope_user_group_id) |
|
703 | self.user_id, self.scope_user_group_id) | |
636 | for perm in user_user_groups_perms: |
|
704 | for perm in user_user_groups_perms: | |
637 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name |
|
705 | ug_k = perm.UserUserGroupToPerm.user_group.users_group_name | |
|
706 | u_k = perm.UserUserGroupToPerm.user.username | |||
638 | p = perm.Permission.permission_name |
|
707 | p = perm.Permission.permission_name | |
|
708 | o = PermOrigin.USERGROUP_USER % u_k | |||
639 | if not self.explicit: |
|
709 | if not self.explicit: | |
640 | cur_perm = self.permissions_user_groups.get( |
|
710 | cur_perm = self.permissions_user_groups.get( | |
641 | u_k, 'usergroup.none') |
|
711 | ug_k, 'usergroup.none') | |
642 | p = self._choose_permission(p, cur_perm) |
|
712 | p = self._choose_permission(p, cur_perm) | |
643 | self.permissions_user_groups[u_k] = p |
|
713 | self.permissions_user_groups[ug_k] = p, o | |
644 |
|
714 | |||
645 | def _choose_permission(self, new_perm, cur_perm): |
|
715 | def _choose_permission(self, new_perm, cur_perm): | |
646 | new_perm_val = Permission.PERM_WEIGHTS[new_perm] |
|
716 | new_perm_val = Permission.PERM_WEIGHTS[new_perm] | |
@@ -865,6 +935,10 b' class AuthUser(object):' | |||||
865 | return auth_tokens |
|
935 | return auth_tokens | |
866 |
|
936 | |||
867 | @property |
|
937 | @property | |
|
938 | def is_default(self): | |||
|
939 | return self.username == User.DEFAULT_USER | |||
|
940 | ||||
|
941 | @property | |||
868 | def is_admin(self): |
|
942 | def is_admin(self): | |
869 | return self.admin |
|
943 | return self.admin | |
870 |
|
944 | |||
@@ -1095,6 +1169,7 b' class LoginRequired(object):' | |||||
1095 | return get_cython_compat_decorator(self.__wrapper, func) |
|
1169 | return get_cython_compat_decorator(self.__wrapper, func) | |
1096 |
|
1170 | |||
1097 | def __wrapper(self, func, *fargs, **fkwargs): |
|
1171 | def __wrapper(self, func, *fargs, **fkwargs): | |
|
1172 | from rhodecode.lib import helpers as h | |||
1098 | cls = fargs[0] |
|
1173 | cls = fargs[0] | |
1099 | user = cls._rhodecode_user |
|
1174 | user = cls._rhodecode_user | |
1100 | loc = "%s:%s" % (cls.__class__.__name__, func.__name__) |
|
1175 | loc = "%s:%s" % (cls.__class__.__name__, func.__name__) | |
@@ -1102,7 +1177,6 b' class LoginRequired(object):' | |||||
1102 | # check if our IP is allowed |
|
1177 | # check if our IP is allowed | |
1103 | ip_access_valid = True |
|
1178 | ip_access_valid = True | |
1104 | if not user.ip_allowed: |
|
1179 | if not user.ip_allowed: | |
1105 | from rhodecode.lib import helpers as h |
|
|||
1106 | h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))), |
|
1180 | h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))), | |
1107 | category='warning') |
|
1181 | category='warning') | |
1108 | ip_access_valid = False |
|
1182 | ip_access_valid = False | |
@@ -1154,7 +1228,7 b' class LoginRequired(object):' | |||||
1154 |
|
1228 | |||
1155 | log.debug('redirecting to login page with %s' % (came_from,)) |
|
1229 | log.debug('redirecting to login page with %s' % (came_from,)) | |
1156 | return redirect( |
|
1230 | return redirect( | |
1157 |
|
|
1231 | h.route_path('login', _query={'came_from': came_from})) | |
1158 |
|
1232 | |||
1159 |
|
1233 | |||
1160 | class NotAnonymous(object): |
|
1234 | class NotAnonymous(object): | |
@@ -1180,7 +1254,8 b' class NotAnonymous(object):' | |||||
1180 | h.flash(_('You need to be a registered user to ' |
|
1254 | h.flash(_('You need to be a registered user to ' | |
1181 | 'perform this action'), |
|
1255 | 'perform this action'), | |
1182 | category='warning') |
|
1256 | category='warning') | |
1183 | return redirect(url('login_home', came_from=came_from)) |
|
1257 | return redirect( | |
|
1258 | h.route_path('login', _query={'came_from': came_from})) | |||
1184 | else: |
|
1259 | else: | |
1185 | return func(*fargs, **fkwargs) |
|
1260 | return func(*fargs, **fkwargs) | |
1186 |
|
1261 | |||
@@ -1263,7 +1338,8 b' class PermsDecorator(object):' | |||||
1263 | import rhodecode.lib.helpers as h |
|
1338 | import rhodecode.lib.helpers as h | |
1264 | h.flash(_('You need to be signed in to view this page'), |
|
1339 | h.flash(_('You need to be signed in to view this page'), | |
1265 | category='warning') |
|
1340 | category='warning') | |
1266 |
return redirect( |
|
1341 | return redirect( | |
|
1342 | h.route_path('login', _query={'came_from': came_from})) | |||
1267 |
|
1343 | |||
1268 | else: |
|
1344 | else: | |
1269 | # redirect with forbidden ret code |
|
1345 | # redirect with forbidden ret code |
@@ -35,7 +35,7 b' def makedate():' | |||||
35 | return time.mktime(lt), tz |
|
35 | return time.mktime(lt), tz | |
36 |
|
36 | |||
37 |
|
37 | |||
38 | def date_fromtimestamp(unixts, tzoffset=0): |
|
38 | def utcdate_fromtimestamp(unixts, tzoffset=0): | |
39 | """ |
|
39 | """ | |
40 | Makes a local datetime object out of unix timestamp |
|
40 | Makes a local datetime object out of unix timestamp | |
41 |
|
41 | |||
@@ -43,7 +43,7 b' def date_fromtimestamp(unixts, tzoffset=' | |||||
43 | :param tzoffset: |
|
43 | :param tzoffset: | |
44 | """ |
|
44 | """ | |
45 |
|
45 | |||
46 | return datetime.datetime.fromtimestamp(float(unixts)) |
|
46 | return datetime.datetime.utcfromtimestamp(float(unixts)) | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | def date_astimestamp(value): |
|
49 | def date_astimestamp(value): |
@@ -537,7 +537,6 b' class DbManage(object):' | |||||
537 | ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'), |
|
537 | ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'), | |
538 | ('support_url', '', 'unicode'), |
|
538 | ('support_url', '', 'unicode'), | |
539 | ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'), |
|
539 | ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'), | |
540 | ('license_key', '', 'unicode'), |
|
|||
541 | ('show_revision_number', True, 'bool'), |
|
540 | ('show_revision_number', True, 'bool'), | |
542 | ('show_sha_length', 12, 'int'), |
|
541 | ('show_sha_length', 12, 'int'), | |
543 | ] |
|
542 | ] |
@@ -36,11 +36,14 b' import urlparse' | |||||
36 | import time |
|
36 | import time | |
37 | import string |
|
37 | import string | |
38 | import hashlib |
|
38 | import hashlib | |
|
39 | import pygments | |||
39 |
|
40 | |||
40 | from datetime import datetime |
|
41 | from datetime import datetime | |
41 | from functools import partial |
|
42 | from functools import partial | |
42 | from pygments.formatters.html import HtmlFormatter |
|
43 | from pygments.formatters.html import HtmlFormatter | |
43 | from pygments import highlight as code_highlight |
|
44 | from pygments import highlight as code_highlight | |
|
45 | from pygments.lexers import ( | |||
|
46 | get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype) | |||
44 | from pylons import url |
|
47 | from pylons import url | |
45 | from pylons.i18n.translation import _, ungettext |
|
48 | from pylons.i18n.translation import _, ungettext | |
46 | from pyramid.threadlocal import get_current_request |
|
49 | from pyramid.threadlocal import get_current_request | |
@@ -68,8 +71,8 b' from rhodecode.lib.annotate import annot' | |||||
68 | from rhodecode.lib.action_parser import action_parser |
|
71 | from rhodecode.lib.action_parser import action_parser | |
69 | from rhodecode.lib.utils import repo_name_slug, get_custom_lexer |
|
72 | from rhodecode.lib.utils import repo_name_slug, get_custom_lexer | |
70 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ |
|
73 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ | |
71 |
get_commit_safe, datetime_to_time, time_to_datetime, |
|
74 | get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \ | |
72 | safe_int, md5, md5_safe |
|
75 | AttributeDict, safe_int, md5, md5_safe | |
73 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
76 | from rhodecode.lib.markup_renderer import MarkupRenderer | |
74 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError |
|
77 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError | |
75 | from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit |
|
78 | from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit | |
@@ -307,6 +310,176 b' class CodeHtmlFormatter(HtmlFormatter):' | |||||
307 | yield 0, '</td></tr></table>' |
|
310 | yield 0, '</td></tr></table>' | |
308 |
|
311 | |||
309 |
|
312 | |||
|
313 | class SearchContentCodeHtmlFormatter(CodeHtmlFormatter): | |||
|
314 | def __init__(self, **kw): | |||
|
315 | # only show these line numbers if set | |||
|
316 | self.only_lines = kw.pop('only_line_numbers', []) | |||
|
317 | self.query_terms = kw.pop('query_terms', []) | |||
|
318 | self.max_lines = kw.pop('max_lines', 5) | |||
|
319 | self.line_context = kw.pop('line_context', 3) | |||
|
320 | self.url = kw.pop('url', None) | |||
|
321 | ||||
|
322 | super(CodeHtmlFormatter, self).__init__(**kw) | |||
|
323 | ||||
|
324 | def _wrap_code(self, source): | |||
|
325 | for cnt, it in enumerate(source): | |||
|
326 | i, t = it | |||
|
327 | t = '<pre>%s</pre>' % t | |||
|
328 | yield i, t | |||
|
329 | ||||
|
330 | def _wrap_tablelinenos(self, inner): | |||
|
331 | yield 0, '<table class="code-highlight %stable">' % self.cssclass | |||
|
332 | ||||
|
333 | last_shown_line_number = 0 | |||
|
334 | current_line_number = 1 | |||
|
335 | ||||
|
336 | for t, line in inner: | |||
|
337 | if not t: | |||
|
338 | yield t, line | |||
|
339 | continue | |||
|
340 | ||||
|
341 | if current_line_number in self.only_lines: | |||
|
342 | if last_shown_line_number + 1 != current_line_number: | |||
|
343 | yield 0, '<tr>' | |||
|
344 | yield 0, '<td class="line">...</td>' | |||
|
345 | yield 0, '<td id="hlcode" class="code"></td>' | |||
|
346 | yield 0, '</tr>' | |||
|
347 | ||||
|
348 | yield 0, '<tr>' | |||
|
349 | if self.url: | |||
|
350 | yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % ( | |||
|
351 | self.url, current_line_number, current_line_number) | |||
|
352 | else: | |||
|
353 | yield 0, '<td class="line"><a href="">%i</a></td>' % ( | |||
|
354 | current_line_number) | |||
|
355 | yield 0, '<td id="hlcode" class="code">' + line + '</td>' | |||
|
356 | yield 0, '</tr>' | |||
|
357 | ||||
|
358 | last_shown_line_number = current_line_number | |||
|
359 | ||||
|
360 | current_line_number += 1 | |||
|
361 | ||||
|
362 | ||||
|
363 | yield 0, '</table>' | |||
|
364 | ||||
|
365 | ||||
|
366 | def extract_phrases(text_query): | |||
|
367 | """ | |||
|
368 | Extracts phrases from search term string making sure phrases | |||
|
369 | contained in double quotes are kept together - and discarding empty values | |||
|
370 | or fully whitespace values eg. | |||
|
371 | ||||
|
372 | 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more'] | |||
|
373 | ||||
|
374 | """ | |||
|
375 | ||||
|
376 | in_phrase = False | |||
|
377 | buf = '' | |||
|
378 | phrases = [] | |||
|
379 | for char in text_query: | |||
|
380 | if in_phrase: | |||
|
381 | if char == '"': # end phrase | |||
|
382 | phrases.append(buf) | |||
|
383 | buf = '' | |||
|
384 | in_phrase = False | |||
|
385 | continue | |||
|
386 | else: | |||
|
387 | buf += char | |||
|
388 | continue | |||
|
389 | else: | |||
|
390 | if char == '"': # start phrase | |||
|
391 | in_phrase = True | |||
|
392 | phrases.append(buf) | |||
|
393 | buf = '' | |||
|
394 | continue | |||
|
395 | elif char == ' ': | |||
|
396 | phrases.append(buf) | |||
|
397 | buf = '' | |||
|
398 | continue | |||
|
399 | else: | |||
|
400 | buf += char | |||
|
401 | ||||
|
402 | phrases.append(buf) | |||
|
403 | phrases = [phrase.strip() for phrase in phrases if phrase.strip()] | |||
|
404 | return phrases | |||
|
405 | ||||
|
406 | ||||
|
407 | def get_matching_offsets(text, phrases): | |||
|
408 | """ | |||
|
409 | Returns a list of string offsets in `text` that the list of `terms` match | |||
|
410 | ||||
|
411 | >>> get_matching_offsets('some text here', ['some', 'here']) | |||
|
412 | [(0, 4), (10, 14)] | |||
|
413 | ||||
|
414 | """ | |||
|
415 | offsets = [] | |||
|
416 | for phrase in phrases: | |||
|
417 | for match in re.finditer(phrase, text): | |||
|
418 | offsets.append((match.start(), match.end())) | |||
|
419 | ||||
|
420 | return offsets | |||
|
421 | ||||
|
422 | ||||
|
423 | def normalize_text_for_matching(x): | |||
|
424 | """ | |||
|
425 | Replaces all non alnum characters to spaces and lower cases the string, | |||
|
426 | useful for comparing two text strings without punctuation | |||
|
427 | """ | |||
|
428 | return re.sub(r'[^\w]', ' ', x.lower()) | |||
|
429 | ||||
|
430 | ||||
|
431 | def get_matching_line_offsets(lines, terms): | |||
|
432 | """ Return a set of `lines` indices (starting from 1) matching a | |||
|
433 | text search query, along with `context` lines above/below matching lines | |||
|
434 | ||||
|
435 | :param lines: list of strings representing lines | |||
|
436 | :param terms: search term string to match in lines eg. 'some text' | |||
|
437 | :param context: number of lines above/below a matching line to add to result | |||
|
438 | :param max_lines: cut off for lines of interest | |||
|
439 | eg. | |||
|
440 | ||||
|
441 | >>> get_matching_line_offsets(''' | |||
|
442 | words words words | |||
|
443 | words words words | |||
|
444 | some text some | |||
|
445 | words words words | |||
|
446 | words words words | |||
|
447 | text here what | |||
|
448 | ''', 'text', context=1) | |||
|
449 | {3: [(5, 9)], 6: [(0, 4)]] | |||
|
450 | """ | |||
|
451 | matching_lines = {} | |||
|
452 | phrases = [normalize_text_for_matching(phrase) | |||
|
453 | for phrase in extract_phrases(terms)] | |||
|
454 | ||||
|
455 | for line_index, line in enumerate(lines, start=1): | |||
|
456 | match_offsets = get_matching_offsets( | |||
|
457 | normalize_text_for_matching(line), phrases) | |||
|
458 | if match_offsets: | |||
|
459 | matching_lines[line_index] = match_offsets | |||
|
460 | ||||
|
461 | return matching_lines | |||
|
462 | ||||
|
463 | def get_lexer_safe(mimetype=None, filepath=None): | |||
|
464 | """ | |||
|
465 | Tries to return a relevant pygments lexer using mimetype/filepath name, | |||
|
466 | defaulting to plain text if none could be found | |||
|
467 | """ | |||
|
468 | lexer = None | |||
|
469 | try: | |||
|
470 | if mimetype: | |||
|
471 | lexer = get_lexer_for_mimetype(mimetype) | |||
|
472 | if not lexer: | |||
|
473 | lexer = get_lexer_for_filename(path) | |||
|
474 | except pygments.util.ClassNotFound: | |||
|
475 | pass | |||
|
476 | ||||
|
477 | if not lexer: | |||
|
478 | lexer = get_lexer_by_name('text') | |||
|
479 | ||||
|
480 | return lexer | |||
|
481 | ||||
|
482 | ||||
310 | def pygmentize(filenode, **kwargs): |
|
483 | def pygmentize(filenode, **kwargs): | |
311 | """ |
|
484 | """ | |
312 | pygmentize function using pygments |
|
485 | pygmentize function using pygments | |
@@ -476,13 +649,20 b' short_id = lambda x: x[:12]' | |||||
476 | hide_credentials = lambda x: ''.join(credentials_filter(x)) |
|
649 | hide_credentials = lambda x: ''.join(credentials_filter(x)) | |
477 |
|
650 | |||
478 |
|
651 | |||
479 | def age_component(datetime_iso, value=None): |
|
652 | def age_component(datetime_iso, value=None, time_is_local=False): | |
480 | title = value or format_date(datetime_iso) |
|
653 | title = value or format_date(datetime_iso) | |
481 |
|
654 | |||
482 |
# detect if we have a timezone info, |
|
655 | # detect if we have a timezone info, otherwise, add it | |
483 | if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo: |
|
656 | if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo: | |
484 | tzinfo = '+00:00' |
|
657 | tzinfo = '+00:00' | |
485 |
|
658 | |||
|
659 | if time_is_local: | |||
|
660 | tzinfo = time.strftime("+%H:%M", | |||
|
661 | time.gmtime( | |||
|
662 | (datetime.now() - datetime.utcnow()).seconds + 1 | |||
|
663 | ) | |||
|
664 | ) | |||
|
665 | ||||
486 | return literal( |
|
666 | return literal( | |
487 | '<time class="timeago tooltip" ' |
|
667 | '<time class="timeago tooltip" ' | |
488 | 'title="{1}" datetime="{0}{2}">{1}</time>'.format( |
|
668 | 'title="{1}" datetime="{0}{2}">{1}</time>'.format( |
@@ -42,7 +42,6 b' class BaseSearch(object):' | |||||
42 | def search(self, query, document_type, search_user, repo_name=None): |
|
42 | def search(self, query, document_type, search_user, repo_name=None): | |
43 | raise Exception('NotImplemented') |
|
43 | raise Exception('NotImplemented') | |
44 |
|
44 | |||
45 |
|
||||
46 | def searcher_from_config(config, prefix='search.'): |
|
45 | def searcher_from_config(config, prefix='search.'): | |
47 | _config = {} |
|
46 | _config = {} | |
48 | for key in config.keys(): |
|
47 | for key in config.keys(): |
@@ -25,6 +25,7 b' Index schema for RhodeCode' | |||||
25 | from __future__ import absolute_import |
|
25 | from __future__ import absolute_import | |
26 | import logging |
|
26 | import logging | |
27 | import os |
|
27 | import os | |
|
28 | import re | |||
28 |
|
29 | |||
29 | from pylons.i18n.translation import _ |
|
30 | from pylons.i18n.translation import _ | |
30 |
|
31 | |||
@@ -59,6 +60,7 b' FRAGMENTER = ContextFragmenter(200)' | |||||
59 | log = logging.getLogger(__name__) |
|
60 | log = logging.getLogger(__name__) | |
60 |
|
61 | |||
61 |
|
62 | |||
|
63 | ||||
62 | class Search(BaseSearch): |
|
64 | class Search(BaseSearch): | |
63 |
|
65 | |||
64 | name = 'whoosh' |
|
66 | name = 'whoosh' | |
@@ -90,7 +92,19 b' class Search(BaseSearch):' | |||||
90 | if self.searcher: |
|
92 | if self.searcher: | |
91 | self.searcher.close() |
|
93 | self.searcher.close() | |
92 |
|
94 | |||
93 | def search(self, query, document_type, search_user, repo_name=None): |
|
95 | def _extend_query(self, query): | |
|
96 | hashes = re.compile('([0-9a-f]{5,40})').findall(query) | |||
|
97 | if hashes: | |||
|
98 | hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes) | |||
|
99 | query = u'(%s) OR %s' % (query, hashes_or_query) | |||
|
100 | return query | |||
|
101 | ||||
|
102 | def search(self, query, document_type, search_user, repo_name=None, | |||
|
103 | requested_page=1, page_limit=10, sort=None): | |||
|
104 | ||||
|
105 | original_query = query | |||
|
106 | query = self._extend_query(query) | |||
|
107 | ||||
94 | log.debug(u'QUERY: %s on %s', query, document_type) |
|
108 | log.debug(u'QUERY: %s on %s', query, document_type) | |
95 | result = { |
|
109 | result = { | |
96 | 'results': [], |
|
110 | 'results': [], | |
@@ -109,13 +123,18 b' class Search(BaseSearch):' | |||||
109 | query = qp.parse(unicode(query)) |
|
123 | query = qp.parse(unicode(query)) | |
110 | log.debug('query: %s (%s)' % (query, repr(query))) |
|
124 | log.debug('query: %s (%s)' % (query, repr(query))) | |
111 |
|
125 | |||
112 | sortedby = None |
|
126 | reverse, sortedby = False, None | |
113 | if search_type == 'message': |
|
127 | if search_type == 'message': | |
114 | sortedby = sorting.FieldFacet('commit_idx', reverse=True) |
|
128 | if sort == 'oldfirst': | |
|
129 | sortedby = 'date' | |||
|
130 | reverse = False | |||
|
131 | elif sort == 'newfirst': | |||
|
132 | sortedby = 'date' | |||
|
133 | reverse = True | |||
115 |
|
134 | |||
116 | whoosh_results = self.searcher.search( |
|
135 | whoosh_results = self.searcher.search( | |
117 | query, filter=allowed_repos_filter, limit=None, |
|
136 | query, filter=allowed_repos_filter, limit=None, | |
118 | sortedby=sortedby,) |
|
137 | sortedby=sortedby, reverse=reverse) | |
119 |
|
138 | |||
120 | # fixes for 32k limit that whoosh uses for highlight |
|
139 | # fixes for 32k limit that whoosh uses for highlight | |
121 | whoosh_results.fragmenter.charlimit = None |
|
140 | whoosh_results.fragmenter.charlimit = None |
@@ -63,7 +63,7 b' COMMIT_SCHEMA = Schema(' | |||||
63 | repository_id=NUMERIC(unique=True, stored=True), |
|
63 | repository_id=NUMERIC(unique=True, stored=True), | |
64 | commit_idx=NUMERIC(stored=True, sortable=True), |
|
64 | commit_idx=NUMERIC(stored=True, sortable=True), | |
65 | commit_idx_sort=ID(), |
|
65 | commit_idx_sort=ID(), | |
66 | date=NUMERIC(stored=True), |
|
66 | date=NUMERIC(stored=True, sortable=True), | |
67 | owner=TEXT(stored=True), |
|
67 | owner=TEXT(stored=True), | |
68 | author=TEXT(stored=True), |
|
68 | author=TEXT(stored=True), | |
69 | message=FieldType(format=Characters(), analyzer=ANALYZER, |
|
69 | message=FieldType(format=Characters(), analyzer=ANALYZER, |
@@ -755,8 +755,8 b' def create_test_env(repos_test_path, con' | |||||
755 | # PART TWO make test repo |
|
755 | # PART TWO make test repo | |
756 | log.debug('making test vcs repositories') |
|
756 | log.debug('making test vcs repositories') | |
757 |
|
757 | |||
758 |
idx_path = config[' |
|
758 | idx_path = config['search.location'] | |
759 |
data_path = config[' |
|
759 | data_path = config['cache_dir'] | |
760 |
|
760 | |||
761 | #clean index and data |
|
761 | # clean index and data | |
762 | if idx_path and os.path.exists(idx_path): |
|
762 | if idx_path and os.path.exists(idx_path): | |
@@ -787,7 +787,6 b' def create_test_env(repos_test_path, con' | |||||
787 | tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO)) |
|
787 | tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO)) | |
788 |
|
788 | |||
789 |
|
789 | |||
790 |
|
||||
791 | #============================================================================== |
|
790 | #============================================================================== | |
792 | # PASTER COMMANDS |
|
791 | # PASTER COMMANDS | |
793 | #============================================================================== |
|
792 | #============================================================================== |
@@ -608,6 +608,16 b' def time_to_datetime(tm):' | |||||
608 | return datetime.datetime.fromtimestamp(tm) |
|
608 | return datetime.datetime.fromtimestamp(tm) | |
609 |
|
609 | |||
610 |
|
610 | |||
|
611 | def time_to_utcdatetime(tm): | |||
|
612 | if tm: | |||
|
613 | if isinstance(tm, basestring): | |||
|
614 | try: | |||
|
615 | tm = float(tm) | |||
|
616 | except ValueError: | |||
|
617 | return | |||
|
618 | return datetime.datetime.utcfromtimestamp(tm) | |||
|
619 | ||||
|
620 | ||||
611 | MENTIONS_REGEX = re.compile( |
|
621 | MENTIONS_REGEX = re.compile( | |
612 | # ^@ or @ without any special chars in front |
|
622 | # ^@ or @ without any special chars in front | |
613 | r'(?:^@|[^a-zA-Z0-9\-\_\.]@)' |
|
623 | r'(?:^@|[^a-zA-Z0-9\-\_\.]@)' |
@@ -409,7 +409,9 b' class BaseRepository(object):' | |||||
409 | shadow_repository_path, target_ref, source_repo, |
|
409 | shadow_repository_path, target_ref, source_repo, | |
410 | source_ref, message, user_name, user_email, dry_run=dry_run) |
|
410 | source_ref, message, user_name, user_email, dry_run=dry_run) | |
411 | except RepositoryError: |
|
411 | except RepositoryError: | |
412 | log.exception('Unexpected failure when running merge') |
|
412 | log.exception( | |
|
413 | 'Unexpected failure when running merge, dry-run=%s', | |||
|
414 | dry_run) | |||
413 | return MergeResponse( |
|
415 | return MergeResponse( | |
414 | False, False, None, MergeFailureReason.UNKNOWN) |
|
416 | False, False, None, MergeFailureReason.UNKNOWN) | |
415 |
|
417 |
@@ -30,7 +30,7 b' from StringIO import StringIO' | |||||
30 |
|
30 | |||
31 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
31 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
32 |
|
32 | |||
33 | from rhodecode.lib.datelib import date_fromtimestamp |
|
33 | from rhodecode.lib.datelib import utcdate_fromtimestamp | |
34 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
34 | from rhodecode.lib.utils import safe_unicode, safe_str | |
35 | from rhodecode.lib.utils2 import safe_int |
|
35 | from rhodecode.lib.utils2 import safe_int | |
36 | from rhodecode.lib.vcs.conf import settings |
|
36 | from rhodecode.lib.vcs.conf import settings | |
@@ -95,7 +95,7 b' class GitCommit(base.BaseCommit):' | |||||
95 | if value: |
|
95 | if value: | |
96 | value = safe_unicode(value) |
|
96 | value = safe_unicode(value) | |
97 | elif attr == "date": |
|
97 | elif attr == "date": | |
98 | value = date_fromtimestamp(*value) |
|
98 | value = utcdate_fromtimestamp(*value) | |
99 | elif attr == "parents": |
|
99 | elif attr == "parents": | |
100 | value = self._make_commits(value) |
|
100 | value = self._make_commits(value) | |
101 | self.__dict__[attr] = value |
|
101 | self.__dict__[attr] = value | |
@@ -135,7 +135,7 b' class GitCommit(base.BaseCommit):' | |||||
135 | def date(self): |
|
135 | def date(self): | |
136 | unix_ts, tz = self._remote.get_object_attrs( |
|
136 | unix_ts, tz = self._remote.get_object_attrs( | |
137 | self.raw_id, self._date_property, self._date_tz_property) |
|
137 | self.raw_id, self._date_property, self._date_tz_property) | |
138 | return date_fromtimestamp(unix_ts, tz) |
|
138 | return utcdate_fromtimestamp(unix_ts, tz) | |
139 |
|
139 | |||
140 | @LazyProperty |
|
140 | @LazyProperty | |
141 | def status(self): |
|
141 | def status(self): |
@@ -31,7 +31,7 b' import time' | |||||
31 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
31 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
32 |
|
32 | |||
33 | from rhodecode.lib.compat import OrderedDict |
|
33 | from rhodecode.lib.compat import OrderedDict | |
34 | from rhodecode.lib.datelib import makedate, date_fromtimestamp |
|
34 | from rhodecode.lib.datelib import makedate, utcdate_fromtimestamp | |
35 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
35 | from rhodecode.lib.utils import safe_unicode, safe_str | |
36 | from rhodecode.lib.vcs import connection, path as vcspath |
|
36 | from rhodecode.lib.vcs import connection, path as vcspath | |
37 | from rhodecode.lib.vcs.backends.base import ( |
|
37 | from rhodecode.lib.vcs.backends.base import ( | |
@@ -269,7 +269,7 b' class GitRepository(BaseRepository):' | |||||
269 | Returns last change made on this repository as |
|
269 | Returns last change made on this repository as | |
270 | `datetime.datetime` object. |
|
270 | `datetime.datetime` object. | |
271 | """ |
|
271 | """ | |
272 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) |
|
272 | return utcdate_fromtimestamp(self._get_mtime(), makedate()[1]) | |
273 |
|
273 | |||
274 | def _get_mtime(self): |
|
274 | def _get_mtime(self): | |
275 | try: |
|
275 | try: | |
@@ -853,7 +853,8 b' class GitRepository(BaseRepository):' | |||||
853 | shadow_repo._checkout(pr_branch, create=True) |
|
853 | shadow_repo._checkout(pr_branch, create=True) | |
854 | try: |
|
854 | try: | |
855 | shadow_repo._local_fetch(source_repo.path, source_ref.name) |
|
855 | shadow_repo._local_fetch(source_repo.path, source_ref.name) | |
856 | except RepositoryError: |
|
856 | except RepositoryError as e: | |
|
857 | log.exception('Failure when doing local fetch on git shadow repo') | |||
857 | return MergeResponse( |
|
858 | return MergeResponse( | |
858 | False, False, None, MergeFailureReason.MISSING_COMMIT) |
|
859 | False, False, None, MergeFailureReason.MISSING_COMMIT) | |
859 |
|
860 | |||
@@ -863,7 +864,8 b' class GitRepository(BaseRepository):' | |||||
863 | shadow_repo._local_merge(merge_message, merger_name, merger_email, |
|
864 | shadow_repo._local_merge(merge_message, merger_name, merger_email, | |
864 | [source_ref.commit_id]) |
|
865 | [source_ref.commit_id]) | |
865 | merge_possible = True |
|
866 | merge_possible = True | |
866 | except RepositoryError: |
|
867 | except RepositoryError as e: | |
|
868 | log.exception('Failure when doing local merge on git shadow repo') | |||
867 | merge_possible = False |
|
869 | merge_possible = False | |
868 | merge_failure_reason = MergeFailureReason.MERGE_FAILED |
|
870 | merge_failure_reason = MergeFailureReason.MERGE_FAILED | |
869 |
|
871 | |||
@@ -877,7 +879,9 b' class GitRepository(BaseRepository):' | |||||
877 | # cannot retrieve the merge commit. |
|
879 | # cannot retrieve the merge commit. | |
878 | shadow_repo = GitRepository(shadow_repository_path) |
|
880 | shadow_repo = GitRepository(shadow_repository_path) | |
879 | merge_commit_id = shadow_repo.branches[pr_branch] |
|
881 | merge_commit_id = shadow_repo.branches[pr_branch] | |
880 | except RepositoryError: |
|
882 | except RepositoryError as e: | |
|
883 | log.exception( | |||
|
884 | 'Failure when doing local push on git shadow repo') | |||
881 | merge_succeeded = False |
|
885 | merge_succeeded = False | |
882 | merge_failure_reason = MergeFailureReason.PUSH_FAILED |
|
886 | merge_failure_reason = MergeFailureReason.PUSH_FAILED | |
883 | else: |
|
887 | else: |
@@ -26,7 +26,7 b' import os' | |||||
26 |
|
26 | |||
27 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
27 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
28 |
|
28 | |||
29 | from rhodecode.lib.datelib import date_fromtimestamp |
|
29 | from rhodecode.lib.datelib import utcdate_fromtimestamp | |
30 | from rhodecode.lib.utils import safe_str, safe_unicode |
|
30 | from rhodecode.lib.utils import safe_str, safe_unicode | |
31 | from rhodecode.lib.vcs import path as vcspath |
|
31 | from rhodecode.lib.vcs import path as vcspath | |
32 | from rhodecode.lib.vcs.backends import base |
|
32 | from rhodecode.lib.vcs.backends import base | |
@@ -78,7 +78,7 b' class MercurialCommit(base.BaseCommit):' | |||||
78 | elif attr == "affected_files": |
|
78 | elif attr == "affected_files": | |
79 | value = map(safe_unicode, value) |
|
79 | value = map(safe_unicode, value) | |
80 | elif attr == "date": |
|
80 | elif attr == "date": | |
81 | value = date_fromtimestamp(*value) |
|
81 | value = utcdate_fromtimestamp(*value) | |
82 | elif attr in ["children", "parents"]: |
|
82 | elif attr in ["children", "parents"]: | |
83 | value = self._make_commits(value) |
|
83 | value = self._make_commits(value) | |
84 | self.__dict__[attr] = value |
|
84 | self.__dict__[attr] = value | |
@@ -114,7 +114,7 b' class MercurialCommit(base.BaseCommit):' | |||||
114 |
|
114 | |||
115 | @LazyProperty |
|
115 | @LazyProperty | |
116 | def date(self): |
|
116 | def date(self): | |
117 | return date_fromtimestamp(*self._remote.ctx_date(self.idx)) |
|
117 | return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx)) | |
118 |
|
118 | |||
119 | @LazyProperty |
|
119 | @LazyProperty | |
120 | def status(self): |
|
120 | def status(self): |
@@ -22,6 +22,7 b'' | |||||
22 | HG repository module |
|
22 | HG repository module | |
23 | """ |
|
23 | """ | |
24 |
|
24 | |||
|
25 | import logging | |||
25 | import binascii |
|
26 | import binascii | |
26 | import os |
|
27 | import os | |
27 | import re |
|
28 | import re | |
@@ -31,9 +32,8 b' import urllib' | |||||
31 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
32 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
32 |
|
33 | |||
33 | from rhodecode.lib.compat import OrderedDict |
|
34 | from rhodecode.lib.compat import OrderedDict | |
34 | from rhodecode.lib.datelib import ( |
|
35 | from rhodecode.lib.datelib import (date_to_timestamp_plus_offset, | |
35 |
date_fromtimestamp, makedate, date_ |
|
36 | utcdate_fromtimestamp, makedate, date_astimestamp) | |
36 | date_astimestamp) |
|
|||
37 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
37 | from rhodecode.lib.utils import safe_unicode, safe_str | |
38 | from rhodecode.lib.vcs import connection |
|
38 | from rhodecode.lib.vcs import connection | |
39 | from rhodecode.lib.vcs.backends.base import ( |
|
39 | from rhodecode.lib.vcs.backends.base import ( | |
@@ -50,6 +50,8 b' from rhodecode.lib.vcs.exceptions import' | |||||
50 | hexlify = binascii.hexlify |
|
50 | hexlify = binascii.hexlify | |
51 | nullid = "\0" * 20 |
|
51 | nullid = "\0" * 20 | |
52 |
|
52 | |||
|
53 | log = logging.getLogger(__name__) | |||
|
54 | ||||
53 |
|
55 | |||
54 | class MercurialRepository(BaseRepository): |
|
56 | class MercurialRepository(BaseRepository): | |
55 | """ |
|
57 | """ | |
@@ -365,7 +367,7 b' class MercurialRepository(BaseRepository' | |||||
365 | Returns last change made on this repository as |
|
367 | Returns last change made on this repository as | |
366 | `datetime.datetime` object |
|
368 | `datetime.datetime` object | |
367 | """ |
|
369 | """ | |
368 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) |
|
370 | return utcdate_fromtimestamp(self._get_mtime(), makedate()[1]) | |
369 |
|
371 | |||
370 | def _get_mtime(self): |
|
372 | def _get_mtime(self): | |
371 | try: |
|
373 | try: | |
@@ -605,6 +607,10 b' class MercurialRepository(BaseRepository' | |||||
605 | self._update(bookmark_name) |
|
607 | self._update(bookmark_name) | |
606 | return self._identify(), True |
|
608 | return self._identify(), True | |
607 | except RepositoryError: |
|
609 | except RepositoryError: | |
|
610 | # The rebase-abort may raise another exception which 'hides' | |||
|
611 | # the original one, therefore we log it here. | |||
|
612 | log.exception('Error while rebasing shadow repo during merge.') | |||
|
613 | ||||
608 | # Cleanup any rebase leftovers |
|
614 | # Cleanup any rebase leftovers | |
609 | self._remote.rebase(abort=True) |
|
615 | self._remote.rebase(abort=True) | |
610 | self._remote.update(clean=True) |
|
616 | self._remote.update(clean=True) | |
@@ -642,6 +648,8 b' class MercurialRepository(BaseRepository' | |||||
642 | shadow_repository_path = self._get_shadow_repository_path(workspace_id) |
|
648 | shadow_repository_path = self._get_shadow_repository_path(workspace_id) | |
643 | if not os.path.exists(shadow_repository_path): |
|
649 | if not os.path.exists(shadow_repository_path): | |
644 | self._local_clone(shadow_repository_path) |
|
650 | self._local_clone(shadow_repository_path) | |
|
651 | log.debug( | |||
|
652 | 'Prepared shadow repository in %s', shadow_repository_path) | |||
645 |
|
653 | |||
646 | return shadow_repository_path |
|
654 | return shadow_repository_path | |
647 |
|
655 | |||
@@ -664,12 +672,15 b' class MercurialRepository(BaseRepository' | |||||
664 |
|
672 | |||
665 | shadow_repo = self._get_shadow_instance(shadow_repository_path) |
|
673 | shadow_repo = self._get_shadow_instance(shadow_repository_path) | |
666 |
|
674 | |||
|
675 | log.debug('Pulling in target reference %s', target_ref) | |||
667 | self._validate_pull_reference(target_ref) |
|
676 | self._validate_pull_reference(target_ref) | |
668 | shadow_repo._local_pull(self.path, target_ref) |
|
677 | shadow_repo._local_pull(self.path, target_ref) | |
669 | try: |
|
678 | try: | |
|
679 | log.debug('Pulling in source reference %s', source_ref) | |||
670 | source_repo._validate_pull_reference(source_ref) |
|
680 | source_repo._validate_pull_reference(source_ref) | |
671 | shadow_repo._local_pull(source_repo.path, source_ref) |
|
681 | shadow_repo._local_pull(source_repo.path, source_ref) | |
672 | except CommitDoesNotExistError: |
|
682 | except CommitDoesNotExistError as e: | |
|
683 | log.exception('Failure when doing local pull on hg shadow repo') | |||
673 | return MergeResponse( |
|
684 | return MergeResponse( | |
674 | False, False, None, MergeFailureReason.MISSING_COMMIT) |
|
685 | False, False, None, MergeFailureReason.MISSING_COMMIT) | |
675 |
|
686 | |||
@@ -681,7 +692,8 b' class MercurialRepository(BaseRepository' | |||||
681 | target_ref, merge_message, merger_name, merger_email, |
|
692 | target_ref, merge_message, merger_name, merger_email, | |
682 | source_ref) |
|
693 | source_ref) | |
683 | merge_possible = True |
|
694 | merge_possible = True | |
684 | except RepositoryError: |
|
695 | except RepositoryError as e: | |
|
696 | log.exception('Failure when doing local merge on hg shadow repo') | |||
685 | merge_possible = False |
|
697 | merge_possible = False | |
686 | merge_failure_reason = MergeFailureReason.MERGE_FAILED |
|
698 | merge_failure_reason = MergeFailureReason.MERGE_FAILED | |
687 |
|
699 | |||
@@ -706,6 +718,9 b' class MercurialRepository(BaseRepository' | |||||
706 | enable_hooks=True) |
|
718 | enable_hooks=True) | |
707 | merge_succeeded = True |
|
719 | merge_succeeded = True | |
708 | except RepositoryError: |
|
720 | except RepositoryError: | |
|
721 | log.exception( | |||
|
722 | 'Failure when doing local push from the shadow ' | |||
|
723 | 'repository to the target repository.') | |||
709 | merge_succeeded = False |
|
724 | merge_succeeded = False | |
710 | merge_failure_reason = MergeFailureReason.PUSH_FAILED |
|
725 | merge_failure_reason = MergeFailureReason.PUSH_FAILED | |
711 | else: |
|
726 | else: |
@@ -1593,7 +1593,7 b' class Repository(Base, BaseModel):' | |||||
1593 | 'repo_id': repo.repo_id, |
|
1593 | 'repo_id': repo.repo_id, | |
1594 | 'repo_name': repo.repo_name, |
|
1594 | 'repo_name': repo.repo_name, | |
1595 | 'repo_type': repo.repo_type, |
|
1595 | 'repo_type': repo.repo_type, | |
1596 | 'clone_uri': repo.clone_uri, |
|
1596 | 'clone_uri': repo.clone_uri or '', | |
1597 | 'private': repo.private, |
|
1597 | 'private': repo.private, | |
1598 | 'created_on': repo.created_on, |
|
1598 | 'created_on': repo.created_on, | |
1599 | 'description': repo.description, |
|
1599 | 'description': repo.description, | |
@@ -2794,7 +2794,9 b' class CacheKey(Base, BaseModel):' | |||||
2794 |
|
2794 | |||
2795 | Session().commit() |
|
2795 | Session().commit() | |
2796 | except Exception: |
|
2796 | except Exception: | |
2797 | log.error(traceback.format_exc()) |
|
2797 | log.exception( | |
|
2798 | 'Cache key invalidation failed for repository %s', | |||
|
2799 | safe_str(repo_name)) | |||
2798 | Session().rollback() |
|
2800 | Session().rollback() | |
2799 |
|
2801 | |||
2800 | @classmethod |
|
2802 | @classmethod |
@@ -396,10 +396,15 b' class PullRequestModel(BaseModel):' | |||||
396 | return commit_ids |
|
396 | return commit_ids | |
397 |
|
397 | |||
398 | def merge(self, pull_request, user, extras): |
|
398 | def merge(self, pull_request, user, extras): | |
|
399 | log.debug("Merging pull request %s", pull_request.pull_request_id) | |||
399 | merge_state = self._merge_pull_request(pull_request, user, extras) |
|
400 | merge_state = self._merge_pull_request(pull_request, user, extras) | |
400 | if merge_state.executed: |
|
401 | if merge_state.executed: | |
|
402 | log.debug( | |||
|
403 | "Merge was successful, updating the pull request comments.") | |||
401 | self._comment_and_close_pr(pull_request, user, merge_state) |
|
404 | self._comment_and_close_pr(pull_request, user, merge_state) | |
402 | self._log_action('user_merged_pull_request', user, pull_request) |
|
405 | self._log_action('user_merged_pull_request', user, pull_request) | |
|
406 | else: | |||
|
407 | log.warn("Merge failed, not updating the pull request.") | |||
403 | return merge_state |
|
408 | return merge_state | |
404 |
|
409 | |||
405 | def _merge_pull_request(self, pull_request, user, extras): |
|
410 | def _merge_pull_request(self, pull_request, user, extras): | |
@@ -907,15 +912,20 b' class PullRequestModel(BaseModel):' | |||||
907 | """ |
|
912 | """ | |
908 | Try to merge the pull request and return the merge status. |
|
913 | Try to merge the pull request and return the merge status. | |
909 | """ |
|
914 | """ | |
|
915 | log.debug( | |||
|
916 | "Trying out if the pull request %s can be merged.", | |||
|
917 | pull_request.pull_request_id) | |||
910 | target_vcs = pull_request.target_repo.scm_instance() |
|
918 | target_vcs = pull_request.target_repo.scm_instance() | |
911 | target_ref = self._refresh_reference( |
|
919 | target_ref = self._refresh_reference( | |
912 | pull_request.target_ref_parts, target_vcs) |
|
920 | pull_request.target_ref_parts, target_vcs) | |
913 |
|
921 | |||
914 | target_locked = pull_request.target_repo.locked |
|
922 | target_locked = pull_request.target_repo.locked | |
915 | if target_locked and target_locked[0]: |
|
923 | if target_locked and target_locked[0]: | |
|
924 | log.debug("The target repository is locked.") | |||
916 | merge_state = MergeResponse( |
|
925 | merge_state = MergeResponse( | |
917 | False, False, None, MergeFailureReason.TARGET_IS_LOCKED) |
|
926 | False, False, None, MergeFailureReason.TARGET_IS_LOCKED) | |
918 | elif self._needs_merge_state_refresh(pull_request, target_ref): |
|
927 | elif self._needs_merge_state_refresh(pull_request, target_ref): | |
|
928 | log.debug("Refreshing the merge status of the repository.") | |||
919 | merge_state = self._refresh_merge_state( |
|
929 | merge_state = self._refresh_merge_state( | |
920 | pull_request, target_vcs, target_ref) |
|
930 | pull_request, target_vcs, target_ref) | |
921 | else: |
|
931 | else: | |
@@ -923,6 +933,7 b' class PullRequestModel(BaseModel):' | |||||
923 | _last_merge_status == MergeFailureReason.NONE |
|
933 | _last_merge_status == MergeFailureReason.NONE | |
924 | merge_state = MergeResponse( |
|
934 | merge_state = MergeResponse( | |
925 | possible, False, None, pull_request._last_merge_status) |
|
935 | possible, False, None, pull_request._last_merge_status) | |
|
936 | log.debug("Merge response: %s", merge_state) | |||
926 | return merge_state |
|
937 | return merge_state | |
927 |
|
938 | |||
928 | def _refresh_reference(self, reference, vcs_repository): |
|
939 | def _refresh_reference(self, reference, vcs_repository): |
@@ -449,7 +449,7 b' class ScmModel(BaseModel):' | |||||
449 | return tip |
|
449 | return tip | |
450 |
|
450 | |||
451 | def _sanitize_path(self, f_path): |
|
451 | def _sanitize_path(self, f_path): | |
452 | if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path: |
|
452 | if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path: | |
453 | raise NonRelativePathError('%s is not an relative path' % f_path) |
|
453 | raise NonRelativePathError('%s is not an relative path' % f_path) | |
454 | if f_path: |
|
454 | if f_path: | |
455 | f_path = os.path.normpath(f_path) |
|
455 | f_path = os.path.normpath(f_path) |
@@ -493,7 +493,7 b' class UserModel(BaseModel):' | |||||
493 | log.error(traceback.format_exc()) |
|
493 | log.error(traceback.format_exc()) | |
494 | raise |
|
494 | raise | |
495 |
|
495 | |||
496 | def reset_password_link(self, data): |
|
496 | def reset_password_link(self, data, pwd_reset_url): | |
497 | from rhodecode.lib.celerylib import tasks, run_task |
|
497 | from rhodecode.lib.celerylib import tasks, run_task | |
498 | from rhodecode.model.notification import EmailNotificationModel |
|
498 | from rhodecode.model.notification import EmailNotificationModel | |
499 | user_email = data['email'] |
|
499 | user_email = data['email'] | |
@@ -502,12 +502,8 b' class UserModel(BaseModel):' | |||||
502 | if user: |
|
502 | if user: | |
503 | log.debug('password reset user found %s', user) |
|
503 | log.debug('password reset user found %s', user) | |
504 |
|
504 | |||
505 | password_reset_url = url( |
|
|||
506 | 'reset_password_confirmation', key=user.api_key, |
|
|||
507 | qualified=True) |
|
|||
508 |
|
||||
509 | email_kwargs = { |
|
505 | email_kwargs = { | |
510 |
'password_reset_url': p |
|
506 | 'password_reset_url': pwd_reset_url, | |
511 | 'user': user, |
|
507 | 'user': user, | |
512 | 'email': user_email, |
|
508 | 'email': user_email, | |
513 | 'date': datetime.datetime.now() |
|
509 | 'date': datetime.datetime.now() |
@@ -216,7 +216,13 b' class UserGroupModel(BaseModel):' | |||||
216 | if 'user' in form_data: |
|
216 | if 'user' in form_data: | |
217 | owner = form_data['user'] |
|
217 | owner = form_data['user'] | |
218 | if isinstance(owner, basestring): |
|
218 | if isinstance(owner, basestring): | |
219 |
|
|
219 | owner = User.get_by_username(form_data['user']) | |
|
220 | ||||
|
221 | if not isinstance(owner, User): | |||
|
222 | raise ValueError( | |||
|
223 | 'invalid owner for user group: %s' % form_data['user']) | |||
|
224 | ||||
|
225 | user_group.user = owner | |||
220 |
|
226 | |||
221 | if 'users_group_members' in form_data: |
|
227 | if 'users_group_members' in form_data: | |
222 | members_id_list = self._clean_members_data( |
|
228 | members_id_list = self._clean_members_data( |
@@ -51,6 +51,11 b' class SearchParamsSchema(colander.Mappin' | |||||
51 | colander.String(), |
|
51 | colander.String(), | |
52 | missing='content', |
|
52 | missing='content', | |
53 | validator=colander.OneOf(['content', 'path', 'commit', 'repository'])) |
|
53 | validator=colander.OneOf(['content', 'path', 'commit', 'repository'])) | |
|
54 | search_sort = colander.SchemaNode( | |||
|
55 | colander.String(), | |||
|
56 | missing='newfirst', | |||
|
57 | validator=colander.OneOf( | |||
|
58 | ['oldfirst', 'newfirst'])) | |||
54 | page_limit = colander.SchemaNode( |
|
59 | page_limit = colander.SchemaNode( | |
55 | colander.Integer(), |
|
60 | colander.Integer(), | |
56 | missing=10, |
|
61 | missing=10, |
@@ -38,9 +38,11 b' from sqlalchemy.sql.expression import tr' | |||||
38 | from sqlalchemy.util import OrderedSet |
|
38 | from sqlalchemy.util import OrderedSet | |
39 | from webhelpers.pylonslib.secure_form import authentication_token |
|
39 | from webhelpers.pylonslib.secure_form import authentication_token | |
40 |
|
40 | |||
|
41 | from rhodecode.authentication import ( | |||
|
42 | legacy_plugin_prefix, _import_legacy_plugin) | |||
|
43 | from rhodecode.authentication.base import loadplugin | |||
41 | from rhodecode.config.routing import ADMIN_PREFIX |
|
44 | from rhodecode.config.routing import ADMIN_PREFIX | |
42 | from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny |
|
45 | from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny | |
43 | from rhodecode.lib.exceptions import LdapImportError |
|
|||
44 | from rhodecode.lib.utils import repo_name_slug, make_db_config |
|
46 | from rhodecode.lib.utils import repo_name_slug, make_db_config | |
45 | from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5 |
|
47 | from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5 | |
46 | from rhodecode.lib.vcs.backends.git.repository import GitRepository |
|
48 | from rhodecode.lib.vcs.backends.git.repository import GitRepository | |
@@ -437,8 +439,7 b' def ValidAuth():' | |||||
437 | password = value['password'] |
|
439 | password = value['password'] | |
438 | username = value['username'] |
|
440 | username = value['username'] | |
439 |
|
441 | |||
440 | if not authenticate(username, password, '', |
|
442 | if not authenticate(username, password, '', HTTP_TYPE, | |
441 | HTTP_TYPE, |
|
|||
442 | skip_missing=True): |
|
443 | skip_missing=True): | |
443 | user = User.get_by_username(username) |
|
444 | user = User.get_by_username(username) | |
444 | if user and not user.active: |
|
445 | if user and not user.active: | |
@@ -448,7 +449,7 b' def ValidAuth():' | |||||
448 | msg, value, state, error_dict={'username': msg} |
|
449 | msg, value, state, error_dict={'username': msg} | |
449 | ) |
|
450 | ) | |
450 | else: |
|
451 | else: | |
451 | log.warning('user %s failed to authenticate', username) |
|
452 | log.warning('user `%s` failed to authenticate', username) | |
452 | msg = M(self, 'invalid_username', state) |
|
453 | msg = M(self, 'invalid_username', state) | |
453 | msg2 = M(self, 'invalid_password', state) |
|
454 | msg2 = M(self, 'invalid_password', state) | |
454 | raise formencode.Invalid( |
|
455 | raise formencode.Invalid( | |
@@ -986,28 +987,71 b' def ValidAuthPlugins():' | |||||
986 | 'import_duplicate': _( |
|
987 | 'import_duplicate': _( | |
987 | u'Plugins %(loaded)s and %(next_to_load)s ' |
|
988 | u'Plugins %(loaded)s and %(next_to_load)s ' | |
988 | u'both export the same name'), |
|
989 | u'both export the same name'), | |
|
990 | 'missing_includeme': _( | |||
|
991 | u'The plugin "%(plugin_id)s" is missing an includeme ' | |||
|
992 | u'function.'), | |||
|
993 | 'import_error': _( | |||
|
994 | u'Can not load plugin "%(plugin_id)s"'), | |||
|
995 | 'no_plugin': _( | |||
|
996 | u'No plugin available with ID "%(plugin_id)s"'), | |||
989 | } |
|
997 | } | |
990 |
|
998 | |||
991 | def _to_python(self, value, state): |
|
999 | def _to_python(self, value, state): | |
992 | # filter empty values |
|
1000 | # filter empty values | |
993 | return filter(lambda s: s not in [None, ''], value) |
|
1001 | return filter(lambda s: s not in [None, ''], value) | |
994 |
|
1002 | |||
995 |
def validate_ |
|
1003 | def _validate_legacy_plugin_id(self, plugin_id, value, state): | |
996 | from rhodecode.authentication.base import loadplugin |
|
1004 | """ | |
997 | module_list = value |
|
1005 | Validates that the plugin import works. It also checks that the | |
998 | unique_names = {} |
|
1006 | plugin has an includeme attribute. | |
|
1007 | """ | |||
999 | try: |
|
1008 | try: | |
1000 | for module in module_list: |
|
1009 | plugin = _import_legacy_plugin(plugin_id) | |
1001 | plugin = loadplugin(module) |
|
1010 | except Exception as e: | |
1002 | plugin_name = plugin.name |
|
1011 | log.exception( | |
1003 | if plugin_name in unique_names: |
|
1012 | 'Exception during import of auth legacy plugin "{}"' | |
|
1013 | .format(plugin_id)) | |||
|
1014 | msg = M(self, 'import_error', plugin_id=plugin_id) | |||
|
1015 | raise formencode.Invalid(msg, value, state) | |||
|
1016 | ||||
|
1017 | if not hasattr(plugin, 'includeme'): | |||
|
1018 | msg = M(self, 'missing_includeme', plugin_id=plugin_id) | |||
|
1019 | raise formencode.Invalid(msg, value, state) | |||
|
1020 | ||||
|
1021 | return plugin | |||
|
1022 | ||||
|
1023 | def _validate_plugin_id(self, plugin_id, value, state): | |||
|
1024 | """ | |||
|
1025 | Plugins are already imported during app start up. Therefore this | |||
|
1026 | validation only retrieves the plugin from the plugin registry and | |||
|
1027 | if it returns something not None everything is OK. | |||
|
1028 | """ | |||
|
1029 | plugin = loadplugin(plugin_id) | |||
|
1030 | ||||
|
1031 | if plugin is None: | |||
|
1032 | msg = M(self, 'no_plugin', plugin_id=plugin_id) | |||
|
1033 | raise formencode.Invalid(msg, value, state) | |||
|
1034 | ||||
|
1035 | return plugin | |||
|
1036 | ||||
|
1037 | def validate_python(self, value, state): | |||
|
1038 | unique_names = {} | |||
|
1039 | for plugin_id in value: | |||
|
1040 | ||||
|
1041 | # Validate legacy or normal plugin. | |||
|
1042 | if plugin_id.startswith(legacy_plugin_prefix): | |||
|
1043 | plugin = self._validate_legacy_plugin_id( | |||
|
1044 | plugin_id, value, state) | |||
|
1045 | else: | |||
|
1046 | plugin = self._validate_plugin_id(plugin_id, value, state) | |||
|
1047 | ||||
|
1048 | # Only allow unique plugin names. | |||
|
1049 | if plugin.name in unique_names: | |||
1004 |
|
|
1050 | msg = M(self, 'import_duplicate', state, | |
1005 |
|
|
1051 | loaded=unique_names[plugin.name], | |
1006 |
|
|
1052 | next_to_load=plugin) | |
1007 |
|
|
1053 | raise formencode.Invalid(msg, value, state) | |
1008 |
|
|
1054 | unique_names[plugin.name] = plugin | |
1009 | except (KeyError, AttributeError, TypeError) as e: |
|
|||
1010 | raise formencode.Invalid(str(e), value, state) |
|
|||
1011 |
|
1055 | |||
1012 | return _validator |
|
1056 | return _validator | |
1013 |
|
1057 |
@@ -514,6 +514,26 b' div.search-code-body {' | |||||
514 | .match { background-color: #faffa6;} |
|
514 | .match { background-color: #faffa6;} | |
515 | .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; } |
|
515 | .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; } | |
516 | } |
|
516 | } | |
|
517 | .code-highlighttable { | |||
|
518 | border-collapse: collapse; | |||
|
519 | ||||
|
520 | tr:hover { | |||
|
521 | background: #fafafa; | |||
|
522 | } | |||
|
523 | td.code { | |||
|
524 | padding-left: 10px; | |||
|
525 | } | |||
|
526 | td.line { | |||
|
527 | border-right: 1px solid #ccc !important; | |||
|
528 | padding-right: 10px; | |||
|
529 | text-align: right; | |||
|
530 | font-family: "Lucida Console",Monaco,monospace; | |||
|
531 | span { | |||
|
532 | white-space: pre-wrap; | |||
|
533 | color: #666666; | |||
|
534 | } | |||
|
535 | } | |||
|
536 | } | |||
517 | } |
|
537 | } | |
518 |
|
538 | |||
519 | div.annotatediv { margin-left: 2px; margin-right: 4px; } |
|
539 | div.annotatediv { margin-left: 2px; margin-right: 4px; } |
@@ -353,7 +353,12 b'' | |||||
353 | .middle-group{ |
|
353 | .middle-group{ | |
354 | width: 10%; |
|
354 | width: 10%; | |
355 | text-align: center; |
|
355 | text-align: center; | |
356 |
padding-top: |
|
356 | padding-top: 4em; | |
|
357 | i { | |||
|
358 | font-size: 18px; | |||
|
359 | cursor: pointer; | |||
|
360 | line-height: 2em; | |||
|
361 | } | |||
357 | } |
|
362 | } | |
358 |
|
363 | |||
359 | } |
|
364 | } |
@@ -1234,6 +1234,13 b' table.issuetracker {' | |||||
1234 | .reviewer { |
|
1234 | .reviewer { | |
1235 | float: left; |
|
1235 | float: left; | |
1236 | } |
|
1236 | } | |
|
1237 | ||||
|
1238 | &.to-delete { | |||
|
1239 | .user, | |||
|
1240 | .reviewer { | |||
|
1241 | text-decoration: line-through; | |||
|
1242 | } | |||
|
1243 | } | |||
1237 | } |
|
1244 | } | |
1238 |
|
1245 | |||
1239 | .reviewer_member_remove { |
|
1246 | .reviewer_member_remove { |
@@ -80,6 +80,11 b'' | |||||
80 | [tag="recommends"] { &:extend(.tag7); } |
|
80 | [tag="recommends"] { &:extend(.tag7); } | |
81 | [tag="see"] { &:extend(.tag8); } |
|
81 | [tag="see"] { &:extend(.tag8); } | |
82 |
|
82 | |||
|
83 | .perm_overriden { | |||
|
84 | text-decoration: line-through; | |||
|
85 | opacity: 0.6; | |||
|
86 | } | |||
|
87 | ||||
83 | .perm_tag { |
|
88 | .perm_tag { | |
84 | &:extend(.tag); |
|
89 | &:extend(.tag); | |
85 |
|
90 |
@@ -1,45 +1,50 b'' | |||||
1 | /* This file is automatically generated. DO NOT change it manually. |
|
1 | ||
2 | * If this file needs to be modified, edit |
|
2 | /****************************************************************************** | |
3 | * rhodecode/utils/file_generation/js_routes_data.py |
|
3 | * * | |
4 | * and run the script invoke -r scripts/ generate.js-routes . |
|
4 | * DO NOT CHANGE THIS FILE MANUALLY * | |
5 | */ |
|
5 | * * | |
|
6 | * * | |||
|
7 | * This file is automatically generated when the app starts up. * | |||
|
8 | * * | |||
|
9 | * To add a route here pass jsroute=True to the route definition in the app * | |||
|
10 | * * | |||
|
11 | ******************************************************************************/ | |||
6 | function registerRCRoutes() { |
|
12 | function registerRCRoutes() { | |
7 | // routes registration |
|
13 | // routes registration | |
8 | pyroutes.register('home', '/', []); |
|
14 | pyroutes.register('home', '/', []); | |
9 |
pyroutes.register(' |
|
15 | pyroutes.register('user_autocomplete_data', '/_users', []); | |
10 | pyroutes.register('gists', '/_admin/gists', []); |
|
|||
11 | pyroutes.register('new_repo', '/_admin/create_repository', []); |
|
16 | pyroutes.register('new_repo', '/_admin/create_repository', []); | |
12 | pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']); |
|
17 | pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); | |
13 | pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']); |
|
18 | pyroutes.register('gists', '/_admin/gists', []); | |
14 | pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
19 | pyroutes.register('new_gist', '/_admin/gists/new', []); | |
|
20 | pyroutes.register('toggle_following', '/_admin/toggle_following', []); | |||
|
21 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); | |||
|
22 | pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); | |||
|
23 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); | |||
|
24 | pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); | |||
15 | pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); |
|
25 | pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); | |
16 | pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); |
|
26 | pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); | |
17 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); |
|
|||
18 | pyroutes.register('user_autocomplete_data', '/_users', []); |
|
|||
19 | pyroutes.register('toggle_following', '/_admin/toggle_following', []); |
|
|||
20 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); |
|
|||
21 | pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']); |
|
|||
22 | pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); |
|
|||
23 | pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']); |
|
27 | pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']); | |
24 | pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']); |
|
28 | pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']); | |
25 | pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); |
|
29 | pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); | |
26 |
pyroutes.register(' |
|
30 | pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']); | |
27 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); |
|
31 | pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']); | |
|
32 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); | |||
|
33 | pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']); | |||
|
34 | pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']); | |||
|
35 | pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']); | |||
|
36 | pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |||
|
37 | pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); | |||
|
38 | pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |||
|
39 | pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); | |||
|
40 | pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']); | |||
|
41 | pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |||
|
42 | pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |||
|
43 | pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |||
|
44 | pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |||
28 | pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']); |
|
45 | pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']); | |
29 | pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
46 | pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
30 | pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
47 | pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
31 |
pyroutes.register(' |
|
48 | pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']); | |
32 |
pyroutes.register(' |
|
49 | pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']); | |
33 | pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
|||
34 | pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']); |
|
|||
35 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); |
|
|||
36 | pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); |
|
|||
37 | pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']); |
|
|||
38 | pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); |
|
|||
39 | pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); |
|
|||
40 | pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']); |
|
|||
41 | pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']); |
|
|||
42 | pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']); |
|
|||
43 | } |
|
50 | } | |
44 |
|
||||
45 | registerRCRoutes(); No newline at end of file |
|
@@ -190,7 +190,7 b' var AgeModule = (function () {' | |||||
190 |
|
190 | |||
191 | }, |
|
191 | }, | |
192 | createTimeComponent: function(dateTime, text) { |
|
192 | createTimeComponent: function(dateTime, text) { | |
193 | return '<time class="timeago tooltip" title="{1}" datetime="{0}">{1}</time>'.format(dateTime, text); |
|
193 | return '<time class="timeago tooltip" title="{1}" datetime="{0}+0000">{1}</time>'.format(dateTime, text); | |
194 | } |
|
194 | } | |
195 | } |
|
195 | } | |
196 | })(); |
|
196 | })(); |
@@ -30,7 +30,6 b' var removeReviewMember = function(review' | |||||
30 | if (reviewer){ |
|
30 | if (reviewer){ | |
31 | // mark as to-remove |
|
31 | // mark as to-remove | |
32 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); |
|
32 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); | |
33 | obj.css("text-decoration", "line-through"); |
|
|||
34 | obj.addClass('to-delete'); |
|
33 | obj.addClass('to-delete'); | |
35 | // now delete the input |
|
34 | // now delete the input | |
36 | $('#reviewer_{0}_input'.format(reviewer_id)).remove(); |
|
35 | $('#reviewer_{0}_input'.format(reviewer_id)).remove(); |
@@ -20,9 +20,11 b'' | |||||
20 |
|
20 | |||
21 |
|
21 | |||
22 | import pylons |
|
22 | import pylons | |
23 | from pyramid.i18n import get_localizer, TranslationStringFactory |
|
|||
24 |
|
23 | |||
25 | tsf = TranslationStringFactory('rc_root') |
|
24 | from pyramid.i18n import get_localizer | |
|
25 | from pyramid.threadlocal import get_current_request | |||
|
26 | ||||
|
27 | from rhodecode.translation import _ as tsf | |||
26 |
|
28 | |||
27 |
|
29 | |||
28 | def add_renderer_globals(event): |
|
30 | def add_renderer_globals(event): | |
@@ -33,8 +35,11 b' def add_renderer_globals(event):' | |||||
33 | event['c'] = pylons.tmpl_context |
|
35 | event['c'] = pylons.tmpl_context | |
34 | event['url'] = pylons.url |
|
36 | event['url'] = pylons.url | |
35 |
|
37 | |||
|
38 | # TODO: When executed in pyramid view context the request is not available | |||
|
39 | # in the event. Find a better solution to get the request. | |||
|
40 | request = event['request'] or get_current_request() | |||
|
41 | ||||
36 | # Add Pyramid translation as '_' to context |
|
42 | # Add Pyramid translation as '_' to context | |
37 | request = event['request'] |
|
|||
38 | event['_'] = request.translate |
|
43 | event['_'] = request.translate | |
39 | event['localizer'] = request.localizer |
|
44 | event['localizer'] = request.localizer | |
40 |
|
45 |
@@ -49,46 +49,44 b'' | |||||
49 | <div class="fields"> |
|
49 | <div class="fields"> | |
50 | ${h.secure_form(request.resource_path(resource, route_name='auth_home'))} |
|
50 | ${h.secure_form(request.resource_path(resource, route_name='auth_home'))} | |
51 | <div class="form"> |
|
51 | <div class="form"> | |
|
52 | ||||
52 | %for node in plugin.get_settings_schema(): |
|
53 | %for node in plugin.get_settings_schema(): | |
53 | <% label_cls = ("label-checkbox" if (node.widget == "bool") else "") %> |
|
54 | <% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %> | |
54 | <div class="field"> |
|
55 | <div class="field"> | |
55 | <div class="label ${label_cls}"><label for="${node.name}">${node.title}</label></div> |
|
56 | <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div> | |
56 | %if node.widget in ["string", "int", "unicode"]: |
|
|||
57 |
|
|
57 | <div class="input"> | |
58 | ${h.text(node.name, class_="medium")} |
|
58 | %if node.widget in ["string", "int", "unicode"]: | |
59 | <p class="help-block">${node.description}</p> |
|
59 | ${h.text(node.name, defaults.get(node.name), class_="medium")} | |
60 | </div> |
|
|||
61 | %elif node.widget == "password": |
|
60 | %elif node.widget == "password": | |
62 | <div class="input"> |
|
61 | ${h.password(node.name, defaults.get(node.name), class_="medium")} | |
63 | ${h.password(node.name, class_="medium")} |
|
62 | %elif node.widget == "bool": | |
|
63 | <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div> | |||
|
64 | %elif node.widget == "select": | |||
|
65 | ${h.select(node.name, defaults.get(node.name), node.validator.choices)} | |||
|
66 | %elif node.widget == "readonly": | |||
|
67 | ${node.default} | |||
|
68 | %else: | |||
|
69 | This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select]. | |||
|
70 | %endif | |||
|
71 | %if node.name in errors: | |||
|
72 | <span class="error-message">${errors.get(node.name)}</span> | |||
|
73 | <br /> | |||
|
74 | %endif | |||
64 |
|
|
75 | <p class="help-block">${node.description}</p> | |
65 |
|
|
76 | </div> | |
66 | %elif node.widget == "bool": |
|
|||
67 | <div class="input"> |
|
|||
68 | <div class="checkbox">${h.checkbox(node.name, True)}</div> |
|
|||
69 | <span class="help-block">${node.description}</span> |
|
|||
70 | </div> |
|
|||
71 | %elif node.widget == "select": |
|
|||
72 | <div class="select"> |
|
|||
73 | ${h.select(node.name, node.default, node.validator.choices)} |
|
|||
74 | <p class="help-block">${node.description}</p> |
|
|||
75 | </div> |
|
|||
76 | %elif node.widget == "readonly": |
|
|||
77 | <div class="input"> |
|
|||
78 | ${node.default} |
|
|||
79 | <p class="help-block">${node.description}</p> |
|
|||
80 | </div> |
|
|||
81 | %else: |
|
|||
82 | <div class="input"> |
|
|||
83 | This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select]. |
|
|||
84 | <p class="help-block">${node.description}</p> |
|
|||
85 | </div> |
|
|||
86 | %endif |
|
|||
87 | </div> |
|
77 | </div> | |
88 | %endfor |
|
78 | %endfor | |
|
79 | ||||
|
80 | ## Allow derived templates to add something below the form | |||
|
81 | ## input fields | |||
|
82 | %if hasattr(next, 'below_form_fields'): | |||
|
83 | ${next.below_form_fields()} | |||
|
84 | %endif | |||
|
85 | ||||
89 | <div class="buttons"> |
|
86 | <div class="buttons"> | |
90 | ${h.submit('save',_('Save'),class_="btn")} |
|
87 | ${h.submit('save',_('Save'),class_="btn")} | |
91 | </div> |
|
88 | </div> | |
|
89 | ||||
92 | </div> |
|
90 | </div> | |
93 | ${h.end_form()} |
|
91 | ${h.end_form()} | |
94 | </div> |
|
92 | </div> |
@@ -66,7 +66,7 b'' | |||||
66 | %if c.gist.gist_expires == -1: |
|
66 | %if c.gist.gist_expires == -1: | |
67 | ${_('never')} |
|
67 | ${_('never')} | |
68 | %else: |
|
68 | %else: | |
69 | ${h.age_component(h.time_to_datetime(c.gist.gist_expires))} |
|
69 | ${h.age_component(h.time_to_utcdatetime(c.gist.gist_expires))} | |
70 | %endif |
|
70 | %endif | |
71 | </span> |
|
71 | </span> | |
72 | </div> |
|
72 | </div> |
@@ -29,7 +29,11 b'' | |||||
29 | <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li> |
|
29 | <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li> | |
30 | <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li> |
|
30 | <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li> | |
31 | <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li> |
|
31 | <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li> | |
32 | <li class="${'active' if c.active=='oauth' else ''}"><a href="${h.url('my_account_oauth')}">${_('OAuth Identities')}</a></li> |
|
32 | ## TODO: Find a better integration of oauth views into navigation. | |
|
33 | %try: | |||
|
34 | <li class="${'active' if c.active=='oauth' else ''}"><a href="${h.route_path('my_account_oauth')}">${_('OAuth Identities')}</a></li> | |||
|
35 | %except KeyError: | |||
|
36 | %endtry | |||
33 | <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li> |
|
37 | <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li> | |
34 | <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li> |
|
38 | <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li> | |
35 | <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li> |
|
39 | <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li> |
@@ -42,9 +42,9 b'' | |||||
42 | ${_('expires')}: ${_('never')} |
|
42 | ${_('expires')}: ${_('never')} | |
43 | %else: |
|
43 | %else: | |
44 | %if auth_token.expired: |
|
44 | %if auth_token.expired: | |
45 | ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} |
|
45 | ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
46 | %else: |
|
46 | %else: | |
47 | ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} |
|
47 | ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
48 | %endif |
|
48 | %endif | |
49 | %endif |
|
49 | %endif | |
50 | </td> |
|
50 | </td> |
@@ -54,9 +54,8 b'' | |||||
54 | setTimeout(function(){ |
|
54 | setTimeout(function () { | |
55 | // we might have a backend problem, try dashboard again |
|
55 | // we might have a backend problem, try dashboard again | |
56 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; |
|
56 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; | |
57 |
}, |
|
57 | }, 3000); | |
58 | } |
|
58 | } else { | |
59 |
|
||||
60 | if (skipCheck || jsonResponse.result === true) { |
|
59 | if (skipCheck || jsonResponse.result === true) { | |
61 | // success, means go to dashboard |
|
60 | // success, means go to dashboard | |
62 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; |
|
61 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; | |
@@ -65,6 +64,7 b'' | |||||
65 | setTimeout(worker, 1000); |
|
64 | setTimeout(worker, 1000); | |
66 | } |
|
65 | } | |
67 | } |
|
66 | } | |
|
67 | } | |||
68 | else { |
|
68 | else { | |
69 | window.location = "${h.url('home')}"; |
|
69 | window.location = "${h.url('home')}"; | |
70 | } |
|
70 | } |
@@ -43,11 +43,14 b'' | |||||
43 | </div> |
|
43 | </div> | |
44 | <div class="field"> |
|
44 | <div class="field"> | |
45 | <div class="label"> |
|
45 | <div class="label"> | |
46 |
<label for="users_group_active">${_(' |
|
46 | <label for="users_group_active">${_('Search')}:</label> | |
|
47 | ${h.text('from_user_group', | |||
|
48 | placeholder="user/usergroup", | |||
|
49 | class_="medium")} | |||
47 | </div> |
|
50 | </div> | |
48 | <div class="select side-by-side-selector"> |
|
51 | <div class="select side-by-side-selector"> | |
49 | <div class="left-group"> |
|
52 | <div class="left-group"> | |
50 |
<label class="text" |
|
53 | <label class="text"><strong>${_('Chosen group members')}</strong></label> | |
51 | ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)} |
|
54 | ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)} | |
52 | <div class="btn" id="remove_all_elements" > |
|
55 | <div class="btn" id="remove_all_elements" > | |
53 | ${_('Remove all elements')} |
|
56 | ${_('Remove all elements')} | |
@@ -60,7 +63,8 b'' | |||||
60 | <i id="remove_element" class="icon-chevron-right"></i> |
|
63 | <i id="remove_element" class="icon-chevron-right"></i> | |
61 | </div> |
|
64 | </div> | |
62 | <div class="right-group"> |
|
65 | <div class="right-group"> | |
63 |
<label class="text" >${_('Available |
|
66 | <label class="text" >${_('Available users')} | |
|
67 | </label> | |||
64 | ${h.select('available_members',[],c.available_members,multiple=True,size=8,)} |
|
68 | ${h.select('available_members',[],c.available_members,multiple=True,size=8,)} | |
65 | <div class="btn" id="add_all_elements" > |
|
69 | <div class="btn" id="add_all_elements" > | |
66 | <i class="icon-chevron-left"></i>${_('Add all elements')} |
|
70 | <i class="icon-chevron-left"></i>${_('Add all elements')} | |
@@ -86,6 +90,42 b'' | |||||
86 | 'dropdownAutoWidth': true |
|
90 | 'dropdownAutoWidth': true | |
87 | }); |
|
91 | }); | |
88 |
|
92 | |||
|
93 | $('#from_user_group').autocomplete({ | |||
|
94 | serviceUrl: pyroutes.url('user_autocomplete_data'), | |||
|
95 | minChars:2, | |||
|
96 | maxHeight:400, | |||
|
97 | width:300, | |||
|
98 | deferRequestBy: 300, //miliseconds | |||
|
99 | showNoSuggestionNotice: true, | |||
|
100 | params: { user_groups:true }, | |||
|
101 | formatResult: autocompleteFormatResult, | |||
|
102 | lookupFilter: autocompleteFilterResult, | |||
|
103 | onSelect: function(element, suggestion){ | |||
|
104 | ||||
|
105 | function preSelectUserIds(uids) { | |||
|
106 | $('#available_members').val(uids); | |||
|
107 | $('#users_group_members').val(uids); | |||
|
108 | } | |||
|
109 | ||||
|
110 | if (suggestion.value_type == 'user_group') { | |||
|
111 | $.getJSON( | |||
|
112 | pyroutes.url('edit_user_group_members', | |||
|
113 | {'user_group_id': suggestion.id}), | |||
|
114 | function(data) { | |||
|
115 | var uids = []; | |||
|
116 | $.each(data.members, function(idx, user) { | |||
|
117 | var userid = user[0], | |||
|
118 | username = user[1]; | |||
|
119 | uids.push(userid.toString()); | |||
|
120 | }); | |||
|
121 | preSelectUserIds(uids) | |||
|
122 | } | |||
|
123 | ); | |||
|
124 | } else if (suggestion.value_type == 'user') { | |||
|
125 | preSelectUserIds([suggestion.id.toString()]); | |||
|
126 | } | |||
|
127 | } | |||
|
128 | }); | |||
89 | UsersAutoComplete('user', '${c.rhodecode_user.user_id}'); |
|
129 | UsersAutoComplete('user', '${c.rhodecode_user.user_id}'); | |
90 | }) |
|
130 | }) | |
91 | </script> |
|
131 | </script> |
@@ -38,9 +38,9 b'' | |||||
38 | ${_('expires')}: ${_('never')} |
|
38 | ${_('expires')}: ${_('never')} | |
39 | %else: |
|
39 | %else: | |
40 | %if auth_token.expired: |
|
40 | %if auth_token.expired: | |
41 | ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} |
|
41 | ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
42 | %else: |
|
42 | %else: | |
43 | ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} |
|
43 | ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
44 | %endif |
|
44 | %endif | |
45 | %endif |
|
45 | %endif | |
46 | </td> |
|
46 | </td> |
@@ -297,7 +297,7 b'' | |||||
297 | <div id="quick_login"> |
|
297 | <div id="quick_login"> | |
298 | %if c.rhodecode_user.username == h.DEFAULT_USER: |
|
298 | %if c.rhodecode_user.username == h.DEFAULT_USER: | |
299 | <h4>${_('Sign in to your account')}</h4> |
|
299 | <h4>${_('Sign in to your account')}</h4> | |
300 |
${h.form(h. |
|
300 | ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)} | |
301 | <div class="form form-vertical"> |
|
301 | <div class="form form-vertical"> | |
302 | <div class="fields"> |
|
302 | <div class="fields"> | |
303 | <div class="field"> |
|
303 | <div class="field"> | |
@@ -312,7 +312,7 b'' | |||||
312 | <div class="field"> |
|
312 | <div class="field"> | |
313 | <div class="label"> |
|
313 | <div class="label"> | |
314 | <label for="password">${_('Password')}:</label> |
|
314 | <label for="password">${_('Password')}:</label> | |
315 |
<span class="forgot_password">${h.link_to(_('(Forgot password?)'),h. |
|
315 | <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'))}</span> | |
316 | </div> |
|
316 | </div> | |
317 | <div class="input"> |
|
317 | <div class="input"> | |
318 | ${h.password('password',class_='focus',tabindex=2)} |
|
318 | ${h.password('password',class_='focus',tabindex=2)} | |
@@ -321,7 +321,7 b'' | |||||
321 | <div class="buttons"> |
|
321 | <div class="buttons"> | |
322 | <div class="register"> |
|
322 | <div class="register"> | |
323 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): |
|
323 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): | |
324 |
${h.link_to(_("Don't have an account ?"),h. |
|
324 | ${h.link_to(_("Don't have an account ?"),h.route_path('register'))} | |
325 | %endif |
|
325 | %endif | |
326 | </div> |
|
326 | </div> | |
327 | <div class="submit"> |
|
327 | <div class="submit"> | |
@@ -341,7 +341,7 b'' | |||||
341 | <ol class="links"> |
|
341 | <ol class="links"> | |
342 | <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li> |
|
342 | <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li> | |
343 | <li class="logout"> |
|
343 | <li class="logout"> | |
344 |
${h.secure_form(h. |
|
344 | ${h.secure_form(h.route_path('logout'))} | |
345 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} |
|
345 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} | |
346 | ${h.end_form()} |
|
346 | ${h.end_form()} | |
347 | </li> |
|
347 | </li> | |
@@ -455,6 +455,9 b'' | |||||
455 | tmpl += '<i class="icon-unlock-alt"></i> '; |
|
455 | tmpl += '<i class="icon-unlock-alt"></i> '; | |
456 | } |
|
456 | } | |
457 | } |
|
457 | } | |
|
458 | if(obj_dict && state.type == 'commit') { | |||
|
459 | tmpl += '<i class="icon-tag"></i>'; | |||
|
460 | } | |||
458 | if(obj_dict && state.type == 'group'){ |
|
461 | if(obj_dict && state.type == 'group'){ | |
459 |
|
|
462 | tmpl += '<i class="icon-folder-close"></i> '; | |
460 | } |
|
463 | } | |
@@ -496,7 +499,7 b'' | |||||
496 | query.callback({results: cachedData.results}); |
|
499 | query.callback({results: cachedData.results}); | |
497 | } else { |
|
500 | } else { | |
498 | $.ajax({ |
|
501 | $.ajax({ | |
499 |
url: "${h.url(' |
|
502 | url: "${h.url('goto_switcher_data')}", | |
500 | data: {'query': query.term}, |
|
503 | data: {'query': query.term}, | |
501 | dataType: 'json', |
|
504 | dataType: 'json', | |
502 | type: 'GET', |
|
505 | type: 'GET', | |
@@ -514,7 +517,7 b'' | |||||
514 |
|
517 | |||
515 | $("#repo_switcher").on('select2-selecting', function(e){ |
|
518 | $("#repo_switcher").on('select2-selecting', function(e){ | |
516 | e.preventDefault(); |
|
519 | e.preventDefault(); | |
517 | window.location = pyroutes.url('summary_home', {'repo_name': e.val}); |
|
520 | window.location = e.choice.url; | |
518 | }); |
|
521 | }); | |
519 |
|
522 | |||
520 | ## Global mouse bindings ## |
|
523 | ## Global mouse bindings ## |
@@ -4,7 +4,6 b'' | |||||
4 | ## ${p.perms_summary(c.perm_user.permissions)} |
|
4 | ## ${p.perms_summary(c.perm_user.permissions)} | |
5 |
|
5 | |||
6 | <%def name="perms_summary(permissions, show_all=False, actions=True)"> |
|
6 | <%def name="perms_summary(permissions, show_all=False, actions=True)"> | |
7 |
|
||||
8 | <div id="perms" class="table fields"> |
|
7 | <div id="perms" class="table fields"> | |
9 | %for section in sorted(permissions.keys()): |
|
8 | %for section in sorted(permissions.keys()): | |
10 | <div class="panel panel-default"> |
|
9 | <div class="panel panel-default"> | |
@@ -134,7 +133,15 b'' | |||||
134 | %endif |
|
133 | %endif | |
135 | </td> |
|
134 | </td> | |
136 | <td class="td-tags"> |
|
135 | <td class="td-tags"> | |
|
136 | %if hasattr(permissions[section], 'perm_origin_stack'): | |||
|
137 | %for i, (perm, origin) in enumerate(reversed(permissions[section].perm_origin_stack[k])): | |||
|
138 | <span class="${i > 0 and 'perm_overriden' or ''} perm_tag ${perm.split('.')[-1]}"> | |||
|
139 | ${perm} (${origin}) | |||
|
140 | </span> | |||
|
141 | %endfor | |||
|
142 | %else: | |||
137 | <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span> |
|
143 | <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span> | |
|
144 | %endif | |||
138 | </td> |
|
145 | </td> | |
139 | %if actions: |
|
146 | %if actions: | |
140 | <td class="td-action"> |
|
147 | <td class="td-action"> |
@@ -115,6 +115,7 b'' | |||||
115 | <!--[if lt IE 9]> |
|
115 | <!--[if lt IE 9]> | |
116 | <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script> |
|
116 | <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script> | |
117 | <![endif]--> |
|
117 | <![endif]--> | |
|
118 | <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script> | |||
118 | <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script> |
|
119 | <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script> | |
119 | <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script> |
|
120 | <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script> | |
120 |
|
121 |
@@ -12,7 +12,7 b'' | |||||
12 | ${base.gravatar_with_user(comment.author.email, 16)} |
|
12 | ${base.gravatar_with_user(comment.author.email, 16)} | |
13 | </div> |
|
13 | </div> | |
14 | <div class="date"> |
|
14 | <div class="date"> | |
15 | ${h.age_component(comment.modified_at)} |
|
15 | ${h.age_component(comment.modified_at, time_is_local=True)} | |
16 | </div> |
|
16 | </div> | |
17 | <div class="status-change"> |
|
17 | <div class="status-change"> | |
18 | %if comment.pull_request: |
|
18 | %if comment.pull_request: | |
@@ -80,7 +80,7 b'' | |||||
80 | ${base.gravatar_with_user(comment.author.email, 16)} |
|
80 | ${base.gravatar_with_user(comment.author.email, 16)} | |
81 | </div> |
|
81 | </div> | |
82 | <div class="date"> |
|
82 | <div class="date"> | |
83 | ${h.age_component(comment.modified_at)} |
|
83 | ${h.age_component(comment.modified_at, time_is_local=True)} | |
84 | </div> |
|
84 | </div> | |
85 | %if comment.status_change: |
|
85 | %if comment.status_change: | |
86 | <span class="changeset-status-container"> |
|
86 | <span class="changeset-status-container"> |
@@ -243,7 +243,7 b'' | |||||
243 |
|
243 | |||
244 | <%def name="gist_created(created_on)"> |
|
244 | <%def name="gist_created(created_on)"> | |
245 | <div class="created"> |
|
245 | <div class="created"> | |
246 | ${h.age_component(created_on)} |
|
246 | ${h.age_component(created_on, time_is_local=True)} | |
247 | </div> |
|
247 | </div> | |
248 | </%def> |
|
248 | </%def> | |
249 |
|
249 | |||
@@ -252,7 +252,7 b'' | |||||
252 | %if expires == -1: |
|
252 | %if expires == -1: | |
253 | ${_('never')} |
|
253 | ${_('never')} | |
254 | %else: |
|
254 | %else: | |
255 | ${h.age_component(h.time_to_datetime(expires))} |
|
255 | ${h.age_component(h.time_to_utcdatetime(expires))} | |
256 | %endif |
|
256 | %endif | |
257 | </div> |
|
257 | </div> | |
258 | </%def> |
|
258 | </%def> | |
@@ -289,7 +289,7 b'' | |||||
289 | </%def> |
|
289 | </%def> | |
290 |
|
290 | |||
291 | <%def name="pullrequest_updated_on(updated_on)"> |
|
291 | <%def name="pullrequest_updated_on(updated_on)"> | |
292 | ${h.age_component(h.time_to_datetime(updated_on))} |
|
292 | ${h.age_component(h.time_to_utcdatetime(updated_on))} | |
293 | </%def> |
|
293 | </%def> | |
294 |
|
294 | |||
295 | <%def name="pullrequest_author(full_contact)"> |
|
295 | <%def name="pullrequest_author(full_contact)"> |
@@ -11,7 +11,7 b'' | |||||
11 | ${base.gravatar_with_user(f.user.email, 16)} |
|
11 | ${base.gravatar_with_user(f.user.email, 16)} | |
12 | </td> |
|
12 | </td> | |
13 | <td class="td-time follower_date"> |
|
13 | <td class="td-time follower_date"> | |
14 | ${h.age_component(f.follows_from)} |
|
14 | ${h.age_component(f.follows_from, time_is_local=True)} | |
15 | </td> |
|
15 | </td> | |
16 | </tr> |
|
16 | </tr> | |
17 | % endfor |
|
17 | % endfor |
@@ -22,7 +22,7 b'' | |||||
22 | <div class="truncate">${f.description}</div> |
|
22 | <div class="truncate">${f.description}</div> | |
23 | </td> |
|
23 | </td> | |
24 | <td class="td-time follower_date"> |
|
24 | <td class="td-time follower_date"> | |
25 | ${h.age_component(f.created_on)} |
|
25 | ${h.age_component(f.created_on, time_is_local=True)} | |
26 | </td> |
|
26 | </td> | |
27 | <td class="td-compare"> |
|
27 | <td class="td-compare"> | |
28 | <a title="${_('Compare fork with %s' % c.repo_name)}" |
|
28 | <a title="${_('Compare fork with %s' % c.repo_name)}" |
@@ -28,7 +28,7 b'' | |||||
28 | </div> |
|
28 | </div> | |
29 | <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div> |
|
29 | <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div> | |
30 | <div class="date"> |
|
30 | <div class="date"> | |
31 | ${h.age_component(entry.action_date)} |
|
31 | ${h.age_component(entry.action_date, time_is_local=True)} | |
32 | </div> |
|
32 | </div> | |
33 | %endfor |
|
33 | %endfor | |
34 | </div> |
|
34 | </div> |
@@ -1,6 +1,5 b'' | |||||
1 | ## -*- coding: utf-8 -*- |
|
1 | ## -*- coding: utf-8 -*- | |
2 | <%inherit file="base/root.html"/> |
|
2 | <%inherit file="base/root.html"/> | |
3 | <%namespace file="base/social_buttons.html" import="render_social_buttons"/> |
|
|||
4 |
|
3 | |||
5 | <%def name="title()"> |
|
4 | <%def name="title()"> | |
6 | ${_('Sign In')} |
|
5 | ${_('Sign In')} | |
@@ -35,21 +34,35 b'' | |||||
35 | <div class="sign-in-title"> |
|
34 | <div class="sign-in-title"> | |
36 | <h1>${_('Sign In')}</h1> |
|
35 | <h1>${_('Sign In')}</h1> | |
37 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): |
|
36 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): | |
38 |
<h4>${h.link_to(_("Go to the registration page to create a new account."), |
|
37 | <h4>${h.link_to(_("Go to the registration page to create a new account."), request.route_path('register'))}</h4> | |
39 | %endif |
|
38 | %endif | |
40 | </div> |
|
39 | </div> | |
41 | <div class="inner form"> |
|
40 | <div class="inner form"> | |
42 |
${h.form( |
|
41 | ${h.form(request.route_path('login', _query={'came_from': came_from}), needs_csrf_token=False)} | |
|
42 | ||||
43 | <label for="username">${_('Username')}:</label> |
|
43 | <label for="username">${_('Username')}:</label> | |
44 | ${h.text('username',class_='focus')} |
|
44 | ${h.text('username', class_='focus', value=defaults.get('username'))} | |
|
45 | %if 'username' in errors: | |||
|
46 | <span class="error-message">${errors.get('username')}</span> | |||
|
47 | <br /> | |||
|
48 | %endif | |||
|
49 | ||||
45 | <label for="password">${_('Password')}:</label> |
|
50 | <label for="password">${_('Password')}:</label> | |
46 | ${h.password('password',class_='focus')} |
|
51 | ${h.password('password', class_='focus')} | |
47 | <input type="checkbox" id="remember" name="remember" /> |
|
52 | %if 'password' in errors: | |
|
53 | <span class="error-message">${errors.get('password')}</span> | |||
|
54 | <br /> | |||
|
55 | %endif | |||
|
56 | ||||
|
57 | ${h.checkbox('remember', value=True, checked=defaults.get('remember'))} | |||
48 | <label class="checkbox" for="remember">${_('Remember me')}</label> |
|
58 | <label class="checkbox" for="remember">${_('Remember me')}</label> | |
|
59 | ||||
49 | <p class="links"> |
|
60 | <p class="links"> | |
50 |
${h.link_to(_('Forgot your password?'), |
|
61 | ${h.link_to(_('Forgot your password?'), h.route_path('reset_password'))} | |
51 | </p> |
|
62 | </p> | |
|
63 | ||||
52 | ${h.submit('sign_in',_('Sign In'),class_="btn sign-in")} |
|
64 | ${h.submit('sign_in', _('Sign In'), class_="btn sign-in")} | |
|
65 | ||||
53 | ${h.end_form()} |
|
66 | ${h.end_form()} | |
54 | <script type="text/javascript"> |
|
67 | <script type="text/javascript"> | |
55 | $(document).ready(function(){ |
|
68 | $(document).ready(function(){ | |
@@ -57,16 +70,8 b'' | |||||
57 | }) |
|
70 | }) | |
58 | </script> |
|
71 | </script> | |
59 | </div> |
|
72 | </div> | |
60 |
|
||||
61 | % if c.social_plugins: |
|
|||
62 | <p>${_('Sign In using one of external services')}:</p> |
|
|||
63 |
|
||||
64 | <p> |
|
|||
65 | ${render_social_buttons(c.social_plugins, 'login')} |
|
|||
66 | </p> |
|
|||
67 | % endif |
|
|||
68 |
|
||||
69 | <!-- end login --> |
|
73 | <!-- end login --> | |
|
74 | <%block name="below_login_button" /> | |||
70 | </div> |
|
75 | </div> | |
71 | </div> |
|
76 | </div> | |
72 | </div> |
|
77 | </div> |
@@ -33,18 +33,26 b'' | |||||
33 | <!-- login --> |
|
33 | <!-- login --> | |
34 | <div class="sign-in-title"> |
|
34 | <div class="sign-in-title"> | |
35 | <h1>${_('Reset your Password')}</h1> |
|
35 | <h1>${_('Reset your Password')}</h1> | |
36 |
<h4>${h.link_to(_("Go to the login page to sign in."), |
|
36 | <h4>${h.link_to(_("Go to the login page to sign in."), request.route_path('login'))}</h4> | |
37 | </div> |
|
37 | </div> | |
38 | <div class="inner form"> |
|
38 | <div class="inner form"> | |
39 |
${h.form( |
|
39 | ${h.form(request.route_path('reset_password'), needs_csrf_token=False)} | |
40 | <label for="email">${_('Email Address')}:</label> |
|
40 | <label for="email">${_('Email Address')}:</label> | |
41 | ${h.text('email')} |
|
41 | ${h.text('email', defaults.get('email'))} | |
|
42 | %if 'email' in errors: | |||
|
43 | <span class="error-message">${errors.get('email')}</span> | |||
|
44 | <br /> | |||
|
45 | %endif | |||
42 |
|
46 | |||
43 |
%if c |
|
47 | %if captcha_active: | |
44 | <div class="login-captcha" |
|
48 | <div class="login-captcha" | |
45 | <label for="email">${_('Captcha')}:</label> |
|
49 | <label for="email">${_('Captcha')}:</label> | |
46 | ${h.hidden('recaptcha_field')} |
|
50 | ${h.hidden('recaptcha_field')} | |
47 | <div id="recaptcha"></div> |
|
51 | <div id="recaptcha"></div> | |
|
52 | %if 'recaptcha_field' in errors: | |||
|
53 | <span class="error-message">${errors.get('recaptcha_field')}</span> | |||
|
54 | <br /> | |||
|
55 | %endif | |||
48 | </div> |
|
56 | </div> | |
49 | %endif |
|
57 | %endif | |
50 |
|
58 | |||
@@ -57,14 +65,14 b'' | |||||
57 | </div> |
|
65 | </div> | |
58 | </div> |
|
66 | </div> | |
59 |
|
67 | |||
60 |
%if c |
|
68 | %if captcha_active: | |
61 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> |
|
69 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> | |
62 | %endif |
|
70 | %endif | |
63 | <script type="text/javascript"> |
|
71 | <script type="text/javascript"> | |
64 | $(document).ready(function(){ |
|
72 | $(document).ready(function(){ | |
65 | $('#email').focus(); |
|
73 | $('#email').focus(); | |
66 |
%if |
|
74 | %if captcha_active: | |
67 |
Recaptcha.create("${c |
|
75 | Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"}); | |
68 | %endif |
|
76 | %endif | |
69 | }); |
|
77 | }); | |
70 | </script> No newline at end of file |
|
78 | </script> |
@@ -271,7 +271,8 b'' | |||||
271 | 'source_ref_type': 'rev', |
|
271 | 'source_ref_type': 'rev', | |
272 | 'target_ref': sourceRef[2], |
|
272 | 'target_ref': sourceRef[2], | |
273 | 'target_ref_type': 'rev', |
|
273 | 'target_ref_type': 'rev', | |
274 | 'merge': true |
|
274 | 'merge': true, | |
|
275 | '_': Date.now() // bypass browser caching | |||
275 | }; // gather the source/target ref and repo here |
|
276 | }; // gather the source/target ref and repo here | |
276 |
|
277 | |||
277 | if (sourceRef.length !== 3 || targetRef.length !== 3) { |
|
278 | if (sourceRef.length !== 3 || targetRef.length !== 3) { |
@@ -1,6 +1,5 b'' | |||||
1 | ## -*- coding: utf-8 -*- |
|
1 | ## -*- coding: utf-8 -*- | |
2 | <%inherit file="base/root.html"/> |
|
2 | <%inherit file="base/root.html"/> | |
3 | <%namespace file="base/social_buttons.html" import="render_social_buttons"/> |
|
|||
4 |
|
3 | |||
5 | <%def name="title()"> |
|
4 | <%def name="title()"> | |
6 | ${_('Create an Account')} |
|
5 | ${_('Create an Account')} | |
@@ -34,65 +33,91 b'' | |||||
34 | <!-- login --> |
|
33 | <!-- login --> | |
35 | <div class="sign-in-title"> |
|
34 | <div class="sign-in-title"> | |
36 | <h1>${_('Create an account')}</h1> |
|
35 | <h1>${_('Create an account')}</h1> | |
37 |
<h4>${h.link_to(_("Go to the login page to sign in with an existing account."), |
|
36 | <h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4> | |
38 | </div> |
|
37 | </div> | |
39 | <div class="inner form"> |
|
38 | <div class="inner form"> | |
40 |
${h.form( |
|
39 | ${h.form(request.route_path('register'), needs_csrf_token=False)} | |
|
40 | ||||
41 | <label for="username">${_('Username')}:</label> |
|
41 | <label for="username">${_('Username')}:</label> | |
42 |
${h.text('username', |
|
42 | ${h.text('username', defaults.get('username'))} | |
|
43 | %if 'username' in errors: | |||
|
44 | <span class="error-message">${errors.get('username')}</span> | |||
|
45 | <br /> | |||
|
46 | %endif | |||
|
47 | ||||
43 | <label for="password">${_('Password')}:</label> |
|
48 | <label for="password">${_('Password')}:</label> | |
44 |
${h.password('password', |
|
49 | ${h.password('password', defaults.get('password'))} | |
45 | <label for="password">${_('Re-enter password')}:</label> |
|
50 | %if 'password' in errors: | |
46 | ${h.password('password_confirmation', c.form_data.get('password'))} |
|
51 | <span class="error-message">${errors.get('password')}</span> | |
|
52 | <br /> | |||
|
53 | %endif | |||
|
54 | ||||
|
55 | <label for="password_confirmation">${_('Re-enter password')}:</label> | |||
|
56 | ${h.password('password_confirmation', defaults.get('password_confirmation'))} | |||
|
57 | %if 'password_confirmation' in errors: | |||
|
58 | <span class="error-message">${errors.get('password_confirmation')}</span> | |||
|
59 | <br /> | |||
|
60 | %endif | |||
|
61 | ||||
47 | <label for="firstname">${_('First Name')}:</label> |
|
62 | <label for="firstname">${_('First Name')}:</label> | |
48 | ${h.text('firstname')} |
|
63 | ${h.text('firstname', defaults.get('firstname'))} | |
|
64 | %if 'firstname' in errors: | |||
|
65 | <span class="error-message">${errors.get('firstname')}</span> | |||
|
66 | <br /> | |||
|
67 | %endif | |||
|
68 | ||||
49 | <label for="lastname">${_('Last Name')}:</label> |
|
69 | <label for="lastname">${_('Last Name')}:</label> | |
50 | ${h.text('lastname')} |
|
70 | ${h.text('lastname', defaults.get('lastname'))} | |
|
71 | %if 'lastname' in errors: | |||
|
72 | <span class="error-message">${errors.get('lastname')}</span> | |||
|
73 | <br /> | |||
|
74 | %endif | |||
|
75 | ||||
51 | <label for="email">${_('Email')}:</label> |
|
76 | <label for="email">${_('Email')}:</label> | |
52 |
${h.text('email', |
|
77 | ${h.text('email', defaults.get('email'))} | |
|
78 | %if 'email' in errors: | |||
|
79 | <span class="error-message">${errors.get('email')}</span> | |||
|
80 | <br /> | |||
|
81 | %endif | |||
53 |
|
82 | |||
54 |
%if c |
|
83 | %if captcha_active: | |
55 | <div> |
|
84 | <div> | |
56 |
<label for=" |
|
85 | <label for="recaptcha">${_('Captcha')}:</label> | |
57 | ${h.hidden('recaptcha_field')} |
|
86 | ${h.hidden('recaptcha_field')} | |
58 | <div id="recaptcha"></div> |
|
87 | <div id="recaptcha"></div> | |
|
88 | %if 'recaptcha_field' in errors: | |||
|
89 | <span class="error-message">${errors.get('recaptcha_field')}</span> | |||
|
90 | <br /> | |||
|
91 | %endif | |||
59 | </div> |
|
92 | </div> | |
60 | %endif |
|
93 | %endif | |
61 |
|
94 | |||
62 |
%if not |
|
95 | %if not auto_active: | |
63 | <p class="activation_msg"> |
|
96 | <p class="activation_msg"> | |
64 | ${_('Account activation requires admin approval.')} |
|
97 | ${_('Account activation requires admin approval.')} | |
65 | </p> |
|
98 | </p> | |
66 | %endif |
|
99 | %endif | |
67 | <p class="register_message"> |
|
100 | <p class="register_message"> | |
68 |
${ |
|
101 | ${register_message|n} | |
69 | </p> |
|
102 | </p> | |
70 |
|
103 | |||
71 | ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")} |
|
104 | ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")} | |
72 |
|
105 | |||
73 | ${h.end_form()} |
|
106 | ${h.end_form()} | |
74 | </div> |
|
107 | </div> | |
75 |
|
108 | <%block name="below_register_button" /> | ||
76 | % if c.social_plugins: |
|
|||
77 | <p>${_('Register using one of external services')}:</p> |
|
|||
78 |
|
||||
79 | <p> |
|
|||
80 | ${render_social_buttons(c.social_plugins, 'register')} |
|
|||
81 | </p> |
|
|||
82 | % endif |
|
|||
83 |
|
||||
84 | </div> |
|
109 | </div> | |
85 | </div> |
|
110 | </div> | |
86 | </div> |
|
111 | </div> | |
87 |
|
112 | |||
88 |
%if c |
|
113 | %if captcha_active: | |
89 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> |
|
114 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> | |
90 | %endif |
|
115 | %endif | |
91 | <script type="text/javascript"> |
|
116 | <script type="text/javascript"> | |
92 | $(document).ready(function(){ |
|
117 | $(document).ready(function(){ | |
93 | $('#username').focus(); |
|
118 | $('#username').focus(); | |
94 |
%if |
|
119 | %if captcha_active: | |
95 |
Recaptcha.create("${c |
|
120 | Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"}); | |
96 | %endif |
|
121 | %endif | |
97 | }); |
|
122 | }); | |
98 | </script> |
|
123 | </script> |
@@ -6,7 +6,13 b'' | |||||
6 | <th>${_('Commit')}</th> |
|
6 | <th>${_('Commit')}</th> | |
7 | <th></th> |
|
7 | <th></th> | |
8 | <th>${_('Commit message')}</th> |
|
8 | <th>${_('Commit message')}</th> | |
9 |
<th> |
|
9 | <th> | |
|
10 | %if c.sort == 'newfirst': | |||
|
11 | <a href="${c.url_generator(sort='oldfirst')}">${_('Age (new first)')}</a> | |||
|
12 | %else: | |||
|
13 | <a href="${c.url_generator(sort='newfirst')}">${_('Age (old first)')}</a> | |||
|
14 | %endif | |||
|
15 | </th> | |||
10 | <th>${_('Author')}</th> |
|
16 | <th>${_('Author')}</th> | |
11 | </tr> |
|
17 | </tr> | |
12 | %for entry in c.formatted_results: |
|
18 | %for entry in c.formatted_results: | |
@@ -33,14 +39,14 b'' | |||||
33 | </div> |
|
39 | </div> | |
34 | </td> |
|
40 | </td> | |
35 | <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open"> |
|
41 | <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open"> | |
36 |
%if entry |
|
42 | %if entry.get('message_hl'): | |
37 | ${h.literal(entry['message_hl'])} |
|
43 | ${h.literal(entry['message_hl'])} | |
38 | %else: |
|
44 | %else: | |
39 | ${h.urlify_commit_message(entry['message'], entry['repository'])} |
|
45 | ${h.urlify_commit_message(entry['message'], entry['repository'])} | |
40 | %endif |
|
46 | %endif | |
41 | </td> |
|
47 | </td> | |
42 | <td class="td-time"> |
|
48 | <td class="td-time"> | |
43 | ${h.age_component(h.time_to_datetime(entry['date']))} |
|
49 | ${h.age_component(h.time_to_utcdatetime(entry['date']))} | |
44 | </td> |
|
50 | </td> | |
45 |
|
51 | |||
46 | <td class="td-user author"> |
|
52 | <td class="td-user author"> |
@@ -1,3 +1,40 b'' | |||||
|
1 | <%def name="highlight_text_file(terms, text, url, line_context=3, | |||
|
2 | max_lines=10, | |||
|
3 | mimetype=None, filepath=None)"> | |||
|
4 | <% | |||
|
5 | lines = text.split('\n') | |||
|
6 | lines_of_interest = set() | |||
|
7 | matching_lines = h.get_matching_line_offsets(lines, terms) | |||
|
8 | shown_matching_lines = 0 | |||
|
9 | ||||
|
10 | for line_number in matching_lines: | |||
|
11 | if len(lines_of_interest) < max_lines: | |||
|
12 | lines_of_interest |= set(range( | |||
|
13 | max(line_number - line_context, 0), | |||
|
14 | min(line_number + line_context, len(lines) + 1))) | |||
|
15 | shown_matching_lines += 1 | |||
|
16 | ||||
|
17 | %> | |||
|
18 | ${h.code_highlight( | |||
|
19 | text, | |||
|
20 | h.get_lexer_safe( | |||
|
21 | mimetype=mimetype, | |||
|
22 | filepath=filepath, | |||
|
23 | ), | |||
|
24 | h.SearchContentCodeHtmlFormatter( | |||
|
25 | linenos=True, | |||
|
26 | cssclass="code-highlight", | |||
|
27 | url=url, | |||
|
28 | query_terms=terms, | |||
|
29 | only_line_numbers=lines_of_interest | |||
|
30 | ))|n} | |||
|
31 | %if len(matching_lines) > shown_matching_lines: | |||
|
32 | <a href="${url}"> | |||
|
33 | ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')} | |||
|
34 | </p> | |||
|
35 | %endif | |||
|
36 | </%def> | |||
|
37 | ||||
1 | <div class="search-results"> |
|
38 | <div class="search-results"> | |
2 | %for entry in c.formatted_results: |
|
39 | %for entry in c.formatted_results: | |
3 | ## search results are additionally filtered, and this check is just a safe gate |
|
40 | ## search results are additionally filtered, and this check is just a safe gate | |
@@ -38,7 +75,9 b'' | |||||
38 | </div> |
|
75 | </div> | |
39 | </div> |
|
76 | </div> | |
40 | <div class="code-body search-code-body"> |
|
77 | <div class="code-body search-code-body"> | |
41 | <pre>${h.literal(entry['content_short_hl'])}</pre> |
|
78 | ${highlight_text_file(c.cur_query, entry['content'], | |
|
79 | url=h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']), | |||
|
80 | mimetype=entry.get('mimetype'), filepath=entry.get('path'))} | |||
42 | </div> |
|
81 | </div> | |
43 | </div> |
|
82 | </div> | |
44 | % endif |
|
83 | % endif | |
@@ -49,3 +88,14 b'' | |||||
49 | ${c.formatted_results.pager('$link_previous ~2~ $link_next')} |
|
88 | ${c.formatted_results.pager('$link_previous ~2~ $link_next')} | |
50 | </div> |
|
89 | </div> | |
51 | %endif |
|
90 | %endif | |
|
91 | ||||
|
92 | %if c.cur_query: | |||
|
93 | <script type="text/javascript"> | |||
|
94 | $(function(){ | |||
|
95 | $(".code").mark( | |||
|
96 | '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}', | |||
|
97 | {"className": 'match', | |||
|
98 | }); | |||
|
99 | }) | |||
|
100 | </script> | |||
|
101 | %endif No newline at end of file |
@@ -43,11 +43,13 b' from nose.plugins.skip import SkipTest' | |||||
43 | import pytest |
|
43 | import pytest | |
44 |
|
44 | |||
45 | from rhodecode import is_windows |
|
45 | from rhodecode import is_windows | |
|
46 | from rhodecode.config.routing import ADMIN_PREFIX | |||
46 | from rhodecode.model.meta import Session |
|
47 | from rhodecode.model.meta import Session | |
47 | from rhodecode.model.db import User |
|
48 | from rhodecode.model.db import User | |
48 | from rhodecode.lib import auth |
|
49 | from rhodecode.lib import auth | |
49 | from rhodecode.lib.helpers import flash, link_to |
|
50 | from rhodecode.lib.helpers import flash, link_to | |
50 | from rhodecode.lib.utils2 import safe_unicode, safe_str |
|
51 | from rhodecode.lib.utils2 import safe_unicode, safe_str | |
|
52 | from rhodecode.tests.utils import get_session_from_response | |||
51 |
|
53 | |||
52 | # TODO: johbo: Solve time zone related issues and remove this tweak |
|
54 | # TODO: johbo: Solve time zone related issues and remove this tweak | |
53 | os.environ['TZ'] = 'UTC' |
|
55 | os.environ['TZ'] = 'UTC' | |
@@ -177,26 +179,29 b' class TestController(object):' | |||||
177 |
|
179 | |||
178 | def login_user_session( |
|
180 | def login_user_session( | |
179 | app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS): |
|
181 | app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS): | |
180 | response = app.post(url(controller='login', action='index'), |
|
182 | from rhodecode.tests.functional.test_login import login_url | |
181 | {'username': username, |
|
183 | response = app.post( | |
182 | 'password': password}) |
|
184 | login_url, | |
183 |
|
185 | {'username': username, 'password': password}) | ||
184 | if 'invalid user name' in response.body: |
|
186 | if 'invalid user name' in response.body: | |
185 | pytest.fail('could not login using %s %s' % (username, password)) |
|
187 | pytest.fail('could not login using %s %s' % (username, password)) | |
186 |
|
188 | |||
187 | assert response.status == '302 Found' |
|
189 | assert response.status == '302 Found' | |
188 | ses = response.session['rhodecode_user'] |
|
|||
189 | assert ses.get('username') == username |
|
|||
190 | response = response.follow() |
|
190 | response = response.follow() | |
191 | assert ses.get('is_authenticated') |
|
191 | assert response.status == '200 OK' | |
192 |
|
192 | |||
193 | return response.session |
|
193 | session = get_session_from_response(response) | |
|
194 | assert 'rhodecode_user' in session | |||
|
195 | rc_user = session['rhodecode_user'] | |||
|
196 | assert rc_user.get('username') == username | |||
|
197 | assert rc_user.get('is_authenticated') | |||
|
198 | ||||
|
199 | return session | |||
194 |
|
200 | |||
195 |
|
201 | |||
196 | def logout_user_session(app, csrf_token): |
|
202 | def logout_user_session(app, csrf_token): | |
197 | app.post( |
|
203 | from rhodecode.tests.functional.test_login import logut_url | |
198 | url(controller='login', action='logout'), |
|
204 | app.post(logut_url, {'csrf_token': csrf_token}, status=302) | |
199 | {'csrf_token': csrf_token}, status=302) |
|
|||
200 |
|
205 | |||
201 |
|
206 | |||
202 | def login_user(app, username=TEST_USER_ADMIN_LOGIN, |
|
207 | def login_user(app, username=TEST_USER_ADMIN_LOGIN, |
@@ -20,7 +20,8 b'' | |||||
20 |
|
20 | |||
21 | import pytest |
|
21 | import pytest | |
22 |
|
22 | |||
23 |
from rhodecode.tests import assert_session_flash |
|
23 | from rhodecode.tests import assert_session_flash | |
|
24 | from rhodecode.tests.utils import AssertResponse | |||
24 | from rhodecode.model.db import Session |
|
25 | from rhodecode.model.db import Session | |
25 | from rhodecode.model.settings import SettingsModel |
|
26 | from rhodecode.model.settings import SettingsModel | |
26 |
|
27 | |||
@@ -150,12 +151,14 b' class TestAuthSettingsController(object)' | |||||
150 | 'egg:rhodecode-enterprise-ce#rhodecode,' |
|
151 | 'egg:rhodecode-enterprise-ce#rhodecode,' | |
151 | 'egg:rhodecode-enterprise-ce#ldap', |
|
152 | 'egg:rhodecode-enterprise-ce#ldap', | |
152 | csrf_token) |
|
153 | csrf_token) | |
|
154 | invalid_port_value = 'invalid-port-number' | |||
153 | response = self._post_ldap_settings(params, override={ |
|
155 | response = self._post_ldap_settings(params, override={ | |
154 |
'port': |
|
156 | 'port': invalid_port_value, | |
155 | }) |
|
157 | }) | |
156 | response.mustcontain( |
|
158 | assertr = AssertResponse(response) | |
157 | '<span class="error-message">"invalid-port-number"' |
|
159 | assertr.element_contains( | |
158 | ' is not a number</span>') |
|
160 | '.form .field #port ~ .error-message', | |
|
161 | invalid_port_value) | |||
159 |
|
162 | |||
160 | def test_ldap_error_form(self, csrf_token): |
|
163 | def test_ldap_error_form(self, csrf_token): | |
161 | params = self._enable_plugins( |
|
164 | params = self._enable_plugins( |
@@ -339,53 +339,3 b' class TestMyAccountController(TestContro' | |||||
339 | new_password_hash = response.session['rhodecode_user']['password'] |
|
339 | new_password_hash = response.session['rhodecode_user']['password'] | |
340 |
|
340 | |||
341 | assert old_password_hash != new_password_hash |
|
341 | assert old_password_hash != new_password_hash | |
342 |
|
||||
343 | def test_my_account_oauth_tokens_empty(self): |
|
|||
344 | usr = self.log_user('test_regular2', 'test12') |
|
|||
345 | User.get(usr['user_id']) |
|
|||
346 | response = self.app.get(url('my_account_oauth')) |
|
|||
347 | response.mustcontain(no=['Connect with GitHub']) |
|
|||
348 | response.mustcontain('You have no accounts linked yet') |
|
|||
349 |
|
||||
350 | def test_my_account_oauth_tokens_present(self): |
|
|||
351 | from rhodecode.model.db import ExternalIdentity |
|
|||
352 | usr = self.log_user('test_regular2', 'test12') |
|
|||
353 | user = User.get(usr['user_id']) |
|
|||
354 |
|
||||
355 | ex_identity = ExternalIdentity() |
|
|||
356 | ex_identity.external_id = '55' |
|
|||
357 | ex_identity.provider_name = 'twitter' |
|
|||
358 | ex_identity.local_user_id = user.user_id |
|
|||
359 | db_session = Session() |
|
|||
360 | db_session.add(ex_identity) |
|
|||
361 | Session.flush() |
|
|||
362 | db_session.commit() |
|
|||
363 | try: |
|
|||
364 | response = self.app.get(url('my_account_oauth')) |
|
|||
365 | response.mustcontain('twitter', |
|
|||
366 | no=['You have no accounts linked yet']) |
|
|||
367 | finally: |
|
|||
368 | db_session = Session() |
|
|||
369 | db_session.delete(ex_identity) |
|
|||
370 | db_session.commit() |
|
|||
371 |
|
||||
372 | def test_my_account_oauth_tokens_delete(self): |
|
|||
373 | from rhodecode.model.db import ExternalIdentity |
|
|||
374 | usr = self.log_user('test_regular2', 'test12') |
|
|||
375 | user = User.get(usr['user_id']) |
|
|||
376 |
|
||||
377 | ex_identity = ExternalIdentity() |
|
|||
378 | ex_identity.external_id = '99' |
|
|||
379 | ex_identity.provider_name = 'twitter' |
|
|||
380 | ex_identity.local_user_id = user.user_id |
|
|||
381 | db_session = Session() |
|
|||
382 | db_session.add(ex_identity) |
|
|||
383 | Session.flush() |
|
|||
384 | db_session.commit() |
|
|||
385 | assert ExternalIdentity.query().count() == 1 |
|
|||
386 | response = self.app.post( |
|
|||
387 | url('my_account_oauth', provider_name='twitter', |
|
|||
388 | external_id='99'), |
|
|||
389 | {'_method': 'delete', 'csrf_token': self.csrf_token}) |
|
|||
390 | assert_session_flash(response, 'OAuth token successfully deleted') |
|
|||
391 | assert ExternalIdentity.query().count() == 0 |
|
@@ -22,6 +22,7 b' import mock' | |||||
22 | import pytest |
|
22 | import pytest | |
23 |
|
23 | |||
24 | import rhodecode |
|
24 | import rhodecode | |
|
25 | from rhodecode.config.routing import ADMIN_PREFIX | |||
25 | from rhodecode.lib.utils2 import md5 |
|
26 | from rhodecode.lib.utils2 import md5 | |
26 | from rhodecode.model.db import RhodeCodeUi |
|
27 | from rhodecode.model.db import RhodeCodeUi | |
27 | from rhodecode.model.meta import Session |
|
28 | from rhodecode.model.meta import Session | |
@@ -157,7 +158,7 b' class TestAdminSettingsGlobal:' | |||||
157 | 'csrf_token': csrf_token, |
|
158 | 'csrf_token': csrf_token, | |
158 | }) |
|
159 | }) | |
159 |
|
160 | |||
160 |
response = self.app.get( |
|
161 | response = self.app.get(ADMIN_PREFIX + '/register') | |
161 | response.mustcontain('captcha') |
|
162 | response.mustcontain('captcha') | |
162 |
|
163 | |||
163 | def test_captcha_deactivate(self, csrf_token): |
|
164 | def test_captcha_deactivate(self, csrf_token): | |
@@ -167,7 +168,7 b' class TestAdminSettingsGlobal:' | |||||
167 | 'csrf_token': csrf_token, |
|
168 | 'csrf_token': csrf_token, | |
168 | }) |
|
169 | }) | |
169 |
|
170 | |||
170 |
response = self.app.get( |
|
171 | response = self.app.get(ADMIN_PREFIX + '/register') | |
171 | response.mustcontain(no=['captcha']) |
|
172 | response.mustcontain(no=['captcha']) | |
172 |
|
173 | |||
173 | def test_title_change(self, csrf_token): |
|
174 | def test_title_change(self, csrf_token): |
@@ -35,7 +35,8 b' class TestAdminUsersGroupsController(Tes' | |||||
35 |
|
35 | |||
36 | def test_index(self): |
|
36 | def test_index(self): | |
37 | self.log_user() |
|
37 | self.log_user() | |
38 | self.app.get(url('users_groups')) |
|
38 | response = self.app.get(url('users_groups')) | |
|
39 | response.status_int == 200 | |||
39 |
|
40 | |||
40 | def test_create(self): |
|
41 | def test_create(self): | |
41 | self.log_user() |
|
42 | self.log_user() | |
@@ -148,7 +149,19 b' class TestAdminUsersGroupsController(Tes' | |||||
148 | fixture.destroy_user_group(users_group_name) |
|
149 | fixture.destroy_user_group(users_group_name) | |
149 |
|
150 | |||
150 | def test_edit(self): |
|
151 | def test_edit(self): | |
151 | self.app.get(url('edit_users_group', user_group_id=1)) |
|
152 | self.log_user() | |
|
153 | ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) | |||
|
154 | response = self.app.get( | |||
|
155 | url('edit_users_group', user_group_id=ug.users_group_id)) | |||
|
156 | fixture.destroy_user_group(TEST_USER_GROUP) | |||
|
157 | ||||
|
158 | def test_edit_user_group_members(self): | |||
|
159 | self.log_user() | |||
|
160 | ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) | |||
|
161 | response = self.app.get( | |||
|
162 | url('edit_user_group_members', user_group_id=ug.users_group_id)) | |||
|
163 | response.mustcontain('No members yet') | |||
|
164 | fixture.destroy_user_group(TEST_USER_GROUP) | |||
152 |
|
165 | |||
153 | def test_usergroup_escape(self): |
|
166 | def test_usergroup_escape(self): | |
154 | user = User.get_by_username('test_admin') |
|
167 | user = User.get_by_username('test_admin') |
@@ -77,7 +77,7 b' class TestCompareController:' | |||||
77 | 'hg': { |
|
77 | 'hg': { | |
78 | 'tag': 'v0.2.0', |
|
78 | 'tag': 'v0.2.0', | |
79 | 'branch': 'default', |
|
79 | 'branch': 'default', | |
80 |
'response': (147, 570 |
|
80 | 'response': (147, 5701, 10177) | |
81 | }, |
|
81 | }, | |
82 | 'git': { |
|
82 | 'git': { | |
83 | 'tag': 'v0.2.2', |
|
83 | 'tag': 'v0.2.2', |
@@ -181,19 +181,25 b' class TestUserAutocompleteData(TestContr' | |||||
181 | def assert_and_get_content(result): |
|
181 | def assert_and_get_content(result): | |
182 | repos = [] |
|
182 | repos = [] | |
183 | groups = [] |
|
183 | groups = [] | |
|
184 | commits = [] | |||
184 | for data in result: |
|
185 | for data in result: | |
185 | for data_item in data['children']: |
|
186 | for data_item in data['children']: | |
186 | assert data_item['id'] |
|
187 | assert data_item['id'] | |
187 | assert data_item['text'] |
|
188 | assert data_item['text'] | |
|
189 | assert data_item['url'] | |||
188 | if data_item['type'] == 'repo': |
|
190 | if data_item['type'] == 'repo': | |
189 | repos.append(data_item) |
|
191 | repos.append(data_item) | |
190 | else: |
|
192 | elif data_item['type'] == 'group': | |
191 | groups.append(data_item) |
|
193 | groups.append(data_item) | |
|
194 | elif data_item['type'] == 'commit': | |||
|
195 | commits.append(data_item) | |||
|
196 | else: | |||
|
197 | raise Exception('invalid type %s' % data_item['type']) | |||
192 |
|
198 | |||
193 | return repos, groups |
|
199 | return repos, groups, commits | |
194 |
|
200 | |||
195 |
|
201 | |||
196 |
class Test |
|
202 | class TestGotoSwitcherData(TestController): | |
197 | required_repos_with_groups = [ |
|
203 | required_repos_with_groups = [ | |
198 | 'abc', |
|
204 | 'abc', | |
199 | 'abc-fork', |
|
205 | 'abc-fork', | |
@@ -253,39 +259,41 b' class TestRepoSwitcherData(TestControlle' | |||||
253 | self.log_user() |
|
259 | self.log_user() | |
254 |
|
260 | |||
255 | response = self.app.get( |
|
261 | response = self.app.get( | |
256 |
url(controller='home', action=' |
|
262 | url(controller='home', action='goto_switcher_data'), | |
257 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
263 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) | |
258 | result = json.loads(response.body)['results'] |
|
264 | result = json.loads(response.body)['results'] | |
259 |
|
265 | |||
260 | repos, groups = assert_and_get_content(result) |
|
266 | repos, groups, commits = assert_and_get_content(result) | |
261 |
|
267 | |||
262 | assert len(repos) == len(Repository.get_all()) |
|
268 | assert len(repos) == len(Repository.get_all()) | |
263 | assert len(groups) == len(RepoGroup.get_all()) |
|
269 | assert len(groups) == len(RepoGroup.get_all()) | |
|
270 | assert len(commits) == 0 | |||
264 |
|
271 | |||
265 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
272 | def test_returns_list_of_repos_and_groups_filtered(self): | |
266 | self.log_user() |
|
273 | self.log_user() | |
267 |
|
274 | |||
268 | response = self.app.get( |
|
275 | response = self.app.get( | |
269 |
url(controller='home', action=' |
|
276 | url(controller='home', action='goto_switcher_data'), | |
270 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
277 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, | |
271 | params={'query': 'abc'}, status=200) |
|
278 | params={'query': 'abc'}, status=200) | |
272 | result = json.loads(response.body)['results'] |
|
279 | result = json.loads(response.body)['results'] | |
273 |
|
280 | |||
274 | repos, groups = assert_and_get_content(result) |
|
281 | repos, groups, commits = assert_and_get_content(result) | |
275 |
|
282 | |||
276 | assert len(repos) == 13 |
|
283 | assert len(repos) == 13 | |
277 | assert len(groups) == 5 |
|
284 | assert len(groups) == 5 | |
|
285 | assert len(commits) == 0 | |||
278 |
|
286 | |||
279 | def test_returns_list_of_properly_sorted_and_filtered(self): |
|
287 | def test_returns_list_of_properly_sorted_and_filtered(self): | |
280 | self.log_user() |
|
288 | self.log_user() | |
281 |
|
289 | |||
282 | response = self.app.get( |
|
290 | response = self.app.get( | |
283 |
url(controller='home', action=' |
|
291 | url(controller='home', action='goto_switcher_data'), | |
284 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
292 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, | |
285 | params={'query': 'abc'}, status=200) |
|
293 | params={'query': 'abc'}, status=200) | |
286 | result = json.loads(response.body)['results'] |
|
294 | result = json.loads(response.body)['results'] | |
287 |
|
295 | |||
288 | repos, groups = assert_and_get_content(result) |
|
296 | repos, groups, commits = assert_and_get_content(result) | |
289 |
|
297 | |||
290 | test_repos = [x['text'] for x in repos[:4]] |
|
298 | test_repos = [x['text'] for x in repos[:4]] | |
291 | assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos |
|
299 | assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos | |
@@ -300,54 +308,58 b' class TestRepoListData(TestController):' | |||||
300 | self.log_user() |
|
308 | self.log_user() | |
301 |
|
309 | |||
302 | response = self.app.get( |
|
310 | response = self.app.get( | |
303 |
url(controller='home', action='repo_ |
|
311 | url(controller='home', action='repo_list_data'), | |
304 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
312 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) | |
305 | result = json.loads(response.body)['results'] |
|
313 | result = json.loads(response.body)['results'] | |
306 |
|
314 | |||
307 | repos, groups = assert_and_get_content(result) |
|
315 | repos, groups, commits = assert_and_get_content(result) | |
308 |
|
316 | |||
309 | assert len(repos) == len(Repository.get_all()) |
|
317 | assert len(repos) == len(Repository.get_all()) | |
310 | assert len(groups) == 0 |
|
318 | assert len(groups) == 0 | |
|
319 | assert len(commits) == 0 | |||
311 |
|
320 | |||
312 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
321 | def test_returns_list_of_repos_and_groups_filtered(self): | |
313 | self.log_user() |
|
322 | self.log_user() | |
314 |
|
323 | |||
315 | response = self.app.get( |
|
324 | response = self.app.get( | |
316 |
url(controller='home', action='repo_ |
|
325 | url(controller='home', action='repo_list_data'), | |
317 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
326 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, | |
318 | params={'query': 'vcs_test_git'}, status=200) |
|
327 | params={'query': 'vcs_test_git'}, status=200) | |
319 | result = json.loads(response.body)['results'] |
|
328 | result = json.loads(response.body)['results'] | |
320 |
|
329 | |||
321 | repos, groups = assert_and_get_content(result) |
|
330 | repos, groups, commits = assert_and_get_content(result) | |
322 |
|
331 | |||
323 | assert len(repos) == len(Repository.query().filter( |
|
332 | assert len(repos) == len(Repository.query().filter( | |
324 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
333 | Repository.repo_name.ilike('%vcs_test_git%')).all()) | |
325 | assert len(groups) == 0 |
|
334 | assert len(groups) == 0 | |
|
335 | assert len(commits) == 0 | |||
326 |
|
336 | |||
327 | def test_returns_list_of_repos_and_groups_filtered_with_type(self): |
|
337 | def test_returns_list_of_repos_and_groups_filtered_with_type(self): | |
328 | self.log_user() |
|
338 | self.log_user() | |
329 |
|
339 | |||
330 | response = self.app.get( |
|
340 | response = self.app.get( | |
331 |
url(controller='home', action='repo_ |
|
341 | url(controller='home', action='repo_list_data'), | |
332 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
342 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, | |
333 | params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200) |
|
343 | params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200) | |
334 | result = json.loads(response.body)['results'] |
|
344 | result = json.loads(response.body)['results'] | |
335 |
|
345 | |||
336 | repos, groups = assert_and_get_content(result) |
|
346 | repos, groups, commits = assert_and_get_content(result) | |
337 |
|
347 | |||
338 | assert len(repos) == len(Repository.query().filter( |
|
348 | assert len(repos) == len(Repository.query().filter( | |
339 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
349 | Repository.repo_name.ilike('%vcs_test_git%')).all()) | |
340 | assert len(groups) == 0 |
|
350 | assert len(groups) == 0 | |
|
351 | assert len(commits) == 0 | |||
341 |
|
352 | |||
342 | def test_returns_list_of_repos_non_ascii_query(self): |
|
353 | def test_returns_list_of_repos_non_ascii_query(self): | |
343 | self.log_user() |
|
354 | self.log_user() | |
344 | response = self.app.get( |
|
355 | response = self.app.get( | |
345 |
url(controller='home', action='repo_ |
|
356 | url(controller='home', action='repo_list_data'), | |
346 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
357 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, | |
347 | params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200) |
|
358 | params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200) | |
348 | result = json.loads(response.body)['results'] |
|
359 | result = json.loads(response.body)['results'] | |
349 |
|
360 | |||
350 | repos, groups = assert_and_get_content(result) |
|
361 | repos, groups, commits = assert_and_get_content(result) | |
351 |
|
362 | |||
352 | assert len(repos) == 0 |
|
363 | assert len(repos) == 0 | |
353 | assert len(groups) == 0 |
|
364 | assert len(groups) == 0 | |
|
365 | assert len(commits) == 0 |
@@ -23,9 +23,11 b' import urlparse' | |||||
23 | import mock |
|
23 | import mock | |
24 | import pytest |
|
24 | import pytest | |
25 |
|
25 | |||
|
26 | from rhodecode.config.routing import ADMIN_PREFIX | |||
26 | from rhodecode.tests import ( |
|
27 | from rhodecode.tests import ( | |
27 | assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN) |
|
28 | assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN) | |
28 | from rhodecode.tests.fixture import Fixture |
|
29 | from rhodecode.tests.fixture import Fixture | |
|
30 | from rhodecode.tests.utils import AssertResponse, get_session_from_response | |||
29 | from rhodecode.lib.auth import check_password, generate_auth_token |
|
31 | from rhodecode.lib.auth import check_password, generate_auth_token | |
30 | from rhodecode.lib import helpers as h |
|
32 | from rhodecode.lib import helpers as h | |
31 | from rhodecode.model.auth_token import AuthTokenModel |
|
33 | from rhodecode.model.auth_token import AuthTokenModel | |
@@ -35,6 +37,14 b' from rhodecode.model.meta import Session' | |||||
35 |
|
37 | |||
36 | fixture = Fixture() |
|
38 | fixture = Fixture() | |
37 |
|
39 | |||
|
40 | # Hardcode URLs because we don't have a request object to use | |||
|
41 | # pyramids URL generation methods. | |||
|
42 | login_url = ADMIN_PREFIX + '/login' | |||
|
43 | logut_url = ADMIN_PREFIX + '/logout' | |||
|
44 | register_url = ADMIN_PREFIX + '/register' | |||
|
45 | pwd_reset_url = ADMIN_PREFIX + '/password_reset' | |||
|
46 | pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation' | |||
|
47 | ||||
38 |
|
48 | |||
39 | @pytest.mark.usefixtures('app') |
|
49 | @pytest.mark.usefixtures('app') | |
40 | class TestLoginController: |
|
50 | class TestLoginController: | |
@@ -52,37 +62,38 b' class TestLoginController:' | |||||
52 | assert Notification.query().all() == [] |
|
62 | assert Notification.query().all() == [] | |
53 |
|
63 | |||
54 | def test_index(self): |
|
64 | def test_index(self): | |
55 |
response = self.app.get( |
|
65 | response = self.app.get(login_url) | |
56 | assert response.status == '200 OK' |
|
66 | assert response.status == '200 OK' | |
57 | # Test response... |
|
67 | # Test response... | |
58 |
|
68 | |||
59 | def test_login_admin_ok(self): |
|
69 | def test_login_admin_ok(self): | |
60 |
response = self.app.post( |
|
70 | response = self.app.post(login_url, | |
61 | {'username': 'test_admin', |
|
71 | {'username': 'test_admin', | |
62 | 'password': 'test12'}) |
|
72 | 'password': 'test12'}) | |
63 | assert response.status == '302 Found' |
|
73 | assert response.status == '302 Found' | |
64 | username = response.session['rhodecode_user'].get('username') |
|
74 | session = get_session_from_response(response) | |
|
75 | username = session['rhodecode_user'].get('username') | |||
65 | assert username == 'test_admin' |
|
76 | assert username == 'test_admin' | |
66 | response = response.follow() |
|
77 | response = response.follow() | |
67 | response.mustcontain('/%s' % HG_REPO) |
|
78 | response.mustcontain('/%s' % HG_REPO) | |
68 |
|
79 | |||
69 | def test_login_regular_ok(self): |
|
80 | def test_login_regular_ok(self): | |
70 |
response = self.app.post( |
|
81 | response = self.app.post(login_url, | |
71 | {'username': 'test_regular', |
|
82 | {'username': 'test_regular', | |
72 | 'password': 'test12'}) |
|
83 | 'password': 'test12'}) | |
73 |
|
84 | |||
74 | assert response.status == '302 Found' |
|
85 | assert response.status == '302 Found' | |
75 | username = response.session['rhodecode_user'].get('username') |
|
86 | session = get_session_from_response(response) | |
|
87 | username = session['rhodecode_user'].get('username') | |||
76 | assert username == 'test_regular' |
|
88 | assert username == 'test_regular' | |
77 | response = response.follow() |
|
89 | response = response.follow() | |
78 | response.mustcontain('/%s' % HG_REPO) |
|
90 | response.mustcontain('/%s' % HG_REPO) | |
79 |
|
91 | |||
80 | def test_login_ok_came_from(self): |
|
92 | def test_login_ok_came_from(self): | |
81 | test_came_from = '/_admin/users?branch=stable' |
|
93 | test_came_from = '/_admin/users?branch=stable' | |
82 | response = self.app.post(url(controller='login', action='index', |
|
94 | _url = '{}?came_from={}'.format(login_url, test_came_from) | |
83 | came_from=test_came_from), |
|
95 | response = self.app.post( | |
84 |
|
|
96 | _url, {'username': 'test_admin', 'password': 'test12'}) | |
85 | 'password': 'test12'}) |
|
|||
86 | assert response.status == '302 Found' |
|
97 | assert response.status == '302 Found' | |
87 | assert 'branch=stable' in response.location |
|
98 | assert 'branch=stable' in response.location | |
88 | response = response.follow() |
|
99 | response = response.follow() | |
@@ -100,33 +111,30 b' class TestLoginController:' | |||||
100 | assert 'branch=stable' in response_query[0][1] |
|
111 | assert 'branch=stable' in response_query[0][1] | |
101 |
|
112 | |||
102 | def test_login_form_with_get_args(self): |
|
113 | def test_login_form_with_get_args(self): | |
103 | kwargs = {'branch': 'stable'} |
|
114 | _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url) | |
104 | response = self.app.get( |
|
115 | response = self.app.get(_url) | |
105 | url(controller='login', action='index', |
|
116 | assert 'branch%3Dstable' in response.form.action | |
106 | came_from='/_admin/users', **kwargs)) |
|
|||
107 | assert 'branch=stable' in response.form.action |
|
|||
108 |
|
117 | |||
109 | @pytest.mark.parametrize("url_came_from", [ |
|
118 | @pytest.mark.parametrize("url_came_from", [ | |
110 |
|
|
119 | 'data:text/html,<script>window.alert("xss")</script>', | |
111 |
|
|
120 | 'mailto:test@rhodecode.org', | |
112 |
|
|
121 | 'file:///etc/passwd', | |
113 |
|
|
122 | 'ftp://some.ftp.server', | |
114 |
|
|
123 | 'http://other.domain', | |
115 |
|
|
124 | '/\r\nX-Forwarded-Host: http://example.org', | |
116 | ]) |
|
125 | ]) | |
117 | def test_login_bad_came_froms(self, url_came_from): |
|
126 | def test_login_bad_came_froms(self, url_came_from): | |
118 | response = self.app.post(url(controller='login', action='index', |
|
127 | _url = '{}?came_from={}'.format(login_url, url_came_from) | |
119 | came_from=url_came_from), |
|
128 | response = self.app.post( | |
120 | {'username': 'test_admin', |
|
129 | _url, | |
121 |
|
|
130 | {'username': 'test_admin', 'password': 'test12'}) | |
122 | assert response.status == '302 Found' |
|
131 | assert response.status == '302 Found' | |
123 | assert response.tmpl_context.came_from == '/' |
|
|||
124 |
|
||||
125 | response = response.follow() |
|
132 | response = response.follow() | |
126 | assert response.status == '200 OK' |
|
133 | assert response.status == '200 OK' | |
|
134 | assert response.request.path == '/' | |||
127 |
|
135 | |||
128 | def test_login_short_password(self): |
|
136 | def test_login_short_password(self): | |
129 |
response = self.app.post( |
|
137 | response = self.app.post(login_url, | |
130 | {'username': 'test_admin', |
|
138 | {'username': 'test_admin', | |
131 | 'password': 'as'}) |
|
139 | 'password': 'as'}) | |
132 | assert response.status == '200 OK' |
|
140 | assert response.status == '200 OK' | |
@@ -135,7 +143,7 b' class TestLoginController:' | |||||
135 |
|
143 | |||
136 | def test_login_wrong_non_ascii_password(self, user_regular): |
|
144 | def test_login_wrong_non_ascii_password(self, user_regular): | |
137 | response = self.app.post( |
|
145 | response = self.app.post( | |
138 | url(controller='login', action='index'), |
|
146 | login_url, | |
139 | {'username': user_regular.username, |
|
147 | {'username': user_regular.username, | |
140 | 'password': u'invalid-non-asci\xe4'.encode('utf8')}) |
|
148 | 'password': u'invalid-non-asci\xe4'.encode('utf8')}) | |
141 |
|
149 | |||
@@ -146,13 +154,13 b' class TestLoginController:' | |||||
146 | password = u'valid-non-ascii\xe4' |
|
154 | password = u'valid-non-ascii\xe4' | |
147 | user = user_util.create_user(password=password) |
|
155 | user = user_util.create_user(password=password) | |
148 | response = self.app.post( |
|
156 | response = self.app.post( | |
149 | url(controller='login', action='index'), |
|
157 | login_url, | |
150 | {'username': user.username, |
|
158 | {'username': user.username, | |
151 | 'password': password.encode('utf-8')}) |
|
159 | 'password': password.encode('utf-8')}) | |
152 | assert response.status_code == 302 |
|
160 | assert response.status_code == 302 | |
153 |
|
161 | |||
154 | def test_login_wrong_username_password(self): |
|
162 | def test_login_wrong_username_password(self): | |
155 |
response = self.app.post( |
|
163 | response = self.app.post(login_url, | |
156 | {'username': 'error', |
|
164 | {'username': 'error', | |
157 | 'password': 'test12'}) |
|
165 | 'password': 'test12'}) | |
158 |
|
166 | |||
@@ -170,12 +178,13 b' class TestLoginController:' | |||||
170 | Session().add(user) |
|
178 | Session().add(user) | |
171 | Session().commit() |
|
179 | Session().commit() | |
172 | self.destroy_users.add(temp_user) |
|
180 | self.destroy_users.add(temp_user) | |
173 |
response = self.app.post( |
|
181 | response = self.app.post(login_url, | |
174 | {'username': temp_user, |
|
182 | {'username': temp_user, | |
175 | 'password': 'test123'}) |
|
183 | 'password': 'test123'}) | |
176 |
|
184 | |||
177 | assert response.status == '302 Found' |
|
185 | assert response.status == '302 Found' | |
178 | username = response.session['rhodecode_user'].get('username') |
|
186 | session = get_session_from_response(response) | |
|
187 | username = session['rhodecode_user'].get('username') | |||
179 | assert username == temp_user |
|
188 | assert username == temp_user | |
180 | response = response.follow() |
|
189 | response = response.follow() | |
181 | response.mustcontain('/%s' % HG_REPO) |
|
190 | response.mustcontain('/%s' % HG_REPO) | |
@@ -186,13 +195,13 b' class TestLoginController:' | |||||
186 |
|
195 | |||
187 | # REGISTRATIONS |
|
196 | # REGISTRATIONS | |
188 | def test_register(self): |
|
197 | def test_register(self): | |
189 |
response = self.app.get( |
|
198 | response = self.app.get(register_url) | |
190 | response.mustcontain('Create an Account') |
|
199 | response.mustcontain('Create an Account') | |
191 |
|
200 | |||
192 | def test_register_err_same_username(self): |
|
201 | def test_register_err_same_username(self): | |
193 | uname = 'test_admin' |
|
202 | uname = 'test_admin' | |
194 | response = self.app.post( |
|
203 | response = self.app.post( | |
195 | url(controller='login', action='register'), |
|
204 | register_url, | |
196 | { |
|
205 | { | |
197 | 'username': uname, |
|
206 | 'username': uname, | |
198 | 'password': 'test12', |
|
207 | 'password': 'test12', | |
@@ -203,13 +212,14 b' class TestLoginController:' | |||||
203 | } |
|
212 | } | |
204 | ) |
|
213 | ) | |
205 |
|
214 | |||
|
215 | assertr = AssertResponse(response) | |||
206 | msg = validators.ValidUsername()._messages['username_exists'] |
|
216 | msg = validators.ValidUsername()._messages['username_exists'] | |
207 |
msg = |
|
217 | msg = msg % {'username': uname} | |
208 | response.mustcontain(msg) |
|
218 | assertr.element_contains('#username+.error-message', msg) | |
209 |
|
219 | |||
210 | def test_register_err_same_email(self): |
|
220 | def test_register_err_same_email(self): | |
211 | response = self.app.post( |
|
221 | response = self.app.post( | |
212 | url(controller='login', action='register'), |
|
222 | register_url, | |
213 | { |
|
223 | { | |
214 | 'username': 'test_admin_0', |
|
224 | 'username': 'test_admin_0', | |
215 | 'password': 'test12', |
|
225 | 'password': 'test12', | |
@@ -220,12 +230,13 b' class TestLoginController:' | |||||
220 | } |
|
230 | } | |
221 | ) |
|
231 | ) | |
222 |
|
232 | |||
|
233 | assertr = AssertResponse(response) | |||
223 | msg = validators.UniqSystemEmail()()._messages['email_taken'] |
|
234 | msg = validators.UniqSystemEmail()()._messages['email_taken'] | |
224 | response.mustcontain(msg) |
|
235 | assertr.element_contains('#email+.error-message', msg) | |
225 |
|
236 | |||
226 | def test_register_err_same_email_case_sensitive(self): |
|
237 | def test_register_err_same_email_case_sensitive(self): | |
227 | response = self.app.post( |
|
238 | response = self.app.post( | |
228 | url(controller='login', action='register'), |
|
239 | register_url, | |
229 | { |
|
240 | { | |
230 | 'username': 'test_admin_1', |
|
241 | 'username': 'test_admin_1', | |
231 | 'password': 'test12', |
|
242 | 'password': 'test12', | |
@@ -235,12 +246,13 b' class TestLoginController:' | |||||
235 | 'lastname': 'test' |
|
246 | 'lastname': 'test' | |
236 | } |
|
247 | } | |
237 | ) |
|
248 | ) | |
|
249 | assertr = AssertResponse(response) | |||
238 | msg = validators.UniqSystemEmail()()._messages['email_taken'] |
|
250 | msg = validators.UniqSystemEmail()()._messages['email_taken'] | |
239 | response.mustcontain(msg) |
|
251 | assertr.element_contains('#email+.error-message', msg) | |
240 |
|
252 | |||
241 | def test_register_err_wrong_data(self): |
|
253 | def test_register_err_wrong_data(self): | |
242 | response = self.app.post( |
|
254 | response = self.app.post( | |
243 | url(controller='login', action='register'), |
|
255 | register_url, | |
244 | { |
|
256 | { | |
245 | 'username': 'xs', |
|
257 | 'username': 'xs', | |
246 | 'password': 'test', |
|
258 | 'password': 'test', | |
@@ -256,7 +268,7 b' class TestLoginController:' | |||||
256 |
|
268 | |||
257 | def test_register_err_username(self): |
|
269 | def test_register_err_username(self): | |
258 | response = self.app.post( |
|
270 | response = self.app.post( | |
259 | url(controller='login', action='register'), |
|
271 | register_url, | |
260 | { |
|
272 | { | |
261 | 'username': 'error user', |
|
273 | 'username': 'error user', | |
262 | 'password': 'test12', |
|
274 | 'password': 'test12', | |
@@ -277,7 +289,7 b' class TestLoginController:' | |||||
277 | def test_register_err_case_sensitive(self): |
|
289 | def test_register_err_case_sensitive(self): | |
278 | usr = 'Test_Admin' |
|
290 | usr = 'Test_Admin' | |
279 | response = self.app.post( |
|
291 | response = self.app.post( | |
280 | url(controller='login', action='register'), |
|
292 | register_url, | |
281 | { |
|
293 | { | |
282 | 'username': usr, |
|
294 | 'username': usr, | |
283 | 'password': 'test12', |
|
295 | 'password': 'test12', | |
@@ -288,14 +300,14 b' class TestLoginController:' | |||||
288 | } |
|
300 | } | |
289 | ) |
|
301 | ) | |
290 |
|
302 | |||
291 | response.mustcontain('An email address must contain a single @') |
|
303 | assertr = AssertResponse(response) | |
292 | msg = validators.ValidUsername()._messages['username_exists'] |
|
304 | msg = validators.ValidUsername()._messages['username_exists'] | |
293 |
msg = |
|
305 | msg = msg % {'username': usr} | |
294 | response.mustcontain(msg) |
|
306 | assertr.element_contains('#username+.error-message', msg) | |
295 |
|
307 | |||
296 | def test_register_special_chars(self): |
|
308 | def test_register_special_chars(self): | |
297 | response = self.app.post( |
|
309 | response = self.app.post( | |
298 | url(controller='login', action='register'), |
|
310 | register_url, | |
299 | { |
|
311 | { | |
300 | 'username': 'xxxaxn', |
|
312 | 'username': 'xxxaxn', | |
301 | 'password': 'ąćźżąśśśś', |
|
313 | 'password': 'ąćźżąśśśś', | |
@@ -311,7 +323,7 b' class TestLoginController:' | |||||
311 |
|
323 | |||
312 | def test_register_password_mismatch(self): |
|
324 | def test_register_password_mismatch(self): | |
313 | response = self.app.post( |
|
325 | response = self.app.post( | |
314 | url(controller='login', action='register'), |
|
326 | register_url, | |
315 | { |
|
327 | { | |
316 | 'username': 'xs', |
|
328 | 'username': 'xs', | |
317 | 'password': '123qwe', |
|
329 | 'password': '123qwe', | |
@@ -332,7 +344,7 b' class TestLoginController:' | |||||
332 | lastname = 'testlastname' |
|
344 | lastname = 'testlastname' | |
333 |
|
345 | |||
334 | response = self.app.post( |
|
346 | response = self.app.post( | |
335 | url(controller='login', action='register'), |
|
347 | register_url, | |
336 | { |
|
348 | { | |
337 | 'username': username, |
|
349 | 'username': username, | |
338 | 'password': password, |
|
350 | 'password': password, | |
@@ -360,7 +372,7 b' class TestLoginController:' | |||||
360 | def test_forgot_password_wrong_mail(self): |
|
372 | def test_forgot_password_wrong_mail(self): | |
361 | bad_email = 'marcin@wrongmail.org' |
|
373 | bad_email = 'marcin@wrongmail.org' | |
362 | response = self.app.post( |
|
374 | response = self.app.post( | |
363 | url(controller='login', action='password_reset'), |
|
375 | pwd_reset_url, | |
364 | {'email': bad_email, } |
|
376 | {'email': bad_email, } | |
365 | ) |
|
377 | ) | |
366 |
|
378 | |||
@@ -369,8 +381,7 b' class TestLoginController:' | |||||
369 | response.mustcontain() |
|
381 | response.mustcontain() | |
370 |
|
382 | |||
371 | def test_forgot_password(self): |
|
383 | def test_forgot_password(self): | |
372 |
response = self.app.get( |
|
384 | response = self.app.get(pwd_reset_url) | |
373 | action='password_reset')) |
|
|||
374 | assert response.status == '200 OK' |
|
385 | assert response.status == '200 OK' | |
375 |
|
386 | |||
376 | username = 'test_password_reset_1' |
|
387 | username = 'test_password_reset_1' | |
@@ -389,8 +400,7 b' class TestLoginController:' | |||||
389 | Session().add(new) |
|
400 | Session().add(new) | |
390 | Session().commit() |
|
401 | Session().commit() | |
391 |
|
402 | |||
392 |
response = self.app.post( |
|
403 | response = self.app.post(pwd_reset_url, | |
393 | action='password_reset'), |
|
|||
394 | {'email': email, }) |
|
404 | {'email': email, }) | |
395 |
|
405 | |||
396 | assert_session_flash( |
|
406 | assert_session_flash( | |
@@ -401,20 +411,18 b' class TestLoginController:' | |||||
401 | # BAD KEY |
|
411 | # BAD KEY | |
402 |
|
412 | |||
403 | key = "bad" |
|
413 | key = "bad" | |
404 | response = self.app.get(url(controller='login', |
|
414 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) | |
405 | action='password_reset_confirmation', |
|
415 | response = self.app.get(confirm_url) | |
406 | key=key)) |
|
|||
407 | assert response.status == '302 Found' |
|
416 | assert response.status == '302 Found' | |
408 |
assert response.location.endswith( |
|
417 | assert response.location.endswith(pwd_reset_url) | |
409 |
|
418 | |||
410 | # GOOD KEY |
|
419 | # GOOD KEY | |
411 |
|
420 | |||
412 | key = User.get_by_username(username).api_key |
|
421 | key = User.get_by_username(username).api_key | |
413 | response = self.app.get(url(controller='login', |
|
422 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) | |
414 | action='password_reset_confirmation', |
|
423 | response = self.app.get(confirm_url) | |
415 | key=key)) |
|
|||
416 | assert response.status == '302 Found' |
|
424 | assert response.status == '302 Found' | |
417 |
assert response.location.endswith( |
|
425 | assert response.location.endswith(login_url) | |
418 |
|
426 | |||
419 | assert_session_flash( |
|
427 | assert_session_flash( | |
420 | response, |
|
428 | response, |
@@ -99,12 +99,13 b' class TestPullrequestsController:' | |||||
99 | in response) != pr_merge_enabled |
|
99 | in response) != pr_merge_enabled | |
100 |
|
100 | |||
101 | def test_close_status_visibility(self, pr_util, csrf_token): |
|
101 | def test_close_status_visibility(self, pr_util, csrf_token): | |
|
102 | from rhodecode.tests.functional.test_login import login_url, logut_url | |||
102 | # Logout |
|
103 | # Logout | |
103 | response = self.app.post( |
|
104 | response = self.app.post( | |
104 | url(controller='login', action='logout'), |
|
105 | logut_url, | |
105 | params={'csrf_token': csrf_token}) |
|
106 | params={'csrf_token': csrf_token}) | |
106 | # Login as regular user |
|
107 | # Login as regular user | |
107 |
response = self.app.post( |
|
108 | response = self.app.post(login_url, | |
108 | {'username': 'test_regular', |
|
109 | {'username': 'test_regular', | |
109 | 'password': 'test12'}) |
|
110 | 'password': 'test12'}) | |
110 |
|
111 |
@@ -129,6 +129,10 b' class TestSearchController(TestControlle' | |||||
129 | ('author:marcin@python-blog.com ' |
|
129 | ('author:marcin@python-blog.com ' | |
130 | 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ |
|
130 | 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ | |
131 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), |
|
131 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |
|
132 | ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ | |||
|
133 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |||
|
134 | ('b986218b', 1, [ | |||
|
135 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |||
132 | ]) |
|
136 | ]) | |
133 | def test_search_commit_messages( |
|
137 | def test_search_commit_messages( | |
134 | self, query, expected_hits, expected_commits, enabled_backends): |
|
138 | self, query, expected_hits, expected_commits, enabled_backends): |
@@ -155,3 +155,29 b' class TestRhodeCodeAuthPlugin(object):' | |||||
155 | self.password_generator_mock = password_generator_patch.start() |
|
155 | self.password_generator_mock = password_generator_patch.start() | |
156 | self.password_generator_mock.return_value = 'new-password' |
|
156 | self.password_generator_mock.return_value = 'new-password' | |
157 | self.finalizers.append(password_generator_patch.stop) |
|
157 | self.finalizers.append(password_generator_patch.stop) | |
|
158 | ||||
|
159 | ||||
|
160 | def test_missing_ldap(): | |||
|
161 | from rhodecode.model.validators import Missing | |||
|
162 | ||||
|
163 | try: | |||
|
164 | import ldap_not_existing | |||
|
165 | except ImportError: | |||
|
166 | # means that python-ldap is not installed | |||
|
167 | ldap_not_existing = Missing | |||
|
168 | ||||
|
169 | # missing is singleton | |||
|
170 | assert ldap_not_existing == Missing | |||
|
171 | ||||
|
172 | ||||
|
173 | def test_import_ldap(): | |||
|
174 | from rhodecode.model.validators import Missing | |||
|
175 | ||||
|
176 | try: | |||
|
177 | import ldap | |||
|
178 | except ImportError: | |||
|
179 | # means that python-ldap is not installed | |||
|
180 | ldap = Missing | |||
|
181 | ||||
|
182 | # missing is singleton | |||
|
183 | assert False is (ldap == Missing) |
@@ -32,6 +32,36 b' from rhodecode.model.user import UserMod' | |||||
32 | from rhodecode.model.user_group import UserGroupModel |
|
32 | from rhodecode.model.user_group import UserGroupModel | |
33 |
|
33 | |||
34 |
|
34 | |||
|
35 | def test_perm_origin_dict(): | |||
|
36 | pod = auth.PermOriginDict() | |||
|
37 | pod['thing'] = 'read', 'default' | |||
|
38 | assert pod['thing'] == 'read' | |||
|
39 | ||||
|
40 | assert pod.perm_origin_stack == { | |||
|
41 | 'thing': [('read', 'default')]} | |||
|
42 | ||||
|
43 | pod['thing'] = 'write', 'admin' | |||
|
44 | assert pod['thing'] == 'write' | |||
|
45 | ||||
|
46 | assert pod.perm_origin_stack == { | |||
|
47 | 'thing': [('read', 'default'), ('write', 'admin')]} | |||
|
48 | ||||
|
49 | pod['other'] = 'write', 'default' | |||
|
50 | ||||
|
51 | assert pod.perm_origin_stack == { | |||
|
52 | 'other': [('write', 'default')], | |||
|
53 | 'thing': [('read', 'default'), ('write', 'admin')]} | |||
|
54 | ||||
|
55 | pod['other'] = 'none', 'override' | |||
|
56 | ||||
|
57 | assert pod.perm_origin_stack == { | |||
|
58 | 'other': [('write', 'default'), ('none', 'override')], | |||
|
59 | 'thing': [('read', 'default'), ('write', 'admin')]} | |||
|
60 | ||||
|
61 | with pytest.raises(ValueError): | |||
|
62 | pod['thing'] = 'read' | |||
|
63 | ||||
|
64 | ||||
35 | def test_cached_perms_data(user_regular, backend_random): |
|
65 | def test_cached_perms_data(user_regular, backend_random): | |
36 | permissions = get_permissions(user_regular) |
|
66 | permissions = get_permissions(user_regular) | |
37 | repo_name = backend_random.repo.repo_name |
|
67 | repo_name = backend_random.repo.repo_name |
@@ -155,3 +155,42 b' def test_get_visual_attr(pylonsapp):' | |||||
155 | def test_chop_at(test_text, inclusive, expected_text): |
|
155 | def test_chop_at(test_text, inclusive, expected_text): | |
156 | assert helpers.chop_at_smart( |
|
156 | assert helpers.chop_at_smart( | |
157 | test_text, '\n', inclusive, '...') == expected_text |
|
157 | test_text, '\n', inclusive, '...') == expected_text | |
|
158 | ||||
|
159 | ||||
|
160 | @pytest.mark.parametrize('test_text, expected_output', [ | |||
|
161 | ('some text', ['some', 'text']), | |||
|
162 | ('some text', ['some', 'text']), | |||
|
163 | ('some text "with a phrase"', ['some', 'text', 'with a phrase']), | |||
|
164 | ('"a phrase" "another phrase"', ['a phrase', 'another phrase']), | |||
|
165 | ('"justphrase"', ['justphrase']), | |||
|
166 | ('""', []), | |||
|
167 | ('', []), | |||
|
168 | (' ', []), | |||
|
169 | ('" "', []), | |||
|
170 | ]) | |||
|
171 | def test_extract_phrases(test_text, expected_output): | |||
|
172 | assert helpers.extract_phrases(test_text) == expected_output | |||
|
173 | ||||
|
174 | ||||
|
175 | @pytest.mark.parametrize('test_text, text_phrases, expected_output', [ | |||
|
176 | ('some text here', ['some', 'here'], [(0, 4), (10, 14)]), | |||
|
177 | ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]), | |||
|
178 | ('irrelevant', ['not found'], []), | |||
|
179 | ('irrelevant', ['not found'], []), | |||
|
180 | ]) | |||
|
181 | def test_get_matching_offsets(test_text, text_phrases, expected_output): | |||
|
182 | assert helpers.get_matching_offsets( | |||
|
183 | test_text, text_phrases) == expected_output | |||
|
184 | ||||
|
185 | def test_normalize_text_for_matching(): | |||
|
186 | assert helpers.normalize_text_for_matching( | |||
|
187 | 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h' | |||
|
188 | ||||
|
189 | def test_get_matching_line_offsets(): | |||
|
190 | assert helpers.get_matching_line_offsets([ | |||
|
191 | 'words words words', | |||
|
192 | 'words words words', | |||
|
193 | 'some text some', | |||
|
194 | 'words words words', | |||
|
195 | 'words words words', | |||
|
196 | 'text here what'], 'text') == {3: [(5, 9)], 6: [(0, 4)]} No newline at end of file |
@@ -270,3 +270,13 b' def is_url_reachable(url):' | |||||
270 | except urllib2.URLError: |
|
270 | except urllib2.URLError: | |
271 | return False |
|
271 | return False | |
272 | return True |
|
272 | return True | |
|
273 | ||||
|
274 | ||||
|
275 | def get_session_from_response(response): | |||
|
276 | """ | |||
|
277 | This returns the session from a response object. Pylons has some magic | |||
|
278 | to make the session available as `response.session`. But pyramid | |||
|
279 | doesn't expose it. | |||
|
280 | """ | |||
|
281 | # TODO: Try to look up the session key also. | |||
|
282 | return response.request.environ['beaker.session'] |
@@ -214,10 +214,12 b' setup(' | |||||
214 | entry_points={ |
|
214 | entry_points={ | |
215 | 'enterprise.plugins1': [ |
|
215 | 'enterprise.plugins1': [ | |
216 | 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory', |
|
216 | 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory', | |
|
217 | 'headers=rhodecode.authentication.plugins.auth_headers:plugin_factory', | |||
217 | 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory', |
|
218 | 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory', | |
218 | 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory', |
|
219 | 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory', | |
219 | 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory', |
|
220 | 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory', | |
220 | 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory', |
|
221 | 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory', | |
|
222 | 'token=rhodecode.authentication.plugins.auth_token:plugin_factory', | |||
221 | ], |
|
223 | ], | |
222 | 'paste.app_factory': [ |
|
224 | 'paste.app_factory': [ | |
223 | 'main=rhodecode.config.middleware:make_pyramid_app', |
|
225 | 'main=rhodecode.config.middleware:make_pyramid_app', |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
This diff has been collapsed as it changes many lines, (3341 lines changed) Show them Hide them |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now