##// END OF EJS Templates
user-groups: new selector for user group memebers....
marcink -
r1089:4d236b89 default
parent child Browse files
Show More
@@ -1,191 +1,190 b''
1 1 {
2 2 "dirs": {
3 3 "css": {
4 4 "src":"rhodecode/public/css",
5 5 "dest":"rhodecode/public/css"
6 6 },
7 7 "js": {
8 8 "src": "rhodecode/public/js/src",
9 9 "dest": "rhodecode/public/js",
10 10 "bower": "bower_components",
11 11 "node_modules": "node_modules"
12 12 }
13 13 },
14 14 "copy": {
15 15 "main": {
16 16 "expand": true,
17 17 "cwd": "bower_components",
18 18 "src": "webcomponentsjs/webcomponents-lite.js",
19 19 "dest": "<%= dirs.js.dest %>/vendors"
20 20 }
21 21 },
22 22 "concat": {
23 23 "polymercss": {
24 24 "src": [
25 25 "<%= dirs.js.src %>/components/root-styles-prefix.html",
26 26 "<%= dirs.css.src %>/style-polymer.css",
27 27 "<%= dirs.js.src %>/components/root-styles-suffix.html"
28 28 ],
29 29 "dest": "<%= dirs.js.dest %>/src/components/root-styles.gen.html",
30 30 "nonull": true
31 31 },
32 32 "dist": {
33 33 "src": [
34 34 "<%= dirs.js.src %>/jquery-1.11.1.min.js",
35 35 "<%= dirs.js.src %>/logging.js",
36 36 "<%= dirs.js.src %>/bootstrap.js",
37 37 "<%= dirs.js.src %>/mousetrap.js",
38 38 "<%= dirs.js.src %>/moment.js",
39 39 "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js",
40 40 "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js",
41 41 "<%= dirs.js.src %>/i18n_utils.js",
42 42 "<%= dirs.js.src %>/deform.js",
43 43 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
44 44 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
45 45 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
46 46 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
47 47 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
48 48 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
49 49 "<%= dirs.js.src %>/plugins/jquery.mark.js",
50 50 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
51 51 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
52 52 "<%= dirs.js.src %>/select2/select2.js",
53 53 "<%= dirs.js.src %>/codemirror/codemirror.js",
54 54 "<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
55 55 "<%= dirs.js.src %>/codemirror/codemirror_hint.js",
56 56 "<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
57 57 "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
58 58 "<%= dirs.js.dest %>/mode/meta.js",
59 59 "<%= dirs.js.dest %>/mode/meta_ext.js",
60 60 "<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js",
61 61 "<%= dirs.js.src %>/rhodecode/utils/array.js",
62 62 "<%= dirs.js.src %>/rhodecode/utils/string.js",
63 63 "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
64 64 "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
65 65 "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
66 66 "<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
67 67 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
68 68 "<%= dirs.js.src %>/rhodecode/utils/os.js",
69 69 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
70 "<%= dirs.js.src %>/rhodecode/widgets/multiselect.js",
71 70 "<%= dirs.js.src %>/rhodecode/init.js",
72 71 "<%= dirs.js.src %>/rhodecode/codemirror.js",
73 72 "<%= dirs.js.src %>/rhodecode/comments.js",
74 73 "<%= dirs.js.src %>/rhodecode/constants.js",
75 74 "<%= dirs.js.src %>/rhodecode/files.js",
76 75 "<%= dirs.js.src %>/rhodecode/followers.js",
77 76 "<%= dirs.js.src %>/rhodecode/menus.js",
78 77 "<%= dirs.js.src %>/rhodecode/notifications.js",
79 78 "<%= dirs.js.src %>/rhodecode/permissions.js",
80 79 "<%= dirs.js.src %>/rhodecode/pjax.js",
81 80 "<%= dirs.js.src %>/rhodecode/pullrequests.js",
82 81 "<%= dirs.js.src %>/rhodecode/settings.js",
83 82 "<%= dirs.js.src %>/rhodecode/select2_widgets.js",
84 83 "<%= dirs.js.src %>/rhodecode/tooltips.js",
85 84 "<%= dirs.js.src %>/rhodecode/users.js",
86 85 "<%= dirs.js.src %>/rhodecode/appenlight.js",
87 86 "<%= dirs.js.src %>/rhodecode.js"
88 87 ],
89 88 "dest": "<%= dirs.js.dest %>/scripts.js",
90 89 "nonull": true
91 90 }
92 91 },
93 92 "crisper": {
94 93 "dist": {
95 94 "options": {
96 95 "cleanup": false,
97 96 "onlySplit": true
98 97 },
99 98 "src": "<%= dirs.js.dest %>/rhodecode-components.html",
100 99 "dest": "<%= dirs.js.dest %>/rhodecode-components.js"
101 100 }
102 101 },
103 102 "less": {
104 103 "development": {
105 104 "options": {
106 105 "compress": false,
107 106 "yuicompress": false,
108 107 "optimization": 0
109 108 },
110 109 "files": {
111 110 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
112 111 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
113 112 }
114 113 },
115 114 "production": {
116 115 "options": {
117 116 "compress": true,
118 117 "yuicompress": true,
119 118 "optimization": 2
120 119 },
121 120 "files": {
122 121 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
123 122 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
124 123 }
125 124 },
126 125 "components": {
127 126 "files": [
128 127 {
129 128 "cwd": "<%= dirs.js.src %>/components/",
130 129 "dest": "<%= dirs.js.src %>/components/",
131 130 "src": [
132 131 "**/*.less"
133 132 ],
134 133 "expand": true,
135 134 "ext": ".css"
136 135 }
137 136 ]
138 137 }
139 138 },
140 139 "watch": {
141 140 "less": {
142 141 "files": [
143 142 "<%= dirs.css.src %>/**/*.less",
144 143 "<%= dirs.js.src %>/components/**/*.less"
145 144 ],
146 145 "tasks": [
147 146 "less:development",
148 147 "less:components",
149 148 "concat:polymercss",
150 149 "vulcanize",
151 150 "crisper",
152 151 "concat:dist"
153 152 ]
154 153 },
155 154 "js": {
156 155 "files": [
157 156 "!<%= dirs.js.src %>/components/root-styles.gen.html",
158 157 "<%= dirs.js.src %>/**/*.js",
159 158 "<%= dirs.js.src %>/components/**/*.html"
160 159 ],
161 160 "tasks": [
162 161 "less:components",
163 162 "concat:polymercss",
164 163 "vulcanize",
165 164 "crisper",
166 165 "concat:dist"
167 166 ]
168 167 }
169 168 },
170 169 "jshint": {
171 170 "rhodecode": {
172 171 "src": "<%= dirs.js.src %>/rhodecode/**/*.js",
173 172 "options": {
174 173 "jshintrc": ".jshintrc"
175 174 }
176 175 }
177 176 },
178 177 "vulcanize": {
179 178 "default": {
180 179 "options": {
181 180 "abspath": "",
182 181 "inlineScripts": true,
183 182 "inlineCss": true,
184 183 "stripComments": true
185 184 },
186 185 "files": {
187 186 "<%= dirs.js.dest %>/rhodecode-components.html": "<%= dirs.js.src %>/components/shared-components.html"
188 187 }
189 188 }
190 189 }
191 190 }
@@ -1,1167 +1,1167 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 action='edit', conditions={'method': ['GET']})
308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 action='edit_members', conditions={'method': ['GET']})
392 action='user_group_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_system', '/settings/system',
507 507 action='settings_system', conditions={'method': ['GET']})
508 508
509 509 m.connect('admin_settings_system_update', '/settings/system/updates',
510 510 action='settings_system_update', conditions={'method': ['GET']})
511 511
512 512 m.connect('admin_settings_supervisor', '/settings/supervisor',
513 513 action='settings_supervisor', conditions={'method': ['GET']})
514 514 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
515 515 action='settings_supervisor_log', conditions={'method': ['GET']})
516 516
517 517 m.connect('admin_settings_labs', '/settings/labs',
518 518 action='settings_labs_update',
519 519 conditions={'method': ['POST']})
520 520 m.connect('admin_settings_labs', '/settings/labs',
521 521 action='settings_labs', conditions={'method': ['GET']})
522 522
523 523 # ADMIN MY ACCOUNT
524 524 with rmap.submapper(path_prefix=ADMIN_PREFIX,
525 525 controller='admin/my_account') as m:
526 526
527 527 m.connect('my_account', '/my_account',
528 528 action='my_account', conditions={'method': ['GET']})
529 529 m.connect('my_account_edit', '/my_account/edit',
530 530 action='my_account_edit', conditions={'method': ['GET']})
531 531 m.connect('my_account', '/my_account',
532 532 action='my_account_update', conditions={'method': ['POST']})
533 533
534 534 m.connect('my_account_password', '/my_account/password',
535 535 action='my_account_password', conditions={'method': ['GET', 'POST']})
536 536
537 537 m.connect('my_account_repos', '/my_account/repos',
538 538 action='my_account_repos', conditions={'method': ['GET']})
539 539
540 540 m.connect('my_account_watched', '/my_account/watched',
541 541 action='my_account_watched', conditions={'method': ['GET']})
542 542
543 543 m.connect('my_account_pullrequests', '/my_account/pull_requests',
544 544 action='my_account_pullrequests', conditions={'method': ['GET']})
545 545
546 546 m.connect('my_account_perms', '/my_account/perms',
547 547 action='my_account_perms', conditions={'method': ['GET']})
548 548
549 549 m.connect('my_account_emails', '/my_account/emails',
550 550 action='my_account_emails', conditions={'method': ['GET']})
551 551 m.connect('my_account_emails', '/my_account/emails',
552 552 action='my_account_emails_add', conditions={'method': ['POST']})
553 553 m.connect('my_account_emails', '/my_account/emails',
554 554 action='my_account_emails_delete', conditions={'method': ['DELETE']})
555 555
556 556 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
557 557 action='my_account_auth_tokens', conditions={'method': ['GET']})
558 558 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
559 559 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
560 560 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
561 561 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
562 562 m.connect('my_account_notifications', '/my_account/notifications',
563 563 action='my_notifications',
564 564 conditions={'method': ['GET']})
565 565 m.connect('my_account_notifications_toggle_visibility',
566 566 '/my_account/toggle_visibility',
567 567 action='my_notifications_toggle_visibility',
568 568 conditions={'method': ['POST']})
569 569
570 570 # NOTIFICATION REST ROUTES
571 571 with rmap.submapper(path_prefix=ADMIN_PREFIX,
572 572 controller='admin/notifications') as m:
573 573 m.connect('notifications', '/notifications',
574 574 action='index', conditions={'method': ['GET']})
575 575 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
576 576 action='mark_all_read', conditions={'method': ['POST']})
577 577 m.connect('/notifications/{notification_id}',
578 578 action='update', conditions={'method': ['PUT']})
579 579 m.connect('/notifications/{notification_id}',
580 580 action='delete', conditions={'method': ['DELETE']})
581 581 m.connect('notification', '/notifications/{notification_id}',
582 582 action='show', conditions={'method': ['GET']})
583 583
584 584 # ADMIN GIST
585 585 with rmap.submapper(path_prefix=ADMIN_PREFIX,
586 586 controller='admin/gists') as m:
587 587 m.connect('gists', '/gists',
588 588 action='create', conditions={'method': ['POST']})
589 589 m.connect('gists', '/gists', jsroute=True,
590 590 action='index', conditions={'method': ['GET']})
591 591 m.connect('new_gist', '/gists/new', jsroute=True,
592 592 action='new', conditions={'method': ['GET']})
593 593
594 594 m.connect('/gists/{gist_id}',
595 595 action='delete', conditions={'method': ['DELETE']})
596 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 597 action='edit_form', conditions={'method': ['GET']})
598 598 m.connect('edit_gist', '/gists/{gist_id}/edit',
599 599 action='edit', conditions={'method': ['POST']})
600 600 m.connect(
601 601 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
602 602 action='check_revision', conditions={'method': ['GET']})
603 603
604 604 m.connect('gist', '/gists/{gist_id}',
605 605 action='show', conditions={'method': ['GET']})
606 606 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
607 607 revision='tip',
608 608 action='show', conditions={'method': ['GET']})
609 609 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
610 610 revision='tip',
611 611 action='show', conditions={'method': ['GET']})
612 612 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
613 613 revision='tip',
614 614 action='show', conditions={'method': ['GET']},
615 615 requirements=URL_NAME_REQUIREMENTS)
616 616
617 617 # ADMIN MAIN PAGES
618 618 with rmap.submapper(path_prefix=ADMIN_PREFIX,
619 619 controller='admin/admin') as m:
620 620 m.connect('admin_home', '', action='index')
621 621 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
622 622 action='add_repo')
623 623 m.connect(
624 624 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
625 625 action='pull_requests')
626 626 m.connect(
627 627 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
628 628 action='pull_requests')
629 629
630 630
631 631 # USER JOURNAL
632 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 633 controller='journal', action='index')
634 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 635 controller='journal', action='journal_rss')
636 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 637 controller='journal', action='journal_atom')
638 638
639 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 640 controller='journal', action='public_journal')
641 641
642 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 643 controller='journal', action='public_journal_rss')
644 644
645 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 646 controller='journal', action='public_journal_rss')
647 647
648 648 rmap.connect('public_journal_atom',
649 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 650 action='public_journal_atom')
651 651
652 652 rmap.connect('public_journal_atom_old',
653 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 654 action='public_journal_atom')
655 655
656 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 657 controller='journal', action='toggle_following', jsroute=True,
658 658 conditions={'method': ['POST']})
659 659
660 660 # FULL TEXT SEARCH
661 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 662 controller='search')
663 663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 664 controller='search',
665 665 action='index',
666 666 conditions={'function': check_repo},
667 667 requirements=URL_NAME_REQUIREMENTS)
668 668
669 669 # FEEDS
670 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 671 controller='feed', action='rss',
672 672 conditions={'function': check_repo},
673 673 requirements=URL_NAME_REQUIREMENTS)
674 674
675 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 676 controller='feed', action='atom',
677 677 conditions={'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679
680 680 #==========================================================================
681 681 # REPOSITORY ROUTES
682 682 #==========================================================================
683 683
684 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 685 controller='admin/repos', action='repo_creating',
686 686 requirements=URL_NAME_REQUIREMENTS)
687 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 688 controller='admin/repos', action='repo_check',
689 689 requirements=URL_NAME_REQUIREMENTS)
690 690
691 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 692 controller='summary', action='repo_stats',
693 693 conditions={'function': check_repo},
694 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 695
696 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 697 controller='summary', action='repo_refs_data', jsroute=True,
698 698 requirements=URL_NAME_REQUIREMENTS)
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 703 controller='summary', action='repo_default_reviewers_data',
704 704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
705 705
706 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
707 707 controller='changeset', revision='tip', jsroute=True,
708 708 conditions={'function': check_repo},
709 709 requirements=URL_NAME_REQUIREMENTS)
710 710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
711 711 controller='changeset', revision='tip', action='changeset_children',
712 712 conditions={'function': check_repo},
713 713 requirements=URL_NAME_REQUIREMENTS)
714 714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
715 715 controller='changeset', revision='tip', action='changeset_parents',
716 716 conditions={'function': check_repo},
717 717 requirements=URL_NAME_REQUIREMENTS)
718 718
719 719 # repo edit options
720 720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
721 721 controller='admin/repos', action='edit',
722 722 conditions={'method': ['GET'], 'function': check_repo},
723 723 requirements=URL_NAME_REQUIREMENTS)
724 724
725 725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
726 726 jsroute=True,
727 727 controller='admin/repos', action='edit_permissions',
728 728 conditions={'method': ['GET'], 'function': check_repo},
729 729 requirements=URL_NAME_REQUIREMENTS)
730 730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
731 731 controller='admin/repos', action='edit_permissions_update',
732 732 conditions={'method': ['PUT'], 'function': check_repo},
733 733 requirements=URL_NAME_REQUIREMENTS)
734 734
735 735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
736 736 controller='admin/repos', action='edit_fields',
737 737 conditions={'method': ['GET'], 'function': check_repo},
738 738 requirements=URL_NAME_REQUIREMENTS)
739 739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
740 740 controller='admin/repos', action='create_repo_field',
741 741 conditions={'method': ['PUT'], 'function': check_repo},
742 742 requirements=URL_NAME_REQUIREMENTS)
743 743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
744 744 controller='admin/repos', action='delete_repo_field',
745 745 conditions={'method': ['DELETE'], 'function': check_repo},
746 746 requirements=URL_NAME_REQUIREMENTS)
747 747
748 748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
749 749 controller='admin/repos', action='edit_advanced',
750 750 conditions={'method': ['GET'], 'function': check_repo},
751 751 requirements=URL_NAME_REQUIREMENTS)
752 752
753 753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
754 754 controller='admin/repos', action='edit_advanced_locking',
755 755 conditions={'method': ['PUT'], 'function': check_repo},
756 756 requirements=URL_NAME_REQUIREMENTS)
757 757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
758 758 controller='admin/repos', action='toggle_locking',
759 759 conditions={'method': ['GET'], 'function': check_repo},
760 760 requirements=URL_NAME_REQUIREMENTS)
761 761
762 762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
763 763 controller='admin/repos', action='edit_advanced_journal',
764 764 conditions={'method': ['PUT'], 'function': check_repo},
765 765 requirements=URL_NAME_REQUIREMENTS)
766 766
767 767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
768 768 controller='admin/repos', action='edit_advanced_fork',
769 769 conditions={'method': ['PUT'], 'function': check_repo},
770 770 requirements=URL_NAME_REQUIREMENTS)
771 771
772 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 773 controller='admin/repos', action='edit_caches_form',
774 774 conditions={'method': ['GET'], 'function': check_repo},
775 775 requirements=URL_NAME_REQUIREMENTS)
776 776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
777 777 controller='admin/repos', action='edit_caches',
778 778 conditions={'method': ['PUT'], 'function': check_repo},
779 779 requirements=URL_NAME_REQUIREMENTS)
780 780
781 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 782 controller='admin/repos', action='edit_remote_form',
783 783 conditions={'method': ['GET'], 'function': check_repo},
784 784 requirements=URL_NAME_REQUIREMENTS)
785 785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
786 786 controller='admin/repos', action='edit_remote',
787 787 conditions={'method': ['PUT'], 'function': check_repo},
788 788 requirements=URL_NAME_REQUIREMENTS)
789 789
790 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 791 controller='admin/repos', action='edit_statistics_form',
792 792 conditions={'method': ['GET'], 'function': check_repo},
793 793 requirements=URL_NAME_REQUIREMENTS)
794 794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
795 795 controller='admin/repos', action='edit_statistics',
796 796 conditions={'method': ['PUT'], 'function': check_repo},
797 797 requirements=URL_NAME_REQUIREMENTS)
798 798 rmap.connect('repo_settings_issuetracker',
799 799 '/{repo_name}/settings/issue-tracker',
800 800 controller='admin/repos', action='repo_issuetracker',
801 801 conditions={'method': ['GET'], 'function': check_repo},
802 802 requirements=URL_NAME_REQUIREMENTS)
803 803 rmap.connect('repo_issuetracker_test',
804 804 '/{repo_name}/settings/issue-tracker/test',
805 805 controller='admin/repos', action='repo_issuetracker_test',
806 806 conditions={'method': ['POST'], 'function': check_repo},
807 807 requirements=URL_NAME_REQUIREMENTS)
808 808 rmap.connect('repo_issuetracker_delete',
809 809 '/{repo_name}/settings/issue-tracker/delete',
810 810 controller='admin/repos', action='repo_issuetracker_delete',
811 811 conditions={'method': ['DELETE'], 'function': check_repo},
812 812 requirements=URL_NAME_REQUIREMENTS)
813 813 rmap.connect('repo_issuetracker_save',
814 814 '/{repo_name}/settings/issue-tracker/save',
815 815 controller='admin/repos', action='repo_issuetracker_save',
816 816 conditions={'method': ['POST'], 'function': check_repo},
817 817 requirements=URL_NAME_REQUIREMENTS)
818 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 819 controller='admin/repos', action='repo_settings_vcs_update',
820 820 conditions={'method': ['POST'], 'function': check_repo},
821 821 requirements=URL_NAME_REQUIREMENTS)
822 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 823 controller='admin/repos', action='repo_settings_vcs',
824 824 conditions={'method': ['GET'], 'function': check_repo},
825 825 requirements=URL_NAME_REQUIREMENTS)
826 826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
827 827 controller='admin/repos', action='repo_delete_svn_pattern',
828 828 conditions={'method': ['DELETE'], 'function': check_repo},
829 829 requirements=URL_NAME_REQUIREMENTS)
830 830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 831 controller='admin/repos', action='repo_settings_pullrequest',
832 832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 833 requirements=URL_NAME_REQUIREMENTS)
834 834
835 835 # still working url for backward compat.
836 836 rmap.connect('raw_changeset_home_depraced',
837 837 '/{repo_name}/raw-changeset/{revision}',
838 838 controller='changeset', action='changeset_raw',
839 839 revision='tip', conditions={'function': check_repo},
840 840 requirements=URL_NAME_REQUIREMENTS)
841 841
842 842 # new URLs
843 843 rmap.connect('changeset_raw_home',
844 844 '/{repo_name}/changeset-diff/{revision}',
845 845 controller='changeset', action='changeset_raw',
846 846 revision='tip', conditions={'function': check_repo},
847 847 requirements=URL_NAME_REQUIREMENTS)
848 848
849 849 rmap.connect('changeset_patch_home',
850 850 '/{repo_name}/changeset-patch/{revision}',
851 851 controller='changeset', action='changeset_patch',
852 852 revision='tip', conditions={'function': check_repo},
853 853 requirements=URL_NAME_REQUIREMENTS)
854 854
855 855 rmap.connect('changeset_download_home',
856 856 '/{repo_name}/changeset-download/{revision}',
857 857 controller='changeset', action='changeset_download',
858 858 revision='tip', conditions={'function': check_repo},
859 859 requirements=URL_NAME_REQUIREMENTS)
860 860
861 861 rmap.connect('changeset_comment',
862 862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
863 863 controller='changeset', revision='tip', action='comment',
864 864 conditions={'function': check_repo},
865 865 requirements=URL_NAME_REQUIREMENTS)
866 866
867 867 rmap.connect('changeset_comment_preview',
868 868 '/{repo_name}/changeset/comment/preview', jsroute=True,
869 869 controller='changeset', action='preview_comment',
870 870 conditions={'function': check_repo, 'method': ['POST']},
871 871 requirements=URL_NAME_REQUIREMENTS)
872 872
873 873 rmap.connect('changeset_comment_delete',
874 874 '/{repo_name}/changeset/comment/{comment_id}/delete',
875 875 controller='changeset', action='delete_comment',
876 876 conditions={'function': check_repo, 'method': ['DELETE']},
877 877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878 878
879 879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
880 880 controller='changeset', action='changeset_info',
881 881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882 882
883 883 rmap.connect('compare_home',
884 884 '/{repo_name}/compare',
885 885 controller='compare', action='index',
886 886 conditions={'function': check_repo},
887 887 requirements=URL_NAME_REQUIREMENTS)
888 888
889 889 rmap.connect('compare_url',
890 890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
891 891 controller='compare', action='compare',
892 892 conditions={'function': check_repo},
893 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894 894
895 895 rmap.connect('pullrequest_home',
896 896 '/{repo_name}/pull-request/new', controller='pullrequests',
897 897 action='index', conditions={'function': check_repo,
898 898 'method': ['GET']},
899 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900 900
901 901 rmap.connect('pullrequest',
902 902 '/{repo_name}/pull-request/new', controller='pullrequests',
903 903 action='create', conditions={'function': check_repo,
904 904 'method': ['POST']},
905 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 906
907 907 rmap.connect('pullrequest_repo_refs',
908 908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
909 909 controller='pullrequests',
910 910 action='get_repo_refs',
911 911 conditions={'function': check_repo, 'method': ['GET']},
912 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913 913
914 914 rmap.connect('pullrequest_repo_destinations',
915 915 '/{repo_name}/pull-request/repo-destinations',
916 916 controller='pullrequests',
917 917 action='get_repo_destinations',
918 918 conditions={'function': check_repo, 'method': ['GET']},
919 919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
920 920
921 921 rmap.connect('pullrequest_show',
922 922 '/{repo_name}/pull-request/{pull_request_id}',
923 923 controller='pullrequests',
924 924 action='show', conditions={'function': check_repo,
925 925 'method': ['GET']},
926 926 requirements=URL_NAME_REQUIREMENTS)
927 927
928 928 rmap.connect('pullrequest_update',
929 929 '/{repo_name}/pull-request/{pull_request_id}',
930 930 controller='pullrequests',
931 931 action='update', conditions={'function': check_repo,
932 932 'method': ['PUT']},
933 933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
934 934
935 935 rmap.connect('pullrequest_merge',
936 936 '/{repo_name}/pull-request/{pull_request_id}',
937 937 controller='pullrequests',
938 938 action='merge', conditions={'function': check_repo,
939 939 'method': ['POST']},
940 940 requirements=URL_NAME_REQUIREMENTS)
941 941
942 942 rmap.connect('pullrequest_delete',
943 943 '/{repo_name}/pull-request/{pull_request_id}',
944 944 controller='pullrequests',
945 945 action='delete', conditions={'function': check_repo,
946 946 'method': ['DELETE']},
947 947 requirements=URL_NAME_REQUIREMENTS)
948 948
949 949 rmap.connect('pullrequest_show_all',
950 950 '/{repo_name}/pull-request',
951 951 controller='pullrequests',
952 952 action='show_all', conditions={'function': check_repo,
953 953 'method': ['GET']},
954 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955 955
956 956 rmap.connect('pullrequest_comment',
957 957 '/{repo_name}/pull-request-comment/{pull_request_id}',
958 958 controller='pullrequests',
959 959 action='comment', conditions={'function': check_repo,
960 960 'method': ['POST']},
961 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
962 962
963 963 rmap.connect('pullrequest_comment_delete',
964 964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
965 965 controller='pullrequests', action='delete_comment',
966 966 conditions={'function': check_repo, 'method': ['DELETE']},
967 967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
968 968
969 969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
970 970 controller='summary', conditions={'function': check_repo},
971 971 requirements=URL_NAME_REQUIREMENTS)
972 972
973 973 rmap.connect('branches_home', '/{repo_name}/branches',
974 974 controller='branches', conditions={'function': check_repo},
975 975 requirements=URL_NAME_REQUIREMENTS)
976 976
977 977 rmap.connect('tags_home', '/{repo_name}/tags',
978 978 controller='tags', conditions={'function': check_repo},
979 979 requirements=URL_NAME_REQUIREMENTS)
980 980
981 981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
982 982 controller='bookmarks', conditions={'function': check_repo},
983 983 requirements=URL_NAME_REQUIREMENTS)
984 984
985 985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
986 986 controller='changelog', conditions={'function': check_repo},
987 987 requirements=URL_NAME_REQUIREMENTS)
988 988
989 989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
990 990 controller='changelog', action='changelog_summary',
991 991 conditions={'function': check_repo},
992 992 requirements=URL_NAME_REQUIREMENTS)
993 993
994 994 rmap.connect('changelog_file_home',
995 995 '/{repo_name}/changelog/{revision}/{f_path}',
996 996 controller='changelog', f_path=None,
997 997 conditions={'function': check_repo},
998 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
999 999
1000 1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
1001 1001 controller='changelog', action='changelog_details',
1002 1002 conditions={'function': check_repo},
1003 1003 requirements=URL_NAME_REQUIREMENTS)
1004 1004
1005 1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1006 1006 controller='files', revision='tip', f_path='',
1007 1007 conditions={'function': check_repo},
1008 1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1009 1009
1010 1010 rmap.connect('files_home_simple_catchrev',
1011 1011 '/{repo_name}/files/{revision}',
1012 1012 controller='files', revision='tip', f_path='',
1013 1013 conditions={'function': check_repo},
1014 1014 requirements=URL_NAME_REQUIREMENTS)
1015 1015
1016 1016 rmap.connect('files_home_simple_catchall',
1017 1017 '/{repo_name}/files',
1018 1018 controller='files', revision='tip', f_path='',
1019 1019 conditions={'function': check_repo},
1020 1020 requirements=URL_NAME_REQUIREMENTS)
1021 1021
1022 1022 rmap.connect('files_history_home',
1023 1023 '/{repo_name}/history/{revision}/{f_path}',
1024 1024 controller='files', action='history', revision='tip', f_path='',
1025 1025 conditions={'function': check_repo},
1026 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1027 1027
1028 1028 rmap.connect('files_authors_home',
1029 1029 '/{repo_name}/authors/{revision}/{f_path}',
1030 1030 controller='files', action='authors', revision='tip', f_path='',
1031 1031 conditions={'function': check_repo},
1032 1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1033 1033
1034 1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1035 1035 controller='files', action='diff', f_path='',
1036 1036 conditions={'function': check_repo},
1037 1037 requirements=URL_NAME_REQUIREMENTS)
1038 1038
1039 1039 rmap.connect('files_diff_2way_home',
1040 1040 '/{repo_name}/diff-2way/{f_path}',
1041 1041 controller='files', action='diff_2way', f_path='',
1042 1042 conditions={'function': check_repo},
1043 1043 requirements=URL_NAME_REQUIREMENTS)
1044 1044
1045 1045 rmap.connect('files_rawfile_home',
1046 1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1047 1047 controller='files', action='rawfile', revision='tip',
1048 1048 f_path='', conditions={'function': check_repo},
1049 1049 requirements=URL_NAME_REQUIREMENTS)
1050 1050
1051 1051 rmap.connect('files_raw_home',
1052 1052 '/{repo_name}/raw/{revision}/{f_path}',
1053 1053 controller='files', action='raw', revision='tip', f_path='',
1054 1054 conditions={'function': check_repo},
1055 1055 requirements=URL_NAME_REQUIREMENTS)
1056 1056
1057 1057 rmap.connect('files_render_home',
1058 1058 '/{repo_name}/render/{revision}/{f_path}',
1059 1059 controller='files', action='index', revision='tip', f_path='',
1060 1060 rendered=True, conditions={'function': check_repo},
1061 1061 requirements=URL_NAME_REQUIREMENTS)
1062 1062
1063 1063 rmap.connect('files_annotate_home',
1064 1064 '/{repo_name}/annotate/{revision}/{f_path}',
1065 1065 controller='files', action='index', revision='tip',
1066 1066 f_path='', annotate=True, conditions={'function': check_repo},
1067 1067 requirements=URL_NAME_REQUIREMENTS)
1068 1068
1069 1069 rmap.connect('files_edit',
1070 1070 '/{repo_name}/edit/{revision}/{f_path}',
1071 1071 controller='files', action='edit', revision='tip',
1072 1072 f_path='',
1073 1073 conditions={'function': check_repo, 'method': ['POST']},
1074 1074 requirements=URL_NAME_REQUIREMENTS)
1075 1075
1076 1076 rmap.connect('files_edit_home',
1077 1077 '/{repo_name}/edit/{revision}/{f_path}',
1078 1078 controller='files', action='edit_home', revision='tip',
1079 1079 f_path='', conditions={'function': check_repo},
1080 1080 requirements=URL_NAME_REQUIREMENTS)
1081 1081
1082 1082 rmap.connect('files_add',
1083 1083 '/{repo_name}/add/{revision}/{f_path}',
1084 1084 controller='files', action='add', revision='tip',
1085 1085 f_path='',
1086 1086 conditions={'function': check_repo, 'method': ['POST']},
1087 1087 requirements=URL_NAME_REQUIREMENTS)
1088 1088
1089 1089 rmap.connect('files_add_home',
1090 1090 '/{repo_name}/add/{revision}/{f_path}',
1091 1091 controller='files', action='add_home', revision='tip',
1092 1092 f_path='', conditions={'function': check_repo},
1093 1093 requirements=URL_NAME_REQUIREMENTS)
1094 1094
1095 1095 rmap.connect('files_delete',
1096 1096 '/{repo_name}/delete/{revision}/{f_path}',
1097 1097 controller='files', action='delete', revision='tip',
1098 1098 f_path='',
1099 1099 conditions={'function': check_repo, 'method': ['POST']},
1100 1100 requirements=URL_NAME_REQUIREMENTS)
1101 1101
1102 1102 rmap.connect('files_delete_home',
1103 1103 '/{repo_name}/delete/{revision}/{f_path}',
1104 1104 controller='files', action='delete_home', revision='tip',
1105 1105 f_path='', conditions={'function': check_repo},
1106 1106 requirements=URL_NAME_REQUIREMENTS)
1107 1107
1108 1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1109 1109 controller='files', action='archivefile',
1110 1110 conditions={'function': check_repo},
1111 1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112 1112
1113 1113 rmap.connect('files_nodelist_home',
1114 1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1115 1115 controller='files', action='nodelist',
1116 1116 conditions={'function': check_repo},
1117 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118 1118
1119 1119 rmap.connect('files_nodetree_full',
1120 1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1121 1121 controller='files', action='nodetree_full',
1122 1122 conditions={'function': check_repo},
1123 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1124 1124
1125 1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1126 1126 controller='forks', action='fork_create',
1127 1127 conditions={'function': check_repo, 'method': ['POST']},
1128 1128 requirements=URL_NAME_REQUIREMENTS)
1129 1129
1130 1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1131 1131 controller='forks', action='fork',
1132 1132 conditions={'function': check_repo},
1133 1133 requirements=URL_NAME_REQUIREMENTS)
1134 1134
1135 1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1136 1136 controller='forks', action='forks',
1137 1137 conditions={'function': check_repo},
1138 1138 requirements=URL_NAME_REQUIREMENTS)
1139 1139
1140 1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1141 1141 controller='followers', action='followers',
1142 1142 conditions={'function': check_repo},
1143 1143 requirements=URL_NAME_REQUIREMENTS)
1144 1144
1145 1145 # must be here for proper group/repo catching pattern
1146 1146 _connect_with_slash(
1147 1147 rmap, 'repo_group_home', '/{group_name}',
1148 1148 controller='home', action='index_repo_group',
1149 1149 conditions={'function': check_group},
1150 1150 requirements=URL_NAME_REQUIREMENTS)
1151 1151
1152 1152 # catch all, at the end
1153 1153 _connect_with_slash(
1154 1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1155 1155 controller='summary', action='index',
1156 1156 conditions={'function': check_repo},
1157 1157 requirements=URL_NAME_REQUIREMENTS)
1158 1158
1159 1159 return rmap
1160 1160
1161 1161
1162 1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1163 1163 """
1164 1164 Connect a route with an optional trailing slash in `path`.
1165 1165 """
1166 1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1167 1167 mapper.connect(name, path, *args, **kwargs)
@@ -1,480 +1,487 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 User Groups crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 import peppercorn
28 29 from formencode import htmlfill
29 30 from pylons import request, tmpl_context as c, url, config
30 31 from pylons.controllers.util import redirect
31 32 from pylons.i18n.translation import _
32 33
33 34 from sqlalchemy.orm import joinedload
34 35
35 36 from rhodecode.lib import auth
36 37 from rhodecode.lib import helpers as h
37 38 from rhodecode.lib.exceptions import UserGroupAssignedException,\
38 39 RepoGroupAssignmentError
39 40 from rhodecode.lib.utils import jsonify, action_logger
40 41 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
41 42 from rhodecode.lib.auth import (
42 43 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
43 HasPermissionAnyDecorator)
44 HasPermissionAnyDecorator, XHRRequired)
44 45 from rhodecode.lib.base import BaseController, render
45 46 from rhodecode.model.permission import PermissionModel
46 47 from rhodecode.model.scm import UserGroupList
47 48 from rhodecode.model.user_group import UserGroupModel
48 49 from rhodecode.model.db import (
49 50 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
50 51 from rhodecode.model.forms import (
51 52 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
52 53 UserPermissionsForm)
53 54 from rhodecode.model.meta import Session
54 55 from rhodecode.lib.utils import action_logger
55 56 from rhodecode.lib.ext_json import json
56 57
57 58 log = logging.getLogger(__name__)
58 59
59 60
60 61 class UserGroupsController(BaseController):
61 62 """REST Controller styled on the Atom Publishing Protocol"""
62 63
63 64 @LoginRequired()
64 65 def __before__(self):
65 66 super(UserGroupsController, self).__before__()
66 67 c.available_permissions = config['available_permissions']
67 68 PermissionModel().set_global_permission_choices(c, translator=_)
68 69
69 70 def __load_data(self, user_group_id):
70 71 c.group_members_obj = [x.user for x in c.user_group.members]
71 72 c.group_members_obj.sort(key=lambda u: u.username.lower())
72
73 73 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
74 74
75 c.available_members = [(x.user_id, x.username)
76 for x in User.query().all()]
77 c.available_members.sort(key=lambda u: u[1].lower())
78
79 75 def __load_defaults(self, user_group_id):
80 76 """
81 77 Load defaults settings for edit, and update
82 78
83 79 :param user_group_id:
84 80 """
85 81 user_group = UserGroup.get_or_404(user_group_id)
86 82 data = user_group.get_dict()
87 83 # fill owner
88 84 if user_group.user:
89 85 data.update({'user': user_group.user.username})
90 86 else:
91 87 replacement_user = User.get_first_super_admin().username
92 88 data.update({'user': replacement_user})
93 89 return data
94 90
95 91 def _revoke_perms_on_yourself(self, form_result):
96 92 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
97 93 form_result['perm_updates'])
98 94 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
99 95 form_result['perm_additions'])
100 96 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
101 97 form_result['perm_deletions'])
102 98 admin_perm = 'usergroup.admin'
103 99 if _updates and _updates[0][1] != admin_perm or \
104 100 _additions and _additions[0][1] != admin_perm or \
105 101 _deletions and _deletions[0][1] != admin_perm:
106 102 return True
107 103 return False
108 104
109 105 # permission check inside
110 106 @NotAnonymous()
111 107 def index(self):
112 108 """GET /users_groups: All items in the collection"""
113 109 # url('users_groups')
114 110
115 111 from rhodecode.lib.utils import PartialRenderer
116 112 _render = PartialRenderer('data_table/_dt_elements.html')
117 113
118 114 def user_group_name(user_group_id, user_group_name):
119 115 return _render("user_group_name", user_group_id, user_group_name)
120 116
121 117 def user_group_actions(user_group_id, user_group_name):
122 118 return _render("user_group_actions", user_group_id, user_group_name)
123 119
124 120 ## json generate
125 121 group_iter = UserGroupList(UserGroup.query().all(),
126 122 perm_set=['usergroup.admin'])
127 123
128 124 user_groups_data = []
129 125 for user_gr in group_iter:
130 126 user_groups_data.append({
131 127 "group_name": user_group_name(
132 128 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
133 129 "group_name_raw": user_gr.users_group_name,
134 130 "desc": h.escape(user_gr.user_group_description),
135 131 "members": len(user_gr.members),
136 132 "active": h.bool2icon(user_gr.users_group_active),
137 133 "owner": h.escape(h.link_to_user(user_gr.user.username)),
138 134 "action": user_group_actions(
139 135 user_gr.users_group_id, user_gr.users_group_name)
140 136 })
141 137
142 138 c.data = json.dumps(user_groups_data)
143 139 return render('admin/user_groups/user_groups.html')
144 140
145 141 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
146 142 @auth.CSRFRequired()
147 143 def create(self):
148 144 """POST /users_groups: Create a new item"""
149 145 # url('users_groups')
150 146
151 147 users_group_form = UserGroupForm()()
152 148 try:
153 149 form_result = users_group_form.to_python(dict(request.POST))
154 150 user_group = UserGroupModel().create(
155 151 name=form_result['users_group_name'],
156 152 description=form_result['user_group_description'],
157 153 owner=c.rhodecode_user.user_id,
158 154 active=form_result['users_group_active'])
159 155 Session().flush()
160 156
161 157 user_group_name = form_result['users_group_name']
162 158 action_logger(c.rhodecode_user,
163 159 'admin_created_users_group:%s' % user_group_name,
164 160 None, self.ip_addr, self.sa)
165 161 user_group_link = h.link_to(h.escape(user_group_name),
166 162 url('edit_users_group',
167 163 user_group_id=user_group.users_group_id))
168 164 h.flash(h.literal(_('Created user group %(user_group_link)s')
169 165 % {'user_group_link': user_group_link}),
170 166 category='success')
171 167 Session().commit()
172 168 except formencode.Invalid as errors:
173 169 return htmlfill.render(
174 170 render('admin/user_groups/user_group_add.html'),
175 171 defaults=errors.value,
176 172 errors=errors.error_dict or {},
177 173 prefix_error=False,
178 174 encoding="UTF-8",
179 175 force_defaults=False)
180 176 except Exception:
181 177 log.exception("Exception creating user group")
182 178 h.flash(_('Error occurred during creation of user group %s') \
183 179 % request.POST.get('users_group_name'), category='error')
184 180
185 181 return redirect(
186 182 url('edit_users_group', user_group_id=user_group.users_group_id))
187 183
188 184 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
189 185 def new(self):
190 186 """GET /user_groups/new: Form to create a new item"""
191 187 # url('new_users_group')
192 188 return render('admin/user_groups/user_group_add.html')
193 189
194 190 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
195 191 @auth.CSRFRequired()
196 192 def update(self, user_group_id):
197 193 """PUT /user_groups/user_group_id: Update an existing item"""
198 194 # Forms posted to this method should contain a hidden field:
199 195 # <input type="hidden" name="_method" value="PUT" />
200 196 # Or using helpers:
201 197 # h.form(url('users_group', user_group_id=ID),
202 198 # method='put')
203 199 # url('users_group', user_group_id=ID)
204 200
205 201 user_group_id = safe_int(user_group_id)
206 202 c.user_group = UserGroup.get_or_404(user_group_id)
207 203 c.active = 'settings'
208 204 self.__load_data(user_group_id)
209 205
210 available_members = [safe_unicode(x[0]) for x in c.available_members]
211
212 206 users_group_form = UserGroupForm(
213 edit=True, old_data=c.user_group.get_dict(),
214 available_members=available_members, allow_disabled=True)()
207 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
215 208
216 209 try:
217 210 form_result = users_group_form.to_python(request.POST)
211 pstruct = peppercorn.parse(request.POST.items())
212 form_result['users_group_members'] = pstruct['user_group_members']
213
218 214 UserGroupModel().update(c.user_group, form_result)
219 gr = form_result['users_group_name']
215 updated_user_group = form_result['users_group_name']
220 216 action_logger(c.rhodecode_user,
221 'admin_updated_users_group:%s' % gr,
217 'admin_updated_users_group:%s' % updated_user_group,
222 218 None, self.ip_addr, self.sa)
223 h.flash(_('Updated user group %s') % gr, category='success')
219 h.flash(_('Updated user group %s') % updated_user_group,
220 category='success')
224 221 Session().commit()
225 222 except formencode.Invalid as errors:
226 223 defaults = errors.value
227 224 e = errors.error_dict or {}
228 225
229 226 return htmlfill.render(
230 227 render('admin/user_groups/user_group_edit.html'),
231 228 defaults=defaults,
232 229 errors=e,
233 230 prefix_error=False,
234 231 encoding="UTF-8",
235 232 force_defaults=False)
236 233 except Exception:
237 234 log.exception("Exception during update of user group")
238 235 h.flash(_('Error occurred during update of user group %s')
239 236 % request.POST.get('users_group_name'), category='error')
240 237
241 238 return redirect(url('edit_users_group', user_group_id=user_group_id))
242 239
243 240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
244 241 @auth.CSRFRequired()
245 242 def delete(self, user_group_id):
246 243 """DELETE /user_groups/user_group_id: Delete an existing item"""
247 244 # Forms posted to this method should contain a hidden field:
248 245 # <input type="hidden" name="_method" value="DELETE" />
249 246 # Or using helpers:
250 247 # h.form(url('users_group', user_group_id=ID),
251 248 # method='delete')
252 249 # url('users_group', user_group_id=ID)
253 250 user_group_id = safe_int(user_group_id)
254 251 c.user_group = UserGroup.get_or_404(user_group_id)
255 252 force = str2bool(request.POST.get('force'))
256 253
257 254 try:
258 255 UserGroupModel().delete(c.user_group, force=force)
259 256 Session().commit()
260 257 h.flash(_('Successfully deleted user group'), category='success')
261 258 except UserGroupAssignedException as e:
262 259 h.flash(str(e), category='error')
263 260 except Exception:
264 261 log.exception("Exception during deletion of user group")
265 262 h.flash(_('An error occurred during deletion of user group'),
266 263 category='error')
267 264 return redirect(url('users_groups'))
268 265
269 266 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
270 267 def edit(self, user_group_id):
271 268 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
272 269 # url('edit_users_group', user_group_id=ID)
273 270
274 271 user_group_id = safe_int(user_group_id)
275 272 c.user_group = UserGroup.get_or_404(user_group_id)
276 273 c.active = 'settings'
277 274 self.__load_data(user_group_id)
278 275
279 276 defaults = self.__load_defaults(user_group_id)
280 277
281 278 return htmlfill.render(
282 279 render('admin/user_groups/user_group_edit.html'),
283 280 defaults=defaults,
284 281 encoding="UTF-8",
285 282 force_defaults=False
286 283 )
287 284
288 285 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
289 286 def edit_perms(self, user_group_id):
290 287 user_group_id = safe_int(user_group_id)
291 288 c.user_group = UserGroup.get_or_404(user_group_id)
292 289 c.active = 'perms'
293 290
294 291 defaults = {}
295 292 # fill user group users
296 293 for p in c.user_group.user_user_group_to_perm:
297 294 defaults.update({'u_perm_%s' % p.user.user_id:
298 295 p.permission.permission_name})
299 296
300 297 for p in c.user_group.user_group_user_group_to_perm:
301 298 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
302 299 p.permission.permission_name})
303 300
304 301 return htmlfill.render(
305 302 render('admin/user_groups/user_group_edit.html'),
306 303 defaults=defaults,
307 304 encoding="UTF-8",
308 305 force_defaults=False
309 306 )
310 307
311 308 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
312 309 @auth.CSRFRequired()
313 310 def update_perms(self, user_group_id):
314 311 """
315 312 grant permission for given usergroup
316 313
317 314 :param user_group_id:
318 315 """
319 316 user_group_id = safe_int(user_group_id)
320 317 c.user_group = UserGroup.get_or_404(user_group_id)
321 318 form = UserGroupPermsForm()().to_python(request.POST)
322 319
323 320 if not c.rhodecode_user.is_admin:
324 321 if self._revoke_perms_on_yourself(form):
325 322 msg = _('Cannot change permission for yourself as admin')
326 323 h.flash(msg, category='warning')
327 324 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
328 325
329 326 try:
330 327 UserGroupModel().update_permissions(user_group_id,
331 328 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
332 329 except RepoGroupAssignmentError:
333 330 h.flash(_('Target group cannot be the same'), category='error')
334 331 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
335 332 #TODO: implement this
336 333 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
337 334 # repo_name, self.ip_addr, self.sa)
338 335 Session().commit()
339 336 h.flash(_('User Group permissions updated'), category='success')
340 337 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
341 338
342 339 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
343 340 def edit_perms_summary(self, user_group_id):
344 341 user_group_id = safe_int(user_group_id)
345 342 c.user_group = UserGroup.get_or_404(user_group_id)
346 343 c.active = 'perms_summary'
347 344 permissions = {
348 345 'repositories': {},
349 346 'repositories_groups': {},
350 347 }
351 348 ugroup_repo_perms = UserGroupRepoToPerm.query()\
352 349 .options(joinedload(UserGroupRepoToPerm.permission))\
353 350 .options(joinedload(UserGroupRepoToPerm.repository))\
354 351 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
355 352 .all()
356 353
357 354 for gr in ugroup_repo_perms:
358 355 permissions['repositories'][gr.repository.repo_name] \
359 356 = gr.permission.permission_name
360 357
361 358 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
362 359 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
363 360 .options(joinedload(UserGroupRepoGroupToPerm.group))\
364 361 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
365 362 .all()
366 363
367 364 for gr in ugroup_group_perms:
368 365 permissions['repositories_groups'][gr.group.group_name] \
369 366 = gr.permission.permission_name
370 367 c.permissions = permissions
371 368 return render('admin/user_groups/user_group_edit.html')
372 369
373 370 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
374 371 def edit_global_perms(self, user_group_id):
375 372 user_group_id = safe_int(user_group_id)
376 373 c.user_group = UserGroup.get_or_404(user_group_id)
377 374 c.active = 'global_perms'
378 375
379 376 c.default_user = User.get_default_user()
380 377 defaults = c.user_group.get_dict()
381 378 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
382 379 defaults.update(c.user_group.get_default_perms())
383 380
384 381 return htmlfill.render(
385 382 render('admin/user_groups/user_group_edit.html'),
386 383 defaults=defaults,
387 384 encoding="UTF-8",
388 385 force_defaults=False
389 386 )
390 387
391 388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
392 389 @auth.CSRFRequired()
393 390 def update_global_perms(self, user_group_id):
394 391 """PUT /users_perm/user_group_id: Update an existing item"""
395 392 # url('users_group_perm', user_group_id=ID, method='put')
396 393 user_group_id = safe_int(user_group_id)
397 394 user_group = UserGroup.get_or_404(user_group_id)
398 395 c.active = 'global_perms'
399 396
400 397 try:
401 398 # first stage that verifies the checkbox
402 399 _form = UserIndividualPermissionsForm()
403 400 form_result = _form.to_python(dict(request.POST))
404 401 inherit_perms = form_result['inherit_default_permissions']
405 402 user_group.inherit_default_permissions = inherit_perms
406 403 Session().add(user_group)
407 404
408 405 if not inherit_perms:
409 406 # only update the individual ones if we un check the flag
410 407 _form = UserPermissionsForm(
411 408 [x[0] for x in c.repo_create_choices],
412 409 [x[0] for x in c.repo_create_on_write_choices],
413 410 [x[0] for x in c.repo_group_create_choices],
414 411 [x[0] for x in c.user_group_create_choices],
415 412 [x[0] for x in c.fork_choices],
416 413 [x[0] for x in c.inherit_default_permission_choices])()
417 414
418 415 form_result = _form.to_python(dict(request.POST))
419 416 form_result.update({'perm_user_group_id': user_group.users_group_id})
420 417
421 418 PermissionModel().update_user_group_permissions(form_result)
422 419
423 420 Session().commit()
424 421 h.flash(_('User Group global permissions updated successfully'),
425 422 category='success')
426 423
427 424 except formencode.Invalid as errors:
428 425 defaults = errors.value
429 426 c.user_group = user_group
430 427 return htmlfill.render(
431 428 render('admin/user_groups/user_group_edit.html'),
432 429 defaults=defaults,
433 430 errors=errors.error_dict or {},
434 431 prefix_error=False,
435 432 encoding="UTF-8",
436 433 force_defaults=False)
437 434
438 435 except Exception:
439 436 log.exception("Exception during permissions saving")
440 437 h.flash(_('An error occurred during permissions saving'),
441 438 category='error')
442 439
443 440 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
444 441
445 442 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
446 443 def edit_advanced(self, user_group_id):
447 444 user_group_id = safe_int(user_group_id)
448 445 c.user_group = UserGroup.get_or_404(user_group_id)
449 446 c.active = 'advanced'
450 447 c.group_members_obj = sorted(
451 448 (x.user for x in c.user_group.members),
452 449 key=lambda u: u.username.lower())
453 450
454 451 c.group_to_repos = sorted(
455 452 (x.repository for x in c.user_group.users_group_repo_to_perm),
456 453 key=lambda u: u.repo_name.lower())
457 454
458 455 c.group_to_repo_groups = sorted(
459 456 (x.group for x in c.user_group.users_group_repo_group_to_perm),
460 457 key=lambda u: u.group_name.lower())
461 458
462 459 return render('admin/user_groups/user_group_edit.html')
463 460
464 461 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
465 def edit_members(self, user_group_id):
462 @XHRRequired()
463 @jsonify
464 def user_group_members(self, user_group_id):
466 465 user_group_id = safe_int(user_group_id)
467 c.user_group = UserGroup.get_or_404(user_group_id)
468 c.active = 'members'
469 c.group_members_obj = sorted((x.user for x in c.user_group.members),
466 user_group = UserGroup.get_or_404(user_group_id)
467 group_members_obj = sorted((x.user for x in user_group.members),
470 468 key=lambda u: u.username.lower())
471 469
472 group_members = [(x.user_id, x.username) for x in c.group_members_obj]
470 group_members = [
471 {
472 'id': user.user_id,
473 'first_name': user.name,
474 'last_name': user.lastname,
475 'username': user.username,
476 'icon_link': h.gravatar_url(user.email, 30),
477 'value_display': h.person(user.email),
478 'value': user.username,
479 'value_type': 'user',
480 'active': user.active,
481 }
482 for user in group_members_obj
483 ]
473 484
474 if request.is_xhr:
475 return jsonify(lambda *a, **k: {
485 return {
476 486 'members': group_members
477 })
478
479 c.group_members = group_members
480 return render('admin/user_groups/user_group_edit.html')
487 }
@@ -1,553 +1,547 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 105 def UserForm(edit=False, available_languages=[], old_data={}):
106 106 class _UserForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 110 v.ValidUsername(edit, old_data))
111 111 if edit:
112 112 new_password = All(
113 113 v.ValidPassword(),
114 114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 115 )
116 116 password_confirmation = All(
117 117 v.ValidPassword(),
118 118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 119 )
120 120 admin = v.StringBoolean(if_missing=False)
121 121 else:
122 122 password = All(
123 123 v.ValidPassword(),
124 124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 125 )
126 126 password_confirmation = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130
131 131 password_change = v.StringBoolean(if_missing=False)
132 132 create_repo_group = v.StringBoolean(if_missing=False)
133 133
134 134 active = v.StringBoolean(if_missing=False)
135 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 138 extern_name = v.UnicodeString(strip=True)
139 139 extern_type = v.UnicodeString(strip=True)
140 140 language = v.OneOf(available_languages, hideList=False,
141 141 testValueList=True, if_missing=None)
142 142 chained_validators = [v.ValidPasswordsMatch()]
143 143 return _UserForm
144 144
145 145
146 def UserGroupForm(edit=False, old_data=None, available_members=None,
147 allow_disabled=False):
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
148 147 old_data = old_data or {}
149 available_members = available_members or []
150 148
151 149 class _UserGroupForm(formencode.Schema):
152 150 allow_extra_fields = True
153 151 filter_extra_fields = True
154 152
155 153 users_group_name = All(
156 154 v.UnicodeString(strip=True, min=1, not_empty=True),
157 155 v.ValidUserGroup(edit, old_data)
158 156 )
159 157 user_group_description = v.UnicodeString(strip=True, min=1,
160 158 not_empty=False)
161 159
162 160 users_group_active = v.StringBoolean(if_missing=False)
163 161
164 162 if edit:
165 users_group_members = v.OneOf(
166 available_members, hideList=False, testValueList=True,
167 if_missing=None, not_empty=False
168 )
169 163 # this is user group owner
170 164 user = All(
171 165 v.UnicodeString(not_empty=True),
172 166 v.ValidRepoUser(allow_disabled))
173 167 return _UserGroupForm
174 168
175 169
176 170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
177 171 can_create_in_root=False, allow_disabled=False):
178 172 old_data = old_data or {}
179 173 available_groups = available_groups or []
180 174
181 175 class _RepoGroupForm(formencode.Schema):
182 176 allow_extra_fields = True
183 177 filter_extra_fields = False
184 178
185 179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
186 180 v.SlugifyName(),)
187 181 group_description = v.UnicodeString(strip=True, min=1,
188 182 not_empty=False)
189 183 group_copy_permissions = v.StringBoolean(if_missing=False)
190 184
191 185 group_parent_id = v.OneOf(available_groups, hideList=False,
192 186 testValueList=True, not_empty=True)
193 187 enable_locking = v.StringBoolean(if_missing=False)
194 188 chained_validators = [
195 189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
196 190
197 191 if edit:
198 192 # this is repo group owner
199 193 user = All(
200 194 v.UnicodeString(not_empty=True),
201 195 v.ValidRepoUser(allow_disabled))
202 196
203 197 return _RepoGroupForm
204 198
205 199
206 200 def RegisterForm(edit=False, old_data={}):
207 201 class _RegisterForm(formencode.Schema):
208 202 allow_extra_fields = True
209 203 filter_extra_fields = True
210 204 username = All(
211 205 v.ValidUsername(edit, old_data),
212 206 v.UnicodeString(strip=True, min=1, not_empty=True)
213 207 )
214 208 password = All(
215 209 v.ValidPassword(),
216 210 v.UnicodeString(strip=False, min=6, not_empty=True)
217 211 )
218 212 password_confirmation = All(
219 213 v.ValidPassword(),
220 214 v.UnicodeString(strip=False, min=6, not_empty=True)
221 215 )
222 216 active = v.StringBoolean(if_missing=False)
223 217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
224 218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
225 219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
226 220
227 221 chained_validators = [v.ValidPasswordsMatch()]
228 222
229 223 return _RegisterForm
230 224
231 225
232 226 def PasswordResetForm():
233 227 class _PasswordResetForm(formencode.Schema):
234 228 allow_extra_fields = True
235 229 filter_extra_fields = True
236 230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
237 231 return _PasswordResetForm
238 232
239 233
240 234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
241 235 allow_disabled=False):
242 236 old_data = old_data or {}
243 237 repo_groups = repo_groups or []
244 238 landing_revs = landing_revs or []
245 239 supported_backends = BACKENDS.keys()
246 240
247 241 class _RepoForm(formencode.Schema):
248 242 allow_extra_fields = True
249 243 filter_extra_fields = False
250 244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
251 245 v.SlugifyName())
252 246 repo_group = All(v.CanWriteGroup(old_data),
253 247 v.OneOf(repo_groups, hideList=True))
254 248 repo_type = v.OneOf(supported_backends, required=False,
255 249 if_missing=old_data.get('repo_type'))
256 250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
257 251 repo_private = v.StringBoolean(if_missing=False)
258 252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
259 253 repo_copy_permissions = v.StringBoolean(if_missing=False)
260 254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
261 255
262 256 repo_enable_statistics = v.StringBoolean(if_missing=False)
263 257 repo_enable_downloads = v.StringBoolean(if_missing=False)
264 258 repo_enable_locking = v.StringBoolean(if_missing=False)
265 259
266 260 if edit:
267 261 # this is repo owner
268 262 user = All(
269 263 v.UnicodeString(not_empty=True),
270 264 v.ValidRepoUser(allow_disabled))
271 265 clone_uri_change = v.UnicodeString(
272 266 not_empty=False, if_missing=v.Missing)
273 267
274 268 chained_validators = [v.ValidCloneUri(),
275 269 v.ValidRepoName(edit, old_data)]
276 270 return _RepoForm
277 271
278 272
279 273 def RepoPermsForm():
280 274 class _RepoPermsForm(formencode.Schema):
281 275 allow_extra_fields = True
282 276 filter_extra_fields = False
283 277 chained_validators = [v.ValidPerms(type_='repo')]
284 278 return _RepoPermsForm
285 279
286 280
287 281 def RepoGroupPermsForm(valid_recursive_choices):
288 282 class _RepoGroupPermsForm(formencode.Schema):
289 283 allow_extra_fields = True
290 284 filter_extra_fields = False
291 285 recursive = v.OneOf(valid_recursive_choices)
292 286 chained_validators = [v.ValidPerms(type_='repo_group')]
293 287 return _RepoGroupPermsForm
294 288
295 289
296 290 def UserGroupPermsForm():
297 291 class _UserPermsForm(formencode.Schema):
298 292 allow_extra_fields = True
299 293 filter_extra_fields = False
300 294 chained_validators = [v.ValidPerms(type_='user_group')]
301 295 return _UserPermsForm
302 296
303 297
304 298 def RepoFieldForm():
305 299 class _RepoFieldForm(formencode.Schema):
306 300 filter_extra_fields = True
307 301 allow_extra_fields = True
308 302
309 303 new_field_key = All(v.FieldKey(),
310 304 v.UnicodeString(strip=True, min=3, not_empty=True))
311 305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
312 306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
313 307 if_missing='str')
314 308 new_field_label = v.UnicodeString(not_empty=False)
315 309 new_field_desc = v.UnicodeString(not_empty=False)
316 310
317 311 return _RepoFieldForm
318 312
319 313
320 314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
321 315 repo_groups=[], landing_revs=[]):
322 316 class _RepoForkForm(formencode.Schema):
323 317 allow_extra_fields = True
324 318 filter_extra_fields = False
325 319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
326 320 v.SlugifyName())
327 321 repo_group = All(v.CanWriteGroup(),
328 322 v.OneOf(repo_groups, hideList=True))
329 323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
330 324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
331 325 private = v.StringBoolean(if_missing=False)
332 326 copy_permissions = v.StringBoolean(if_missing=False)
333 327 fork_parent_id = v.UnicodeString()
334 328 chained_validators = [v.ValidForkName(edit, old_data)]
335 329 landing_rev = v.OneOf(landing_revs, hideList=True)
336 330
337 331 return _RepoForkForm
338 332
339 333
340 334 def ApplicationSettingsForm():
341 335 class _ApplicationSettingsForm(formencode.Schema):
342 336 allow_extra_fields = True
343 337 filter_extra_fields = False
344 338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
345 339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
346 340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
347 341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
348 342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
349 343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
350 344
351 345 return _ApplicationSettingsForm
352 346
353 347
354 348 def ApplicationVisualisationForm():
355 349 class _ApplicationVisualisationForm(formencode.Schema):
356 350 allow_extra_fields = True
357 351 filter_extra_fields = False
358 352 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
359 353 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
360 354 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
361 355
362 356 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
363 357 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
364 358 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
365 359 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
366 360 rhodecode_show_version = v.StringBoolean(if_missing=False)
367 361 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
368 362 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
369 363 rhodecode_gravatar_url = v.UnicodeString(min=3)
370 364 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
371 365 rhodecode_support_url = v.UnicodeString()
372 366 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
373 367 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
374 368
375 369 return _ApplicationVisualisationForm
376 370
377 371
378 372 class _BaseVcsSettingsForm(formencode.Schema):
379 373 allow_extra_fields = True
380 374 filter_extra_fields = False
381 375 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
382 376 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
383 377 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
384 378
385 379 extensions_largefiles = v.StringBoolean(if_missing=False)
386 380 phases_publish = v.StringBoolean(if_missing=False)
387 381
388 382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
389 383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
390 384 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
391 385
392 386 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
393 387 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
394 388
395 389
396 390 def ApplicationUiSettingsForm():
397 391 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
398 392 web_push_ssl = v.StringBoolean(if_missing=False)
399 393 paths_root_path = All(
400 394 v.ValidPath(),
401 395 v.UnicodeString(strip=True, min=1, not_empty=True)
402 396 )
403 397 extensions_hgsubversion = v.StringBoolean(if_missing=False)
404 398 extensions_hggit = v.StringBoolean(if_missing=False)
405 399 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
406 400 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
407 401
408 402 return _ApplicationUiSettingsForm
409 403
410 404
411 405 def RepoVcsSettingsForm(repo_name):
412 406 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
413 407 inherit_global_settings = v.StringBoolean(if_missing=False)
414 408 new_svn_branch = v.ValidSvnPattern(
415 409 section='vcs_svn_branch', repo_name=repo_name)
416 410 new_svn_tag = v.ValidSvnPattern(
417 411 section='vcs_svn_tag', repo_name=repo_name)
418 412
419 413 return _RepoVcsSettingsForm
420 414
421 415
422 416 def LabsSettingsForm():
423 417 class _LabSettingsForm(formencode.Schema):
424 418 allow_extra_fields = True
425 419 filter_extra_fields = False
426 420
427 421 return _LabSettingsForm
428 422
429 423
430 424 def ApplicationPermissionsForm(
431 425 register_choices, password_reset_choices, extern_activate_choices):
432 426 class _DefaultPermissionsForm(formencode.Schema):
433 427 allow_extra_fields = True
434 428 filter_extra_fields = True
435 429
436 430 anonymous = v.StringBoolean(if_missing=False)
437 431 default_register = v.OneOf(register_choices)
438 432 default_register_message = v.UnicodeString()
439 433 default_password_reset = v.OneOf(password_reset_choices)
440 434 default_extern_activate = v.OneOf(extern_activate_choices)
441 435
442 436 return _DefaultPermissionsForm
443 437
444 438
445 439 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
446 440 user_group_perms_choices):
447 441 class _ObjectPermissionsForm(formencode.Schema):
448 442 allow_extra_fields = True
449 443 filter_extra_fields = True
450 444 overwrite_default_repo = v.StringBoolean(if_missing=False)
451 445 overwrite_default_group = v.StringBoolean(if_missing=False)
452 446 overwrite_default_user_group = v.StringBoolean(if_missing=False)
453 447 default_repo_perm = v.OneOf(repo_perms_choices)
454 448 default_group_perm = v.OneOf(group_perms_choices)
455 449 default_user_group_perm = v.OneOf(user_group_perms_choices)
456 450
457 451 return _ObjectPermissionsForm
458 452
459 453
460 454 def UserPermissionsForm(create_choices, create_on_write_choices,
461 455 repo_group_create_choices, user_group_create_choices,
462 456 fork_choices, inherit_default_permissions_choices):
463 457 class _DefaultPermissionsForm(formencode.Schema):
464 458 allow_extra_fields = True
465 459 filter_extra_fields = True
466 460
467 461 anonymous = v.StringBoolean(if_missing=False)
468 462
469 463 default_repo_create = v.OneOf(create_choices)
470 464 default_repo_create_on_write = v.OneOf(create_on_write_choices)
471 465 default_user_group_create = v.OneOf(user_group_create_choices)
472 466 default_repo_group_create = v.OneOf(repo_group_create_choices)
473 467 default_fork_create = v.OneOf(fork_choices)
474 468 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
475 469
476 470 return _DefaultPermissionsForm
477 471
478 472
479 473 def UserIndividualPermissionsForm():
480 474 class _DefaultPermissionsForm(formencode.Schema):
481 475 allow_extra_fields = True
482 476 filter_extra_fields = True
483 477
484 478 inherit_default_permissions = v.StringBoolean(if_missing=False)
485 479
486 480 return _DefaultPermissionsForm
487 481
488 482
489 483 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
490 484 class _DefaultsForm(formencode.Schema):
491 485 allow_extra_fields = True
492 486 filter_extra_fields = True
493 487 default_repo_type = v.OneOf(supported_backends)
494 488 default_repo_private = v.StringBoolean(if_missing=False)
495 489 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
496 490 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
497 491 default_repo_enable_locking = v.StringBoolean(if_missing=False)
498 492
499 493 return _DefaultsForm
500 494
501 495
502 496 def AuthSettingsForm():
503 497 class _AuthSettingsForm(formencode.Schema):
504 498 allow_extra_fields = True
505 499 filter_extra_fields = True
506 500 auth_plugins = All(v.ValidAuthPlugins(),
507 501 v.UniqueListFromString()(not_empty=True))
508 502
509 503 return _AuthSettingsForm
510 504
511 505
512 506 def UserExtraEmailForm():
513 507 class _UserExtraEmailForm(formencode.Schema):
514 508 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
515 509 return _UserExtraEmailForm
516 510
517 511
518 512 def UserExtraIpForm():
519 513 class _UserExtraIpForm(formencode.Schema):
520 514 ip = v.ValidIp()(not_empty=True)
521 515 return _UserExtraIpForm
522 516
523 517
524 518
525 519 def PullRequestForm(repo_id):
526 520 class ReviewerForm(formencode.Schema):
527 521 user_id = v.Int(not_empty=True)
528 522 reasons = All()
529 523
530 524 class _PullRequestForm(formencode.Schema):
531 525 allow_extra_fields = True
532 526 filter_extra_fields = True
533 527
534 528 user = v.UnicodeString(strip=True, required=True)
535 529 source_repo = v.UnicodeString(strip=True, required=True)
536 530 source_ref = v.UnicodeString(strip=True, required=True)
537 531 target_repo = v.UnicodeString(strip=True, required=True)
538 532 target_ref = v.UnicodeString(strip=True, required=True)
539 533 revisions = All(#v.NotReviewedRevisions(repo_id)(),
540 534 v.UniqueList()(not_empty=True))
541 535 review_members = formencode.ForEach(ReviewerForm())
542 536 pullrequest_title = v.UnicodeString(strip=True, required=True)
543 537 pullrequest_desc = v.UnicodeString(strip=True, required=False)
544 538
545 539 return _PullRequestForm
546 540
547 541
548 542 def IssueTrackerPatternsForm():
549 543 class _IssueTrackerPatternsForm(formencode.Schema):
550 544 allow_extra_fields = True
551 545 filter_extra_fields = False
552 546 chained_validators = [v.ValidPattern()]
553 547 return _IssueTrackerPatternsForm
@@ -1,517 +1,514 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 user group model for RhodeCode
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.model import BaseModel
32 32 from rhodecode.model.db import UserGroupMember, UserGroup,\
33 33 UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm,\
34 34 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm
35 35 from rhodecode.lib.exceptions import UserGroupAssignedException,\
36 36 RepoGroupAssignmentError
37 37 from rhodecode.lib.utils2 import get_current_rhodecode_user, action_logger_generic
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class UserGroupModel(BaseModel):
43 43
44 44 cls = UserGroup
45 45
46 46 def _get_user_group(self, user_group):
47 47 return self._get_instance(UserGroup, user_group,
48 48 callback=UserGroup.get_by_group_name)
49 49
50 50 def _create_default_perms(self, user_group):
51 51 # create default permission
52 52 default_perm = 'usergroup.read'
53 53 def_user = User.get_default_user()
54 54 for p in def_user.user_perms:
55 55 if p.permission.permission_name.startswith('usergroup.'):
56 56 default_perm = p.permission.permission_name
57 57 break
58 58
59 59 user_group_to_perm = UserUserGroupToPerm()
60 60 user_group_to_perm.permission = Permission.get_by_key(default_perm)
61 61
62 62 user_group_to_perm.user_group = user_group
63 63 user_group_to_perm.user_id = def_user.user_id
64 64 return user_group_to_perm
65 65
66 66 def update_permissions(self, user_group, perm_additions=None, perm_updates=None,
67 67 perm_deletions=None, check_perms=True, cur_user=None):
68 68 from rhodecode.lib.auth import HasUserGroupPermissionAny
69 69 if not perm_additions:
70 70 perm_additions = []
71 71 if not perm_updates:
72 72 perm_updates = []
73 73 if not perm_deletions:
74 74 perm_deletions = []
75 75
76 76 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
77 77
78 78 # update permissions
79 79 for member_id, perm, member_type in perm_updates:
80 80 member_id = int(member_id)
81 81 if member_type == 'user':
82 82 # this updates existing one
83 83 self.grant_user_permission(
84 84 user_group=user_group, user=member_id, perm=perm
85 85 )
86 86 else:
87 87 # check if we have permissions to alter this usergroup
88 88 member_name = UserGroup.get(member_id).users_group_name
89 89 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
90 90 self.grant_user_group_permission(
91 91 target_user_group=user_group, user_group=member_id, perm=perm
92 92 )
93 93
94 94 # set new permissions
95 95 for member_id, perm, member_type in perm_additions:
96 96 member_id = int(member_id)
97 97 if member_type == 'user':
98 98 self.grant_user_permission(
99 99 user_group=user_group, user=member_id, perm=perm
100 100 )
101 101 else:
102 102 # check if we have permissions to alter this usergroup
103 103 member_name = UserGroup.get(member_id).users_group_name
104 104 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
105 105 self.grant_user_group_permission(
106 106 target_user_group=user_group, user_group=member_id, perm=perm
107 107 )
108 108
109 109 # delete permissions
110 110 for member_id, perm, member_type in perm_deletions:
111 111 member_id = int(member_id)
112 112 if member_type == 'user':
113 113 self.revoke_user_permission(user_group=user_group, user=member_id)
114 114 else:
115 115 #check if we have permissions to alter this usergroup
116 116 member_name = UserGroup.get(member_id).users_group_name
117 117 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
118 118 self.revoke_user_group_permission(
119 119 target_user_group=user_group, user_group=member_id
120 120 )
121 121
122 122 def get(self, user_group_id, cache=False):
123 123 return UserGroup.get(user_group_id)
124 124
125 125 def get_group(self, user_group):
126 126 return self._get_user_group(user_group)
127 127
128 128 def get_by_name(self, name, cache=False, case_insensitive=False):
129 129 return UserGroup.get_by_group_name(name, cache, case_insensitive)
130 130
131 131 def create(self, name, description, owner, active=True, group_data=None):
132 132 try:
133 133 new_user_group = UserGroup()
134 134 new_user_group.user = self._get_user(owner)
135 135 new_user_group.users_group_name = name
136 136 new_user_group.user_group_description = description
137 137 new_user_group.users_group_active = active
138 138 if group_data:
139 139 new_user_group.group_data = group_data
140 140 self.sa.add(new_user_group)
141 141 perm_obj = self._create_default_perms(new_user_group)
142 142 self.sa.add(perm_obj)
143 143
144 144 self.grant_user_permission(user_group=new_user_group,
145 145 user=owner, perm='usergroup.admin')
146 146
147 147 return new_user_group
148 148 except Exception:
149 149 log.error(traceback.format_exc())
150 150 raise
151 151
152 152 def _get_memberships_for_user_ids(self, user_group, user_id_list):
153 153 members = []
154 154 for user_id in user_id_list:
155 155 member = self._get_membership(user_group.users_group_id, user_id)
156 156 members.append(member)
157 157 return members
158 158
159 159 def _get_added_and_removed_user_ids(self, user_group, user_id_list):
160 160 current_members = user_group.members or []
161 161 current_members_ids = [m.user.user_id for m in current_members]
162 162
163 163 added_members = [
164 164 user_id for user_id in user_id_list
165 165 if user_id not in current_members_ids]
166 166 if user_id_list == []:
167 167 # all members were deleted
168 168 deleted_members = current_members_ids
169 169 else:
170 170 deleted_members = [
171 171 user_id for user_id in current_members_ids
172 172 if user_id not in user_id_list]
173 173
174 174 return (added_members, deleted_members)
175 175
176 176 def _set_users_as_members(self, user_group, user_ids):
177 177 user_group.members = []
178 178 self.sa.flush()
179 179 members = self._get_memberships_for_user_ids(
180 180 user_group, user_ids)
181 181 user_group.members = members
182 182 self.sa.add(user_group)
183 183
184 184 def _update_members_from_user_ids(self, user_group, user_ids):
185 185 added, removed = self._get_added_and_removed_user_ids(
186 186 user_group, user_ids)
187 187 self._set_users_as_members(user_group, user_ids)
188 188 self._log_user_changes('added to', user_group, added)
189 189 self._log_user_changes('removed from', user_group, removed)
190 190
191 191 def _clean_members_data(self, members_data):
192 # TODO: anderson: this should be in the form validation but I couldn't
193 # make it work there as it conflicts with the other validator
194 192 if not members_data:
195 193 members_data = []
196 194
197 if isinstance(members_data, basestring):
198 new_members = [members_data]
199 else:
200 new_members = members_data
201
202 new_members = [int(uid) for uid in new_members]
203 return new_members
195 members = []
196 for user in members_data:
197 uid = int(user['member_user_id'])
198 if uid not in members and user['type'] in ['new', 'existing']:
199 members.append(uid)
200 return members
204 201
205 202 def update(self, user_group, form_data):
206 203 user_group = self._get_user_group(user_group)
207 204 if 'users_group_name' in form_data:
208 205 user_group.users_group_name = form_data['users_group_name']
209 206 if 'users_group_active' in form_data:
210 207 user_group.users_group_active = form_data['users_group_active']
211 208 if 'user_group_description' in form_data:
212 209 user_group.user_group_description = form_data[
213 210 'user_group_description']
214 211
215 212 # handle owner change
216 213 if 'user' in form_data:
217 214 owner = form_data['user']
218 215 if isinstance(owner, basestring):
219 216 owner = User.get_by_username(form_data['user'])
220 217
221 218 if not isinstance(owner, User):
222 219 raise ValueError(
223 220 'invalid owner for user group: %s' % form_data['user'])
224 221
225 222 user_group.user = owner
226 223
227 224 if 'users_group_members' in form_data:
228 225 members_id_list = self._clean_members_data(
229 226 form_data['users_group_members'])
230 227 self._update_members_from_user_ids(user_group, members_id_list)
231 228
232 229 self.sa.add(user_group)
233 230
234 231 def delete(self, user_group, force=False):
235 232 """
236 233 Deletes repository group, unless force flag is used
237 234 raises exception if there are members in that group, else deletes
238 235 group and users
239 236
240 237 :param user_group:
241 238 :param force:
242 239 """
243 240 user_group = self._get_user_group(user_group)
244 241 try:
245 242 # check if this group is not assigned to repo
246 243 assigned_to_repo = [x.repository for x in UserGroupRepoToPerm.query()\
247 244 .filter(UserGroupRepoToPerm.users_group == user_group).all()]
248 245 # check if this group is not assigned to repo
249 246 assigned_to_repo_group = [x.group for x in UserGroupRepoGroupToPerm.query()\
250 247 .filter(UserGroupRepoGroupToPerm.users_group == user_group).all()]
251 248
252 249 if (assigned_to_repo or assigned_to_repo_group) and not force:
253 250 assigned = ','.join(map(safe_str,
254 251 assigned_to_repo+assigned_to_repo_group))
255 252
256 253 raise UserGroupAssignedException(
257 254 'UserGroup assigned to %s' % (assigned,))
258 255 self.sa.delete(user_group)
259 256 except Exception:
260 257 log.error(traceback.format_exc())
261 258 raise
262 259
263 260 def _log_user_changes(self, action, user_group, user_or_users):
264 261 users = user_or_users
265 262 if not isinstance(users, (list, tuple)):
266 263 users = [users]
267 264 rhodecode_user = get_current_rhodecode_user()
268 265 ipaddr = getattr(rhodecode_user, 'ip_addr', '')
269 266 group_name = user_group.users_group_name
270 267
271 268 for user_or_user_id in users:
272 269 user = self._get_user(user_or_user_id)
273 270 log_text = 'User {user} {action} {group}'.format(
274 271 action=action, user=user.username, group=group_name)
275 272 log.info('Logging action: {0} by {1} ip:{2}'.format(
276 273 log_text, rhodecode_user, ipaddr))
277 274
278 275 def _find_user_in_group(self, user, user_group):
279 276 user_group_member = None
280 277 for m in user_group.members:
281 278 if m.user_id == user.user_id:
282 279 # Found this user's membership row
283 280 user_group_member = m
284 281 break
285 282
286 283 return user_group_member
287 284
288 285 def _get_membership(self, user_group_id, user_id):
289 286 user_group_member = UserGroupMember(user_group_id, user_id)
290 287 return user_group_member
291 288
292 289 def add_user_to_group(self, user_group, user):
293 290 user_group = self._get_user_group(user_group)
294 291 user = self._get_user(user)
295 292 user_member = self._find_user_in_group(user, user_group)
296 293 if user_member:
297 294 # user already in the group, skip
298 295 return True
299 296
300 297 member = self._get_membership(
301 298 user_group.users_group_id, user.user_id)
302 299 user_group.members.append(member)
303 300
304 301 try:
305 302 self.sa.add(member)
306 303 except Exception:
307 304 # what could go wrong here?
308 305 log.error(traceback.format_exc())
309 306 raise
310 307
311 308 self._log_user_changes('added to', user_group, user)
312 309 return member
313 310
314 311 def remove_user_from_group(self, user_group, user):
315 312 user_group = self._get_user_group(user_group)
316 313 user = self._get_user(user)
317 314 user_group_member = self._find_user_in_group(user, user_group)
318 315
319 316 if not user_group_member:
320 317 # User isn't in that group
321 318 return False
322 319
323 320 try:
324 321 self.sa.delete(user_group_member)
325 322 except Exception:
326 323 log.error(traceback.format_exc())
327 324 raise
328 325
329 326 self._log_user_changes('removed from', user_group, user)
330 327 return True
331 328
332 329 def has_perm(self, user_group, perm):
333 330 user_group = self._get_user_group(user_group)
334 331 perm = self._get_perm(perm)
335 332
336 333 return UserGroupToPerm.query()\
337 334 .filter(UserGroupToPerm.users_group == user_group)\
338 335 .filter(UserGroupToPerm.permission == perm).scalar() is not None
339 336
340 337 def grant_perm(self, user_group, perm):
341 338 user_group = self._get_user_group(user_group)
342 339 perm = self._get_perm(perm)
343 340
344 341 # if this permission is already granted skip it
345 342 _perm = UserGroupToPerm.query()\
346 343 .filter(UserGroupToPerm.users_group == user_group)\
347 344 .filter(UserGroupToPerm.permission == perm)\
348 345 .scalar()
349 346 if _perm:
350 347 return
351 348
352 349 new = UserGroupToPerm()
353 350 new.users_group = user_group
354 351 new.permission = perm
355 352 self.sa.add(new)
356 353 return new
357 354
358 355 def revoke_perm(self, user_group, perm):
359 356 user_group = self._get_user_group(user_group)
360 357 perm = self._get_perm(perm)
361 358
362 359 obj = UserGroupToPerm.query()\
363 360 .filter(UserGroupToPerm.users_group == user_group)\
364 361 .filter(UserGroupToPerm.permission == perm).scalar()
365 362 if obj:
366 363 self.sa.delete(obj)
367 364
368 365 def grant_user_permission(self, user_group, user, perm):
369 366 """
370 367 Grant permission for user on given user group, or update
371 368 existing one if found
372 369
373 370 :param user_group: Instance of UserGroup, users_group_id,
374 371 or users_group_name
375 372 :param user: Instance of User, user_id or username
376 373 :param perm: Instance of Permission, or permission_name
377 374 """
378 375
379 376 user_group = self._get_user_group(user_group)
380 377 user = self._get_user(user)
381 378 permission = self._get_perm(perm)
382 379
383 380 # check if we have that permission already
384 381 obj = self.sa.query(UserUserGroupToPerm)\
385 382 .filter(UserUserGroupToPerm.user == user)\
386 383 .filter(UserUserGroupToPerm.user_group == user_group)\
387 384 .scalar()
388 385 if obj is None:
389 386 # create new !
390 387 obj = UserUserGroupToPerm()
391 388 obj.user_group = user_group
392 389 obj.user = user
393 390 obj.permission = permission
394 391 self.sa.add(obj)
395 392 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
396 393 action_logger_generic(
397 394 'granted permission: {} to user: {} on usergroup: {}'.format(
398 395 perm, user, user_group), namespace='security.usergroup')
399 396
400 397 return obj
401 398
402 399 def revoke_user_permission(self, user_group, user):
403 400 """
404 401 Revoke permission for user on given user group
405 402
406 403 :param user_group: Instance of UserGroup, users_group_id,
407 404 or users_group name
408 405 :param user: Instance of User, user_id or username
409 406 """
410 407
411 408 user_group = self._get_user_group(user_group)
412 409 user = self._get_user(user)
413 410
414 411 obj = self.sa.query(UserUserGroupToPerm)\
415 412 .filter(UserUserGroupToPerm.user == user)\
416 413 .filter(UserUserGroupToPerm.user_group == user_group)\
417 414 .scalar()
418 415 if obj:
419 416 self.sa.delete(obj)
420 417 log.debug('Revoked perm on %s on %s', user_group, user)
421 418 action_logger_generic(
422 419 'revoked permission from user: {} on usergroup: {}'.format(
423 420 user, user_group), namespace='security.usergroup')
424 421
425 422 def grant_user_group_permission(self, target_user_group, user_group, perm):
426 423 """
427 424 Grant user group permission for given target_user_group
428 425
429 426 :param target_user_group:
430 427 :param user_group:
431 428 :param perm:
432 429 """
433 430 target_user_group = self._get_user_group(target_user_group)
434 431 user_group = self._get_user_group(user_group)
435 432 permission = self._get_perm(perm)
436 433 # forbid assigning same user group to itself
437 434 if target_user_group == user_group:
438 435 raise RepoGroupAssignmentError('target repo:%s cannot be '
439 436 'assigned to itself' % target_user_group)
440 437
441 438 # check if we have that permission already
442 439 obj = self.sa.query(UserGroupUserGroupToPerm)\
443 440 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
444 441 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
445 442 .scalar()
446 443 if obj is None:
447 444 # create new !
448 445 obj = UserGroupUserGroupToPerm()
449 446 obj.user_group = user_group
450 447 obj.target_user_group = target_user_group
451 448 obj.permission = permission
452 449 self.sa.add(obj)
453 450 log.debug(
454 451 'Granted perm %s to %s on %s', perm, target_user_group, user_group)
455 452 action_logger_generic(
456 453 'granted permission: {} to usergroup: {} on usergroup: {}'.format(
457 454 perm, user_group, target_user_group),
458 455 namespace='security.usergroup')
459 456
460 457 return obj
461 458
462 459 def revoke_user_group_permission(self, target_user_group, user_group):
463 460 """
464 461 Revoke user group permission for given target_user_group
465 462
466 463 :param target_user_group:
467 464 :param user_group:
468 465 """
469 466 target_user_group = self._get_user_group(target_user_group)
470 467 user_group = self._get_user_group(user_group)
471 468
472 469 obj = self.sa.query(UserGroupUserGroupToPerm)\
473 470 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
474 471 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
475 472 .scalar()
476 473 if obj:
477 474 self.sa.delete(obj)
478 475 log.debug(
479 476 'Revoked perm on %s on %s', target_user_group, user_group)
480 477 action_logger_generic(
481 478 'revoked permission from usergroup: {} on usergroup: {}'.format(
482 479 user_group, target_user_group),
483 480 namespace='security.repogroup')
484 481
485 482 def enforce_groups(self, user, groups, extern_type=None):
486 483 user = self._get_user(user)
487 484 log.debug('Enforcing groups %s on user %s', groups, user)
488 485 current_groups = user.group_member
489 486 # find the external created groups
490 487 externals = [x.users_group for x in current_groups
491 488 if 'extern_type' in x.users_group.group_data]
492 489
493 490 # calculate from what groups user should be removed
494 491 # externals that are not in groups
495 492 for gr in externals:
496 493 if gr.users_group_name not in groups:
497 494 log.debug('Removing user %s from user group %s', user, gr)
498 495 self.remove_user_from_group(gr, user)
499 496
500 497 # now we calculate in which groups user should be == groups params
501 498 owner = User.get_first_super_admin().username
502 499 for gr in set(groups):
503 500 existing_group = UserGroup.get_by_group_name(gr)
504 501 if not existing_group:
505 502 desc = 'Automatically created from plugin:%s' % extern_type
506 503 # we use first admin account to set the owner of the group
507 504 existing_group = UserGroupModel().create(gr, desc, owner,
508 505 group_data={'extern_type': extern_type})
509 506
510 507 # we can only add users to special groups created via plugins
511 508 managed = 'extern_type' in existing_group.group_data
512 509 if managed:
513 510 log.debug('Adding user %s to user group %s', user, gr)
514 511 UserGroupModel().add_user_to_group(existing_group, user)
515 512 else:
516 513 log.debug('Skipping addition to group %s since it is '
517 514 'not managed by auth plugins' % gr)
@@ -1,2184 +1,2204 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wrapper {
113 113 position: relative;
114 114 max-width: @wrapper-maxwidth;
115 115 margin: 0 auto;
116 116 }
117 117
118 118 #content {
119 119 clear: both;
120 120 padding: 0 @contentpadding;
121 121 }
122 122
123 123 .advanced-settings-fields{
124 124 input{
125 125 margin-left: @textmargin;
126 126 margin-right: @padding/2;
127 127 }
128 128 }
129 129
130 130 .cs_files_title {
131 131 margin: @pagepadding 0 0;
132 132 }
133 133
134 134 input.inline[type="file"] {
135 135 display: inline;
136 136 }
137 137
138 138 .error_page {
139 139 margin: 10% auto;
140 140
141 141 h1 {
142 142 color: @grey2;
143 143 }
144 144
145 145 .alert {
146 146 margin: @padding 0;
147 147 }
148 148
149 149 .error-branding {
150 150 font-family: @text-semibold;
151 151 color: @grey4;
152 152 }
153 153
154 154 .error_message {
155 155 font-family: @text-regular;
156 156 }
157 157
158 158 .sidebar {
159 159 min-height: 275px;
160 160 margin: 0;
161 161 padding: 0 0 @sidebarpadding @sidebarpadding;
162 162 border: none;
163 163 }
164 164
165 165 .main-content {
166 166 position: relative;
167 167 margin: 0 @sidebarpadding @sidebarpadding;
168 168 padding: 0 0 0 @sidebarpadding;
169 169 border-left: @border-thickness solid @grey5;
170 170
171 171 @media (max-width:767px) {
172 172 clear: both;
173 173 width: 100%;
174 174 margin: 0;
175 175 border: none;
176 176 }
177 177 }
178 178
179 179 .inner-column {
180 180 float: left;
181 181 width: 29.75%;
182 182 min-height: 150px;
183 183 margin: @sidebarpadding 2% 0 0;
184 184 padding: 0 2% 0 0;
185 185 border-right: @border-thickness solid @grey5;
186 186
187 187 @media (max-width:767px) {
188 188 clear: both;
189 189 width: 100%;
190 190 border: none;
191 191 }
192 192
193 193 ul {
194 194 padding-left: 1.25em;
195 195 }
196 196
197 197 &:last-child {
198 198 margin: @sidebarpadding 0 0;
199 199 border: none;
200 200 }
201 201
202 202 h4 {
203 203 margin: 0 0 @padding;
204 204 font-family: @text-semibold;
205 205 }
206 206 }
207 207 }
208 208 .error-page-logo {
209 209 width: 130px;
210 210 height: 160px;
211 211 }
212 212
213 213 // HEADER
214 214 .header {
215 215
216 216 // TODO: johbo: Fix login pages, so that they work without a min-height
217 217 // for the header and then remove the min-height. I chose a smaller value
218 218 // intentionally here to avoid rendering issues in the main navigation.
219 219 min-height: 49px;
220 220
221 221 position: relative;
222 222 vertical-align: bottom;
223 223 padding: 0 @header-padding;
224 224 background-color: @grey2;
225 225 color: @grey5;
226 226
227 227 .title {
228 228 overflow: visible;
229 229 }
230 230
231 231 &:before,
232 232 &:after {
233 233 content: "";
234 234 clear: both;
235 235 width: 100%;
236 236 }
237 237
238 238 // TODO: johbo: Avoids breaking "Repositories" chooser
239 239 .select2-container .select2-choice .select2-arrow {
240 240 display: none;
241 241 }
242 242 }
243 243
244 244 #header-inner {
245 245 &.title {
246 246 margin: 0;
247 247 }
248 248 &:before,
249 249 &:after {
250 250 content: "";
251 251 clear: both;
252 252 }
253 253 }
254 254
255 255 // Gists
256 256 #files_data {
257 257 clear: both; //for firefox
258 258 }
259 259 #gistid {
260 260 margin-right: @padding;
261 261 }
262 262
263 263 // Global Settings Editor
264 264 .textarea.editor {
265 265 float: left;
266 266 position: relative;
267 267 max-width: @texteditor-width;
268 268
269 269 select {
270 270 position: absolute;
271 271 top:10px;
272 272 right:0;
273 273 }
274 274
275 275 .CodeMirror {
276 276 margin: 0;
277 277 }
278 278
279 279 .help-block {
280 280 margin: 0 0 @padding;
281 281 padding:.5em;
282 282 background-color: @grey6;
283 283 }
284 284 }
285 285
286 286 ul.auth_plugins {
287 287 margin: @padding 0 @padding @legend-width;
288 288 padding: 0;
289 289
290 290 li {
291 291 margin-bottom: @padding;
292 292 line-height: 1em;
293 293 list-style-type: none;
294 294
295 295 .auth_buttons .btn {
296 296 margin-right: @padding;
297 297 }
298 298
299 299 &:before { content: none; }
300 300 }
301 301 }
302 302
303 303
304 304 // My Account PR list
305 305
306 306 #show_closed {
307 307 margin: 0 1em 0 0;
308 308 }
309 309
310 310 .pullrequestlist {
311 311 .closed {
312 312 background-color: @grey6;
313 313 }
314 314 .td-status {
315 315 padding-left: .5em;
316 316 }
317 317 .log-container .truncate {
318 318 height: 2.75em;
319 319 white-space: pre-line;
320 320 }
321 321 table.rctable .user {
322 322 padding-left: 0;
323 323 }
324 324 table.rctable {
325 325 td.td-description,
326 326 .rc-user {
327 327 min-width: auto;
328 328 }
329 329 }
330 330 }
331 331
332 332 // Pull Requests
333 333
334 334 .pullrequests_section_head {
335 335 display: block;
336 336 clear: both;
337 337 margin: @padding 0;
338 338 font-family: @text-bold;
339 339 }
340 340
341 341 .pr-origininfo, .pr-targetinfo {
342 342 position: relative;
343 343
344 344 .tag {
345 345 display: inline-block;
346 346 margin: 0 1em .5em 0;
347 347 }
348 348
349 349 .clone-url {
350 350 display: inline-block;
351 351 margin: 0 0 .5em 0;
352 352 padding: 0;
353 353 line-height: 1.2em;
354 354 }
355 355 }
356 356
357 357 .pr-pullinfo {
358 358 clear: both;
359 359 margin: .5em 0;
360 360 }
361 361
362 362 #pr-title-input {
363 363 width: 72%;
364 364 font-size: 1em;
365 365 font-family: @text-bold;
366 366 margin: 0;
367 367 padding: 0 0 0 @padding/4;
368 368 line-height: 1.7em;
369 369 color: @text-color;
370 370 letter-spacing: .02em;
371 371 }
372 372
373 373 #pullrequest_title {
374 374 width: 100%;
375 375 box-sizing: border-box;
376 376 }
377 377
378 378 #pr_open_message {
379 379 border: @border-thickness solid #fff;
380 380 border-radius: @border-radius;
381 381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
382 382 text-align: right;
383 383 overflow: hidden;
384 384 }
385 385
386 386 .pr-submit-button {
387 387 float: right;
388 388 margin: 0 0 0 5px;
389 389 }
390 390
391 391 .pr-spacing-container {
392 392 padding: 20px;
393 393 clear: both
394 394 }
395 395
396 396 #pr-description-input {
397 397 margin-bottom: 0;
398 398 }
399 399
400 400 .pr-description-label {
401 401 vertical-align: top;
402 402 }
403 403
404 404 .perms_section_head {
405 405 min-width: 625px;
406 406
407 407 h2 {
408 408 margin-bottom: 0;
409 409 }
410 410
411 411 .label-checkbox {
412 412 float: left;
413 413 }
414 414
415 415 &.field {
416 416 margin: @space 0 @padding;
417 417 }
418 418
419 419 &:first-child.field {
420 420 margin-top: 0;
421 421
422 422 .label {
423 423 margin-top: 0;
424 424 padding-top: 0;
425 425 }
426 426
427 427 .radios {
428 428 padding-top: 0;
429 429 }
430 430 }
431 431
432 432 .radios {
433 433 float: right;
434 434 position: relative;
435 435 width: 405px;
436 436 }
437 437 }
438 438
439 439 //--- MODULES ------------------//
440 440
441 441
442 442 // Server Announcement
443 443 #server-announcement {
444 444 width: 95%;
445 445 margin: @padding auto;
446 446 padding: @padding;
447 447 border-width: 2px;
448 448 border-style: solid;
449 449 .border-radius(2px);
450 450 font-family: @text-bold;
451 451
452 452 &.info { border-color: @alert4; background-color: @alert4-inner; }
453 453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
454 454 &.error { border-color: @alert2; background-color: @alert2-inner; }
455 455 &.success { border-color: @alert1; background-color: @alert1-inner; }
456 456 &.neutral { border-color: @grey3; background-color: @grey6; }
457 457 }
458 458
459 459 // Fixed Sidebar Column
460 460 .sidebar-col-wrapper {
461 461 padding-left: @sidebar-all-width;
462 462
463 463 .sidebar {
464 464 width: @sidebar-width;
465 465 margin-left: -@sidebar-all-width;
466 466 }
467 467 }
468 468
469 469 .sidebar-col-wrapper.scw-small {
470 470 padding-left: @sidebar-small-all-width;
471 471
472 472 .sidebar {
473 473 width: @sidebar-small-width;
474 474 margin-left: -@sidebar-small-all-width;
475 475 }
476 476 }
477 477
478 478
479 479 // FOOTER
480 480 #footer {
481 481 padding: 0;
482 482 text-align: center;
483 483 vertical-align: middle;
484 484 color: @grey2;
485 485 background-color: @grey6;
486 486
487 487 p {
488 488 margin: 0;
489 489 padding: 1em;
490 490 line-height: 1em;
491 491 }
492 492
493 493 .server-instance { //server instance
494 494 display: none;
495 495 }
496 496
497 497 .title {
498 498 float: none;
499 499 margin: 0 auto;
500 500 }
501 501 }
502 502
503 503 button.close {
504 504 padding: 0;
505 505 cursor: pointer;
506 506 background: transparent;
507 507 border: 0;
508 508 .box-shadow(none);
509 509 -webkit-appearance: none;
510 510 }
511 511
512 512 .close {
513 513 float: right;
514 514 font-size: 21px;
515 515 font-family: @text-bootstrap;
516 516 line-height: 1em;
517 517 font-weight: bold;
518 518 color: @grey2;
519 519
520 520 &:hover,
521 521 &:focus {
522 522 color: @grey1;
523 523 text-decoration: none;
524 524 cursor: pointer;
525 525 }
526 526 }
527 527
528 528 // GRID
529 529 .sorting,
530 530 .sorting_desc,
531 531 .sorting_asc {
532 532 cursor: pointer;
533 533 }
534 534 .sorting_desc:after {
535 535 content: "\00A0\25B2";
536 536 font-size: .75em;
537 537 }
538 538 .sorting_asc:after {
539 539 content: "\00A0\25BC";
540 540 font-size: .68em;
541 541 }
542 542
543 543
544 544 .user_auth_tokens {
545 545
546 546 &.truncate {
547 547 white-space: nowrap;
548 548 overflow: hidden;
549 549 text-overflow: ellipsis;
550 550 }
551 551
552 552 .fields .field .input {
553 553 margin: 0;
554 554 }
555 555
556 556 input#description {
557 557 width: 100px;
558 558 margin: 0;
559 559 }
560 560
561 561 .drop-menu {
562 562 // TODO: johbo: Remove this, should work out of the box when
563 563 // having multiple inputs inline
564 564 margin: 0 0 0 5px;
565 565 }
566 566 }
567 567 #user_list_table {
568 568 .closed {
569 569 background-color: @grey6;
570 570 }
571 571 }
572 572
573 573
574 574 input {
575 575 &.disabled {
576 576 opacity: .5;
577 577 }
578 578 }
579 579
580 580 // remove extra padding in firefox
581 581 input::-moz-focus-inner { border:0; padding:0 }
582 582
583 583 .adjacent input {
584 584 margin-bottom: @padding;
585 585 }
586 586
587 587 .permissions_boxes {
588 588 display: block;
589 589 }
590 590
591 591 //TODO: lisa: this should be in tables
592 592 .show_more_col {
593 593 width: 20px;
594 594 }
595 595
596 596 //FORMS
597 597
598 598 .medium-inline,
599 599 input#description.medium-inline {
600 600 display: inline;
601 601 width: @medium-inline-input-width;
602 602 min-width: 100px;
603 603 }
604 604
605 605 select {
606 606 //reset
607 607 -webkit-appearance: none;
608 608 -moz-appearance: none;
609 609
610 610 display: inline-block;
611 611 height: 28px;
612 612 width: auto;
613 613 margin: 0 @padding @padding 0;
614 614 padding: 0 18px 0 8px;
615 615 line-height:1em;
616 616 font-size: @basefontsize;
617 617 border: @border-thickness solid @rcblue;
618 618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
619 619 color: @rcblue;
620 620
621 621 &:after {
622 622 content: "\00A0\25BE";
623 623 }
624 624
625 625 &:focus {
626 626 outline: none;
627 627 }
628 628 }
629 629
630 630 option {
631 631 &:focus {
632 632 outline: none;
633 633 }
634 634 }
635 635
636 636 input,
637 637 textarea {
638 638 padding: @input-padding;
639 639 border: @input-border-thickness solid @border-highlight-color;
640 640 .border-radius (@border-radius);
641 641 font-family: @text-light;
642 642 font-size: @basefontsize;
643 643
644 644 &.input-sm {
645 645 padding: 5px;
646 646 }
647 647
648 648 &#description {
649 649 min-width: @input-description-minwidth;
650 650 min-height: 1em;
651 651 padding: 10px;
652 652 }
653 653 }
654 654
655 655 .field-sm {
656 656 input,
657 657 textarea {
658 658 padding: 5px;
659 659 }
660 660 }
661 661
662 662 textarea {
663 663 display: block;
664 664 clear: both;
665 665 width: 100%;
666 666 min-height: 100px;
667 667 margin-bottom: @padding;
668 668 .box-sizing(border-box);
669 669 overflow: auto;
670 670 }
671 671
672 672 label {
673 673 font-family: @text-light;
674 674 }
675 675
676 676 // GRAVATARS
677 677 // centers gravatar on username to the right
678 678
679 679 .gravatar {
680 680 display: inline;
681 681 min-width: 16px;
682 682 min-height: 16px;
683 683 margin: -5px 0;
684 684 padding: 0;
685 685 line-height: 1em;
686 686 border: 1px solid @grey4;
687 687
688 688 &.gravatar-large {
689 689 margin: -0.5em .25em -0.5em 0;
690 690 }
691 691
692 692 & + .user {
693 693 display: inline;
694 694 margin: 0;
695 695 padding: 0 0 0 .17em;
696 696 line-height: 1em;
697 697 }
698 698 }
699 699
700 700 .user-inline-data {
701 701 display: inline-block;
702 702 float: left;
703 703 padding-left: .5em;
704 704 line-height: 1.3em;
705 705 }
706 706
707 707 .rc-user { // gravatar + user wrapper
708 708 float: left;
709 709 position: relative;
710 710 min-width: 100px;
711 711 max-width: 200px;
712 712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
713 713 display: block;
714 714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
715 715
716 716
717 717 .gravatar {
718 718 display: block;
719 719 position: absolute;
720 720 top: 0;
721 721 left: 0;
722 722 min-width: @gravatar-size;
723 723 min-height: @gravatar-size;
724 724 margin: 0;
725 725 }
726 726
727 727 .user {
728 728 display: block;
729 729 max-width: 175px;
730 730 padding-top: 2px;
731 731 overflow: hidden;
732 732 text-overflow: ellipsis;
733 733 }
734 734 }
735 735
736 736 .gist-gravatar,
737 737 .journal_container {
738 738 .gravatar-large {
739 739 margin: 0 .5em -10px 0;
740 740 }
741 741 }
742 742
743 743
744 744 // ADMIN SETTINGS
745 745
746 746 // Tag Patterns
747 747 .tag_patterns {
748 748 .tag_input {
749 749 margin-bottom: @padding;
750 750 }
751 751 }
752 752
753 753 .locked_input {
754 754 position: relative;
755 755
756 756 input {
757 757 display: inline;
758 758 margin-top: 3px;
759 759 }
760 760
761 761 br {
762 762 display: none;
763 763 }
764 764
765 765 .error-message {
766 766 float: left;
767 767 width: 100%;
768 768 }
769 769
770 770 .lock_input_button {
771 771 display: inline;
772 772 }
773 773
774 774 .help-block {
775 775 clear: both;
776 776 }
777 777 }
778 778
779 779 // Notifications
780 780
781 781 .notifications_buttons {
782 782 margin: 0 0 @space 0;
783 783 padding: 0;
784 784
785 785 .btn {
786 786 display: inline-block;
787 787 }
788 788 }
789 789
790 790 .notification-list {
791 791
792 792 div {
793 793 display: inline-block;
794 794 vertical-align: middle;
795 795 }
796 796
797 797 .container {
798 798 display: block;
799 799 margin: 0 0 @padding 0;
800 800 }
801 801
802 802 .delete-notifications {
803 803 margin-left: @padding;
804 804 text-align: right;
805 805 cursor: pointer;
806 806 }
807 807
808 808 .read-notifications {
809 809 margin-left: @padding/2;
810 810 text-align: right;
811 811 width: 35px;
812 812 cursor: pointer;
813 813 }
814 814
815 815 .icon-minus-sign {
816 816 color: @alert2;
817 817 }
818 818
819 819 .icon-ok-sign {
820 820 color: @alert1;
821 821 }
822 822 }
823 823
824 824 .user_settings {
825 825 float: left;
826 826 clear: both;
827 827 display: block;
828 828 width: 100%;
829 829
830 830 .gravatar_box {
831 831 margin-bottom: @padding;
832 832
833 833 &:after {
834 834 content: " ";
835 835 clear: both;
836 836 width: 100%;
837 837 }
838 838 }
839 839
840 840 .fields .field {
841 841 clear: both;
842 842 }
843 843 }
844 844
845 845 .advanced_settings {
846 846 margin-bottom: @space;
847 847
848 848 .help-block {
849 849 margin-left: 0;
850 850 }
851 851
852 852 button + .help-block {
853 853 margin-top: @padding;
854 854 }
855 855 }
856 856
857 857 // admin settings radio buttons and labels
858 858 .label-2 {
859 859 float: left;
860 860 width: @label2-width;
861 861
862 862 label {
863 863 color: @grey1;
864 864 }
865 865 }
866 866 .checkboxes {
867 867 float: left;
868 868 width: @checkboxes-width;
869 869 margin-bottom: @padding;
870 870
871 871 .checkbox {
872 872 width: 100%;
873 873
874 874 label {
875 875 margin: 0;
876 876 padding: 0;
877 877 }
878 878 }
879 879
880 880 .checkbox + .checkbox {
881 881 display: inline-block;
882 882 }
883 883
884 884 label {
885 885 margin-right: 1em;
886 886 }
887 887 }
888 888
889 889 // CHANGELOG
890 890 .container_header {
891 891 float: left;
892 892 display: block;
893 893 width: 100%;
894 894 margin: @padding 0 @padding;
895 895
896 896 #filter_changelog {
897 897 float: left;
898 898 margin-right: @padding;
899 899 }
900 900
901 901 .breadcrumbs_light {
902 902 display: inline-block;
903 903 }
904 904 }
905 905
906 906 .info_box {
907 907 float: right;
908 908 }
909 909
910 910
911 911 #graph_nodes {
912 912 padding-top: 43px;
913 913 }
914 914
915 915 #graph_content{
916 916
917 917 // adjust for table headers so that graph renders properly
918 918 // #graph_nodes padding - table cell padding
919 919 padding-top: (@space - (@basefontsize * 2.4));
920 920
921 921 &.graph_full_width {
922 922 width: 100%;
923 923 max-width: 100%;
924 924 }
925 925 }
926 926
927 927 #graph {
928 928 .flag_status {
929 929 margin: 0;
930 930 }
931 931
932 932 .pagination-left {
933 933 float: left;
934 934 clear: both;
935 935 }
936 936
937 937 .log-container {
938 938 max-width: 345px;
939 939
940 940 .message{
941 941 max-width: 340px;
942 942 }
943 943 }
944 944
945 945 .graph-col-wrapper {
946 946 padding-left: 110px;
947 947
948 948 #graph_nodes {
949 949 width: 100px;
950 950 margin-left: -110px;
951 951 float: left;
952 952 clear: left;
953 953 }
954 954 }
955 955 }
956 956
957 957 #filter_changelog {
958 958 float: left;
959 959 }
960 960
961 961
962 962 //--- THEME ------------------//
963 963
964 964 #logo {
965 965 float: left;
966 966 margin: 9px 0 0 0;
967 967
968 968 .header {
969 969 background-color: transparent;
970 970 }
971 971
972 972 a {
973 973 display: inline-block;
974 974 }
975 975
976 976 img {
977 977 height:30px;
978 978 }
979 979 }
980 980
981 981 .logo-wrapper {
982 982 float:left;
983 983 }
984 984
985 985 .branding{
986 986 float: left;
987 987 padding: 9px 2px;
988 988 line-height: 1em;
989 989 font-size: @navigation-fontsize;
990 990 }
991 991
992 992 img {
993 993 border: none;
994 994 outline: none;
995 995 }
996 996 user-profile-header
997 997 label {
998 998
999 999 input[type="checkbox"] {
1000 1000 margin-right: 1em;
1001 1001 }
1002 1002 input[type="radio"] {
1003 1003 margin-right: 1em;
1004 1004 }
1005 1005 }
1006 1006
1007 1007 .flag_status {
1008 1008 margin: 2px 8px 6px 2px;
1009 1009 &.under_review {
1010 1010 .circle(5px, @alert3);
1011 1011 }
1012 1012 &.approved {
1013 1013 .circle(5px, @alert1);
1014 1014 }
1015 1015 &.rejected,
1016 1016 &.forced_closed{
1017 1017 .circle(5px, @alert2);
1018 1018 }
1019 1019 &.not_reviewed {
1020 1020 .circle(5px, @grey5);
1021 1021 }
1022 1022 }
1023 1023
1024 1024 .flag_status_comment_box {
1025 1025 margin: 5px 6px 0px 2px;
1026 1026 }
1027 1027 .test_pattern_preview {
1028 1028 margin: @space 0;
1029 1029
1030 1030 p {
1031 1031 margin-bottom: 0;
1032 1032 border-bottom: @border-thickness solid @border-default-color;
1033 1033 color: @grey3;
1034 1034 }
1035 1035
1036 1036 .btn {
1037 1037 margin-bottom: @padding;
1038 1038 }
1039 1039 }
1040 1040 #test_pattern_result {
1041 1041 display: none;
1042 1042 &:extend(pre);
1043 1043 padding: .9em;
1044 1044 color: @grey3;
1045 1045 background-color: @grey7;
1046 1046 border-right: @border-thickness solid @border-default-color;
1047 1047 border-bottom: @border-thickness solid @border-default-color;
1048 1048 border-left: @border-thickness solid @border-default-color;
1049 1049 }
1050 1050
1051 1051 #repo_vcs_settings {
1052 1052 #inherit_overlay_vcs_default {
1053 1053 display: none;
1054 1054 }
1055 1055 #inherit_overlay_vcs_custom {
1056 1056 display: custom;
1057 1057 }
1058 1058 &.inherited {
1059 1059 #inherit_overlay_vcs_default {
1060 1060 display: block;
1061 1061 }
1062 1062 #inherit_overlay_vcs_custom {
1063 1063 display: none;
1064 1064 }
1065 1065 }
1066 1066 }
1067 1067
1068 1068 .issue-tracker-link {
1069 1069 color: @rcblue;
1070 1070 }
1071 1071
1072 1072 // Issue Tracker Table Show/Hide
1073 1073 #repo_issue_tracker {
1074 1074 #inherit_overlay {
1075 1075 display: none;
1076 1076 }
1077 1077 #custom_overlay {
1078 1078 display: custom;
1079 1079 }
1080 1080 &.inherited {
1081 1081 #inherit_overlay {
1082 1082 display: block;
1083 1083 }
1084 1084 #custom_overlay {
1085 1085 display: none;
1086 1086 }
1087 1087 }
1088 1088 }
1089 1089 table.issuetracker {
1090 1090 &.readonly {
1091 1091 tr, td {
1092 1092 color: @grey3;
1093 1093 }
1094 1094 }
1095 1095 .edit {
1096 1096 display: none;
1097 1097 }
1098 1098 .editopen {
1099 1099 .edit {
1100 1100 display: inline;
1101 1101 }
1102 1102 .entry {
1103 1103 display: none;
1104 1104 }
1105 1105 }
1106 1106 tr td.td-action {
1107 1107 min-width: 117px;
1108 1108 }
1109 1109 td input {
1110 1110 max-width: none;
1111 1111 min-width: 30px;
1112 1112 width: 80%;
1113 1113 }
1114 1114 .issuetracker_pref input {
1115 1115 width: 40%;
1116 1116 }
1117 1117 input.edit_issuetracker_update {
1118 1118 margin-right: 0;
1119 1119 width: auto;
1120 1120 }
1121 1121 }
1122 1122
1123 1123 table.integrations {
1124 1124 .td-icon {
1125 1125 width: 20px;
1126 1126 .integration-icon {
1127 1127 height: 20px;
1128 1128 width: 20px;
1129 1129 }
1130 1130 }
1131 1131 }
1132 1132
1133 1133 .integrations {
1134 1134 a.integration-box {
1135 1135 color: @text-color;
1136 1136 &:hover {
1137 1137 .panel {
1138 1138 background: #fbfbfb;
1139 1139 }
1140 1140 }
1141 1141 .integration-icon {
1142 1142 width: 30px;
1143 1143 height: 30px;
1144 1144 margin-right: 20px;
1145 1145 float: left;
1146 1146 }
1147 1147
1148 1148 .panel-body {
1149 1149 padding: 10px;
1150 1150 }
1151 1151 .panel {
1152 1152 margin-bottom: 10px;
1153 1153 }
1154 1154 h2 {
1155 1155 display: inline-block;
1156 1156 margin: 0;
1157 1157 min-width: 140px;
1158 1158 }
1159 1159 }
1160 1160 }
1161 1161
1162 1162 //Permissions Settings
1163 1163 #add_perm {
1164 1164 margin: 0 0 @padding;
1165 1165 cursor: pointer;
1166 1166 }
1167 1167
1168 1168 .perm_ac {
1169 1169 input {
1170 1170 width: 95%;
1171 1171 }
1172 1172 }
1173 1173
1174 1174 .autocomplete-suggestions {
1175 1175 width: auto !important; // overrides autocomplete.js
1176 1176 margin: 0;
1177 1177 border: @border-thickness solid @rcblue;
1178 1178 border-radius: @border-radius;
1179 1179 color: @rcblue;
1180 1180 background-color: white;
1181 1181 }
1182 1182 .autocomplete-selected {
1183 1183 background: #F0F0F0;
1184 1184 }
1185 1185 .ac-container-wrap {
1186 1186 margin: 0;
1187 1187 padding: 8px;
1188 1188 border-bottom: @border-thickness solid @rclightblue;
1189 1189 list-style-type: none;
1190 1190 cursor: pointer;
1191 1191
1192 1192 &:hover {
1193 1193 background-color: @rclightblue;
1194 1194 }
1195 1195
1196 1196 img {
1197 1197 height: @gravatar-size;
1198 1198 width: @gravatar-size;
1199 1199 margin-right: 1em;
1200 1200 }
1201 1201
1202 1202 strong {
1203 1203 font-weight: normal;
1204 1204 }
1205 1205 }
1206 1206
1207 1207 // Settings Dropdown
1208 1208 .user-menu .container {
1209 1209 padding: 0 4px;
1210 1210 margin: 0;
1211 1211 }
1212 1212
1213 1213 .user-menu .gravatar {
1214 1214 cursor: pointer;
1215 1215 }
1216 1216
1217 1217 .codeblock {
1218 1218 margin-bottom: @padding;
1219 1219 clear: both;
1220 1220
1221 1221 .stats{
1222 1222 overflow: hidden;
1223 1223 }
1224 1224
1225 1225 .message{
1226 1226 textarea{
1227 1227 margin: 0;
1228 1228 }
1229 1229 }
1230 1230
1231 1231 .code-header {
1232 1232 .stats {
1233 1233 line-height: 2em;
1234 1234
1235 1235 .revision_id {
1236 1236 margin-left: 0;
1237 1237 }
1238 1238 .buttons {
1239 1239 padding-right: 0;
1240 1240 }
1241 1241 }
1242 1242
1243 1243 .item{
1244 1244 margin-right: 0.5em;
1245 1245 }
1246 1246 }
1247 1247
1248 1248 #editor_container{
1249 1249 position: relative;
1250 1250 margin: @padding;
1251 1251 }
1252 1252 }
1253 1253
1254 1254 #file_history_container {
1255 1255 display: none;
1256 1256 }
1257 1257
1258 1258 .file-history-inner {
1259 1259 margin-bottom: 10px;
1260 1260 }
1261 1261
1262 1262 // Pull Requests
1263 1263 .summary-details {
1264 1264 width: 72%;
1265 1265 }
1266 1266 .pr-summary {
1267 1267 border-bottom: @border-thickness solid @grey5;
1268 1268 margin-bottom: @space;
1269 1269 }
1270 1270 .reviewers-title {
1271 1271 width: 25%;
1272 1272 min-width: 200px;
1273 1273 }
1274 1274 .reviewers {
1275 1275 width: 25%;
1276 1276 min-width: 200px;
1277 1277 }
1278 1278 .reviewers ul li {
1279 1279 position: relative;
1280 1280 width: 100%;
1281 1281 margin-bottom: 8px;
1282 1282 }
1283 1283 .reviewers_member {
1284 1284 width: 100%;
1285 1285 overflow: auto;
1286 1286 }
1287 1287 .reviewer_reason {
1288 1288 padding-left: 20px;
1289 1289 }
1290 1290 .reviewer_status {
1291 1291 display: inline-block;
1292 1292 vertical-align: top;
1293 1293 width: 7%;
1294 1294 min-width: 20px;
1295 1295 height: 1.2em;
1296 1296 margin-top: 3px;
1297 1297 line-height: 1em;
1298 1298 }
1299 1299
1300 1300 .reviewer_name {
1301 1301 display: inline-block;
1302 1302 max-width: 83%;
1303 1303 padding-right: 20px;
1304 1304 vertical-align: middle;
1305 1305 line-height: 1;
1306 1306
1307 1307 .rc-user {
1308 1308 min-width: 0;
1309 1309 margin: -2px 1em 0 0;
1310 1310 }
1311 1311
1312 1312 .reviewer {
1313 1313 float: left;
1314 1314 }
1315 1315
1316 1316 &.to-delete {
1317 1317 .user,
1318 1318 .reviewer {
1319 1319 text-decoration: line-through;
1320 1320 }
1321 1321 }
1322 1322 }
1323 1323
1324 1324 .reviewer_member_remove {
1325 1325 position: absolute;
1326 1326 right: 0;
1327 1327 top: 0;
1328 1328 width: 16px;
1329 1329 margin-bottom: 10px;
1330 1330 padding: 0;
1331 1331 color: black;
1332 1332 }
1333 1333 .reviewer_member_status {
1334 1334 margin-top: 5px;
1335 1335 }
1336 1336 .pr-summary #summary{
1337 1337 width: 100%;
1338 1338 }
1339 1339 .pr-summary .action_button:hover {
1340 1340 border: 0;
1341 1341 cursor: pointer;
1342 1342 }
1343 1343 .pr-details-title {
1344 1344 padding-bottom: 8px;
1345 1345 border-bottom: @border-thickness solid @grey5;
1346 1346
1347 1347 .action_button.disabled {
1348 1348 color: @grey4;
1349 1349 cursor: inherit;
1350 1350 }
1351 1351 .action_button {
1352 1352 color: @rcblue;
1353 1353 }
1354 1354 }
1355 1355 .pr-details-content {
1356 1356 margin-top: @textmargin;
1357 1357 margin-bottom: @textmargin;
1358 1358 }
1359 1359 .pr-description {
1360 1360 white-space:pre-wrap;
1361 1361 }
1362 1362 .group_members {
1363 1363 margin-top: 0;
1364 1364 padding: 0;
1365 1365 list-style: outside none none;
1366 1366
1367 1367 img {
1368 1368 height: @gravatar-size;
1369 1369 width: @gravatar-size;
1370 1370 margin-right: .5em;
1371 1371 margin-left: 3px;
1372 1372 }
1373
1374 .to-delete {
1375 .user {
1376 text-decoration: line-through;
1373 1377 }
1378 }
1379 }
1380
1381 // new entry in group_members
1382 .td-author-new-entry {
1383 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1384 }
1385
1386 .usergroup_member_remove {
1387 width: 16px;
1388 margin-bottom: 10px;
1389 padding: 0;
1390 color: black !important;
1391 cursor: pointer;
1392 }
1393
1374 1394 .reviewer_ac .ac-input {
1375 1395 width: 92%;
1376 1396 margin-bottom: 1em;
1377 1397 }
1378 1398 #update_commits {
1379 1399 float: right;
1380 1400 }
1381 1401 .compare_view_commits tr{
1382 1402 height: 20px;
1383 1403 }
1384 1404 .compare_view_commits td {
1385 1405 vertical-align: top;
1386 1406 padding-top: 10px;
1387 1407 }
1388 1408 .compare_view_commits .author {
1389 1409 margin-left: 5px;
1390 1410 }
1391 1411
1392 1412 .compare_view_files {
1393 1413 width: 100%;
1394 1414
1395 1415 td {
1396 1416 vertical-align: middle;
1397 1417 }
1398 1418 }
1399 1419
1400 1420 .compare_view_filepath {
1401 1421 color: @grey1;
1402 1422 }
1403 1423
1404 1424 .show_more {
1405 1425 display: inline-block;
1406 1426 position: relative;
1407 1427 vertical-align: middle;
1408 1428 width: 4px;
1409 1429 height: @basefontsize;
1410 1430
1411 1431 &:after {
1412 1432 content: "\00A0\25BE";
1413 1433 display: inline-block;
1414 1434 width:10px;
1415 1435 line-height: 5px;
1416 1436 font-size: 12px;
1417 1437 cursor: pointer;
1418 1438 }
1419 1439 }
1420 1440
1421 1441 .journal_more .show_more {
1422 1442 display: inline;
1423 1443
1424 1444 &:after {
1425 1445 content: none;
1426 1446 }
1427 1447 }
1428 1448
1429 1449 .open .show_more:after,
1430 1450 .select2-dropdown-open .show_more:after {
1431 1451 .rotate(180deg);
1432 1452 margin-left: 4px;
1433 1453 }
1434 1454
1435 1455
1436 1456 .compare_view_commits .collapse_commit:after {
1437 1457 cursor: pointer;
1438 1458 content: "\00A0\25B4";
1439 1459 margin-left: -3px;
1440 1460 font-size: 17px;
1441 1461 color: @grey4;
1442 1462 }
1443 1463
1444 1464 .diff_links {
1445 1465 margin-left: 8px;
1446 1466 }
1447 1467
1448 1468 p.ancestor {
1449 1469 margin: @padding 0;
1450 1470 }
1451 1471
1452 1472 .cs_icon_td input[type="checkbox"] {
1453 1473 display: none;
1454 1474 }
1455 1475
1456 1476 .cs_icon_td .expand_file_icon:after {
1457 1477 cursor: pointer;
1458 1478 content: "\00A0\25B6";
1459 1479 font-size: 12px;
1460 1480 color: @grey4;
1461 1481 }
1462 1482
1463 1483 .cs_icon_td .collapse_file_icon:after {
1464 1484 cursor: pointer;
1465 1485 content: "\00A0\25BC";
1466 1486 font-size: 12px;
1467 1487 color: @grey4;
1468 1488 }
1469 1489
1470 1490 /*new binary
1471 1491 NEW_FILENODE = 1
1472 1492 DEL_FILENODE = 2
1473 1493 MOD_FILENODE = 3
1474 1494 RENAMED_FILENODE = 4
1475 1495 COPIED_FILENODE = 5
1476 1496 CHMOD_FILENODE = 6
1477 1497 BIN_FILENODE = 7
1478 1498 */
1479 1499 .cs_files_expand {
1480 1500 font-size: @basefontsize + 5px;
1481 1501 line-height: 1.8em;
1482 1502 float: right;
1483 1503 }
1484 1504
1485 1505 .cs_files_expand span{
1486 1506 color: @rcblue;
1487 1507 cursor: pointer;
1488 1508 }
1489 1509 .cs_files {
1490 1510 clear: both;
1491 1511 padding-bottom: @padding;
1492 1512
1493 1513 .cur_cs {
1494 1514 margin: 10px 2px;
1495 1515 font-weight: bold;
1496 1516 }
1497 1517
1498 1518 .node {
1499 1519 float: left;
1500 1520 }
1501 1521
1502 1522 .changes {
1503 1523 float: right;
1504 1524 color: white;
1505 1525 font-size: @basefontsize - 4px;
1506 1526 margin-top: 4px;
1507 1527 opacity: 0.6;
1508 1528 filter: Alpha(opacity=60); /* IE8 and earlier */
1509 1529
1510 1530 .added {
1511 1531 background-color: @alert1;
1512 1532 float: left;
1513 1533 text-align: center;
1514 1534 }
1515 1535
1516 1536 .deleted {
1517 1537 background-color: @alert2;
1518 1538 float: left;
1519 1539 text-align: center;
1520 1540 }
1521 1541
1522 1542 .bin {
1523 1543 background-color: @alert1;
1524 1544 text-align: center;
1525 1545 }
1526 1546
1527 1547 /*new binary*/
1528 1548 .bin.bin1 {
1529 1549 background-color: @alert1;
1530 1550 text-align: center;
1531 1551 }
1532 1552
1533 1553 /*deleted binary*/
1534 1554 .bin.bin2 {
1535 1555 background-color: @alert2;
1536 1556 text-align: center;
1537 1557 }
1538 1558
1539 1559 /*mod binary*/
1540 1560 .bin.bin3 {
1541 1561 background-color: @grey2;
1542 1562 text-align: center;
1543 1563 }
1544 1564
1545 1565 /*rename file*/
1546 1566 .bin.bin4 {
1547 1567 background-color: @alert4;
1548 1568 text-align: center;
1549 1569 }
1550 1570
1551 1571 /*copied file*/
1552 1572 .bin.bin5 {
1553 1573 background-color: @alert4;
1554 1574 text-align: center;
1555 1575 }
1556 1576
1557 1577 /*chmod file*/
1558 1578 .bin.bin6 {
1559 1579 background-color: @grey2;
1560 1580 text-align: center;
1561 1581 }
1562 1582 }
1563 1583 }
1564 1584
1565 1585 .cs_files .cs_added, .cs_files .cs_A,
1566 1586 .cs_files .cs_added, .cs_files .cs_M,
1567 1587 .cs_files .cs_added, .cs_files .cs_D {
1568 1588 height: 16px;
1569 1589 padding-right: 10px;
1570 1590 margin-top: 7px;
1571 1591 text-align: left;
1572 1592 }
1573 1593
1574 1594 .cs_icon_td {
1575 1595 min-width: 16px;
1576 1596 width: 16px;
1577 1597 }
1578 1598
1579 1599 .pull-request-merge {
1580 1600 padding: 10px 0;
1581 1601 margin-top: 10px;
1582 1602 margin-bottom: 20px;
1583 1603 }
1584 1604
1585 1605 .pull-request-merge .pull-request-wrap {
1586 1606 height: 25px;
1587 1607 padding: 5px 0;
1588 1608 }
1589 1609
1590 1610 .pull-request-merge span {
1591 1611 margin-right: 10px;
1592 1612 }
1593 1613 #close_pull_request {
1594 1614 margin-right: 0px;
1595 1615 }
1596 1616
1597 1617 .empty_data {
1598 1618 color: @grey4;
1599 1619 }
1600 1620
1601 1621 #changeset_compare_view_content {
1602 1622 margin-bottom: @space;
1603 1623 clear: both;
1604 1624 width: 100%;
1605 1625 box-sizing: border-box;
1606 1626 .border-radius(@border-radius);
1607 1627
1608 1628 .help-block {
1609 1629 margin: @padding 0;
1610 1630 color: @text-color;
1611 1631 }
1612 1632
1613 1633 .empty_data {
1614 1634 margin: @padding 0;
1615 1635 }
1616 1636
1617 1637 .alert {
1618 1638 margin-bottom: @space;
1619 1639 }
1620 1640 }
1621 1641
1622 1642 .table_disp {
1623 1643 .status {
1624 1644 width: auto;
1625 1645
1626 1646 .flag_status {
1627 1647 float: left;
1628 1648 }
1629 1649 }
1630 1650 }
1631 1651
1632 1652 .status_box_menu {
1633 1653 margin: 0;
1634 1654 }
1635 1655
1636 1656 .notification-table{
1637 1657 margin-bottom: @space;
1638 1658 display: table;
1639 1659 width: 100%;
1640 1660
1641 1661 .container{
1642 1662 display: table-row;
1643 1663
1644 1664 .notification-header{
1645 1665 border-bottom: @border-thickness solid @border-default-color;
1646 1666 }
1647 1667
1648 1668 .notification-subject{
1649 1669 display: table-cell;
1650 1670 }
1651 1671 }
1652 1672 }
1653 1673
1654 1674 // Notifications
1655 1675 .notification-header{
1656 1676 display: table;
1657 1677 width: 100%;
1658 1678 padding: floor(@basefontsize/2) 0;
1659 1679 line-height: 1em;
1660 1680
1661 1681 .desc, .delete-notifications, .read-notifications{
1662 1682 display: table-cell;
1663 1683 text-align: left;
1664 1684 }
1665 1685
1666 1686 .desc{
1667 1687 width: 1163px;
1668 1688 }
1669 1689
1670 1690 .delete-notifications, .read-notifications{
1671 1691 width: 35px;
1672 1692 min-width: 35px; //fixes when only one button is displayed
1673 1693 }
1674 1694 }
1675 1695
1676 1696 .notification-body {
1677 1697 .markdown-block,
1678 1698 .rst-block {
1679 1699 padding: @padding 0;
1680 1700 }
1681 1701
1682 1702 .notification-subject {
1683 1703 padding: @textmargin 0;
1684 1704 border-bottom: @border-thickness solid @border-default-color;
1685 1705 }
1686 1706 }
1687 1707
1688 1708
1689 1709 .notifications_buttons{
1690 1710 float: right;
1691 1711 }
1692 1712
1693 1713 #notification-status{
1694 1714 display: inline;
1695 1715 }
1696 1716
1697 1717 // Repositories
1698 1718
1699 1719 #summary.fields{
1700 1720 display: table;
1701 1721
1702 1722 .field{
1703 1723 display: table-row;
1704 1724
1705 1725 .label-summary{
1706 1726 display: table-cell;
1707 1727 min-width: @label-summary-minwidth;
1708 1728 padding-top: @padding/2;
1709 1729 padding-bottom: @padding/2;
1710 1730 padding-right: @padding/2;
1711 1731 }
1712 1732
1713 1733 .input{
1714 1734 display: table-cell;
1715 1735 padding: @padding/2;
1716 1736
1717 1737 input{
1718 1738 min-width: 29em;
1719 1739 padding: @padding/4;
1720 1740 }
1721 1741 }
1722 1742 .statistics, .downloads{
1723 1743 .disabled{
1724 1744 color: @grey4;
1725 1745 }
1726 1746 }
1727 1747 }
1728 1748 }
1729 1749
1730 1750 #summary{
1731 1751 width: 70%;
1732 1752 }
1733 1753
1734 1754
1735 1755 // Journal
1736 1756 .journal.title {
1737 1757 h5 {
1738 1758 float: left;
1739 1759 margin: 0;
1740 1760 width: 70%;
1741 1761 }
1742 1762
1743 1763 ul {
1744 1764 float: right;
1745 1765 display: inline-block;
1746 1766 margin: 0;
1747 1767 width: 30%;
1748 1768 text-align: right;
1749 1769
1750 1770 li {
1751 1771 display: inline;
1752 1772 font-size: @journal-fontsize;
1753 1773 line-height: 1em;
1754 1774
1755 1775 &:before { content: none; }
1756 1776 }
1757 1777 }
1758 1778 }
1759 1779
1760 1780 .filterexample {
1761 1781 position: absolute;
1762 1782 top: 95px;
1763 1783 left: @contentpadding;
1764 1784 color: @rcblue;
1765 1785 font-size: 11px;
1766 1786 font-family: @text-regular;
1767 1787 cursor: help;
1768 1788
1769 1789 &:hover {
1770 1790 color: @rcdarkblue;
1771 1791 }
1772 1792
1773 1793 @media (max-width:768px) {
1774 1794 position: relative;
1775 1795 top: auto;
1776 1796 left: auto;
1777 1797 display: block;
1778 1798 }
1779 1799 }
1780 1800
1781 1801
1782 1802 #journal{
1783 1803 margin-bottom: @space;
1784 1804
1785 1805 .journal_day{
1786 1806 margin-bottom: @textmargin/2;
1787 1807 padding-bottom: @textmargin/2;
1788 1808 font-size: @journal-fontsize;
1789 1809 border-bottom: @border-thickness solid @border-default-color;
1790 1810 }
1791 1811
1792 1812 .journal_container{
1793 1813 margin-bottom: @space;
1794 1814
1795 1815 .journal_user{
1796 1816 display: inline-block;
1797 1817 }
1798 1818 .journal_action_container{
1799 1819 display: block;
1800 1820 margin-top: @textmargin;
1801 1821
1802 1822 div{
1803 1823 display: inline;
1804 1824 }
1805 1825
1806 1826 div.journal_action_params{
1807 1827 display: block;
1808 1828 }
1809 1829
1810 1830 div.journal_repo:after{
1811 1831 content: "\A";
1812 1832 white-space: pre;
1813 1833 }
1814 1834
1815 1835 div.date{
1816 1836 display: block;
1817 1837 margin-bottom: @textmargin;
1818 1838 }
1819 1839 }
1820 1840 }
1821 1841 }
1822 1842
1823 1843 // Files
1824 1844 .edit-file-title {
1825 1845 border-bottom: @border-thickness solid @border-default-color;
1826 1846
1827 1847 .breadcrumbs {
1828 1848 margin-bottom: 0;
1829 1849 }
1830 1850 }
1831 1851
1832 1852 .edit-file-fieldset {
1833 1853 margin-top: @sidebarpadding;
1834 1854
1835 1855 .fieldset {
1836 1856 .left-label {
1837 1857 width: 13%;
1838 1858 }
1839 1859 .right-content {
1840 1860 width: 87%;
1841 1861 max-width: 100%;
1842 1862 }
1843 1863 .filename-label {
1844 1864 margin-top: 13px;
1845 1865 }
1846 1866 .commit-message-label {
1847 1867 margin-top: 4px;
1848 1868 }
1849 1869 .file-upload-input {
1850 1870 input {
1851 1871 display: none;
1852 1872 }
1853 1873 }
1854 1874 p {
1855 1875 margin-top: 5px;
1856 1876 }
1857 1877
1858 1878 }
1859 1879 .custom-path-link {
1860 1880 margin-left: 5px;
1861 1881 }
1862 1882 #commit {
1863 1883 resize: vertical;
1864 1884 }
1865 1885 }
1866 1886
1867 1887 .delete-file-preview {
1868 1888 max-height: 250px;
1869 1889 }
1870 1890
1871 1891 .new-file,
1872 1892 #filter_activate,
1873 1893 #filter_deactivate {
1874 1894 float: left;
1875 1895 margin: 0 0 0 15px;
1876 1896 }
1877 1897
1878 1898 h3.files_location{
1879 1899 line-height: 2.4em;
1880 1900 }
1881 1901
1882 1902 .browser-nav {
1883 1903 display: table;
1884 1904 margin-bottom: @space;
1885 1905
1886 1906
1887 1907 .info_box {
1888 1908 display: inline-table;
1889 1909 height: 2.5em;
1890 1910
1891 1911 .browser-cur-rev, .info_box_elem {
1892 1912 display: table-cell;
1893 1913 vertical-align: middle;
1894 1914 }
1895 1915
1896 1916 .info_box_elem {
1897 1917 border-top: @border-thickness solid @rcblue;
1898 1918 border-bottom: @border-thickness solid @rcblue;
1899 1919
1900 1920 #at_rev, a {
1901 1921 padding: 0.6em 0.9em;
1902 1922 margin: 0;
1903 1923 .box-shadow(none);
1904 1924 border: 0;
1905 1925 height: 12px;
1906 1926 }
1907 1927
1908 1928 input#at_rev {
1909 1929 max-width: 50px;
1910 1930 text-align: right;
1911 1931 }
1912 1932
1913 1933 &.previous {
1914 1934 border: @border-thickness solid @rcblue;
1915 1935 .disabled {
1916 1936 color: @grey4;
1917 1937 cursor: not-allowed;
1918 1938 }
1919 1939 }
1920 1940
1921 1941 &.next {
1922 1942 border: @border-thickness solid @rcblue;
1923 1943 .disabled {
1924 1944 color: @grey4;
1925 1945 cursor: not-allowed;
1926 1946 }
1927 1947 }
1928 1948 }
1929 1949
1930 1950 .browser-cur-rev {
1931 1951
1932 1952 span{
1933 1953 margin: 0;
1934 1954 color: @rcblue;
1935 1955 height: 12px;
1936 1956 display: inline-block;
1937 1957 padding: 0.7em 1em ;
1938 1958 border: @border-thickness solid @rcblue;
1939 1959 margin-right: @padding;
1940 1960 }
1941 1961 }
1942 1962 }
1943 1963
1944 1964 .search_activate {
1945 1965 display: table-cell;
1946 1966 vertical-align: middle;
1947 1967
1948 1968 input, label{
1949 1969 margin: 0;
1950 1970 padding: 0;
1951 1971 }
1952 1972
1953 1973 input{
1954 1974 margin-left: @textmargin;
1955 1975 }
1956 1976
1957 1977 }
1958 1978 }
1959 1979
1960 1980 .browser-cur-rev{
1961 1981 margin-bottom: @textmargin;
1962 1982 }
1963 1983
1964 1984 #node_filter_box_loading{
1965 1985 .info_text;
1966 1986 }
1967 1987
1968 1988 .browser-search {
1969 1989 margin: -25px 0px 5px 0px;
1970 1990 }
1971 1991
1972 1992 .node-filter {
1973 1993 font-size: @repo-title-fontsize;
1974 1994 padding: 4px 0px 0px 0px;
1975 1995
1976 1996 .node-filter-path {
1977 1997 float: left;
1978 1998 color: @grey4;
1979 1999 }
1980 2000 .node-filter-input {
1981 2001 float: left;
1982 2002 margin: -2px 0px 0px 2px;
1983 2003 input {
1984 2004 padding: 2px;
1985 2005 border: none;
1986 2006 font-size: @repo-title-fontsize;
1987 2007 }
1988 2008 }
1989 2009 }
1990 2010
1991 2011
1992 2012 .browser-result{
1993 2013 td a{
1994 2014 margin-left: 0.5em;
1995 2015 display: inline-block;
1996 2016
1997 2017 em{
1998 2018 font-family: @text-bold;
1999 2019 }
2000 2020 }
2001 2021 }
2002 2022
2003 2023 .browser-highlight{
2004 2024 background-color: @grey5-alpha;
2005 2025 }
2006 2026
2007 2027
2008 2028 // Search
2009 2029
2010 2030 .search-form{
2011 2031 #q {
2012 2032 width: @search-form-width;
2013 2033 }
2014 2034 .fields{
2015 2035 margin: 0 0 @space;
2016 2036 }
2017 2037
2018 2038 label{
2019 2039 display: inline-block;
2020 2040 margin-right: @textmargin;
2021 2041 padding-top: 0.25em;
2022 2042 }
2023 2043
2024 2044
2025 2045 .results{
2026 2046 clear: both;
2027 2047 margin: 0 0 @padding;
2028 2048 }
2029 2049 }
2030 2050
2031 2051 div.search-feedback-items {
2032 2052 display: inline-block;
2033 2053 padding:0px 0px 0px 96px;
2034 2054 }
2035 2055
2036 2056 div.search-code-body {
2037 2057 background-color: #ffffff; padding: 5px 0 5px 10px;
2038 2058 pre {
2039 2059 .match { background-color: #faffa6;}
2040 2060 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2041 2061 }
2042 2062 }
2043 2063
2044 2064 .expand_commit.search {
2045 2065 .show_more.open {
2046 2066 height: auto;
2047 2067 max-height: none;
2048 2068 }
2049 2069 }
2050 2070
2051 2071 .search-results {
2052 2072
2053 2073 h2 {
2054 2074 margin-bottom: 0;
2055 2075 }
2056 2076 .codeblock {
2057 2077 border: none;
2058 2078 background: transparent;
2059 2079 }
2060 2080
2061 2081 .codeblock-header {
2062 2082 border: none;
2063 2083 background: transparent;
2064 2084 }
2065 2085
2066 2086 .code-body {
2067 2087 border: @border-thickness solid @border-default-color;
2068 2088 .border-radius(@border-radius);
2069 2089 }
2070 2090
2071 2091 .td-commit {
2072 2092 &:extend(pre);
2073 2093 border-bottom: @border-thickness solid @border-default-color;
2074 2094 }
2075 2095
2076 2096 .message {
2077 2097 height: auto;
2078 2098 max-width: 350px;
2079 2099 white-space: normal;
2080 2100 text-overflow: initial;
2081 2101 overflow: visible;
2082 2102
2083 2103 .match { background-color: #faffa6;}
2084 2104 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2085 2105 }
2086 2106
2087 2107 }
2088 2108
2089 2109 table.rctable td.td-search-results div {
2090 2110 max-width: 100%;
2091 2111 }
2092 2112
2093 2113 #tip-box, .tip-box{
2094 2114 padding: @menupadding/2;
2095 2115 display: block;
2096 2116 border: @border-thickness solid @border-highlight-color;
2097 2117 .border-radius(@border-radius);
2098 2118 background-color: white;
2099 2119 z-index: 99;
2100 2120 white-space: pre-wrap;
2101 2121 }
2102 2122
2103 2123 #linktt {
2104 2124 width: 79px;
2105 2125 }
2106 2126
2107 2127 #help_kb .modal-content{
2108 2128 max-width: 750px;
2109 2129 margin: 10% auto;
2110 2130
2111 2131 table{
2112 2132 td,th{
2113 2133 border-bottom: none;
2114 2134 line-height: 2.5em;
2115 2135 }
2116 2136 th{
2117 2137 padding-bottom: @textmargin/2;
2118 2138 }
2119 2139 td.keys{
2120 2140 text-align: center;
2121 2141 }
2122 2142 }
2123 2143
2124 2144 .block-left{
2125 2145 width: 45%;
2126 2146 margin-right: 5%;
2127 2147 }
2128 2148 .modal-footer{
2129 2149 clear: both;
2130 2150 }
2131 2151 .key.tag{
2132 2152 padding: 0.5em;
2133 2153 background-color: @rcblue;
2134 2154 color: white;
2135 2155 border-color: @rcblue;
2136 2156 .box-shadow(none);
2137 2157 }
2138 2158 }
2139 2159
2140 2160
2141 2161
2142 2162 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2143 2163
2144 2164 @import 'statistics-graph';
2145 2165 @import 'tables';
2146 2166 @import 'forms';
2147 2167 @import 'diff';
2148 2168 @import 'summary';
2149 2169 @import 'navigation';
2150 2170
2151 2171 //--- SHOW/HIDE SECTIONS --//
2152 2172
2153 2173 .btn-collapse {
2154 2174 float: right;
2155 2175 text-align: right;
2156 2176 font-family: @text-light;
2157 2177 font-size: @basefontsize;
2158 2178 cursor: pointer;
2159 2179 border: none;
2160 2180 color: @rcblue;
2161 2181 }
2162 2182
2163 2183 table.rctable,
2164 2184 table.dataTable {
2165 2185 .btn-collapse {
2166 2186 float: right;
2167 2187 text-align: right;
2168 2188 }
2169 2189 }
2170 2190
2171 2191
2172 2192 // TODO: johbo: Fix for IE10, this avoids that we see a border
2173 2193 // and padding around checkboxes and radio boxes. Move to the right place,
2174 2194 // or better: Remove this once we did the form refactoring.
2175 2195 input[type=checkbox],
2176 2196 input[type=radio] {
2177 2197 padding: 0;
2178 2198 border: none;
2179 2199 }
2180 2200
2181 2201 .toggle-ajax-spinner{
2182 2202 height: 16px;
2183 2203 width: 16px;
2184 2204 }
@@ -1,53 +1,54 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
18 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
19 20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
20 21 pyroutes.register('gists', '/_admin/gists', []);
21 22 pyroutes.register('new_gist', '/_admin/gists/new', []);
22 23 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
23 24 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
24 25 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
25 26 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
26 27 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
27 28 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
28 29 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
29 30 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
30 31 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
31 32 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
32 33 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
33 34 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
34 35 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']);
35 36 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
36 37 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
37 38 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
38 39 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
39 40 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
40 41 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
41 42 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
42 43 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
43 44 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
44 45 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
45 46 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 47 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 48 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 49 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
49 50 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 51 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
51 52 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
52 53 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
53 54 }
@@ -1,47 +1,46 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s user group settings') % c.user_group.users_group_name}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.link_to(_('Admin'),h.url('admin_home'))}
13 13 &raquo;
14 14 ${h.link_to(_('User Groups'),h.url('users_groups'))}
15 15 &raquo;
16 16 ${c.user_group.users_group_name}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_nav()">
20 20 ${self.menu_items(active='admin')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <div class="box">
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28
29 29 ##main
30 30 <div class="sidebar-col-wrapper">
31 31 <div class="sidebar">
32 32 <ul class="nav nav-pills nav-stacked">
33 33 <li class="${'active' if c.active=='settings' else ''}"><a href="${h.url('edit_users_group', user_group_id=c.user_group.users_group_id)}">${_('Settings')}</a></li>
34 34 <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('edit_user_group_perms', user_group_id=c.user_group.users_group_id)}">${_('Permissions')}</a></li>
35 35 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_group_advanced', user_group_id=c.user_group.users_group_id)}">${_('Advanced')}</a></li>
36 36 <li class="${'active' if c.active=='global_perms' else ''}"><a href="${h.url('edit_user_group_global_perms', user_group_id=c.user_group.users_group_id)}">${_('Global permissions')}</a></li>
37 37 <li class="${'active' if c.active=='perms_summary' else ''}"><a href="${h.url('edit_user_group_perms_summary', user_group_id=c.user_group.users_group_id)}">${_('Permissions summary')}</a></li>
38 <li class="${'active' if c.active=='members' else ''}"><a href="${h.url('edit_user_group_members', user_group_id=c.user_group.users_group_id)}">${_('Members')}</a></li>
39 38 </ul>
40 39 </div>
41 40
42 41 <div class="main-content-full-width">
43 42 <%include file="/admin/user_groups/user_group_edit_${c.active}.html"/>
44 43 </div>
45 44 </div>
46 45 </div>
47 46 </%def>
@@ -1,144 +1,186 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.html"/>
3 3
4 4 <div class="panel panel-default">
5 5 <div class="panel-heading">
6 6 <h3 class="panel-title">${_('User Group: %s') % c.user_group.users_group_name}</h3>
7 7 </div>
8 8 <div class="panel-body">
9 9 ${h.secure_form(url('update_users_group', user_group_id=c.user_group.users_group_id),method='put', id='edit_users_group')}
10 10 <div class="form">
11 11 <!-- fields -->
12 12 <div class="fields">
13 13 <div class="field">
14 14 <div class="label">
15 15 <label for="users_group_name">${_('Group name')}:</label>
16 16 </div>
17 17 <div class="input">
18 18 ${h.text('users_group_name',class_='medium')}
19 19 </div>
20 20 </div>
21 21
22 22 <div class="field badged-field">
23 23 <div class="label">
24 24 <label for="user">${_('Owner')}:</label>
25 25 </div>
26 26 <div class="input">
27 27 <div class="badge-input-container">
28 28 <div class="user-badge">
29 29 ${base.gravatar_with_user(c.user_group.user.email, show_disabled=not c.user_group.user.active)}
30 30 </div>
31 31 <div class="badge-input-wrap">
32 32 ${h.text('user', class_="medium", autocomplete="off")}
33 33 </div>
34 34 </div>
35 35 <form:error name="user"/>
36 36 <p class="help-block">${_('Change owner of this user group.')}</p>
37 37 </div>
38 38 </div>
39 39
40 40 <div class="field">
41 41 <div class="label label-textarea">
42 42 <label for="user_group_description">${_('Description')}:</label>
43 43 </div>
44 44 <div class="textarea textarea-small editor">
45 45 ${h.textarea('user_group_description',cols=23,rows=5,class_="medium")}
46 46 <span class="help-block">${_('Short, optional description for this user group.')}</span>
47 47 </div>
48 48 </div>
49 49 <div class="field">
50 50 <div class="label label-checkbox">
51 51 <label for="users_group_active">${_('Active')}:</label>
52 52 </div>
53 53 <div class="checkboxes">
54 54 ${h.checkbox('users_group_active',value=True)}
55 55 </div>
56 56 </div>
57
57 58 <div class="field">
58 <div class="label">
59 <label for="users_group_active">${_('Search')}:</label>
60 ${h.text('from_user_group',
61 placeholder="user/usergroup",
62 class_="medium")}
59 <div class="label label-checkbox">
60 <label for="users_group_active">${_('Add members')}:</label>
63 61 </div>
64 <div class="select side-by-side-selector">
65 <div class="left-group">
66 <label class="text"><strong>${_('Chosen group members')}</strong></label>
67 ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)}
68 <div class="btn" id="remove_all_elements" >
69 ${_('Remove all elements')}
70 <i class="icon-chevron-right"></i>
62 <div class="input">
63 ${h.text('user_group_add_members', placeholder="user/usergroup", class_="medium")}
71 64 </div>
72 65 </div>
73 <div class="middle-group">
74 <i id="add_element" class="icon-chevron-left"></i>
75 <br />
76 <i id="remove_element" class="icon-chevron-right"></i>
66
67 <input type="hidden" name="__start__" value="user_group_members:sequence"/>
68 <table id="group_members_placeholder" class="rctable group_members">
69 <tr>
70 <th>${_('Username')}</th>
71 <th>${_('Action')}</th>
72 </tr>
73
74 % if c.group_members_obj:
75 % for user in c.group_members_obj:
76 <tr>
77 <td id="member_user_${user.user_id}" class="td-author">
78 <div class="group_member">
79 ${base.gravatar(user.email, 16)}
80 <span class="username user">${h.link_to(h.person(user), h.url( 'edit_user',user_id=user.user_id))}</span>
81 <input type="hidden" name="__start__" value="member:mapping">
82 <input type="hidden" name="member_user_id" value="${user.user_id}">
83 <input type="hidden" name="type" value="existing" id="member_${user.user_id}">
84 <input type="hidden" name="__end__" value="member:mapping">
77 85 </div>
78 <div class="right-group">
79 <label class="text" >${_('Available users')}
80 </label>
81 ${h.select('available_members',[],c.available_members,multiple=True,size=8,)}
82 <div class="btn" id="add_all_elements" >
83 <i class="icon-chevron-left"></i>${_('Add all elements')}
86 </td>
87 <td class="">
88 <div class="usergroup_member_remove action_button" onclick="removeUserGroupMember(${user.user_id}, true)" style="visibility: visible;">
89 <i class="icon-remove-sign"></i>
84 90 </div>
85 </div>
86 </div>
87 </div>
91 </td>
92 </tr>
93 % endfor
94
95 % else:
96 <tr><td colspan="2">${_('No members yet')}</td></tr>
97 % endif
98 </table>
99 <input type="hidden" name="__end__" value="user_group_members:sequence"/>
100
88 101 <div class="buttons">
89 102 ${h.submit('Save',_('Save'),class_="btn")}
90 103 </div>
91 104 </div>
92 105 </div>
93 106 ${h.end_form()}
94 107 </div>
95 108 </div>
96 109 <script>
97 110 $(document).ready(function(){
98 MultiSelectWidget('users_group_members','available_members','edit_users_group');
99
100 111 $("#group_parent_id").select2({
101 112 'containerCssClass': "drop-menu",
102 113 'dropdownCssClass': "drop-menu-dropdown",
103 114 'dropdownAutoWidth': true
104 115 });
105 116
106 $('#from_user_group').autocomplete({
117 removeUserGroupMember = function(userId){
118 $('#member_'+userId).val('remove');
119 $('#member_user_'+userId).addClass('to-delete');
120 };
121
122 $('#user_group_add_members').autocomplete({
107 123 serviceUrl: pyroutes.url('user_autocomplete_data'),
108 124 minChars:2,
109 125 maxHeight:400,
110 126 width:300,
111 127 deferRequestBy: 300, //miliseconds
112 128 showNoSuggestionNotice: true,
113 129 params: { user_groups:true },
114 130 formatResult: autocompleteFormatResult,
115 131 lookupFilter: autocompleteFilterResult,
116 132 onSelect: function(element, suggestion){
117 133
118 function preSelectUserIds(uids) {
119 $('#available_members').val(uids);
120 $('#users_group_members').val(uids);
134 function addMember(user, fromUserGroup) {
135 var gravatar = user.icon_link;
136 var username = user.value_display;
137 var userLink = pyroutes.url('edit_user', {"user_id": user.id});
138 var uid = user.id;
139
140 if (fromUserGroup) {
141 username = username +" "+ _gettext('(from usergroup {0})'.format(fromUserGroup))
142 }
143
144 var elem = $(
145 ('<tr>'+
146 '<td id="member_user_{6}" class="td-author td-author-new-entry">'+
147 '<div class="group_member">'+
148 '<img class="gravatar" src="{0}" height="16" width="16">'+
149 '<span class="username user"><a href="{1}">{2}</a></span>'+
150 '<input type="hidden" name="__start__" value="member:mapping">'+
151 '<input type="hidden" name="member_user_id" value="{3}">'+
152 '<input type="hidden" name="type" value="new" id="member_{4}">'+
153 '<input type="hidden" name="__end__" value="member:mapping">'+
154 '</div>'+
155 '</td>'+
156 '<td class="td-author-new-entry">'+
157 '<div class="usergroup_member_remove action_button" onclick="removeUserGroupMember({5}, true)" style="visibility: visible;">'+
158 '<i class="icon-remove-sign"></i>'+
159 '</div>'+
160 '</td>'+
161 '</tr>').format(gravatar, userLink, username,
162 uid, uid, uid, uid)
163 );
164 $('#group_members_placeholder').append(elem)
121 165 }
122 166
123 167 if (suggestion.value_type == 'user_group') {
124 168 $.getJSON(
125 169 pyroutes.url('edit_user_group_members',
126 170 {'user_group_id': suggestion.id}),
127 171 function(data) {
128 var uids = [];
129 172 $.each(data.members, function(idx, user) {
130 var userid = user[0],
131 username = user[1];
132 uids.push(userid.toString());
173 addMember(user, suggestion.value)
133 174 });
134 preSelectUserIds(uids)
135 175 }
136 176 );
137 177 } else if (suggestion.value_type == 'user') {
138 preSelectUserIds([suggestion.id.toString()]);
178 addMember(suggestion, null);
139 179 }
140 180 }
141 181 });
182
183
142 184 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
143 185 })
144 186 </script>
@@ -1,631 +1,619 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/debug_style/index.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 6 &raquo;
7 7 ${c.active}
8 8 </%def>
9 9
10 10
11 11 <%def name="real_main()">
12 12 <div class="box">
13 13 <div class="title">
14 14 ${self.breadcrumbs()}
15 15 </div>
16 16
17 17 <div class='sidebar-col-wrapper'>
18 18 ${self.sidebar()}
19 19
20 20 <div class="main-content">
21 21
22 22 <h2>Simple form elements (Depreciated)</h2>
23 23
24 24 <div class="bs-example">
25 25 <form method='post' action='none'>
26 26 <div class='form'>
27 27 <div class='fields'>
28 28
29 29 <div class='field'>
30 30 <div class='label'>
31 31 <label for='example_input_ro'>Example input readonly:</label>
32 32 </div>
33 33 <div class='input'>
34 34 <input id="example_input_ro" type="text" readonly="readonly" placeholder="Example input">
35 35 </div>
36 36 </div>
37 37
38 38 <div class='field'>
39 39 <div class='label'>
40 40 <label for='example_input'>Example text:</label>
41 41 </div>
42 42 <div class='input'>
43 43 <div class='text-as-placeholder'>
44 44 http://something.example.com
45 45 <span class="link" id="edit_clone_uri"><i class="icon-edit"></i>${_('edit')}</span>
46 46 </div>
47 47 <p class='help-block'>Help text in a paragraph.</p>
48 48 </div>
49 49 </div>
50 50
51 51 <div class='field'>
52 52 <div class='label'>
53 53 <label for='example_select'>Example select input:</label>
54 54 </div>
55 55 <div class="select">
56 56 <select id="example_select" >
57 57 <option value="#">${_('Templates...')}</option>
58 58 <option value="ga">Google Analytics</option>
59 59 <option value="clicky">Clicky</option>
60 60 <option value="server_announce">${_('Server Announcement')}</option>
61 61 </select>
62 62 </div>
63 63 </div>
64 64 <script>
65 65 $(document).ready(function() {
66 66 $('#example_select').select2({
67 67 containerCssClass: 'drop-menu',
68 68 dropdownCssClass: 'drop-menu-dropdown',
69 69 dropdownAutoWidth: true,
70 70 minimumResultsForSearch: -1
71 71 });
72 72 });
73 73 </script>
74 74
75 75 <div class='field'>
76 76 <div class='label'>
77 77 <label for='example_select'>Example select input:</label>
78 78 </div>
79 79 <div class="select">
80 80 text before
81 81 <select id="example_select2" >
82 82 <option value="#">${_('Templates...')}</option>
83 83 <option value="ga">Google Analytics</option>
84 84 <option value="clicky">Clicky</option>
85 85 <option value="server_announce">${_('Server Announcement')}</option>
86 86 </select>
87 87 text after
88 88 </div>
89 89 </div>
90 90 <script>
91 91 $(document).ready(function() {
92 92 $('#example_select2').select2({
93 93 containerCssClass: 'drop-menu',
94 94 dropdownCssClass: 'drop-menu-dropdown',
95 95 dropdownAutoWidth: true,
96 96 minimumResultsForSearch: -1
97 97 });
98 98 });
99 99 </script>
100 100
101 101 <div class='field'>
102 102 <div class='label'>
103 103 <label for='example_select'>Example select input with submenus:</label>
104 104 </div>
105 105 <div class="select">
106 106 <select id="example_select_sub" >
107 107 <option value="#">${_('Default')}</option>
108 108 <optgroup label="Group 1">
109 109 <option>Option 1.1</option>
110 110 </optgroup>
111 111 <optgroup label="Group 2">
112 112 <option>Option 2.1</option>
113 113 <option>Option 2.2</option>
114 114 </optgroup>
115 115 <optgroup label="Group 3" disabled>
116 116 <option>Option 3.1</option>
117 117 <option>Option 3.2</option>
118 118 <option>Option 3.3</option>
119 119 </optgroup>
120 120 </select>
121 121 </div>
122 122 </div>
123 123 <script>
124 124 $(document).ready(function() {
125 125 $('#example_select_sub').select2({
126 126 containerCssClass: 'drop-menu',
127 127 dropdownCssClass: 'drop-menu-dropdown',
128 128 dropdownAutoWidth: true,
129 129 minimumResultsForSearch: -1
130 130 });
131 131 });
132 132 </script>
133 133
134 134 <div class='field'>
135 135 <div class='label'>
136 136 <label for='example_checkbox'>Example checkbox:</label>
137 137 </div>
138 138 <div class="checkboxes">
139 139 <div class="checkbox">
140 140 <input id="example_checkbox" type="checkbox">
141 141 <label for="example_checkbox">Label of the checkbox</label>
142 142 </div>
143 143 </div>
144 144 </div>
145 145
146 146 <div class='field'>
147 147 <div class='label'>
148 148 <label for='example_checkboxes'>Example multiple checkboxes:</label>
149 149 </div>
150 150 <div class="checkboxes">
151 151 <div class="checkbox">
152 152 <input id="example_checkboxes_01" type="checkbox">
153 153 <label for="example_checkboxes_01">Label of the first checkbox</label>
154 154 </div>
155 155 <div class="checkbox">
156 156 <input id="example_checkboxes_02" type="checkbox">
157 157 <label for="example_checkboxes_02">Label of the first checkbox</label>
158 158 </div>
159 159 <div class="checkbox">
160 160 <input id="example_checkboxes_03" type="checkbox">
161 161 <label for="example_checkboxes_03">Label of the first checkbox</label>
162 162 </div>
163 163 </div>
164 164 </div>
165 165
166 166
167 167 <div class='field'>
168 168 <div class='label'>
169 169 <label for='example_checkboxes'>Example multiple checkboxes:</label>
170 170 </div>
171 171 ## TODO: johbo: This is off compared to the checkboxes
172 172 <div class="radios">
173 173 <label><input type="radio" checked="checked" value="hg.create.repository" name="default_repo_create" id="default_repo_create_hgcreaterepository">Enabled</label>
174 174 <label><input type="radio" value="hg.create.none" name="default_repo_create" id="default_repo_create_hgcreatenone">Disabled</label>
175 175 <span class="help-block">
176 176 Permission to allow repository creation. This includes ability to create
177 177 repositories in root level. If this option is disabled admin of
178 178 repository group can still create repositories inside that
179 179 repository group.
180 180 </span>
181 181 </div>
182 182 </div>
183 183
184 184 <div class="buttons">
185 185 <input type="submit" value="Save" id="example_save" class="btn">
186 186 <input type="reset" value="Reset" id="example_reset" class="btn">
187 187 </div>
188 188
189 189 </div>
190 190 </div>
191 191 </form>
192 192 </div>
193 193
194 194
195 195
196 196
197 197 <h2>Help text in form elements</h2>
198 198
199 199 <div class="bs-example">
200 200 <form method='post' action=''>
201 201 <div class='form'>
202 202 <div class='fields'>
203 203
204 204 <div class='field'>
205 205 <div class='label'>
206 206 <label for='02_example_input'>Example input label:</label>
207 207 </div>
208 208 <div class='input'>
209 209 <input id="02_example_input" type="text" placeholder="Placeholder text">
210 210 <span class="help-block">
211 211 Example help text for this input element. This help text
212 212 will be shown under the input element itself. It can be
213 213 so long that it will span multiple lines.
214 214 </span>
215 215
216 216 </div>
217 217 </div>
218 218
219 219 <div class='field'>
220 220 <div class='label'>
221 221 <label for='example_select_help'>Example select input:</label>
222 222 </div>
223 223 <div class="select">
224 224 <select id="example_select_help" >
225 225 <option value="#">${_('Templates...')}</option>
226 226 <option value="ga">Google Analytics</option>
227 227 <option value="clicky">Clicky</option>
228 228 <option value="server_announce">${_('Server Announcement')}</option>
229 229 </select>
230 230 <span class="help-block">
231 231 Example help text for this input element. This help text
232 232 will be shown under the input element itself. It can be
233 233 so long that it will span multiple lines.
234 234 </span>
235 235 </div>
236 236 </div>
237 237 <script>
238 238 $(document).ready(function() {
239 239 $('#example_select_help').select2({
240 240 containerCssClass: 'drop-menu',
241 241 dropdownCssClass: 'drop-menu-dropdown',
242 242 dropdownAutoWidth: true,
243 243 minimumResultsForSearch: -1
244 244 });
245 245 });
246 246 </script>
247 247
248 248 <div class='field'>
249 249 <div class='label'>
250 250 <label for='02_example_checkbox'>Example checkbox with help block:</label>
251 251 </div>
252 252 <div class="checkboxes">
253 253 <div class="checkbox">
254 254 <input id="02_example_checkbox" type="checkbox">
255 255 <label for="02_example_checkbox">Label of the checkbox</label>
256 256 </div>
257 257 <span class="help-block">
258 258 Example help text for this checkbox element. This help text
259 259 will be shown under the checkbox element itself. It can be
260 260 so long that it will span multiple lines.
261 261 </span>
262 262 </div>
263 263 </div>
264 264
265 265
266 266 <div class='field'>
267 267 <div class='label'>
268 268 <label>Multiple checkboxes:</label>
269 269 </div>
270 270 <div class="checkboxes">
271 271 <div class="checkbox">
272 272 <input id="02_example_checkboxes_01" type="checkbox">
273 273 <label for="02_example_checkboxes_01">Label of the first checkbox</label>
274 274 </div>
275 275 <div class="checkbox">
276 276 <input id="02_example_checkboxes_02" type="checkbox">
277 277 <label for="02_example_checkboxes_02">Label of the first checkbox</label>
278 278 </div>
279 279 <div class="checkbox">
280 280 <input id="02_example_checkboxes_03" type="checkbox">
281 281 <label for="02_example_checkboxes_03">Label of the first checkbox</label>
282 282 </div>
283 283 <span class="help-block">
284 284 Example help text for this checkbox element. This help text
285 285 will be shown under the checkbox element itself. It can be
286 286 so long that it will span multiple lines.
287 287 </span>
288 288 </div>
289 289 </div>
290 290
291 291
292 292 </div>
293 293 </div>
294 294 </form>
295 295 </div>
296 296
297 297
298 298
299 299
300 300 <h2>Error messages</h2>
301 301
302 302 <div class="bs-example">
303 303 <form method='post' action=''>
304 304 <div class='form'>
305 305 <div class='fields'>
306 306
307 307 <div class='field'>
308 308 <div class='label'>
309 309 <label for='04_example_input'>Example input label:</label>
310 310 </div>
311 311 <div class='input'>
312 312 <input id="04_example_input" type="text" placeholder="Example input"/>
313 313 <span class="error-message">
314 314 If form validation fails, some input fields can show an
315 315 error message close to the field.
316 316 </span>
317 317 </div>
318 318 </div>
319 319
320 320 </div>
321 321 </div>
322 322 </form>
323 323 </div>
324 324
325 325
326 326
327 327
328 328 <h2>Fields with buttons</h2>
329 329
330 330 <div class="bs-example">
331 331 <form method='post' action=''>
332 332 <div class='form'>
333 333 <div class='fields'>
334 334
335 335 <div class='field'>
336 336 <div class='label'>
337 337 <label for='05_example_input'>Example input label:</label>
338 338 </div>
339 339 <div class='input'>
340 340 <input id="05_example_input" type="text" readonly="readonly" placeholder="Example input">
341 341 <span class="btn btn-x">
342 342 <i class="icon-remove-sign"></i>
343 343 delete
344 344 </span>
345 345 <button class='btn btn-primary'>Action</button>
346 346 <span class="help-block">
347 347 Used if there is a list of values and the user can remove
348 348 single entries.
349 349 </span>
350 350 </div>
351 351 </div>
352 352
353 353
354 354 <div class='field'>
355 355 <div class='label'>
356 356 <label for='05_example_input'>Example input label:</label>
357 357 </div>
358 358 <div class='input'>
359 359 <input id="05_example_input" type="text" readonly="readonly" placeholder="Example input">
360 360 <span title="Click to unlock. You must restart RhodeCode in order to make this setting take effect."
361 361 class="tooltip" id="path_unlock"
362 362 tt_title="Click to unlock. You must restart RhodeCode in order to make this setting take effect.">
363 363 <div class="btn btn-default">
364 364 <span><i class="icon-lock" id="path_unlock_icon"></i></span>
365 365 </div>
366 366 <button class='btn btn-primary'>Action</button>
367 367 </span>
368 368 <span class="help-block">
369 369 Used together with locked fields, the user has to first
370 370 unlock and afterwards it is possible to change the value.
371 371 </span>
372 372 </div>
373 373 </div>
374 374
375 375 <div class='field'>
376 376 <div class='label'>
377 377 <label for='05_example_select'>Example input label:</label>
378 378 </div>
379 379 <div class="select">
380 380 <select id="05_example_select" >
381 381 <option value="#">${_('Templates...')}</option>
382 382 <option value="ga">Google Analytics</option>
383 383 <option value="clicky">Clicky</option>
384 384 <option value="server_announce">${_('Server Announcement')}</option>
385 385 </select>
386 386 <button class='btn btn-primary'>Action</button>
387 387 </div>
388 388 </div>
389 389 <script>
390 390 $(document).ready(function() {
391 391 $('#05_example_select').select2({
392 392 containerCssClass: 'drop-menu',
393 393 dropdownCssClass: 'drop-menu-dropdown',
394 394 dropdownAutoWidth: true
395 395 });
396 396 });
397 397 </script>
398 398
399 399 <div class='field'>
400 400 <div class='label'>
401 401 <label for='05_example_select2'>Example input label:</label>
402 402 </div>
403 403 <div class="select">
404 404 <span>Some text</span>
405 405 before
406 406 <select id="05_example_select2" >
407 407 <option value="#">${_('Templates...')}</option>
408 408 <option value="ga">Google Analytics</option>
409 409 <option value="clicky">Clicky</option>
410 410 <option value="server_announce">${_('Server Announcement')}</option>
411 411 </select>
412 412 after
413 413 <button class='btn btn-primary'>Action</button>
414 414 Some text
415 415 </div>
416 416 </div>
417 417 <script>
418 418 $(document).ready(function() {
419 419 $('#05_example_select2').select2({
420 420 containerCssClass: 'drop-menu',
421 421 dropdownCssClass: 'drop-menu-dropdown',
422 422 dropdownAutoWidth: true
423 423 });
424 424 });
425 425 </script>
426 426
427 427
428 428 </div>
429 429 </div>
430 430 </form>
431 431 </div>
432 432
433 433
434 434
435 435 <h2>Definition lists together with forms</h2>
436 436
437 437 <p>Some pages list values in a definition list. These lists align
438 438 properly with form elements on the same page.</p>
439 439
440 440 <div class="bs-example">
441 441
442 442 <dl class="dl-horizontal">
443 443 <dt>RhodeCode version:</dt>
444 444 <dd title="">3.0.0</dd>
445 445 <dt>License token:</dt>
446 446 <dd title=""><pre>abra-cada-bra1-rce3</pre></dd>
447 447 <dt>License issued to:</dt>
448 448 <dd title="">RhodeCode Trial (RhodeCode GmbH)</dd>
449 449 <dt>License issued on:</dt>
450 450 <dd title="">Sun, 07 Dec 2014 16:34:10</dd>
451 451 <dt>License expires on:</dt>
452 452 <dd title="">Fri, 05 Jun 2015 17:34:10</dd>
453 453 <dt>License type:</dt>
454 454 <dd title="">trial</dd>
455 455 <dt>License users limit:</dt>
456 456 <dd title="">20</dd>
457 457 </dl>
458 458
459 459 <form method='post' action=''>
460 460 <div class='form'>
461 461 <div class='fields'>
462 462
463 463 <div class='field'>
464 464 <div class='label'>
465 465 <label for='07_example_input'>Example input label:</label>
466 466 </div>
467 467 <div class='input'>
468 468 <input id="07_example_input" type="text" placeholder="Example input">
469 469 </div>
470 470 </div>
471 471
472 472 <div class="buttons">
473 473 <input type="submit" value="Save" id="07_example_save" class="btn">
474 474 <input type="reset" value="Reset" id="07_example_reset" class="btn">
475 475 </div>
476 476 </div>
477 477 </div>
478 478 </form>
479 479
480 480 </div>
481 481
482 482
483 483
484 484
485 485
486 486 <h2>Multi select widget</h2>
487 487
488 488 <p>This example shows two multi select widgets, one having no selects
489 489 currently. It is mixed up with other form elements to show the
490 490 magin effects.</p>
491 491
492 492 <div class="bs-example">
493 493
494 494 <form method='post' action=''>
495 495 <div class='form'>
496 496 <div class='fields'>
497 497
498 498 <div class='field'>
499 499 <div class='label'>
500 500 <label for='example_input'>Example input label:</label>
501 501 </div>
502 502 <div class='input'>
503 503 <input id="example_input" type="text" placeholder="Example input">
504 504 </div>
505 505 </div>
506 506
507 507 <div class="field">
508 508 <div class="label">
509 509 <label for="users_group_active">${_('Members')}:</label>
510 510 </div>
511 511 <div class="select side-by-side-selector">
512 512 <div class="left-group">
513 513 <label class="text" >${_('Chosen group members')}</label>
514 514 <select id="users_group_members" multiple size='8'>
515 515 <option value="#">${_('Templates...')}</option>
516 516 <option value="ga">Google Analytics</option>
517 517 <option value="clicky">Clicky</option>
518 518 <option value="server_announce">${_('Server Announcement')}</option>
519 519 <option value="#">${_('Templates...')}</option>
520 520 <option value="ga">Google Analytics</option>
521 521 <option value="clicky">Clicky</option>
522 522 <option value="server_announce">${_('Server Announcement')}</option>
523 523 </select>
524 524 <div class="btn" id="remove_all_elements" >
525 525 ${_('Remove all elements')}
526 526 <i class="icon-chevron-right"></i>
527 527 </div>
528 528 </div>
529 529 <div class="middle-group">
530 530 <i id="add_element" class="icon-chevron-left"></i>
531 531 <br />
532 532 <i id="remove_element" class="icon-chevron-right"></i>
533 533 </div>
534 534 <div class="right-group">
535 535 <label class="text" >${_('Available members')}</label>
536 536 <select id="available_members" multiple size='8'>
537 537 <option value="#">${_('Templates...')}</option>
538 538 <option value="ga">Google Analytics</option>
539 539 <option value="clicky">Clicky</option>
540 540 <option value="server_announce">${_('Server Announcement')}</option>
541 541 </select>
542 542 <div class="btn" id="add_all_elements" >
543 543 <i class="icon-chevron-left"></i>${_('Add all elements')}
544 544 </div>
545 545 </div>
546 546 </div>
547 547
548 <script>
549 $(document).ready(function(){
550 MultiSelectWidget('users_group_members','available_members','edit_users_group');
551 })
552 </script>
553
554 548 </div>
555 549
556 550 <div class='field'>
557 551 <div class='label'>
558 552 <label for='example_input'>Example input label:</label>
559 553 </div>
560 554 <div class='input'>
561 555 <input id="example_input" type="text" placeholder="Example input">
562 556 </div>
563 557 </div>
564 558
565 559 <div class="field">
566 560 <div class="label">
567 561 <label for="users_group_active2">Members with one side empty:</label>
568 562 </div>
569 563 <div class="select side-by-side-selector">
570 564 <div class="left-group">
571 565 <label class="text" >${_('Chosen group members')}</label>
572 566 <select id="users_group_members2" multiple size='8'>
573 567 </select>
574 568 <div class="btn" id="remove_all_elements2" >
575 569 ${_('Remove all elements')}
576 570 <i class="icon-chevron-right"></i>
577 571 </div>
578 572 </div>
579 573 <div class="middle-group">
580 574 <i id="add_element2" class="icon-chevron-left"></i>
581 575 <br />
582 576 <i id="remove_element2" class="icon-chevron-right"></i>
583 577 </div>
584 578 <div class="right-group">
585 579 <label class="text" >${_('Available members')}</label>
586 580 <select id="available_members2" multiple size='8'>
587 581 <option value="#">${_('Templates...')}</option>
588 582 <option value="ga">Google Analytics</option>
589 583 <option value="clicky">Clicky</option>
590 584 <option value="server_announce">${_('Server Announcement')}</option>
591 585 </select>
592 586 <div class="btn" id="add_all_elements2" >
593 587 <i class="icon-chevron-left"></i>${_('Add all elements')}
594 588 </div>
595 589 </div>
596 590 </div>
597 591
598 <script>
599 $(document).ready(function(){
600 MultiSelectWidget('users_group_members2','available_members','edit_users_group');
601 })
602 </script>
603
604 592 </div>
605 593
606 594 <div class='field'>
607 595 <div class='label'>
608 596 <label for='example_input'>Example input label:</label>
609 597 </div>
610 598 <div class='input'>
611 599 <input id="example_input" type="text" placeholder="Example input">
612 600 </div>
613 601 </div>
614 602
615 603 <div class="buttons">
616 604 <input type="submit" value="Save" id="07_example_save" class="btn">
617 605 <input type="reset" value="Reset" id="07_example_reset" class="btn">
618 606 </div>
619 607 </div>
620 608 </div>
621 609 </form>
622 610
623 611 </div>
624 612
625 613
626 614
627 615
628 616 </div>
629 617 </div> <!-- .main-content -->
630 618 </div> <!-- .box -->
631 619 </%def>
@@ -1,192 +1,233 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import (
24 24 TestController, url, assert_session_flash, link_to)
25 25 from rhodecode.model.db import User, UserGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 TEST_USER_GROUP = 'admins_test'
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 class TestAdminUsersGroupsController(TestController):
35 35
36 36 def test_index(self):
37 37 self.log_user()
38 38 response = self.app.get(url('users_groups'))
39 response.status_int == 200
39 assert response.status_int == 200
40 40
41 41 def test_create(self):
42 42 self.log_user()
43 43 users_group_name = TEST_USER_GROUP
44 44 response = self.app.post(url('users_groups'), {
45 45 'users_group_name': users_group_name,
46 46 'user_group_description': 'DESC',
47 47 'active': True,
48 48 'csrf_token': self.csrf_token})
49 49
50 50 user_group_link = link_to(
51 51 users_group_name,
52 52 url('edit_users_group',
53 53 user_group_id=UserGroup.get_by_group_name(
54 54 users_group_name).users_group_id))
55 55 assert_session_flash(
56 56 response,
57 57 'Created user group %s' % user_group_link)
58 58
59 59 def test_delete(self):
60 60 self.log_user()
61 61 users_group_name = TEST_USER_GROUP + 'another'
62 62 response = self.app.post(url('users_groups'), {
63 63 'users_group_name': users_group_name,
64 64 'user_group_description': 'DESC',
65 65 'active': True,
66 66 'csrf_token': self.csrf_token})
67 67
68 68 user_group_link = link_to(
69 69 users_group_name,
70 70 url('edit_users_group',
71 71 user_group_id=UserGroup.get_by_group_name(
72 72 users_group_name).users_group_id))
73 73 assert_session_flash(
74 74 response,
75 75 'Created user group %s' % user_group_link)
76 76
77 77 group = Session().query(UserGroup).filter(
78 78 UserGroup.users_group_name == users_group_name).one()
79 79
80 80 response = self.app.post(
81 81 url('delete_users_group', user_group_id=group.users_group_id),
82 82 params={'_method': 'delete', 'csrf_token': self.csrf_token})
83 83
84 84 group = Session().query(UserGroup).filter(
85 85 UserGroup.users_group_name == users_group_name).scalar()
86 86
87 87 assert group is None
88 88
89 89 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
90 90 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
91 91 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
92 92 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
93 93 ('', '', '', '', '', '', True, False),
94 94 ])
95 95 def test_global_perms_on_group(
96 96 self, repo_create, repo_create_write, user_group_create,
97 97 repo_group_create, fork_create, expect_error, expect_form_error,
98 98 inherit_default_permissions):
99 99 self.log_user()
100 100 users_group_name = TEST_USER_GROUP + 'another2'
101 101 response = self.app.post(url('users_groups'),
102 102 {'users_group_name': users_group_name,
103 103 'user_group_description': 'DESC',
104 104 'active': True,
105 105 'csrf_token': self.csrf_token})
106 106
107 107 ug = UserGroup.get_by_group_name(users_group_name)
108 108 user_group_link = link_to(
109 109 users_group_name,
110 110 url('edit_users_group', user_group_id=ug.users_group_id))
111 111 assert_session_flash(
112 112 response,
113 113 'Created user group %s' % user_group_link)
114 114 response.follow()
115 115
116 116 # ENABLE REPO CREATE ON A GROUP
117 117 perm_params = {
118 118 'inherit_default_permissions': False,
119 119 'default_repo_create': repo_create,
120 120 'default_repo_create_on_write': repo_create_write,
121 121 'default_user_group_create': user_group_create,
122 122 'default_repo_group_create': repo_group_create,
123 123 'default_fork_create': fork_create,
124 124 'default_inherit_default_permissions': inherit_default_permissions,
125 125
126 126 '_method': 'put',
127 127 'csrf_token': self.csrf_token,
128 128 }
129 129 response = self.app.post(
130 130 url('edit_user_group_global_perms',
131 131 user_group_id=ug.users_group_id),
132 132 params=perm_params)
133 133
134 134 if expect_form_error:
135 135 assert response.status_int == 200
136 136 response.mustcontain('Value must be one of')
137 137 else:
138 138 if expect_error:
139 139 msg = 'An error occurred during permissions saving'
140 140 else:
141 141 msg = 'User Group global permissions updated successfully'
142 142 ug = UserGroup.get_by_group_name(users_group_name)
143 143 del perm_params['_method']
144 144 del perm_params['csrf_token']
145 145 del perm_params['inherit_default_permissions']
146 146 assert perm_params == ug.get_default_perms()
147 147 assert_session_flash(response, msg)
148 148
149 149 fixture.destroy_user_group(users_group_name)
150 150
151 def test_edit(self):
151 def test_edit_autocomplete(self):
152 152 self.log_user()
153 153 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
154 154 response = self.app.get(
155 155 url('edit_users_group', user_group_id=ug.users_group_id))
156 156 fixture.destroy_user_group(TEST_USER_GROUP)
157 157
158 def test_edit_user_group_members(self):
158 def test_edit_user_group_autocomplete_members(self, xhr_header):
159 159 self.log_user()
160 160 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
161 161 response = self.app.get(
162 url('edit_user_group_members', user_group_id=ug.users_group_id))
163 response.mustcontain('No members yet')
162 url('edit_user_group_members', user_group_id=ug.users_group_id),
163 extra_environ=xhr_header)
164
165 assert response.body == '{"members": []}'
164 166 fixture.destroy_user_group(TEST_USER_GROUP)
165 167
166 168 def test_usergroup_escape(self):
167 169 user = User.get_by_username('test_admin')
168 170 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
169 171 user.lastname = (
170 172 '<img src="/image2" onload="alert(\'Hello, World!\');">')
171 173 Session().add(user)
172 174 Session().commit()
173 175
174 176 self.log_user()
175 177 users_group_name = 'samplegroup'
176 178 data = {
177 179 'users_group_name': users_group_name,
178 180 'user_group_description': (
179 181 '<strong onload="alert();">DESC</strong>'),
180 182 'active': True,
181 183 'csrf_token': self.csrf_token
182 184 }
183 185
184 response = self.app.post(url('users_groups'), data)
186 self.app.post(url('users_groups'), data)
185 187 response = self.app.get(url('users_groups'))
186 188
187 189 response.mustcontain(
188 190 '&lt;strong onload=&#34;alert();&#34;&gt;'
189 191 'DESC&lt;/strong&gt;')
190 192 response.mustcontain(
191 193 '&lt;img src=&#34;/image2&#34; onload=&#34;'
192 194 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
195
196 def test_update_members_from_user_ids(self, user_regular):
197 uid = user_regular.user_id
198 username = user_regular.username
199 self.log_user()
200
201 user_group = fixture.create_user_group('test_gr_ids')
202 assert user_group.members == []
203 assert user_group.user != user_regular
204 expected_active_state = not user_group.users_group_active
205
206 form_data = [
207 ('csrf_token', self.csrf_token),
208 ('_method', 'put'),
209 ('user', username),
210 ('users_group_name', 'changed_name'),
211 ('users_group_active', expected_active_state),
212 ('user_group_description', 'changed_description'),
213
214 ('__start__', 'user_group_members:sequence'),
215 ('__start__', 'member:mapping'),
216 ('member_user_id', uid),
217 ('type', 'existing'),
218 ('__end__', 'member:mapping'),
219 ('__end__', 'user_group_members:sequence'),
220 ]
221 ugid = user_group.users_group_id
222 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
223
224 user_group = UserGroup.get(ugid)
225 assert user_group
226
227 assert user_group.members[0].user_id == uid
228 assert user_group.user_id == uid
229 assert 'changed_name' in user_group.users_group_name
230 assert 'changed_description' in user_group.user_group_description
231 assert user_group.users_group_active == expected_active_state
232
233 fixture.destroy_user_group(user_group)
@@ -1,181 +1,161 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.model.db import User
25 25 from rhodecode.tests import TEST_USER_REGULAR_LOGIN
26 26 from rhodecode.tests.fixture import Fixture
27 27 from rhodecode.model.user_group import UserGroupModel
28 28 from rhodecode.model.meta import Session
29 29
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 def teardown_module(self):
35 35 _delete_all_user_groups()
36 36
37 37
38 38 @pytest.mark.parametrize(
39 39 "pre_existing, regular_should_be, external_should_be, groups, "
40 40 "expected", [
41 41 ([], [], [], [], []),
42 42 # no changes of regular
43 43 ([], ['regular'], [], [], ['regular']),
44 44 # not added to regular group
45 45 (['some_other'], [], [], ['some_other'], []),
46 46 (
47 47 [], ['regular'], ['container'], ['container'],
48 48 ['regular', 'container']
49 49 ),
50 50 (
51 51 [], ['regular'], [], ['container', 'container2'],
52 52 ['regular', 'container', 'container2']
53 53 ),
54 54 # remove not used
55 55 ([], ['regular'], ['other'], [], ['regular']),
56 56 (
57 57 ['some_other'], ['regular'], ['other', 'container'],
58 58 ['container', 'container2'],
59 59 ['regular', 'container', 'container2']
60 60 ),
61 61 ])
62 62 def test_enforce_groups(pre_existing, regular_should_be,
63 63 external_should_be, groups, expected, backend_hg):
64 64 # TODO: anderson: adding backend_hg fixture so it sets up the database
65 65 # for when running this file alone
66 66 _delete_all_user_groups()
67 67
68 68 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
69 69 for gr in pre_existing:
70 70 gr = fixture.create_user_group(gr)
71 71 Session().commit()
72 72
73 73 # make sure use is just in those groups
74 74 for gr in regular_should_be:
75 75 gr = fixture.create_user_group(gr)
76 76 Session().commit()
77 77 UserGroupModel().add_user_to_group(gr, user)
78 78 Session().commit()
79 79
80 80 # now special external groups created by auth plugins
81 81 for gr in external_should_be:
82 82 gr = fixture.create_user_group(
83 83 gr, user_group_data={'extern_type': 'container'})
84 84 Session().commit()
85 85 UserGroupModel().add_user_to_group(gr, user)
86 86 Session().commit()
87 87
88 88 UserGroupModel().enforce_groups(user, groups, 'container')
89 89 Session().commit()
90 90
91 91 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
92 92 in_groups = user.group_member
93 93
94 94 expected.sort()
95 95 assert (
96 96 expected == sorted(x.users_group.users_group_name for x in in_groups))
97 97
98 98
99 99 def _delete_all_user_groups():
100 100 for gr in UserGroupModel.get_all():
101 101 fixture.destroy_user_group(gr)
102 102 Session().commit()
103 103
104 104
105 105 def test_add_and_remove_user_from_group(user_regular, user_util):
106 106 user_group = user_util.create_user_group()
107 107 assert user_group.members == []
108 108 UserGroupModel().add_user_to_group(user_group, user_regular)
109 109 Session().commit()
110 110 assert user_group.members[0].user == user_regular
111 111 UserGroupModel().remove_user_from_group(user_group, user_regular)
112 112 Session().commit()
113 113 assert user_group.members == []
114 114
115 115
116 @pytest.mark.parametrize(
117 'data, expected', [
118 ("1", [1]), (["1", "2"], [1, 2])
119 ]
120 )
116 @pytest.mark.parametrize('data, expected', [
117 ([], []),
118 ([{"member_user_id": 1, "type": "new"}], [1]),
119 ([{"member_user_id": 1, "type": "new"},
120 {"member_user_id": 1, "type": "existing"}], [1]),
121 ([{"member_user_id": 1, "type": "new"},
122 {"member_user_id": 2, "type": "new"},
123 {"member_user_id": 3, "type": "remove"}], [1, 2])
124 ])
121 125 def test_clean_members_data(data, expected):
122 126 cleaned = UserGroupModel()._clean_members_data(data)
123 127 assert cleaned == expected
124 128
125 129
126 def test_update_members_from_user_ids(user_regular, user_util):
127 user_group = user_util.create_user_group()
128 assert user_group.members == []
129 assert user_group.user != user_regular
130 expected_active_state = not user_group.users_group_active
131
132 form_data = {
133 'users_group_members': str(user_regular.user_id),
134 'user': str(user_regular.username),
135 'users_group_name': 'changed_name',
136 'users_group_active': expected_active_state,
137 'user_group_description': 'changed_description'
138 }
139
140 UserGroupModel().update(user_group, form_data)
141 assert user_group.members[0].user_id == user_regular.user_id
142 assert user_group.user_id == user_regular.user_id
143 assert 'changed_name' in user_group.users_group_name
144 assert 'changed_description' in user_group.user_group_description
145 assert user_group.users_group_active == expected_active_state
146 # Ignore changes on the test
147 Session().rollback()
148
149
150 130 def _create_test_members():
151 131 members = []
152 132 for member_number in range(3):
153 133 member = mock.Mock()
154 134 member.user_id = member_number + 1
155 135 member.user.user_id = member_number + 1
156 136 members.append(member)
157 137 return members
158 138
159 139
160 140 def test_get_added_and_removed_users():
161 141 members = _create_test_members()
162 142 mock_user_group = mock.Mock()
163 143 mock_user_group.members = [members[0], members[1]]
164 144 new_users_list = [members[1].user.user_id, members[2].user.user_id]
165 145 model = UserGroupModel()
166 146
167 147 added, removed = model._get_added_and_removed_user_ids(
168 148 mock_user_group, new_users_list)
169 149
170 150 assert added == [members[2].user.user_id]
171 151 assert removed == [members[0].user.user_id]
172 152
173 153
174 154 def test_set_users_as_members_and_find_user_in_group(
175 155 user_util, user_regular, user_admin):
176 156 user_group = user_util.create_user_group()
177 157 assert len(user_group.members) == 0
178 158 user_list = [user_regular.user_id, user_admin.user_id]
179 159 UserGroupModel()._set_users_as_members(user_group, user_list)
180 160 assert len(user_group.members) == 2
181 161 assert UserGroupModel()._find_user_in_group(user_regular, user_group)
@@ -1,1791 +1,1792 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import collections
22 22 import datetime
23 23 import hashlib
24 24 import os
25 25 import re
26 26 import pprint
27 27 import shutil
28 28 import socket
29 29 import subprocess32
30 30 import time
31 31 import uuid
32 32
33 33 import mock
34 34 import pyramid.testing
35 35 import pytest
36 36 import colander
37 37 import requests
38 38 from webtest.app import TestApp
39 39
40 40 import rhodecode
41 from rhodecode.lib.utils2 import AttributeDict
41 42 from rhodecode.model.changeset_status import ChangesetStatusModel
42 43 from rhodecode.model.comment import ChangesetCommentsModel
43 44 from rhodecode.model.db import (
44 45 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
45 46 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
46 47 from rhodecode.model.meta import Session
47 48 from rhodecode.model.pull_request import PullRequestModel
48 49 from rhodecode.model.repo import RepoModel
49 50 from rhodecode.model.repo_group import RepoGroupModel
50 51 from rhodecode.model.user import UserModel
51 52 from rhodecode.model.settings import VcsSettingsModel
52 53 from rhodecode.model.user_group import UserGroupModel
53 54 from rhodecode.model.integration import IntegrationModel
54 55 from rhodecode.integrations import integration_type_registry
55 56 from rhodecode.integrations.types.base import IntegrationTypeBase
56 57 from rhodecode.lib.utils import repo2db_mapper
57 58 from rhodecode.lib.vcs import create_vcsserver_proxy
58 59 from rhodecode.lib.vcs.backends import get_backend
59 60 from rhodecode.lib.vcs.nodes import FileNode
60 61 from rhodecode.tests import (
61 62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 64 TEST_USER_REGULAR_PASS)
64 65 from rhodecode.tests.fixture import Fixture
65 66
66 67
67 68 def _split_comma(value):
68 69 return value.split(',')
69 70
70 71
71 72 def pytest_addoption(parser):
72 73 parser.addoption(
73 74 '--keep-tmp-path', action='store_true',
74 75 help="Keep the test temporary directories")
75 76 parser.addoption(
76 77 '--backends', action='store', type=_split_comma,
77 78 default=['git', 'hg', 'svn'],
78 79 help="Select which backends to test for backend specific tests.")
79 80 parser.addoption(
80 81 '--dbs', action='store', type=_split_comma,
81 82 default=['sqlite'],
82 83 help="Select which database to test for database specific tests. "
83 84 "Possible options are sqlite,postgres,mysql")
84 85 parser.addoption(
85 86 '--appenlight', '--ae', action='store_true',
86 87 help="Track statistics in appenlight.")
87 88 parser.addoption(
88 89 '--appenlight-api-key', '--ae-key',
89 90 help="API key for Appenlight.")
90 91 parser.addoption(
91 92 '--appenlight-url', '--ae-url',
92 93 default="https://ae.rhodecode.com",
93 94 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
94 95 parser.addoption(
95 96 '--sqlite-connection-string', action='store',
96 97 default='', help="Connection string for the dbs tests with SQLite")
97 98 parser.addoption(
98 99 '--postgres-connection-string', action='store',
99 100 default='', help="Connection string for the dbs tests with Postgres")
100 101 parser.addoption(
101 102 '--mysql-connection-string', action='store',
102 103 default='', help="Connection string for the dbs tests with MySQL")
103 104 parser.addoption(
104 105 '--repeat', type=int, default=100,
105 106 help="Number of repetitions in performance tests.")
106 107
107 108
108 109 def pytest_configure(config):
109 110 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
110 111 from rhodecode.config import patches
111 112 patches.kombu_1_5_1_python_2_7_11()
112 113
113 114
114 115 def pytest_collection_modifyitems(session, config, items):
115 116 # nottest marked, compare nose, used for transition from nose to pytest
116 117 remaining = [
117 118 i for i in items if getattr(i.obj, '__test__', True)]
118 119 items[:] = remaining
119 120
120 121
121 122 def pytest_generate_tests(metafunc):
122 123 # Support test generation based on --backend parameter
123 124 if 'backend_alias' in metafunc.fixturenames:
124 125 backends = get_backends_from_metafunc(metafunc)
125 126 scope = None
126 127 if not backends:
127 128 pytest.skip("Not enabled for any of selected backends")
128 129 metafunc.parametrize('backend_alias', backends, scope=scope)
129 130 elif hasattr(metafunc.function, 'backends'):
130 131 backends = get_backends_from_metafunc(metafunc)
131 132 if not backends:
132 133 pytest.skip("Not enabled for any of selected backends")
133 134
134 135
135 136 def get_backends_from_metafunc(metafunc):
136 137 requested_backends = set(metafunc.config.getoption('--backends'))
137 138 if hasattr(metafunc.function, 'backends'):
138 139 # Supported backends by this test function, created from
139 140 # pytest.mark.backends
140 141 backends = metafunc.function.backends.args
141 142 elif hasattr(metafunc.cls, 'backend_alias'):
142 143 # Support class attribute "backend_alias", this is mainly
143 144 # for legacy reasons for tests not yet using pytest.mark.backends
144 145 backends = [metafunc.cls.backend_alias]
145 146 else:
146 147 backends = metafunc.config.getoption('--backends')
147 148 return requested_backends.intersection(backends)
148 149
149 150
150 151 @pytest.fixture(scope='session', autouse=True)
151 152 def activate_example_rcextensions(request):
152 153 """
153 154 Patch in an example rcextensions module which verifies passed in kwargs.
154 155 """
155 156 from rhodecode.tests.other import example_rcextensions
156 157
157 158 old_extensions = rhodecode.EXTENSIONS
158 159 rhodecode.EXTENSIONS = example_rcextensions
159 160
160 161 @request.addfinalizer
161 162 def cleanup():
162 163 rhodecode.EXTENSIONS = old_extensions
163 164
164 165
165 166 @pytest.fixture
166 167 def capture_rcextensions():
167 168 """
168 169 Returns the recorded calls to entry points in rcextensions.
169 170 """
170 171 calls = rhodecode.EXTENSIONS.calls
171 172 calls.clear()
172 173 # Note: At this moment, it is still the empty dict, but that will
173 174 # be filled during the test run and since it is a reference this
174 175 # is enough to make it work.
175 176 return calls
176 177
177 178
178 179 @pytest.fixture(scope='session')
179 180 def http_environ_session():
180 181 """
181 182 Allow to use "http_environ" in session scope.
182 183 """
183 184 return http_environ(
184 185 http_host_stub=http_host_stub())
185 186
186 187
187 188 @pytest.fixture
188 189 def http_host_stub():
189 190 """
190 191 Value of HTTP_HOST in the test run.
191 192 """
192 193 return 'test.example.com:80'
193 194
194 195
195 196 @pytest.fixture
196 197 def http_environ(http_host_stub):
197 198 """
198 199 HTTP extra environ keys.
199 200
200 201 User by the test application and as well for setting up the pylons
201 202 environment. In the case of the fixture "app" it should be possible
202 203 to override this for a specific test case.
203 204 """
204 205 return {
205 206 'SERVER_NAME': http_host_stub.split(':')[0],
206 207 'SERVER_PORT': http_host_stub.split(':')[1],
207 208 'HTTP_HOST': http_host_stub,
208 209 }
209 210
210 211
211 212 @pytest.fixture(scope='function')
212 213 def app(request, pylonsapp, http_environ):
213 214 app = TestApp(
214 215 pylonsapp,
215 216 extra_environ=http_environ)
216 217 if request.cls:
217 218 request.cls.app = app
218 219 return app
219 220
220 221
221 222 @pytest.fixture(scope='session')
222 223 def app_settings(pylonsapp, pylons_config):
223 224 """
224 225 Settings dictionary used to create the app.
225 226
226 227 Parses the ini file and passes the result through the sanitize and apply
227 228 defaults mechanism in `rhodecode.config.middleware`.
228 229 """
229 230 from paste.deploy.loadwsgi import loadcontext, APP
230 231 from rhodecode.config.middleware import (
231 232 sanitize_settings_and_apply_defaults)
232 233 context = loadcontext(APP, 'config:' + pylons_config)
233 234 settings = sanitize_settings_and_apply_defaults(context.config())
234 235 return settings
235 236
236 237
237 238 @pytest.fixture(scope='session')
238 239 def db(app_settings):
239 240 """
240 241 Initializes the database connection.
241 242
242 243 It uses the same settings which are used to create the ``pylonsapp`` or
243 244 ``app`` fixtures.
244 245 """
245 246 from rhodecode.config.utils import initialize_database
246 247 initialize_database(app_settings)
247 248
248 249
249 250 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
250 251
251 252
252 253 def _autologin_user(app, *args):
253 254 session = login_user_session(app, *args)
254 255 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
255 256 return LoginData(csrf_token, session['rhodecode_user'])
256 257
257 258
258 259 @pytest.fixture
259 260 def autologin_user(app):
260 261 """
261 262 Utility fixture which makes sure that the admin user is logged in
262 263 """
263 264 return _autologin_user(app)
264 265
265 266
266 267 @pytest.fixture
267 268 def autologin_regular_user(app):
268 269 """
269 270 Utility fixture which makes sure that the regular user is logged in
270 271 """
271 272 return _autologin_user(
272 273 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
273 274
274 275
275 276 @pytest.fixture(scope='function')
276 277 def csrf_token(request, autologin_user):
277 278 return autologin_user.csrf_token
278 279
279 280
280 281 @pytest.fixture(scope='function')
281 282 def xhr_header(request):
282 283 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
283 284
284 285
285 286 @pytest.fixture
286 287 def real_crypto_backend(monkeypatch):
287 288 """
288 289 Switch the production crypto backend on for this test.
289 290
290 291 During the test run the crypto backend is replaced with a faster
291 292 implementation based on the MD5 algorithm.
292 293 """
293 294 monkeypatch.setattr(rhodecode, 'is_test', False)
294 295
295 296
296 297 @pytest.fixture(scope='class')
297 298 def index_location(request, pylonsapp):
298 299 index_location = pylonsapp.config['app_conf']['search.location']
299 300 if request.cls:
300 301 request.cls.index_location = index_location
301 302 return index_location
302 303
303 304
304 305 @pytest.fixture(scope='session', autouse=True)
305 306 def tests_tmp_path(request):
306 307 """
307 308 Create temporary directory to be used during the test session.
308 309 """
309 310 if not os.path.exists(TESTS_TMP_PATH):
310 311 os.makedirs(TESTS_TMP_PATH)
311 312
312 313 if not request.config.getoption('--keep-tmp-path'):
313 314 @request.addfinalizer
314 315 def remove_tmp_path():
315 316 shutil.rmtree(TESTS_TMP_PATH)
316 317
317 318 return TESTS_TMP_PATH
318 319
319 320
320 321 @pytest.fixture(scope='session', autouse=True)
321 322 def patch_pyro_request_scope_proxy_factory(request):
322 323 """
323 324 Patch the pyro proxy factory to always use the same dummy request object
324 325 when under test. This will return the same pyro proxy on every call.
325 326 """
326 327 dummy_request = pyramid.testing.DummyRequest()
327 328
328 329 def mocked_call(self, request=None):
329 330 return self.getProxy(request=dummy_request)
330 331
331 332 patcher = mock.patch(
332 333 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
333 334 new=mocked_call)
334 335 patcher.start()
335 336
336 337 @request.addfinalizer
337 338 def undo_patching():
338 339 patcher.stop()
339 340
340 341
341 342 @pytest.fixture
342 343 def test_repo_group(request):
343 344 """
344 345 Create a temporary repository group, and destroy it after
345 346 usage automatically
346 347 """
347 348 fixture = Fixture()
348 349 repogroupid = 'test_repo_group_%s' % int(time.time())
349 350 repo_group = fixture.create_repo_group(repogroupid)
350 351
351 352 def _cleanup():
352 353 fixture.destroy_repo_group(repogroupid)
353 354
354 355 request.addfinalizer(_cleanup)
355 356 return repo_group
356 357
357 358
358 359 @pytest.fixture
359 360 def test_user_group(request):
360 361 """
361 362 Create a temporary user group, and destroy it after
362 363 usage automatically
363 364 """
364 365 fixture = Fixture()
365 366 usergroupid = 'test_user_group_%s' % int(time.time())
366 367 user_group = fixture.create_user_group(usergroupid)
367 368
368 369 def _cleanup():
369 370 fixture.destroy_user_group(user_group)
370 371
371 372 request.addfinalizer(_cleanup)
372 373 return user_group
373 374
374 375
375 376 @pytest.fixture(scope='session')
376 377 def test_repo(request):
377 378 container = TestRepoContainer()
378 379 request.addfinalizer(container._cleanup)
379 380 return container
380 381
381 382
382 383 class TestRepoContainer(object):
383 384 """
384 385 Container for test repositories which are used read only.
385 386
386 387 Repositories will be created on demand and re-used during the lifetime
387 388 of this object.
388 389
389 390 Usage to get the svn test repository "minimal"::
390 391
391 392 test_repo = TestContainer()
392 393 repo = test_repo('minimal', 'svn')
393 394
394 395 """
395 396
396 397 dump_extractors = {
397 398 'git': utils.extract_git_repo_from_dump,
398 399 'hg': utils.extract_hg_repo_from_dump,
399 400 'svn': utils.extract_svn_repo_from_dump,
400 401 }
401 402
402 403 def __init__(self):
403 404 self._cleanup_repos = []
404 405 self._fixture = Fixture()
405 406 self._repos = {}
406 407
407 408 def __call__(self, dump_name, backend_alias):
408 409 key = (dump_name, backend_alias)
409 410 if key not in self._repos:
410 411 repo = self._create_repo(dump_name, backend_alias)
411 412 self._repos[key] = repo.repo_id
412 413 return Repository.get(self._repos[key])
413 414
414 415 def _create_repo(self, dump_name, backend_alias):
415 416 repo_name = '%s-%s' % (backend_alias, dump_name)
416 417 backend_class = get_backend(backend_alias)
417 418 dump_extractor = self.dump_extractors[backend_alias]
418 419 repo_path = dump_extractor(dump_name, repo_name)
419 420 vcs_repo = backend_class(repo_path)
420 421 repo2db_mapper({repo_name: vcs_repo})
421 422 repo = RepoModel().get_by_repo_name(repo_name)
422 423 self._cleanup_repos.append(repo_name)
423 424 return repo
424 425
425 426 def _cleanup(self):
426 427 for repo_name in reversed(self._cleanup_repos):
427 428 self._fixture.destroy_repo(repo_name)
428 429
429 430
430 431 @pytest.fixture
431 432 def backend(request, backend_alias, pylonsapp, test_repo):
432 433 """
433 434 Parametrized fixture which represents a single backend implementation.
434 435
435 436 It respects the option `--backends` to focus the test run on specific
436 437 backend implementations.
437 438
438 439 It also supports `pytest.mark.xfail_backends` to mark tests as failing
439 440 for specific backends. This is intended as a utility for incremental
440 441 development of a new backend implementation.
441 442 """
442 443 if backend_alias not in request.config.getoption('--backends'):
443 444 pytest.skip("Backend %s not selected." % (backend_alias, ))
444 445
445 446 utils.check_xfail_backends(request.node, backend_alias)
446 447 utils.check_skip_backends(request.node, backend_alias)
447 448
448 449 repo_name = 'vcs_test_%s' % (backend_alias, )
449 450 backend = Backend(
450 451 alias=backend_alias,
451 452 repo_name=repo_name,
452 453 test_name=request.node.name,
453 454 test_repo_container=test_repo)
454 455 request.addfinalizer(backend.cleanup)
455 456 return backend
456 457
457 458
458 459 @pytest.fixture
459 460 def backend_git(request, pylonsapp, test_repo):
460 461 return backend(request, 'git', pylonsapp, test_repo)
461 462
462 463
463 464 @pytest.fixture
464 465 def backend_hg(request, pylonsapp, test_repo):
465 466 return backend(request, 'hg', pylonsapp, test_repo)
466 467
467 468
468 469 @pytest.fixture
469 470 def backend_svn(request, pylonsapp, test_repo):
470 471 return backend(request, 'svn', pylonsapp, test_repo)
471 472
472 473
473 474 @pytest.fixture
474 475 def backend_random(backend_git):
475 476 """
476 477 Use this to express that your tests need "a backend.
477 478
478 479 A few of our tests need a backend, so that we can run the code. This
479 480 fixture is intended to be used for such cases. It will pick one of the
480 481 backends and run the tests.
481 482
482 483 The fixture `backend` would run the test multiple times for each
483 484 available backend which is a pure waste of time if the test is
484 485 independent of the backend type.
485 486 """
486 487 # TODO: johbo: Change this to pick a random backend
487 488 return backend_git
488 489
489 490
490 491 @pytest.fixture
491 492 def backend_stub(backend_git):
492 493 """
493 494 Use this to express that your tests need a backend stub
494 495
495 496 TODO: mikhail: Implement a real stub logic instead of returning
496 497 a git backend
497 498 """
498 499 return backend_git
499 500
500 501
501 502 @pytest.fixture
502 503 def repo_stub(backend_stub):
503 504 """
504 505 Use this to express that your tests need a repository stub
505 506 """
506 507 return backend_stub.create_repo()
507 508
508 509
509 510 class Backend(object):
510 511 """
511 512 Represents the test configuration for one supported backend
512 513
513 514 Provides easy access to different test repositories based on
514 515 `__getitem__`. Such repositories will only be created once per test
515 516 session.
516 517 """
517 518
518 519 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
519 520 _master_repo = None
520 521 _commit_ids = {}
521 522
522 523 def __init__(self, alias, repo_name, test_name, test_repo_container):
523 524 self.alias = alias
524 525 self.repo_name = repo_name
525 526 self._cleanup_repos = []
526 527 self._test_name = test_name
527 528 self._test_repo_container = test_repo_container
528 529 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
529 530 # Fixture will survive in the end.
530 531 self._fixture = Fixture()
531 532
532 533 def __getitem__(self, key):
533 534 return self._test_repo_container(key, self.alias)
534 535
535 536 @property
536 537 def repo(self):
537 538 """
538 539 Returns the "current" repository. This is the vcs_test repo or the
539 540 last repo which has been created with `create_repo`.
540 541 """
541 542 from rhodecode.model.db import Repository
542 543 return Repository.get_by_repo_name(self.repo_name)
543 544
544 545 @property
545 546 def default_branch_name(self):
546 547 VcsRepository = get_backend(self.alias)
547 548 return VcsRepository.DEFAULT_BRANCH_NAME
548 549
549 550 @property
550 551 def default_head_id(self):
551 552 """
552 553 Returns the default head id of the underlying backend.
553 554
554 555 This will be the default branch name in case the backend does have a
555 556 default branch. In the other cases it will point to a valid head
556 557 which can serve as the base to create a new commit on top of it.
557 558 """
558 559 vcsrepo = self.repo.scm_instance()
559 560 head_id = (
560 561 vcsrepo.DEFAULT_BRANCH_NAME or
561 562 vcsrepo.commit_ids[-1])
562 563 return head_id
563 564
564 565 @property
565 566 def commit_ids(self):
566 567 """
567 568 Returns the list of commits for the last created repository
568 569 """
569 570 return self._commit_ids
570 571
571 572 def create_master_repo(self, commits):
572 573 """
573 574 Create a repository and remember it as a template.
574 575
575 576 This allows to easily create derived repositories to construct
576 577 more complex scenarios for diff, compare and pull requests.
577 578
578 579 Returns a commit map which maps from commit message to raw_id.
579 580 """
580 581 self._master_repo = self.create_repo(commits=commits)
581 582 return self._commit_ids
582 583
583 584 def create_repo(
584 585 self, commits=None, number_of_commits=0, heads=None,
585 586 name_suffix=u'', **kwargs):
586 587 """
587 588 Create a repository and record it for later cleanup.
588 589
589 590 :param commits: Optional. A sequence of dict instances.
590 591 Will add a commit per entry to the new repository.
591 592 :param number_of_commits: Optional. If set to a number, this number of
592 593 commits will be added to the new repository.
593 594 :param heads: Optional. Can be set to a sequence of of commit
594 595 names which shall be pulled in from the master repository.
595 596
596 597 """
597 598 self.repo_name = self._next_repo_name() + name_suffix
598 599 repo = self._fixture.create_repo(
599 600 self.repo_name, repo_type=self.alias, **kwargs)
600 601 self._cleanup_repos.append(repo.repo_name)
601 602
602 603 commits = commits or [
603 604 {'message': 'Commit %s of %s' % (x, self.repo_name)}
604 605 for x in xrange(number_of_commits)]
605 606 self._add_commits_to_repo(repo.scm_instance(), commits)
606 607 if heads:
607 608 self.pull_heads(repo, heads)
608 609
609 610 return repo
610 611
611 612 def pull_heads(self, repo, heads):
612 613 """
613 614 Make sure that repo contains all commits mentioned in `heads`
614 615 """
615 616 vcsmaster = self._master_repo.scm_instance()
616 617 vcsrepo = repo.scm_instance()
617 618 vcsrepo.config.clear_section('hooks')
618 619 commit_ids = [self._commit_ids[h] for h in heads]
619 620 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
620 621
621 622 def create_fork(self):
622 623 repo_to_fork = self.repo_name
623 624 self.repo_name = self._next_repo_name()
624 625 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
625 626 self._cleanup_repos.append(self.repo_name)
626 627 return repo
627 628
628 629 def new_repo_name(self, suffix=u''):
629 630 self.repo_name = self._next_repo_name() + suffix
630 631 self._cleanup_repos.append(self.repo_name)
631 632 return self.repo_name
632 633
633 634 def _next_repo_name(self):
634 635 return u"%s_%s" % (
635 636 self.invalid_repo_name.sub(u'_', self._test_name),
636 637 len(self._cleanup_repos))
637 638
638 639 def ensure_file(self, filename, content='Test content\n'):
639 640 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
640 641 commits = [
641 642 {'added': [
642 643 FileNode(filename, content=content),
643 644 ]},
644 645 ]
645 646 self._add_commits_to_repo(self.repo.scm_instance(), commits)
646 647
647 648 def enable_downloads(self):
648 649 repo = self.repo
649 650 repo.enable_downloads = True
650 651 Session().add(repo)
651 652 Session().commit()
652 653
653 654 def cleanup(self):
654 655 for repo_name in reversed(self._cleanup_repos):
655 656 self._fixture.destroy_repo(repo_name)
656 657
657 658 def _add_commits_to_repo(self, repo, commits):
658 659 commit_ids = _add_commits_to_repo(repo, commits)
659 660 if not commit_ids:
660 661 return
661 662 self._commit_ids = commit_ids
662 663
663 664 # Creating refs for Git to allow fetching them from remote repository
664 665 if self.alias == 'git':
665 666 refs = {}
666 667 for message in self._commit_ids:
667 668 # TODO: mikhail: do more special chars replacements
668 669 ref_name = 'refs/test-refs/{}'.format(
669 670 message.replace(' ', ''))
670 671 refs[ref_name] = self._commit_ids[message]
671 672 self._create_refs(repo, refs)
672 673
673 674 def _create_refs(self, repo, refs):
674 675 for ref_name in refs:
675 676 repo.set_refs(ref_name, refs[ref_name])
676 677
677 678
678 679 @pytest.fixture
679 680 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
680 681 """
681 682 Parametrized fixture which represents a single vcs backend implementation.
682 683
683 684 See the fixture `backend` for more details. This one implements the same
684 685 concept, but on vcs level. So it does not provide model instances etc.
685 686
686 687 Parameters are generated dynamically, see :func:`pytest_generate_tests`
687 688 for how this works.
688 689 """
689 690 if backend_alias not in request.config.getoption('--backends'):
690 691 pytest.skip("Backend %s not selected." % (backend_alias, ))
691 692
692 693 utils.check_xfail_backends(request.node, backend_alias)
693 694 utils.check_skip_backends(request.node, backend_alias)
694 695
695 696 repo_name = 'vcs_test_%s' % (backend_alias, )
696 697 repo_path = os.path.join(tests_tmp_path, repo_name)
697 698 backend = VcsBackend(
698 699 alias=backend_alias,
699 700 repo_path=repo_path,
700 701 test_name=request.node.name,
701 702 test_repo_container=test_repo)
702 703 request.addfinalizer(backend.cleanup)
703 704 return backend
704 705
705 706
706 707 @pytest.fixture
707 708 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
708 709 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
709 710
710 711
711 712 @pytest.fixture
712 713 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
713 714 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
714 715
715 716
716 717 @pytest.fixture
717 718 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
718 719 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
719 720
720 721
721 722 @pytest.fixture
722 723 def vcsbackend_random(vcsbackend_git):
723 724 """
724 725 Use this to express that your tests need "a vcsbackend".
725 726
726 727 The fixture `vcsbackend` would run the test multiple times for each
727 728 available vcs backend which is a pure waste of time if the test is
728 729 independent of the vcs backend type.
729 730 """
730 731 # TODO: johbo: Change this to pick a random backend
731 732 return vcsbackend_git
732 733
733 734
734 735 @pytest.fixture
735 736 def vcsbackend_stub(vcsbackend_git):
736 737 """
737 738 Use this to express that your test just needs a stub of a vcsbackend.
738 739
739 740 Plan is to eventually implement an in-memory stub to speed tests up.
740 741 """
741 742 return vcsbackend_git
742 743
743 744
744 745 class VcsBackend(object):
745 746 """
746 747 Represents the test configuration for one supported vcs backend.
747 748 """
748 749
749 750 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
750 751
751 752 def __init__(self, alias, repo_path, test_name, test_repo_container):
752 753 self.alias = alias
753 754 self._repo_path = repo_path
754 755 self._cleanup_repos = []
755 756 self._test_name = test_name
756 757 self._test_repo_container = test_repo_container
757 758
758 759 def __getitem__(self, key):
759 760 return self._test_repo_container(key, self.alias).scm_instance()
760 761
761 762 @property
762 763 def repo(self):
763 764 """
764 765 Returns the "current" repository. This is the vcs_test repo of the last
765 766 repo which has been created.
766 767 """
767 768 Repository = get_backend(self.alias)
768 769 return Repository(self._repo_path)
769 770
770 771 @property
771 772 def backend(self):
772 773 """
773 774 Returns the backend implementation class.
774 775 """
775 776 return get_backend(self.alias)
776 777
777 778 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
778 779 repo_name = self._next_repo_name()
779 780 self._repo_path = get_new_dir(repo_name)
780 781 repo_class = get_backend(self.alias)
781 782 src_url = None
782 783 if _clone_repo:
783 784 src_url = _clone_repo.path
784 785 repo = repo_class(self._repo_path, create=True, src_url=src_url)
785 786 self._cleanup_repos.append(repo)
786 787
787 788 commits = commits or [
788 789 {'message': 'Commit %s of %s' % (x, repo_name)}
789 790 for x in xrange(number_of_commits)]
790 791 _add_commits_to_repo(repo, commits)
791 792 return repo
792 793
793 794 def clone_repo(self, repo):
794 795 return self.create_repo(_clone_repo=repo)
795 796
796 797 def cleanup(self):
797 798 for repo in self._cleanup_repos:
798 799 shutil.rmtree(repo.path)
799 800
800 801 def new_repo_path(self):
801 802 repo_name = self._next_repo_name()
802 803 self._repo_path = get_new_dir(repo_name)
803 804 return self._repo_path
804 805
805 806 def _next_repo_name(self):
806 807 return "%s_%s" % (
807 808 self.invalid_repo_name.sub('_', self._test_name),
808 809 len(self._cleanup_repos))
809 810
810 811 def add_file(self, repo, filename, content='Test content\n'):
811 812 imc = repo.in_memory_commit
812 813 imc.add(FileNode(filename, content=content))
813 814 imc.commit(
814 815 message=u'Automatic commit from vcsbackend fixture',
815 816 author=u'Automatic')
816 817
817 818 def ensure_file(self, filename, content='Test content\n'):
818 819 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
819 820 self.add_file(self.repo, filename, content)
820 821
821 822
822 823 def _add_commits_to_repo(vcs_repo, commits):
823 824 commit_ids = {}
824 825 if not commits:
825 826 return commit_ids
826 827
827 828 imc = vcs_repo.in_memory_commit
828 829 commit = None
829 830
830 831 for idx, commit in enumerate(commits):
831 832 message = unicode(commit.get('message', 'Commit %s' % idx))
832 833
833 834 for node in commit.get('added', []):
834 835 imc.add(FileNode(node.path, content=node.content))
835 836 for node in commit.get('changed', []):
836 837 imc.change(FileNode(node.path, content=node.content))
837 838 for node in commit.get('removed', []):
838 839 imc.remove(FileNode(node.path))
839 840
840 841 parents = [
841 842 vcs_repo.get_commit(commit_id=commit_ids[p])
842 843 for p in commit.get('parents', [])]
843 844
844 845 operations = ('added', 'changed', 'removed')
845 846 if not any((commit.get(o) for o in operations)):
846 847 imc.add(FileNode('file_%s' % idx, content=message))
847 848
848 849 commit = imc.commit(
849 850 message=message,
850 851 author=unicode(commit.get('author', 'Automatic')),
851 852 date=commit.get('date'),
852 853 branch=commit.get('branch'),
853 854 parents=parents)
854 855
855 856 commit_ids[commit.message] = commit.raw_id
856 857
857 858 return commit_ids
858 859
859 860
860 861 @pytest.fixture
861 862 def reposerver(request):
862 863 """
863 864 Allows to serve a backend repository
864 865 """
865 866
866 867 repo_server = RepoServer()
867 868 request.addfinalizer(repo_server.cleanup)
868 869 return repo_server
869 870
870 871
871 872 class RepoServer(object):
872 873 """
873 874 Utility to serve a local repository for the duration of a test case.
874 875
875 876 Supports only Subversion so far.
876 877 """
877 878
878 879 url = None
879 880
880 881 def __init__(self):
881 882 self._cleanup_servers = []
882 883
883 884 def serve(self, vcsrepo):
884 885 if vcsrepo.alias != 'svn':
885 886 raise TypeError("Backend %s not supported" % vcsrepo.alias)
886 887
887 888 proc = subprocess32.Popen(
888 889 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
889 890 '--root', vcsrepo.path])
890 891 self._cleanup_servers.append(proc)
891 892 self.url = 'svn://localhost'
892 893
893 894 def cleanup(self):
894 895 for proc in self._cleanup_servers:
895 896 proc.terminate()
896 897
897 898
898 899 @pytest.fixture
899 900 def pr_util(backend, request):
900 901 """
901 902 Utility for tests of models and for functional tests around pull requests.
902 903
903 904 It gives an instance of :class:`PRTestUtility` which provides various
904 905 utility methods around one pull request.
905 906
906 907 This fixture uses `backend` and inherits its parameterization.
907 908 """
908 909
909 910 util = PRTestUtility(backend)
910 911
911 912 @request.addfinalizer
912 913 def cleanup():
913 914 util.cleanup()
914 915
915 916 return util
916 917
917 918
918 919 class PRTestUtility(object):
919 920
920 921 pull_request = None
921 922 pull_request_id = None
922 923 mergeable_patcher = None
923 924 mergeable_mock = None
924 925 notification_patcher = None
925 926
926 927 def __init__(self, backend):
927 928 self.backend = backend
928 929
929 930 def create_pull_request(
930 931 self, commits=None, target_head=None, source_head=None,
931 932 revisions=None, approved=False, author=None, mergeable=False,
932 933 enable_notifications=True, name_suffix=u'', reviewers=None,
933 934 title=u"Test", description=u"Description"):
934 935 self.set_mergeable(mergeable)
935 936 if not enable_notifications:
936 937 # mock notification side effect
937 938 self.notification_patcher = mock.patch(
938 939 'rhodecode.model.notification.NotificationModel.create')
939 940 self.notification_patcher.start()
940 941
941 942 if not self.pull_request:
942 943 if not commits:
943 944 commits = [
944 945 {'message': 'c1'},
945 946 {'message': 'c2'},
946 947 {'message': 'c3'},
947 948 ]
948 949 target_head = 'c1'
949 950 source_head = 'c2'
950 951 revisions = ['c2']
951 952
952 953 self.commit_ids = self.backend.create_master_repo(commits)
953 954 self.target_repository = self.backend.create_repo(
954 955 heads=[target_head], name_suffix=name_suffix)
955 956 self.source_repository = self.backend.create_repo(
956 957 heads=[source_head], name_suffix=name_suffix)
957 958 self.author = author or UserModel().get_by_username(
958 959 TEST_USER_ADMIN_LOGIN)
959 960
960 961 model = PullRequestModel()
961 962 self.create_parameters = {
962 963 'created_by': self.author,
963 964 'source_repo': self.source_repository.repo_name,
964 965 'source_ref': self._default_branch_reference(source_head),
965 966 'target_repo': self.target_repository.repo_name,
966 967 'target_ref': self._default_branch_reference(target_head),
967 968 'revisions': [self.commit_ids[r] for r in revisions],
968 969 'reviewers': reviewers or self._get_reviewers(),
969 970 'title': title,
970 971 'description': description,
971 972 }
972 973 self.pull_request = model.create(**self.create_parameters)
973 974 assert model.get_versions(self.pull_request) == []
974 975
975 976 self.pull_request_id = self.pull_request.pull_request_id
976 977
977 978 if approved:
978 979 self.approve()
979 980
980 981 Session().add(self.pull_request)
981 982 Session().commit()
982 983
983 984 return self.pull_request
984 985
985 986 def approve(self):
986 987 self.create_status_votes(
987 988 ChangesetStatus.STATUS_APPROVED,
988 989 *self.pull_request.reviewers)
989 990
990 991 def close(self):
991 992 PullRequestModel().close_pull_request(self.pull_request, self.author)
992 993
993 994 def _default_branch_reference(self, commit_message):
994 995 reference = '%s:%s:%s' % (
995 996 'branch',
996 997 self.backend.default_branch_name,
997 998 self.commit_ids[commit_message])
998 999 return reference
999 1000
1000 1001 def _get_reviewers(self):
1001 1002 model = UserModel()
1002 1003 return [
1003 1004 model.get_by_username(TEST_USER_REGULAR_LOGIN),
1004 1005 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
1005 1006 ]
1006 1007
1007 1008 def update_source_repository(self, head=None):
1008 1009 heads = [head or 'c3']
1009 1010 self.backend.pull_heads(self.source_repository, heads=heads)
1010 1011
1011 1012 def add_one_commit(self, head=None):
1012 1013 self.update_source_repository(head=head)
1013 1014 old_commit_ids = set(self.pull_request.revisions)
1014 1015 PullRequestModel().update_commits(self.pull_request)
1015 1016 commit_ids = set(self.pull_request.revisions)
1016 1017 new_commit_ids = commit_ids - old_commit_ids
1017 1018 assert len(new_commit_ids) == 1
1018 1019 return new_commit_ids.pop()
1019 1020
1020 1021 def remove_one_commit(self):
1021 1022 assert len(self.pull_request.revisions) == 2
1022 1023 source_vcs = self.source_repository.scm_instance()
1023 1024 removed_commit_id = source_vcs.commit_ids[-1]
1024 1025
1025 1026 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1026 1027 # remove the if once that's sorted out.
1027 1028 if self.backend.alias == "git":
1028 1029 kwargs = {'branch_name': self.backend.default_branch_name}
1029 1030 else:
1030 1031 kwargs = {}
1031 1032 source_vcs.strip(removed_commit_id, **kwargs)
1032 1033
1033 1034 PullRequestModel().update_commits(self.pull_request)
1034 1035 assert len(self.pull_request.revisions) == 1
1035 1036 return removed_commit_id
1036 1037
1037 1038 def create_comment(self, linked_to=None):
1038 1039 comment = ChangesetCommentsModel().create(
1039 1040 text=u"Test comment",
1040 1041 repo=self.target_repository.repo_name,
1041 1042 user=self.author,
1042 1043 pull_request=self.pull_request)
1043 1044 assert comment.pull_request_version_id is None
1044 1045
1045 1046 if linked_to:
1046 1047 PullRequestModel()._link_comments_to_version(linked_to)
1047 1048
1048 1049 return comment
1049 1050
1050 1051 def create_inline_comment(
1051 1052 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1052 1053 comment = ChangesetCommentsModel().create(
1053 1054 text=u"Test comment",
1054 1055 repo=self.target_repository.repo_name,
1055 1056 user=self.author,
1056 1057 line_no=line_no,
1057 1058 f_path=file_path,
1058 1059 pull_request=self.pull_request)
1059 1060 assert comment.pull_request_version_id is None
1060 1061
1061 1062 if linked_to:
1062 1063 PullRequestModel()._link_comments_to_version(linked_to)
1063 1064
1064 1065 return comment
1065 1066
1066 1067 def create_version_of_pull_request(self):
1067 1068 pull_request = self.create_pull_request()
1068 1069 version = PullRequestModel()._create_version_from_snapshot(
1069 1070 pull_request)
1070 1071 return version
1071 1072
1072 1073 def create_status_votes(self, status, *reviewers):
1073 1074 for reviewer in reviewers:
1074 1075 ChangesetStatusModel().set_status(
1075 1076 repo=self.pull_request.target_repo,
1076 1077 status=status,
1077 1078 user=reviewer.user_id,
1078 1079 pull_request=self.pull_request)
1079 1080
1080 1081 def set_mergeable(self, value):
1081 1082 if not self.mergeable_patcher:
1082 1083 self.mergeable_patcher = mock.patch.object(
1083 1084 VcsSettingsModel, 'get_general_settings')
1084 1085 self.mergeable_mock = self.mergeable_patcher.start()
1085 1086 self.mergeable_mock.return_value = {
1086 1087 'rhodecode_pr_merge_enabled': value}
1087 1088
1088 1089 def cleanup(self):
1089 1090 # In case the source repository is already cleaned up, the pull
1090 1091 # request will already be deleted.
1091 1092 pull_request = PullRequest().get(self.pull_request_id)
1092 1093 if pull_request:
1093 1094 PullRequestModel().delete(pull_request)
1094 1095 Session().commit()
1095 1096
1096 1097 if self.notification_patcher:
1097 1098 self.notification_patcher.stop()
1098 1099
1099 1100 if self.mergeable_patcher:
1100 1101 self.mergeable_patcher.stop()
1101 1102
1102 1103
1103 1104 @pytest.fixture
1104 1105 def user_admin(pylonsapp):
1105 1106 """
1106 1107 Provides the default admin test user as an instance of `db.User`.
1107 1108 """
1108 1109 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1109 1110 return user
1110 1111
1111 1112
1112 1113 @pytest.fixture
1113 1114 def user_regular(pylonsapp):
1114 1115 """
1115 1116 Provides the default regular test user as an instance of `db.User`.
1116 1117 """
1117 1118 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1118 1119 return user
1119 1120
1120 1121
1121 1122 @pytest.fixture
1122 1123 def user_util(request, pylonsapp):
1123 1124 """
1124 1125 Provides a wired instance of `UserUtility` with integrated cleanup.
1125 1126 """
1126 1127 utility = UserUtility(test_name=request.node.name)
1127 1128 request.addfinalizer(utility.cleanup)
1128 1129 return utility
1129 1130
1130 1131
1131 1132 # TODO: johbo: Split this up into utilities per domain or something similar
1132 1133 class UserUtility(object):
1133 1134
1134 1135 def __init__(self, test_name="test"):
1135 1136 self._test_name = test_name
1136 1137 self.fixture = Fixture()
1137 1138 self.repo_group_ids = []
1138 1139 self.user_ids = []
1139 1140 self.user_group_ids = []
1140 1141 self.user_repo_permission_ids = []
1141 1142 self.user_group_repo_permission_ids = []
1142 1143 self.user_repo_group_permission_ids = []
1143 1144 self.user_group_repo_group_permission_ids = []
1144 1145 self.user_user_group_permission_ids = []
1145 1146 self.user_group_user_group_permission_ids = []
1146 1147 self.user_permissions = []
1147 1148
1148 1149 def create_repo_group(
1149 1150 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1150 1151 group_name = "{prefix}_repogroup_{count}".format(
1151 1152 prefix=self._test_name,
1152 1153 count=len(self.repo_group_ids))
1153 1154 repo_group = self.fixture.create_repo_group(
1154 1155 group_name, cur_user=owner)
1155 1156 if auto_cleanup:
1156 1157 self.repo_group_ids.append(repo_group.group_id)
1157 1158 return repo_group
1158 1159
1159 1160 def create_user(self, auto_cleanup=True, **kwargs):
1160 1161 user_name = "{prefix}_user_{count}".format(
1161 1162 prefix=self._test_name,
1162 1163 count=len(self.user_ids))
1163 1164 user = self.fixture.create_user(user_name, **kwargs)
1164 1165 if auto_cleanup:
1165 1166 self.user_ids.append(user.user_id)
1166 1167 return user
1167 1168
1168 1169 def create_user_with_group(self):
1169 1170 user = self.create_user()
1170 1171 user_group = self.create_user_group(members=[user])
1171 1172 return user, user_group
1172 1173
1173 1174 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1174 1175 group_name = "{prefix}_usergroup_{count}".format(
1175 1176 prefix=self._test_name,
1176 1177 count=len(self.user_group_ids))
1177 1178 user_group = self.fixture.create_user_group(group_name, **kwargs)
1178 1179 if auto_cleanup:
1179 1180 self.user_group_ids.append(user_group.users_group_id)
1180 1181 if members:
1181 1182 for user in members:
1182 1183 UserGroupModel().add_user_to_group(user_group, user)
1183 1184 return user_group
1184 1185
1185 1186 def grant_user_permission(self, user_name, permission_name):
1186 1187 self._inherit_default_user_permissions(user_name, False)
1187 1188 self.user_permissions.append((user_name, permission_name))
1188 1189
1189 1190 def grant_user_permission_to_repo_group(
1190 1191 self, repo_group, user, permission_name):
1191 1192 permission = RepoGroupModel().grant_user_permission(
1192 1193 repo_group, user, permission_name)
1193 1194 self.user_repo_group_permission_ids.append(
1194 1195 (repo_group.group_id, user.user_id))
1195 1196 return permission
1196 1197
1197 1198 def grant_user_group_permission_to_repo_group(
1198 1199 self, repo_group, user_group, permission_name):
1199 1200 permission = RepoGroupModel().grant_user_group_permission(
1200 1201 repo_group, user_group, permission_name)
1201 1202 self.user_group_repo_group_permission_ids.append(
1202 1203 (repo_group.group_id, user_group.users_group_id))
1203 1204 return permission
1204 1205
1205 1206 def grant_user_permission_to_repo(
1206 1207 self, repo, user, permission_name):
1207 1208 permission = RepoModel().grant_user_permission(
1208 1209 repo, user, permission_name)
1209 1210 self.user_repo_permission_ids.append(
1210 1211 (repo.repo_id, user.user_id))
1211 1212 return permission
1212 1213
1213 1214 def grant_user_group_permission_to_repo(
1214 1215 self, repo, user_group, permission_name):
1215 1216 permission = RepoModel().grant_user_group_permission(
1216 1217 repo, user_group, permission_name)
1217 1218 self.user_group_repo_permission_ids.append(
1218 1219 (repo.repo_id, user_group.users_group_id))
1219 1220 return permission
1220 1221
1221 1222 def grant_user_permission_to_user_group(
1222 1223 self, target_user_group, user, permission_name):
1223 1224 permission = UserGroupModel().grant_user_permission(
1224 1225 target_user_group, user, permission_name)
1225 1226 self.user_user_group_permission_ids.append(
1226 1227 (target_user_group.users_group_id, user.user_id))
1227 1228 return permission
1228 1229
1229 1230 def grant_user_group_permission_to_user_group(
1230 1231 self, target_user_group, user_group, permission_name):
1231 1232 permission = UserGroupModel().grant_user_group_permission(
1232 1233 target_user_group, user_group, permission_name)
1233 1234 self.user_group_user_group_permission_ids.append(
1234 1235 (target_user_group.users_group_id, user_group.users_group_id))
1235 1236 return permission
1236 1237
1237 1238 def revoke_user_permission(self, user_name, permission_name):
1238 1239 self._inherit_default_user_permissions(user_name, True)
1239 1240 UserModel().revoke_perm(user_name, permission_name)
1240 1241
1241 1242 def _inherit_default_user_permissions(self, user_name, value):
1242 1243 user = UserModel().get_by_username(user_name)
1243 1244 user.inherit_default_permissions = value
1244 1245 Session().add(user)
1245 1246 Session().commit()
1246 1247
1247 1248 def cleanup(self):
1248 1249 self._cleanup_permissions()
1249 1250 self._cleanup_repo_groups()
1250 1251 self._cleanup_user_groups()
1251 1252 self._cleanup_users()
1252 1253
1253 1254 def _cleanup_permissions(self):
1254 1255 if self.user_permissions:
1255 1256 for user_name, permission_name in self.user_permissions:
1256 1257 self.revoke_user_permission(user_name, permission_name)
1257 1258
1258 1259 for permission in self.user_repo_permission_ids:
1259 1260 RepoModel().revoke_user_permission(*permission)
1260 1261
1261 1262 for permission in self.user_group_repo_permission_ids:
1262 1263 RepoModel().revoke_user_group_permission(*permission)
1263 1264
1264 1265 for permission in self.user_repo_group_permission_ids:
1265 1266 RepoGroupModel().revoke_user_permission(*permission)
1266 1267
1267 1268 for permission in self.user_group_repo_group_permission_ids:
1268 1269 RepoGroupModel().revoke_user_group_permission(*permission)
1269 1270
1270 1271 for permission in self.user_user_group_permission_ids:
1271 1272 UserGroupModel().revoke_user_permission(*permission)
1272 1273
1273 1274 for permission in self.user_group_user_group_permission_ids:
1274 1275 UserGroupModel().revoke_user_group_permission(*permission)
1275 1276
1276 1277 def _cleanup_repo_groups(self):
1277 1278 def _repo_group_compare(first_group_id, second_group_id):
1278 1279 """
1279 1280 Gives higher priority to the groups with the most complex paths
1280 1281 """
1281 1282 first_group = RepoGroup.get(first_group_id)
1282 1283 second_group = RepoGroup.get(second_group_id)
1283 1284 first_group_parts = (
1284 1285 len(first_group.group_name.split('/')) if first_group else 0)
1285 1286 second_group_parts = (
1286 1287 len(second_group.group_name.split('/')) if second_group else 0)
1287 1288 return cmp(second_group_parts, first_group_parts)
1288 1289
1289 1290 sorted_repo_group_ids = sorted(
1290 1291 self.repo_group_ids, cmp=_repo_group_compare)
1291 1292 for repo_group_id in sorted_repo_group_ids:
1292 1293 self.fixture.destroy_repo_group(repo_group_id)
1293 1294
1294 1295 def _cleanup_user_groups(self):
1295 1296 def _user_group_compare(first_group_id, second_group_id):
1296 1297 """
1297 1298 Gives higher priority to the groups with the most complex paths
1298 1299 """
1299 1300 first_group = UserGroup.get(first_group_id)
1300 1301 second_group = UserGroup.get(second_group_id)
1301 1302 first_group_parts = (
1302 1303 len(first_group.users_group_name.split('/'))
1303 1304 if first_group else 0)
1304 1305 second_group_parts = (
1305 1306 len(second_group.users_group_name.split('/'))
1306 1307 if second_group else 0)
1307 1308 return cmp(second_group_parts, first_group_parts)
1308 1309
1309 1310 sorted_user_group_ids = sorted(
1310 1311 self.user_group_ids, cmp=_user_group_compare)
1311 1312 for user_group_id in sorted_user_group_ids:
1312 1313 self.fixture.destroy_user_group(user_group_id)
1313 1314
1314 1315 def _cleanup_users(self):
1315 1316 for user_id in self.user_ids:
1316 1317 self.fixture.destroy_user(user_id)
1317 1318
1318 1319
1319 1320 # TODO: Think about moving this into a pytest-pyro package and make it a
1320 1321 # pytest plugin
1321 1322 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1322 1323 def pytest_runtest_makereport(item, call):
1323 1324 """
1324 1325 Adding the remote traceback if the exception has this information.
1325 1326
1326 1327 Pyro4 attaches this information as the attribute `_pyroTraceback`
1327 1328 to the exception instance.
1328 1329 """
1329 1330 outcome = yield
1330 1331 report = outcome.get_result()
1331 1332 if call.excinfo:
1332 1333 _add_pyro_remote_traceback(report, call.excinfo.value)
1333 1334
1334 1335
1335 1336 def _add_pyro_remote_traceback(report, exc):
1336 1337 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1337 1338
1338 1339 if pyro_traceback:
1339 1340 traceback = ''.join(pyro_traceback)
1340 1341 section = 'Pyro4 remote traceback ' + report.when
1341 1342 report.sections.append((section, traceback))
1342 1343
1343 1344
1344 1345 @pytest.fixture(scope='session')
1345 1346 def testrun():
1346 1347 return {
1347 1348 'uuid': uuid.uuid4(),
1348 1349 'start': datetime.datetime.utcnow().isoformat(),
1349 1350 'timestamp': int(time.time()),
1350 1351 }
1351 1352
1352 1353
1353 1354 @pytest.fixture(autouse=True)
1354 1355 def collect_appenlight_stats(request, testrun):
1355 1356 """
1356 1357 This fixture reports memory consumtion of single tests.
1357 1358
1358 1359 It gathers data based on `psutil` and sends them to Appenlight. The option
1359 1360 ``--ae`` has te be used to enable this fixture and the API key for your
1360 1361 application has to be provided in ``--ae-key``.
1361 1362 """
1362 1363 try:
1363 1364 # cygwin cannot have yet psutil support.
1364 1365 import psutil
1365 1366 except ImportError:
1366 1367 return
1367 1368
1368 1369 if not request.config.getoption('--appenlight'):
1369 1370 return
1370 1371 else:
1371 1372 # Only request the pylonsapp fixture if appenlight tracking is
1372 1373 # enabled. This will speed up a test run of unit tests by 2 to 3
1373 1374 # seconds if appenlight is not enabled.
1374 1375 pylonsapp = request.getfuncargvalue("pylonsapp")
1375 1376 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1376 1377 client = AppenlightClient(
1377 1378 url=url,
1378 1379 api_key=request.config.getoption('--appenlight-api-key'),
1379 1380 namespace=request.node.nodeid,
1380 1381 request=str(testrun['uuid']),
1381 1382 testrun=testrun)
1382 1383
1383 1384 client.collect({
1384 1385 'message': "Starting",
1385 1386 })
1386 1387
1387 1388 server_and_port = pylonsapp.config['vcs.server']
1388 1389 server = create_vcsserver_proxy(server_and_port)
1389 1390 with server:
1390 1391 vcs_pid = server.get_pid()
1391 1392 server.run_gc()
1392 1393 vcs_process = psutil.Process(vcs_pid)
1393 1394 mem = vcs_process.memory_info()
1394 1395 client.tag_before('vcsserver.rss', mem.rss)
1395 1396 client.tag_before('vcsserver.vms', mem.vms)
1396 1397
1397 1398 test_process = psutil.Process()
1398 1399 mem = test_process.memory_info()
1399 1400 client.tag_before('test.rss', mem.rss)
1400 1401 client.tag_before('test.vms', mem.vms)
1401 1402
1402 1403 client.tag_before('time', time.time())
1403 1404
1404 1405 @request.addfinalizer
1405 1406 def send_stats():
1406 1407 client.tag_after('time', time.time())
1407 1408 with server:
1408 1409 gc_stats = server.run_gc()
1409 1410 for tag, value in gc_stats.items():
1410 1411 client.tag_after(tag, value)
1411 1412 mem = vcs_process.memory_info()
1412 1413 client.tag_after('vcsserver.rss', mem.rss)
1413 1414 client.tag_after('vcsserver.vms', mem.vms)
1414 1415
1415 1416 mem = test_process.memory_info()
1416 1417 client.tag_after('test.rss', mem.rss)
1417 1418 client.tag_after('test.vms', mem.vms)
1418 1419
1419 1420 client.collect({
1420 1421 'message': "Finished",
1421 1422 })
1422 1423 client.send_stats()
1423 1424
1424 1425 return client
1425 1426
1426 1427
1427 1428 class AppenlightClient():
1428 1429
1429 1430 url_template = '{url}?protocol_version=0.5'
1430 1431
1431 1432 def __init__(
1432 1433 self, url, api_key, add_server=True, add_timestamp=True,
1433 1434 namespace=None, request=None, testrun=None):
1434 1435 self.url = self.url_template.format(url=url)
1435 1436 self.api_key = api_key
1436 1437 self.add_server = add_server
1437 1438 self.add_timestamp = add_timestamp
1438 1439 self.namespace = namespace
1439 1440 self.request = request
1440 1441 self.server = socket.getfqdn(socket.gethostname())
1441 1442 self.tags_before = {}
1442 1443 self.tags_after = {}
1443 1444 self.stats = []
1444 1445 self.testrun = testrun or {}
1445 1446
1446 1447 def tag_before(self, tag, value):
1447 1448 self.tags_before[tag] = value
1448 1449
1449 1450 def tag_after(self, tag, value):
1450 1451 self.tags_after[tag] = value
1451 1452
1452 1453 def collect(self, data):
1453 1454 if self.add_server:
1454 1455 data.setdefault('server', self.server)
1455 1456 if self.add_timestamp:
1456 1457 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1457 1458 if self.namespace:
1458 1459 data.setdefault('namespace', self.namespace)
1459 1460 if self.request:
1460 1461 data.setdefault('request', self.request)
1461 1462 self.stats.append(data)
1462 1463
1463 1464 def send_stats(self):
1464 1465 tags = [
1465 1466 ('testrun', self.request),
1466 1467 ('testrun.start', self.testrun['start']),
1467 1468 ('testrun.timestamp', self.testrun['timestamp']),
1468 1469 ('test', self.namespace),
1469 1470 ]
1470 1471 for key, value in self.tags_before.items():
1471 1472 tags.append((key + '.before', value))
1472 1473 try:
1473 1474 delta = self.tags_after[key] - value
1474 1475 tags.append((key + '.delta', delta))
1475 1476 except Exception:
1476 1477 pass
1477 1478 for key, value in self.tags_after.items():
1478 1479 tags.append((key + '.after', value))
1479 1480 self.collect({
1480 1481 'message': "Collected tags",
1481 1482 'tags': tags,
1482 1483 })
1483 1484
1484 1485 response = requests.post(
1485 1486 self.url,
1486 1487 headers={
1487 1488 'X-appenlight-api-key': self.api_key},
1488 1489 json=self.stats,
1489 1490 )
1490 1491
1491 1492 if not response.status_code == 200:
1492 1493 pprint.pprint(self.stats)
1493 1494 print response.headers
1494 1495 print response.text
1495 1496 raise Exception('Sending to appenlight failed')
1496 1497
1497 1498
1498 1499 @pytest.fixture
1499 1500 def gist_util(request, pylonsapp):
1500 1501 """
1501 1502 Provides a wired instance of `GistUtility` with integrated cleanup.
1502 1503 """
1503 1504 utility = GistUtility()
1504 1505 request.addfinalizer(utility.cleanup)
1505 1506 return utility
1506 1507
1507 1508
1508 1509 class GistUtility(object):
1509 1510 def __init__(self):
1510 1511 self.fixture = Fixture()
1511 1512 self.gist_ids = []
1512 1513
1513 1514 def create_gist(self, **kwargs):
1514 1515 gist = self.fixture.create_gist(**kwargs)
1515 1516 self.gist_ids.append(gist.gist_id)
1516 1517 return gist
1517 1518
1518 1519 def cleanup(self):
1519 1520 for id_ in self.gist_ids:
1520 1521 self.fixture.destroy_gists(str(id_))
1521 1522
1522 1523
1523 1524 @pytest.fixture
1524 1525 def enabled_backends(request):
1525 1526 backends = request.config.option.backends
1526 1527 return backends[:]
1527 1528
1528 1529
1529 1530 @pytest.fixture
1530 1531 def settings_util(request):
1531 1532 """
1532 1533 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1533 1534 """
1534 1535 utility = SettingsUtility()
1535 1536 request.addfinalizer(utility.cleanup)
1536 1537 return utility
1537 1538
1538 1539
1539 1540 class SettingsUtility(object):
1540 1541 def __init__(self):
1541 1542 self.rhodecode_ui_ids = []
1542 1543 self.rhodecode_setting_ids = []
1543 1544 self.repo_rhodecode_ui_ids = []
1544 1545 self.repo_rhodecode_setting_ids = []
1545 1546
1546 1547 def create_repo_rhodecode_ui(
1547 1548 self, repo, section, value, key=None, active=True, cleanup=True):
1548 1549 key = key or hashlib.sha1(
1549 1550 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1550 1551
1551 1552 setting = RepoRhodeCodeUi()
1552 1553 setting.repository_id = repo.repo_id
1553 1554 setting.ui_section = section
1554 1555 setting.ui_value = value
1555 1556 setting.ui_key = key
1556 1557 setting.ui_active = active
1557 1558 Session().add(setting)
1558 1559 Session().commit()
1559 1560
1560 1561 if cleanup:
1561 1562 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1562 1563 return setting
1563 1564
1564 1565 def create_rhodecode_ui(
1565 1566 self, section, value, key=None, active=True, cleanup=True):
1566 1567 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1567 1568
1568 1569 setting = RhodeCodeUi()
1569 1570 setting.ui_section = section
1570 1571 setting.ui_value = value
1571 1572 setting.ui_key = key
1572 1573 setting.ui_active = active
1573 1574 Session().add(setting)
1574 1575 Session().commit()
1575 1576
1576 1577 if cleanup:
1577 1578 self.rhodecode_ui_ids.append(setting.ui_id)
1578 1579 return setting
1579 1580
1580 1581 def create_repo_rhodecode_setting(
1581 1582 self, repo, name, value, type_, cleanup=True):
1582 1583 setting = RepoRhodeCodeSetting(
1583 1584 repo.repo_id, key=name, val=value, type=type_)
1584 1585 Session().add(setting)
1585 1586 Session().commit()
1586 1587
1587 1588 if cleanup:
1588 1589 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1589 1590 return setting
1590 1591
1591 1592 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1592 1593 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1593 1594 Session().add(setting)
1594 1595 Session().commit()
1595 1596
1596 1597 if cleanup:
1597 1598 self.rhodecode_setting_ids.append(setting.app_settings_id)
1598 1599
1599 1600 return setting
1600 1601
1601 1602 def cleanup(self):
1602 1603 for id_ in self.rhodecode_ui_ids:
1603 1604 setting = RhodeCodeUi.get(id_)
1604 1605 Session().delete(setting)
1605 1606
1606 1607 for id_ in self.rhodecode_setting_ids:
1607 1608 setting = RhodeCodeSetting.get(id_)
1608 1609 Session().delete(setting)
1609 1610
1610 1611 for id_ in self.repo_rhodecode_ui_ids:
1611 1612 setting = RepoRhodeCodeUi.get(id_)
1612 1613 Session().delete(setting)
1613 1614
1614 1615 for id_ in self.repo_rhodecode_setting_ids:
1615 1616 setting = RepoRhodeCodeSetting.get(id_)
1616 1617 Session().delete(setting)
1617 1618
1618 1619 Session().commit()
1619 1620
1620 1621
1621 1622 @pytest.fixture
1622 1623 def no_notifications(request):
1623 1624 notification_patcher = mock.patch(
1624 1625 'rhodecode.model.notification.NotificationModel.create')
1625 1626 notification_patcher.start()
1626 1627 request.addfinalizer(notification_patcher.stop)
1627 1628
1628 1629
1629 1630 @pytest.fixture
1630 1631 def silence_action_logger(request):
1631 1632 notification_patcher = mock.patch(
1632 1633 'rhodecode.lib.utils.action_logger')
1633 1634 notification_patcher.start()
1634 1635 request.addfinalizer(notification_patcher.stop)
1635 1636
1636 1637
1637 1638 @pytest.fixture(scope='session')
1638 1639 def repeat(request):
1639 1640 """
1640 1641 The number of repetitions is based on this fixture.
1641 1642
1642 1643 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1643 1644 tests are not too slow in our default test suite.
1644 1645 """
1645 1646 return request.config.getoption('--repeat')
1646 1647
1647 1648
1648 1649 @pytest.fixture
1649 1650 def rhodecode_fixtures():
1650 1651 return Fixture()
1651 1652
1652 1653
1653 1654 @pytest.fixture
1654 1655 def request_stub():
1655 1656 """
1656 1657 Stub request object.
1657 1658 """
1658 1659 request = pyramid.testing.DummyRequest()
1659 1660 request.scheme = 'https'
1660 1661 return request
1661 1662
1662 1663
1663 1664 @pytest.fixture
1664 1665 def config_stub(request, request_stub):
1665 1666 """
1666 1667 Set up pyramid.testing and return the Configurator.
1667 1668 """
1668 1669 config = pyramid.testing.setUp(request=request_stub)
1669 1670
1670 1671 @request.addfinalizer
1671 1672 def cleanup():
1672 1673 pyramid.testing.tearDown()
1673 1674
1674 1675 return config
1675 1676
1676 1677
1677 1678 @pytest.fixture
1678 1679 def StubIntegrationType():
1679 1680 class _StubIntegrationType(IntegrationTypeBase):
1680 1681 """ Test integration type class """
1681 1682
1682 1683 key = 'test'
1683 1684 display_name = 'Test integration type'
1684 1685 description = 'A test integration type for testing'
1685 1686 icon = 'test_icon_html_image'
1686 1687
1687 1688 def __init__(self, settings):
1688 1689 super(_StubIntegrationType, self).__init__(settings)
1689 1690 self.sent_events = [] # for testing
1690 1691
1691 1692 def send_event(self, event):
1692 1693 self.sent_events.append(event)
1693 1694
1694 1695 def settings_schema(self):
1695 1696 class SettingsSchema(colander.Schema):
1696 1697 test_string_field = colander.SchemaNode(
1697 1698 colander.String(),
1698 1699 missing=colander.required,
1699 1700 title='test string field',
1700 1701 )
1701 1702 test_int_field = colander.SchemaNode(
1702 1703 colander.Int(),
1703 1704 title='some integer setting',
1704 1705 )
1705 1706 return SettingsSchema()
1706 1707
1707 1708
1708 1709 integration_type_registry.register_integration_type(_StubIntegrationType)
1709 1710 return _StubIntegrationType
1710 1711
1711 1712 @pytest.fixture
1712 1713 def stub_integration_settings():
1713 1714 return {
1714 1715 'test_string_field': 'some data',
1715 1716 'test_int_field': 100,
1716 1717 }
1717 1718
1718 1719
1719 1720 @pytest.fixture
1720 1721 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1721 1722 stub_integration_settings):
1722 1723 integration = IntegrationModel().create(
1723 1724 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1724 1725 name='test repo integration',
1725 1726 repo=repo_stub, repo_group=None, child_repos_only=None)
1726 1727
1727 1728 @request.addfinalizer
1728 1729 def cleanup():
1729 1730 IntegrationModel().delete(integration)
1730 1731
1731 1732 return integration
1732 1733
1733 1734
1734 1735 @pytest.fixture
1735 1736 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1736 1737 stub_integration_settings):
1737 1738 integration = IntegrationModel().create(
1738 1739 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1739 1740 name='test repogroup integration',
1740 1741 repo=None, repo_group=test_repo_group, child_repos_only=True)
1741 1742
1742 1743 @request.addfinalizer
1743 1744 def cleanup():
1744 1745 IntegrationModel().delete(integration)
1745 1746
1746 1747 return integration
1747 1748
1748 1749
1749 1750 @pytest.fixture
1750 1751 def repogroup_recursive_integration_stub(request, test_repo_group,
1751 1752 StubIntegrationType, stub_integration_settings):
1752 1753 integration = IntegrationModel().create(
1753 1754 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1754 1755 name='test recursive repogroup integration',
1755 1756 repo=None, repo_group=test_repo_group, child_repos_only=False)
1756 1757
1757 1758 @request.addfinalizer
1758 1759 def cleanup():
1759 1760 IntegrationModel().delete(integration)
1760 1761
1761 1762 return integration
1762 1763
1763 1764
1764 1765 @pytest.fixture
1765 1766 def global_integration_stub(request, StubIntegrationType,
1766 1767 stub_integration_settings):
1767 1768 integration = IntegrationModel().create(
1768 1769 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1769 1770 name='test global integration',
1770 1771 repo=None, repo_group=None, child_repos_only=None)
1771 1772
1772 1773 @request.addfinalizer
1773 1774 def cleanup():
1774 1775 IntegrationModel().delete(integration)
1775 1776
1776 1777 return integration
1777 1778
1778 1779
1779 1780 @pytest.fixture
1780 1781 def root_repos_integration_stub(request, StubIntegrationType,
1781 1782 stub_integration_settings):
1782 1783 integration = IntegrationModel().create(
1783 1784 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1784 1785 name='test global integration',
1785 1786 repo=None, repo_group=None, child_repos_only=True)
1786 1787
1787 1788 @request.addfinalizer
1788 1789 def cleanup():
1789 1790 IntegrationModel().delete(integration)
1790 1791
1791 1792 return integration
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now