##// END OF EJS Templates
user/user-groups: show if users or user groups are a part of review rules....
marcink -
r2054:0026cc40 default
parent child Browse files
Show More
@@ -1,413 +1,417 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 28 import peppercorn
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, config
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.exceptions import UserGroupAssignedException,\
38 38 RepoGroupAssignmentError
39 39 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
40 40 from rhodecode.lib.auth import (
41 41 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
42 42 HasPermissionAnyDecorator)
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.model.permission import PermissionModel
45 45 from rhodecode.model.user_group import UserGroupModel
46 46 from rhodecode.model.db import User, UserGroup
47 47 from rhodecode.model.forms import (
48 48 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
49 49 UserPermissionsForm)
50 50 from rhodecode.model.meta import Session
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class UserGroupsController(BaseController):
57 57 """REST Controller styled on the Atom Publishing Protocol"""
58 58
59 59 @LoginRequired()
60 60 def __before__(self):
61 61 super(UserGroupsController, self).__before__()
62 62 c.available_permissions = config['available_permissions']
63 63 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
64 64
65 65 def __load_data(self, user_group_id):
66 66 c.group_members_obj = [x.user for x in c.user_group.members]
67 67 c.group_members_obj.sort(key=lambda u: u.username.lower())
68 68 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
69 69
70 70 def __load_defaults(self, user_group_id):
71 71 """
72 72 Load defaults settings for edit, and update
73 73
74 74 :param user_group_id:
75 75 """
76 76 user_group = UserGroup.get_or_404(user_group_id)
77 77 data = user_group.get_dict()
78 78 # fill owner
79 79 if user_group.user:
80 80 data.update({'user': user_group.user.username})
81 81 else:
82 82 replacement_user = User.get_first_super_admin().username
83 83 data.update({'user': replacement_user})
84 84 return data
85 85
86 86 def _revoke_perms_on_yourself(self, form_result):
87 87 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
88 88 form_result['perm_updates'])
89 89 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
90 90 form_result['perm_additions'])
91 91 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
92 92 form_result['perm_deletions'])
93 93 admin_perm = 'usergroup.admin'
94 94 if _updates and _updates[0][1] != admin_perm or \
95 95 _additions and _additions[0][1] != admin_perm or \
96 96 _deletions and _deletions[0][1] != admin_perm:
97 97 return True
98 98 return False
99 99
100 100 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
101 101 @auth.CSRFRequired()
102 102 def create(self):
103 103
104 104 users_group_form = UserGroupForm()()
105 105 try:
106 106 form_result = users_group_form.to_python(dict(request.POST))
107 107 user_group = UserGroupModel().create(
108 108 name=form_result['users_group_name'],
109 109 description=form_result['user_group_description'],
110 110 owner=c.rhodecode_user.user_id,
111 111 active=form_result['users_group_active'])
112 112 Session().flush()
113 113 creation_data = user_group.get_api_data()
114 114 user_group_name = form_result['users_group_name']
115 115
116 116 audit_logger.store_web(
117 117 'user_group.create', action_data={'data': creation_data},
118 118 user=c.rhodecode_user)
119 119
120 120 user_group_link = h.link_to(
121 121 h.escape(user_group_name),
122 122 url('edit_users_group', user_group_id=user_group.users_group_id))
123 123 h.flash(h.literal(_('Created user group %(user_group_link)s')
124 124 % {'user_group_link': user_group_link}),
125 125 category='success')
126 126 Session().commit()
127 127 except formencode.Invalid as errors:
128 128 return htmlfill.render(
129 129 render('admin/user_groups/user_group_add.mako'),
130 130 defaults=errors.value,
131 131 errors=errors.error_dict or {},
132 132 prefix_error=False,
133 133 encoding="UTF-8",
134 134 force_defaults=False)
135 135 except Exception:
136 136 log.exception("Exception creating user group")
137 137 h.flash(_('Error occurred during creation of user group %s') \
138 138 % request.POST.get('users_group_name'), category='error')
139 139
140 140 return redirect(
141 141 url('edit_users_group', user_group_id=user_group.users_group_id))
142 142
143 143 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
144 144 def new(self):
145 145 """GET /user_groups/new: Form to create a new item"""
146 146 # url('new_users_group')
147 147 return render('admin/user_groups/user_group_add.mako')
148 148
149 149 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
150 150 @auth.CSRFRequired()
151 151 def update(self, user_group_id):
152 152
153 153 user_group_id = safe_int(user_group_id)
154 154 c.user_group = UserGroup.get_or_404(user_group_id)
155 155 c.active = 'settings'
156 156 self.__load_data(user_group_id)
157 157
158 158 users_group_form = UserGroupForm(
159 159 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
160 160
161 161 old_values = c.user_group.get_api_data()
162 162 try:
163 163 form_result = users_group_form.to_python(request.POST)
164 164 pstruct = peppercorn.parse(request.POST.items())
165 165 form_result['users_group_members'] = pstruct['user_group_members']
166 166
167 167 user_group, added_members, removed_members = \
168 168 UserGroupModel().update(c.user_group, form_result)
169 169 updated_user_group = form_result['users_group_name']
170 170
171 171 audit_logger.store_web(
172 172 'user_group.edit', action_data={'old_data': old_values},
173 173 user=c.rhodecode_user)
174 174
175 175 # TODO(marcink): use added/removed to set user_group.edit.member.add
176 176
177 177 h.flash(_('Updated user group %s') % updated_user_group,
178 178 category='success')
179 179 Session().commit()
180 180 except formencode.Invalid as errors:
181 181 defaults = errors.value
182 182 e = errors.error_dict or {}
183 183
184 184 return htmlfill.render(
185 185 render('admin/user_groups/user_group_edit.mako'),
186 186 defaults=defaults,
187 187 errors=e,
188 188 prefix_error=False,
189 189 encoding="UTF-8",
190 190 force_defaults=False)
191 191 except Exception:
192 192 log.exception("Exception during update of user group")
193 193 h.flash(_('Error occurred during update of user group %s')
194 194 % request.POST.get('users_group_name'), category='error')
195 195
196 196 return redirect(url('edit_users_group', user_group_id=user_group_id))
197 197
198 198 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
199 199 @auth.CSRFRequired()
200 200 def delete(self, user_group_id):
201 201 user_group_id = safe_int(user_group_id)
202 202 c.user_group = UserGroup.get_or_404(user_group_id)
203 203 force = str2bool(request.POST.get('force'))
204 204
205 205 old_values = c.user_group.get_api_data()
206 206 try:
207 207 UserGroupModel().delete(c.user_group, force=force)
208 208 audit_logger.store_web(
209 209 'user.delete', action_data={'old_data': old_values},
210 210 user=c.rhodecode_user)
211 211 Session().commit()
212 212 h.flash(_('Successfully deleted user group'), category='success')
213 213 except UserGroupAssignedException as e:
214 214 h.flash(str(e), category='error')
215 215 except Exception:
216 216 log.exception("Exception during deletion of user group")
217 217 h.flash(_('An error occurred during deletion of user group'),
218 218 category='error')
219 219 return redirect(url('users_groups'))
220 220
221 221 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
222 222 def edit(self, user_group_id):
223 223 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
224 224 # url('edit_users_group', user_group_id=ID)
225 225
226 226 user_group_id = safe_int(user_group_id)
227 227 c.user_group = UserGroup.get_or_404(user_group_id)
228 228 c.active = 'settings'
229 229 self.__load_data(user_group_id)
230 230
231 231 defaults = self.__load_defaults(user_group_id)
232 232
233 233 return htmlfill.render(
234 234 render('admin/user_groups/user_group_edit.mako'),
235 235 defaults=defaults,
236 236 encoding="UTF-8",
237 237 force_defaults=False
238 238 )
239 239
240 240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
241 241 def edit_perms(self, user_group_id):
242 242 user_group_id = safe_int(user_group_id)
243 243 c.user_group = UserGroup.get_or_404(user_group_id)
244 244 c.active = 'perms'
245 245
246 246 defaults = {}
247 247 # fill user group users
248 248 for p in c.user_group.user_user_group_to_perm:
249 249 defaults.update({'u_perm_%s' % p.user.user_id:
250 250 p.permission.permission_name})
251 251
252 252 for p in c.user_group.user_group_user_group_to_perm:
253 253 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
254 254 p.permission.permission_name})
255 255
256 256 return htmlfill.render(
257 257 render('admin/user_groups/user_group_edit.mako'),
258 258 defaults=defaults,
259 259 encoding="UTF-8",
260 260 force_defaults=False
261 261 )
262 262
263 263 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
264 264 @auth.CSRFRequired()
265 265 def update_perms(self, user_group_id):
266 266 """
267 267 grant permission for given usergroup
268 268
269 269 :param user_group_id:
270 270 """
271 271 user_group_id = safe_int(user_group_id)
272 272 c.user_group = UserGroup.get_or_404(user_group_id)
273 273 form = UserGroupPermsForm()().to_python(request.POST)
274 274
275 275 if not c.rhodecode_user.is_admin:
276 276 if self._revoke_perms_on_yourself(form):
277 277 msg = _('Cannot change permission for yourself as admin')
278 278 h.flash(msg, category='warning')
279 279 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
280 280
281 281 try:
282 282 UserGroupModel().update_permissions(user_group_id,
283 283 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
284 284 except RepoGroupAssignmentError:
285 285 h.flash(_('Target group cannot be the same'), category='error')
286 286 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
287 287
288 288 # TODO(marcink): implement global permissions
289 289 # audit_log.store_web('user_group.edit.permissions')
290 290 Session().commit()
291 291 h.flash(_('User Group permissions updated'), category='success')
292 292 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
293 293
294 294
295 295
296 296 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
297 297 def edit_global_perms(self, user_group_id):
298 298 user_group_id = safe_int(user_group_id)
299 299 c.user_group = UserGroup.get_or_404(user_group_id)
300 300 c.active = 'global_perms'
301 301
302 302 c.default_user = User.get_default_user()
303 303 defaults = c.user_group.get_dict()
304 304 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
305 305 defaults.update(c.user_group.get_default_perms())
306 306
307 307 return htmlfill.render(
308 308 render('admin/user_groups/user_group_edit.mako'),
309 309 defaults=defaults,
310 310 encoding="UTF-8",
311 311 force_defaults=False
312 312 )
313 313
314 314 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
315 315 @auth.CSRFRequired()
316 316 def update_global_perms(self, user_group_id):
317 317 user_group_id = safe_int(user_group_id)
318 318 user_group = UserGroup.get_or_404(user_group_id)
319 319 c.active = 'global_perms'
320 320
321 321 try:
322 322 # first stage that verifies the checkbox
323 323 _form = UserIndividualPermissionsForm()
324 324 form_result = _form.to_python(dict(request.POST))
325 325 inherit_perms = form_result['inherit_default_permissions']
326 326 user_group.inherit_default_permissions = inherit_perms
327 327 Session().add(user_group)
328 328
329 329 if not inherit_perms:
330 330 # only update the individual ones if we un check the flag
331 331 _form = UserPermissionsForm(
332 332 [x[0] for x in c.repo_create_choices],
333 333 [x[0] for x in c.repo_create_on_write_choices],
334 334 [x[0] for x in c.repo_group_create_choices],
335 335 [x[0] for x in c.user_group_create_choices],
336 336 [x[0] for x in c.fork_choices],
337 337 [x[0] for x in c.inherit_default_permission_choices])()
338 338
339 339 form_result = _form.to_python(dict(request.POST))
340 340 form_result.update({'perm_user_group_id': user_group.users_group_id})
341 341
342 342 PermissionModel().update_user_group_permissions(form_result)
343 343
344 344 Session().commit()
345 345 h.flash(_('User Group global permissions updated successfully'),
346 346 category='success')
347 347
348 348 except formencode.Invalid as errors:
349 349 defaults = errors.value
350 350 c.user_group = user_group
351 351 return htmlfill.render(
352 352 render('admin/user_groups/user_group_edit.mako'),
353 353 defaults=defaults,
354 354 errors=errors.error_dict or {},
355 355 prefix_error=False,
356 356 encoding="UTF-8",
357 357 force_defaults=False)
358 358 except Exception:
359 359 log.exception("Exception during permissions saving")
360 360 h.flash(_('An error occurred during permissions saving'),
361 361 category='error')
362 362
363 363 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
364 364
365 365 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
366 366 def edit_advanced(self, user_group_id):
367 367 user_group_id = safe_int(user_group_id)
368 368 c.user_group = UserGroup.get_or_404(user_group_id)
369 369 c.active = 'advanced'
370 370 c.group_members_obj = sorted(
371 371 (x.user for x in c.user_group.members),
372 372 key=lambda u: u.username.lower())
373 373
374 374 c.group_to_repos = sorted(
375 375 (x.repository for x in c.user_group.users_group_repo_to_perm),
376 376 key=lambda u: u.repo_name.lower())
377 377
378 378 c.group_to_repo_groups = sorted(
379 379 (x.group for x in c.user_group.users_group_repo_group_to_perm),
380 380 key=lambda u: u.group_name.lower())
381 381
382 c.group_to_review_rules = sorted(
383 (x.users_group for x in c.user_group.user_group_review_rules),
384 key=lambda u: u.users_group_name.lower())
385
382 386 return render('admin/user_groups/user_group_edit.mako')
383 387
384 388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
385 389 def edit_advanced_set_synchronization(self, user_group_id):
386 390 user_group_id = safe_int(user_group_id)
387 391 user_group = UserGroup.get_or_404(user_group_id)
388 392
389 393 existing = user_group.group_data.get('extern_type')
390 394
391 395 if existing:
392 396 new_state = user_group.group_data
393 397 new_state['extern_type'] = None
394 398 else:
395 399 new_state = user_group.group_data
396 400 new_state['extern_type'] = 'manual'
397 401 new_state['extern_type_set_by'] = c.rhodecode_user.username
398 402
399 403 try:
400 404 user_group.group_data = new_state
401 405 Session().add(user_group)
402 406 Session().commit()
403 407
404 408 h.flash(_('User Group synchronization updated successfully'),
405 409 category='success')
406 410 except Exception:
407 411 log.exception("Exception during sync settings saving")
408 412 h.flash(_('An error occurred during synchronization update'),
409 413 category='error')
410 414
411 415 return redirect(
412 416 url('edit_user_group_advanced', user_group_id=user_group_id))
413 417
@@ -1,491 +1,496 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Users crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 from formencode import htmlfill
29 29 from pylons import request, tmpl_context as c, url, config
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator, AuthUser)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.exceptions import (
42 42 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
43 43 UserOwnsUserGroupsException, UserCreationError)
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 from rhodecode.model.db import (
47 47 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
48 48 from rhodecode.model.forms import (
49 49 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51 from rhodecode.model.user import UserModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.permission import PermissionModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UsersController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60
61 61 @LoginRequired()
62 62 def __before__(self):
63 63 super(UsersController, self).__before__()
64 64 c.available_permissions = config['available_permissions']
65 65 c.allowed_languages = [
66 66 ('en', 'English (en)'),
67 67 ('de', 'German (de)'),
68 68 ('fr', 'French (fr)'),
69 69 ('it', 'Italian (it)'),
70 70 ('ja', 'Japanese (ja)'),
71 71 ('pl', 'Polish (pl)'),
72 72 ('pt', 'Portuguese (pt)'),
73 73 ('ru', 'Russian (ru)'),
74 74 ('zh', 'Chinese (zh)'),
75 75 ]
76 76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
77 77
78 78 def _get_personal_repo_group_template_vars(self):
79 79 DummyUser = AttributeDict({
80 80 'username': '${username}',
81 81 'user_id': '${user_id}',
82 82 })
83 83 c.default_create_repo_group = RepoGroupModel() \
84 84 .get_default_create_personal_repo_group()
85 85 c.personal_repo_group_name = RepoGroupModel() \
86 86 .get_personal_group_name(DummyUser)
87 87
88 88 @HasPermissionAllDecorator('hg.admin')
89 89 @auth.CSRFRequired()
90 90 def create(self):
91 91 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
92 92 user_model = UserModel()
93 93 user_form = UserForm()()
94 94 try:
95 95 form_result = user_form.to_python(dict(request.POST))
96 96 user = user_model.create(form_result)
97 97 Session().flush()
98 98 creation_data = user.get_api_data()
99 99 username = form_result['username']
100 100
101 101 audit_logger.store_web(
102 102 'user.create', action_data={'data': creation_data},
103 103 user=c.rhodecode_user)
104 104
105 105 user_link = h.link_to(h.escape(username),
106 106 url('edit_user',
107 107 user_id=user.user_id))
108 108 h.flash(h.literal(_('Created user %(user_link)s')
109 109 % {'user_link': user_link}), category='success')
110 110 Session().commit()
111 111 except formencode.Invalid as errors:
112 112 self._get_personal_repo_group_template_vars()
113 113 return htmlfill.render(
114 114 render('admin/users/user_add.mako'),
115 115 defaults=errors.value,
116 116 errors=errors.error_dict or {},
117 117 prefix_error=False,
118 118 encoding="UTF-8",
119 119 force_defaults=False)
120 120 except UserCreationError as e:
121 121 h.flash(e, 'error')
122 122 except Exception:
123 123 log.exception("Exception creation of user")
124 124 h.flash(_('Error occurred during creation of user %s')
125 125 % request.POST.get('username'), category='error')
126 126 return redirect(h.route_path('users'))
127 127
128 128 @HasPermissionAllDecorator('hg.admin')
129 129 def new(self):
130 130 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
131 131 self._get_personal_repo_group_template_vars()
132 132 return render('admin/users/user_add.mako')
133 133
134 134 @HasPermissionAllDecorator('hg.admin')
135 135 @auth.CSRFRequired()
136 136 def update(self, user_id):
137 137
138 138 user_id = safe_int(user_id)
139 139 c.user = User.get_or_404(user_id)
140 140 c.active = 'profile'
141 141 c.extern_type = c.user.extern_type
142 142 c.extern_name = c.user.extern_name
143 143 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
144 144 available_languages = [x[0] for x in c.allowed_languages]
145 145 _form = UserForm(edit=True, available_languages=available_languages,
146 146 old_data={'user_id': user_id,
147 147 'email': c.user.email})()
148 148 form_result = {}
149 149 old_values = c.user.get_api_data()
150 150 try:
151 151 form_result = _form.to_python(dict(request.POST))
152 152 skip_attrs = ['extern_type', 'extern_name']
153 153 # TODO: plugin should define if username can be updated
154 154 if c.extern_type != "rhodecode":
155 155 # forbid updating username for external accounts
156 156 skip_attrs.append('username')
157 157
158 158 UserModel().update_user(
159 159 user_id, skip_attrs=skip_attrs, **form_result)
160 160
161 161 audit_logger.store_web(
162 162 'user.edit', action_data={'old_data': old_values},
163 163 user=c.rhodecode_user)
164 164
165 165 Session().commit()
166 166 h.flash(_('User updated successfully'), category='success')
167 167 except formencode.Invalid as errors:
168 168 defaults = errors.value
169 169 e = errors.error_dict or {}
170 170
171 171 return htmlfill.render(
172 172 render('admin/users/user_edit.mako'),
173 173 defaults=defaults,
174 174 errors=e,
175 175 prefix_error=False,
176 176 encoding="UTF-8",
177 177 force_defaults=False)
178 178 except UserCreationError as e:
179 179 h.flash(e, 'error')
180 180 except Exception:
181 181 log.exception("Exception updating user")
182 182 h.flash(_('Error occurred during update of user %s')
183 183 % form_result.get('username'), category='error')
184 184 return redirect(url('edit_user', user_id=user_id))
185 185
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @auth.CSRFRequired()
188 188 def delete(self, user_id):
189 189 user_id = safe_int(user_id)
190 190 c.user = User.get_or_404(user_id)
191 191
192 192 _repos = c.user.repositories
193 193 _repo_groups = c.user.repository_groups
194 194 _user_groups = c.user.user_groups
195 195
196 196 handle_repos = None
197 197 handle_repo_groups = None
198 198 handle_user_groups = None
199 199 # dummy call for flash of handle
200 200 set_handle_flash_repos = lambda: None
201 201 set_handle_flash_repo_groups = lambda: None
202 202 set_handle_flash_user_groups = lambda: None
203 203
204 204 if _repos and request.POST.get('user_repos'):
205 205 do = request.POST['user_repos']
206 206 if do == 'detach':
207 207 handle_repos = 'detach'
208 208 set_handle_flash_repos = lambda: h.flash(
209 209 _('Detached %s repositories') % len(_repos),
210 210 category='success')
211 211 elif do == 'delete':
212 212 handle_repos = 'delete'
213 213 set_handle_flash_repos = lambda: h.flash(
214 214 _('Deleted %s repositories') % len(_repos),
215 215 category='success')
216 216
217 217 if _repo_groups and request.POST.get('user_repo_groups'):
218 218 do = request.POST['user_repo_groups']
219 219 if do == 'detach':
220 220 handle_repo_groups = 'detach'
221 221 set_handle_flash_repo_groups = lambda: h.flash(
222 222 _('Detached %s repository groups') % len(_repo_groups),
223 223 category='success')
224 224 elif do == 'delete':
225 225 handle_repo_groups = 'delete'
226 226 set_handle_flash_repo_groups = lambda: h.flash(
227 227 _('Deleted %s repository groups') % len(_repo_groups),
228 228 category='success')
229 229
230 230 if _user_groups and request.POST.get('user_user_groups'):
231 231 do = request.POST['user_user_groups']
232 232 if do == 'detach':
233 233 handle_user_groups = 'detach'
234 234 set_handle_flash_user_groups = lambda: h.flash(
235 235 _('Detached %s user groups') % len(_user_groups),
236 236 category='success')
237 237 elif do == 'delete':
238 238 handle_user_groups = 'delete'
239 239 set_handle_flash_user_groups = lambda: h.flash(
240 240 _('Deleted %s user groups') % len(_user_groups),
241 241 category='success')
242 242
243 243 old_values = c.user.get_api_data()
244 244 try:
245 245 UserModel().delete(c.user, handle_repos=handle_repos,
246 246 handle_repo_groups=handle_repo_groups,
247 247 handle_user_groups=handle_user_groups)
248 248
249 249 audit_logger.store_web(
250 250 'user.delete', action_data={'old_data': old_values},
251 251 user=c.rhodecode_user)
252 252
253 253 Session().commit()
254 254 set_handle_flash_repos()
255 255 set_handle_flash_repo_groups()
256 256 set_handle_flash_user_groups()
257 257 h.flash(_('Successfully deleted user'), category='success')
258 258 except (UserOwnsReposException, UserOwnsRepoGroupsException,
259 259 UserOwnsUserGroupsException, DefaultUserException) as e:
260 260 h.flash(e, category='warning')
261 261 except Exception:
262 262 log.exception("Exception during deletion of user")
263 263 h.flash(_('An error occurred during deletion of user'),
264 264 category='error')
265 265 return redirect(h.route_path('users'))
266 266
267 267 @HasPermissionAllDecorator('hg.admin')
268 268 @auth.CSRFRequired()
269 269 def reset_password(self, user_id):
270 270 """
271 271 toggle reset password flag for this user
272 272 """
273 273 user_id = safe_int(user_id)
274 274 c.user = User.get_or_404(user_id)
275 275 try:
276 276 old_value = c.user.user_data.get('force_password_change')
277 277 c.user.update_userdata(force_password_change=not old_value)
278 278
279 279 if old_value:
280 280 msg = _('Force password change disabled for user')
281 281 audit_logger.store_web(
282 282 'user.edit.password_reset.disabled',
283 283 user=c.rhodecode_user)
284 284 else:
285 285 msg = _('Force password change enabled for user')
286 286 audit_logger.store_web(
287 287 'user.edit.password_reset.enabled',
288 288 user=c.rhodecode_user)
289 289
290 290 Session().commit()
291 291 h.flash(msg, category='success')
292 292 except Exception:
293 293 log.exception("Exception during password reset for user")
294 294 h.flash(_('An error occurred during password reset for user'),
295 295 category='error')
296 296
297 297 return redirect(url('edit_user_advanced', user_id=user_id))
298 298
299 299 @HasPermissionAllDecorator('hg.admin')
300 300 @auth.CSRFRequired()
301 301 def create_personal_repo_group(self, user_id):
302 302 """
303 303 Create personal repository group for this user
304 304 """
305 305 from rhodecode.model.repo_group import RepoGroupModel
306 306
307 307 user_id = safe_int(user_id)
308 308 c.user = User.get_or_404(user_id)
309 309 personal_repo_group = RepoGroup.get_user_personal_repo_group(
310 310 c.user.user_id)
311 311 if personal_repo_group:
312 312 return redirect(url('edit_user_advanced', user_id=user_id))
313 313
314 314 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
315 315 c.user)
316 316 named_personal_group = RepoGroup.get_by_group_name(
317 317 personal_repo_group_name)
318 318 try:
319 319
320 320 if named_personal_group and named_personal_group.user_id == c.user.user_id:
321 321 # migrate the same named group, and mark it as personal
322 322 named_personal_group.personal = True
323 323 Session().add(named_personal_group)
324 324 Session().commit()
325 325 msg = _('Linked repository group `%s` as personal' % (
326 326 personal_repo_group_name,))
327 327 h.flash(msg, category='success')
328 328 elif not named_personal_group:
329 329 RepoGroupModel().create_personal_repo_group(c.user)
330 330
331 331 msg = _('Created repository group `%s`' % (
332 332 personal_repo_group_name,))
333 333 h.flash(msg, category='success')
334 334 else:
335 335 msg = _('Repository group `%s` is already taken' % (
336 336 personal_repo_group_name,))
337 337 h.flash(msg, category='warning')
338 338 except Exception:
339 339 log.exception("Exception during repository group creation")
340 340 msg = _(
341 341 'An error occurred during repository group creation for user')
342 342 h.flash(msg, category='error')
343 343 Session().rollback()
344 344
345 345 return redirect(url('edit_user_advanced', user_id=user_id))
346 346
347 347 @HasPermissionAllDecorator('hg.admin')
348 348 def show(self, user_id):
349 349 """GET /users/user_id: Show a specific item"""
350 350 # url('user', user_id=ID)
351 351 User.get_or_404(-1)
352 352
353 353 @HasPermissionAllDecorator('hg.admin')
354 354 def edit(self, user_id):
355 355 """GET /users/user_id/edit: Form to edit an existing item"""
356 356 # url('edit_user', user_id=ID)
357 357 user_id = safe_int(user_id)
358 358 c.user = User.get_or_404(user_id)
359 359 if c.user.username == User.DEFAULT_USER:
360 360 h.flash(_("You can't edit this user"), category='warning')
361 361 return redirect(h.route_path('users'))
362 362
363 363 c.active = 'profile'
364 364 c.extern_type = c.user.extern_type
365 365 c.extern_name = c.user.extern_name
366 366 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
367 367
368 368 defaults = c.user.get_dict()
369 369 defaults.update({'language': c.user.user_data.get('language')})
370 370 return htmlfill.render(
371 371 render('admin/users/user_edit.mako'),
372 372 defaults=defaults,
373 373 encoding="UTF-8",
374 374 force_defaults=False)
375 375
376 376 @HasPermissionAllDecorator('hg.admin')
377 377 def edit_advanced(self, user_id):
378 378 user_id = safe_int(user_id)
379 379 user = c.user = User.get_or_404(user_id)
380 380 if user.username == User.DEFAULT_USER:
381 381 h.flash(_("You can't edit this user"), category='warning')
382 382 return redirect(h.route_path('users'))
383 383
384 384 c.active = 'advanced'
385 385 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
386 386 c.personal_repo_group_name = RepoGroupModel()\
387 387 .get_personal_group_name(user)
388
389 c.user_to_review_rules = sorted(
390 (x.user for x in c.user.user_review_rules),
391 key=lambda u: u.username.lower())
392
388 393 c.first_admin = User.get_first_super_admin()
389 394 defaults = user.get_dict()
390 395
391 396 # Interim workaround if the user participated on any pull requests as a
392 397 # reviewer.
393 398 has_review = len(user.reviewer_pull_requests)
394 399 c.can_delete_user = not has_review
395 400 c.can_delete_user_message = ''
396 401 inactive_link = h.link_to(
397 402 'inactive', h.url('edit_user', user_id=user_id, anchor='active'))
398 403 if has_review == 1:
399 404 c.can_delete_user_message = h.literal(_(
400 405 'The user participates as reviewer in {} pull request and '
401 406 'cannot be deleted. \nYou can set the user to '
402 407 '"{}" instead of deleting it.').format(
403 408 has_review, inactive_link))
404 409 elif has_review:
405 410 c.can_delete_user_message = h.literal(_(
406 411 'The user participates as reviewer in {} pull requests and '
407 412 'cannot be deleted. \nYou can set the user to '
408 413 '"{}" instead of deleting it.').format(
409 414 has_review, inactive_link))
410 415
411 416 return htmlfill.render(
412 417 render('admin/users/user_edit.mako'),
413 418 defaults=defaults,
414 419 encoding="UTF-8",
415 420 force_defaults=False)
416 421
417 422 @HasPermissionAllDecorator('hg.admin')
418 423 def edit_global_perms(self, user_id):
419 424 user_id = safe_int(user_id)
420 425 c.user = User.get_or_404(user_id)
421 426 if c.user.username == User.DEFAULT_USER:
422 427 h.flash(_("You can't edit this user"), category='warning')
423 428 return redirect(h.route_path('users'))
424 429
425 430 c.active = 'global_perms'
426 431
427 432 c.default_user = User.get_default_user()
428 433 defaults = c.user.get_dict()
429 434 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
430 435 defaults.update(c.default_user.get_default_perms())
431 436 defaults.update(c.user.get_default_perms())
432 437
433 438 return htmlfill.render(
434 439 render('admin/users/user_edit.mako'),
435 440 defaults=defaults,
436 441 encoding="UTF-8",
437 442 force_defaults=False)
438 443
439 444 @HasPermissionAllDecorator('hg.admin')
440 445 @auth.CSRFRequired()
441 446 def update_global_perms(self, user_id):
442 447 user_id = safe_int(user_id)
443 448 user = User.get_or_404(user_id)
444 449 c.active = 'global_perms'
445 450 try:
446 451 # first stage that verifies the checkbox
447 452 _form = UserIndividualPermissionsForm()
448 453 form_result = _form.to_python(dict(request.POST))
449 454 inherit_perms = form_result['inherit_default_permissions']
450 455 user.inherit_default_permissions = inherit_perms
451 456 Session().add(user)
452 457
453 458 if not inherit_perms:
454 459 # only update the individual ones if we un check the flag
455 460 _form = UserPermissionsForm(
456 461 [x[0] for x in c.repo_create_choices],
457 462 [x[0] for x in c.repo_create_on_write_choices],
458 463 [x[0] for x in c.repo_group_create_choices],
459 464 [x[0] for x in c.user_group_create_choices],
460 465 [x[0] for x in c.fork_choices],
461 466 [x[0] for x in c.inherit_default_permission_choices])()
462 467
463 468 form_result = _form.to_python(dict(request.POST))
464 469 form_result.update({'perm_user_id': user.user_id})
465 470
466 471 PermissionModel().update_user_permissions(form_result)
467 472
468 473 # TODO(marcink): implement global permissions
469 474 # audit_log.store_web('user.edit.permissions')
470 475
471 476 Session().commit()
472 477 h.flash(_('User global permissions updated successfully'),
473 478 category='success')
474 479
475 480 except formencode.Invalid as errors:
476 481 defaults = errors.value
477 482 c.user = user
478 483 return htmlfill.render(
479 484 render('admin/users/user_edit.mako'),
480 485 defaults=defaults,
481 486 errors=errors.error_dict or {},
482 487 prefix_error=False,
483 488 encoding="UTF-8",
484 489 force_defaults=False)
485 490 except Exception:
486 491 log.exception("Exception during permissions saving")
487 492 h.flash(_('An error occurred during permissions saving'),
488 493 category='error')
489 494 return redirect(url('edit_user_global_perms', user_id=user_id))
490 495
491 496
@@ -1,4191 +1,4194 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from sqlalchemy.sql.functions import coalesce, count # noqa
45 45 from sqlalchemy.exc import IntegrityError # noqa
46 46 from beaker.cache import cache_region
47 47 from zope.cachedescriptors.property import Lazy as LazyProperty
48 48
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from rhodecode.translation import _
52 52 from rhodecode.lib.vcs import get_vcs_instance
53 53 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
54 54 from rhodecode.lib.utils2 import (
55 55 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
56 56 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
57 57 glob2re, StrictAttributeDict, cleaned_uri)
58 58 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
59 59 from rhodecode.lib.ext_json import json
60 60 from rhodecode.lib.caching_query import FromCache
61 61 from rhodecode.lib.encrypt import AESCipher
62 62
63 63 from rhodecode.model.meta import Base, Session
64 64
65 65 URL_SEP = '/'
66 66 log = logging.getLogger(__name__)
67 67
68 68 # =============================================================================
69 69 # BASE CLASSES
70 70 # =============================================================================
71 71
72 72 # this is propagated from .ini file rhodecode.encrypted_values.secret or
73 73 # beaker.session.secret if first is not set.
74 74 # and initialized at environment.py
75 75 ENCRYPTION_KEY = None
76 76
77 77 # used to sort permissions by types, '#' used here is not allowed to be in
78 78 # usernames, and it's very early in sorted string.printable table.
79 79 PERMISSION_TYPE_SORT = {
80 80 'admin': '####',
81 81 'write': '###',
82 82 'read': '##',
83 83 'none': '#',
84 84 }
85 85
86 86
87 87 def display_sort(obj):
88 88 """
89 89 Sort function used to sort permissions in .permissions() function of
90 90 Repository, RepoGroup, UserGroup. Also it put the default user in front
91 91 of all other resources
92 92 """
93 93
94 94 if obj.username == User.DEFAULT_USER:
95 95 return '#####'
96 96 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
97 97 return prefix + obj.username
98 98
99 99
100 100 def _hash_key(k):
101 101 return md5_safe(k)
102 102
103 103
104 104 def in_filter_generator(qry, items, limit=500):
105 105 """
106 106 Splits IN() into multiple with OR
107 107 e.g.::
108 108 cnt = Repository.query().filter(
109 109 or_(
110 110 *in_filter_generator(Repository.repo_id, range(100000))
111 111 )).count()
112 112 """
113 113 parts = []
114 114 for chunk in xrange(0, len(items), limit):
115 115 parts.append(
116 116 qry.in_(items[chunk: chunk + limit])
117 117 )
118 118
119 119 return parts
120 120
121 121
122 122 class EncryptedTextValue(TypeDecorator):
123 123 """
124 124 Special column for encrypted long text data, use like::
125 125
126 126 value = Column("encrypted_value", EncryptedValue(), nullable=False)
127 127
128 128 This column is intelligent so if value is in unencrypted form it return
129 129 unencrypted form, but on save it always encrypts
130 130 """
131 131 impl = Text
132 132
133 133 def process_bind_param(self, value, dialect):
134 134 if not value:
135 135 return value
136 136 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
137 137 # protect against double encrypting if someone manually starts
138 138 # doing
139 139 raise ValueError('value needs to be in unencrypted format, ie. '
140 140 'not starting with enc$aes')
141 141 return 'enc$aes_hmac$%s' % AESCipher(
142 142 ENCRYPTION_KEY, hmac=True).encrypt(value)
143 143
144 144 def process_result_value(self, value, dialect):
145 145 import rhodecode
146 146
147 147 if not value:
148 148 return value
149 149
150 150 parts = value.split('$', 3)
151 151 if not len(parts) == 3:
152 152 # probably not encrypted values
153 153 return value
154 154 else:
155 155 if parts[0] != 'enc':
156 156 # parts ok but without our header ?
157 157 return value
158 158 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
159 159 'rhodecode.encrypted_values.strict') or True)
160 160 # at that stage we know it's our encryption
161 161 if parts[1] == 'aes':
162 162 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
163 163 elif parts[1] == 'aes_hmac':
164 164 decrypted_data = AESCipher(
165 165 ENCRYPTION_KEY, hmac=True,
166 166 strict_verification=enc_strict_mode).decrypt(parts[2])
167 167 else:
168 168 raise ValueError(
169 169 'Encryption type part is wrong, must be `aes` '
170 170 'or `aes_hmac`, got `%s` instead' % (parts[1]))
171 171 return decrypted_data
172 172
173 173
174 174 class BaseModel(object):
175 175 """
176 176 Base Model for all classes
177 177 """
178 178
179 179 @classmethod
180 180 def _get_keys(cls):
181 181 """return column names for this model """
182 182 return class_mapper(cls).c.keys()
183 183
184 184 def get_dict(self):
185 185 """
186 186 return dict with keys and values corresponding
187 187 to this model data """
188 188
189 189 d = {}
190 190 for k in self._get_keys():
191 191 d[k] = getattr(self, k)
192 192
193 193 # also use __json__() if present to get additional fields
194 194 _json_attr = getattr(self, '__json__', None)
195 195 if _json_attr:
196 196 # update with attributes from __json__
197 197 if callable(_json_attr):
198 198 _json_attr = _json_attr()
199 199 for k, val in _json_attr.iteritems():
200 200 d[k] = val
201 201 return d
202 202
203 203 def get_appstruct(self):
204 204 """return list with keys and values tuples corresponding
205 205 to this model data """
206 206
207 207 l = []
208 208 for k in self._get_keys():
209 209 l.append((k, getattr(self, k),))
210 210 return l
211 211
212 212 def populate_obj(self, populate_dict):
213 213 """populate model with data from given populate_dict"""
214 214
215 215 for k in self._get_keys():
216 216 if k in populate_dict:
217 217 setattr(self, k, populate_dict[k])
218 218
219 219 @classmethod
220 220 def query(cls):
221 221 return Session().query(cls)
222 222
223 223 @classmethod
224 224 def get(cls, id_):
225 225 if id_:
226 226 return cls.query().get(id_)
227 227
228 228 @classmethod
229 229 def get_or_404(cls, id_):
230 230 from pyramid.httpexceptions import HTTPNotFound
231 231
232 232 try:
233 233 id_ = int(id_)
234 234 except (TypeError, ValueError):
235 235 raise HTTPNotFound()
236 236
237 237 res = cls.query().get(id_)
238 238 if not res:
239 239 raise HTTPNotFound()
240 240 return res
241 241
242 242 @classmethod
243 243 def getAll(cls):
244 244 # deprecated and left for backward compatibility
245 245 return cls.get_all()
246 246
247 247 @classmethod
248 248 def get_all(cls):
249 249 return cls.query().all()
250 250
251 251 @classmethod
252 252 def delete(cls, id_):
253 253 obj = cls.query().get(id_)
254 254 Session().delete(obj)
255 255
256 256 @classmethod
257 257 def identity_cache(cls, session, attr_name, value):
258 258 exist_in_session = []
259 259 for (item_cls, pkey), instance in session.identity_map.items():
260 260 if cls == item_cls and getattr(instance, attr_name) == value:
261 261 exist_in_session.append(instance)
262 262 if exist_in_session:
263 263 if len(exist_in_session) == 1:
264 264 return exist_in_session[0]
265 265 log.exception(
266 266 'multiple objects with attr %s and '
267 267 'value %s found with same name: %r',
268 268 attr_name, value, exist_in_session)
269 269
270 270 def __repr__(self):
271 271 if hasattr(self, '__unicode__'):
272 272 # python repr needs to return str
273 273 try:
274 274 return safe_str(self.__unicode__())
275 275 except UnicodeDecodeError:
276 276 pass
277 277 return '<DB:%s>' % (self.__class__.__name__)
278 278
279 279
280 280 class RhodeCodeSetting(Base, BaseModel):
281 281 __tablename__ = 'rhodecode_settings'
282 282 __table_args__ = (
283 283 UniqueConstraint('app_settings_name'),
284 284 {'extend_existing': True, 'mysql_engine': 'InnoDB',
285 285 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
286 286 )
287 287
288 288 SETTINGS_TYPES = {
289 289 'str': safe_str,
290 290 'int': safe_int,
291 291 'unicode': safe_unicode,
292 292 'bool': str2bool,
293 293 'list': functools.partial(aslist, sep=',')
294 294 }
295 295 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
296 296 GLOBAL_CONF_KEY = 'app_settings'
297 297
298 298 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
299 299 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
300 300 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
301 301 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
302 302
303 303 def __init__(self, key='', val='', type='unicode'):
304 304 self.app_settings_name = key
305 305 self.app_settings_type = type
306 306 self.app_settings_value = val
307 307
308 308 @validates('_app_settings_value')
309 309 def validate_settings_value(self, key, val):
310 310 assert type(val) == unicode
311 311 return val
312 312
313 313 @hybrid_property
314 314 def app_settings_value(self):
315 315 v = self._app_settings_value
316 316 _type = self.app_settings_type
317 317 if _type:
318 318 _type = self.app_settings_type.split('.')[0]
319 319 # decode the encrypted value
320 320 if 'encrypted' in self.app_settings_type:
321 321 cipher = EncryptedTextValue()
322 322 v = safe_unicode(cipher.process_result_value(v, None))
323 323
324 324 converter = self.SETTINGS_TYPES.get(_type) or \
325 325 self.SETTINGS_TYPES['unicode']
326 326 return converter(v)
327 327
328 328 @app_settings_value.setter
329 329 def app_settings_value(self, val):
330 330 """
331 331 Setter that will always make sure we use unicode in app_settings_value
332 332
333 333 :param val:
334 334 """
335 335 val = safe_unicode(val)
336 336 # encode the encrypted value
337 337 if 'encrypted' in self.app_settings_type:
338 338 cipher = EncryptedTextValue()
339 339 val = safe_unicode(cipher.process_bind_param(val, None))
340 340 self._app_settings_value = val
341 341
342 342 @hybrid_property
343 343 def app_settings_type(self):
344 344 return self._app_settings_type
345 345
346 346 @app_settings_type.setter
347 347 def app_settings_type(self, val):
348 348 if val.split('.')[0] not in self.SETTINGS_TYPES:
349 349 raise Exception('type must be one of %s got %s'
350 350 % (self.SETTINGS_TYPES.keys(), val))
351 351 self._app_settings_type = val
352 352
353 353 def __unicode__(self):
354 354 return u"<%s('%s:%s[%s]')>" % (
355 355 self.__class__.__name__,
356 356 self.app_settings_name, self.app_settings_value,
357 357 self.app_settings_type
358 358 )
359 359
360 360
361 361 class RhodeCodeUi(Base, BaseModel):
362 362 __tablename__ = 'rhodecode_ui'
363 363 __table_args__ = (
364 364 UniqueConstraint('ui_key'),
365 365 {'extend_existing': True, 'mysql_engine': 'InnoDB',
366 366 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
367 367 )
368 368
369 369 HOOK_REPO_SIZE = 'changegroup.repo_size'
370 370 # HG
371 371 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
372 372 HOOK_PULL = 'outgoing.pull_logger'
373 373 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
374 374 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
375 375 HOOK_PUSH = 'changegroup.push_logger'
376 376 HOOK_PUSH_KEY = 'pushkey.key_push'
377 377
378 378 # TODO: johbo: Unify way how hooks are configured for git and hg,
379 379 # git part is currently hardcoded.
380 380
381 381 # SVN PATTERNS
382 382 SVN_BRANCH_ID = 'vcs_svn_branch'
383 383 SVN_TAG_ID = 'vcs_svn_tag'
384 384
385 385 ui_id = Column(
386 386 "ui_id", Integer(), nullable=False, unique=True, default=None,
387 387 primary_key=True)
388 388 ui_section = Column(
389 389 "ui_section", String(255), nullable=True, unique=None, default=None)
390 390 ui_key = Column(
391 391 "ui_key", String(255), nullable=True, unique=None, default=None)
392 392 ui_value = Column(
393 393 "ui_value", String(255), nullable=True, unique=None, default=None)
394 394 ui_active = Column(
395 395 "ui_active", Boolean(), nullable=True, unique=None, default=True)
396 396
397 397 def __repr__(self):
398 398 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
399 399 self.ui_key, self.ui_value)
400 400
401 401
402 402 class RepoRhodeCodeSetting(Base, BaseModel):
403 403 __tablename__ = 'repo_rhodecode_settings'
404 404 __table_args__ = (
405 405 UniqueConstraint(
406 406 'app_settings_name', 'repository_id',
407 407 name='uq_repo_rhodecode_setting_name_repo_id'),
408 408 {'extend_existing': True, 'mysql_engine': 'InnoDB',
409 409 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
410 410 )
411 411
412 412 repository_id = Column(
413 413 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
414 414 nullable=False)
415 415 app_settings_id = Column(
416 416 "app_settings_id", Integer(), nullable=False, unique=True,
417 417 default=None, primary_key=True)
418 418 app_settings_name = Column(
419 419 "app_settings_name", String(255), nullable=True, unique=None,
420 420 default=None)
421 421 _app_settings_value = Column(
422 422 "app_settings_value", String(4096), nullable=True, unique=None,
423 423 default=None)
424 424 _app_settings_type = Column(
425 425 "app_settings_type", String(255), nullable=True, unique=None,
426 426 default=None)
427 427
428 428 repository = relationship('Repository')
429 429
430 430 def __init__(self, repository_id, key='', val='', type='unicode'):
431 431 self.repository_id = repository_id
432 432 self.app_settings_name = key
433 433 self.app_settings_type = type
434 434 self.app_settings_value = val
435 435
436 436 @validates('_app_settings_value')
437 437 def validate_settings_value(self, key, val):
438 438 assert type(val) == unicode
439 439 return val
440 440
441 441 @hybrid_property
442 442 def app_settings_value(self):
443 443 v = self._app_settings_value
444 444 type_ = self.app_settings_type
445 445 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
446 446 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
447 447 return converter(v)
448 448
449 449 @app_settings_value.setter
450 450 def app_settings_value(self, val):
451 451 """
452 452 Setter that will always make sure we use unicode in app_settings_value
453 453
454 454 :param val:
455 455 """
456 456 self._app_settings_value = safe_unicode(val)
457 457
458 458 @hybrid_property
459 459 def app_settings_type(self):
460 460 return self._app_settings_type
461 461
462 462 @app_settings_type.setter
463 463 def app_settings_type(self, val):
464 464 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
465 465 if val not in SETTINGS_TYPES:
466 466 raise Exception('type must be one of %s got %s'
467 467 % (SETTINGS_TYPES.keys(), val))
468 468 self._app_settings_type = val
469 469
470 470 def __unicode__(self):
471 471 return u"<%s('%s:%s:%s[%s]')>" % (
472 472 self.__class__.__name__, self.repository.repo_name,
473 473 self.app_settings_name, self.app_settings_value,
474 474 self.app_settings_type
475 475 )
476 476
477 477
478 478 class RepoRhodeCodeUi(Base, BaseModel):
479 479 __tablename__ = 'repo_rhodecode_ui'
480 480 __table_args__ = (
481 481 UniqueConstraint(
482 482 'repository_id', 'ui_section', 'ui_key',
483 483 name='uq_repo_rhodecode_ui_repository_id_section_key'),
484 484 {'extend_existing': True, 'mysql_engine': 'InnoDB',
485 485 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
486 486 )
487 487
488 488 repository_id = Column(
489 489 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
490 490 nullable=False)
491 491 ui_id = Column(
492 492 "ui_id", Integer(), nullable=False, unique=True, default=None,
493 493 primary_key=True)
494 494 ui_section = Column(
495 495 "ui_section", String(255), nullable=True, unique=None, default=None)
496 496 ui_key = Column(
497 497 "ui_key", String(255), nullable=True, unique=None, default=None)
498 498 ui_value = Column(
499 499 "ui_value", String(255), nullable=True, unique=None, default=None)
500 500 ui_active = Column(
501 501 "ui_active", Boolean(), nullable=True, unique=None, default=True)
502 502
503 503 repository = relationship('Repository')
504 504
505 505 def __repr__(self):
506 506 return '<%s[%s:%s]%s=>%s]>' % (
507 507 self.__class__.__name__, self.repository.repo_name,
508 508 self.ui_section, self.ui_key, self.ui_value)
509 509
510 510
511 511 class User(Base, BaseModel):
512 512 __tablename__ = 'users'
513 513 __table_args__ = (
514 514 UniqueConstraint('username'), UniqueConstraint('email'),
515 515 Index('u_username_idx', 'username'),
516 516 Index('u_email_idx', 'email'),
517 517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
518 518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
519 519 )
520 520 DEFAULT_USER = 'default'
521 521 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
522 522 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
523 523
524 524 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
525 525 username = Column("username", String(255), nullable=True, unique=None, default=None)
526 526 password = Column("password", String(255), nullable=True, unique=None, default=None)
527 527 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
528 528 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
529 529 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
530 530 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
531 531 _email = Column("email", String(255), nullable=True, unique=None, default=None)
532 532 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
533 533 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
534 534
535 535 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
536 536 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
537 537 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
538 538 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
539 539 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
540 540 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
541 541
542 542 user_log = relationship('UserLog')
543 543 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
544 544
545 545 repositories = relationship('Repository')
546 546 repository_groups = relationship('RepoGroup')
547 547 user_groups = relationship('UserGroup')
548 548
549 549 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
550 550 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
551 551
552 552 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
553 553 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
554 554 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
555 555
556 556 group_member = relationship('UserGroupMember', cascade='all')
557 557
558 558 notifications = relationship('UserNotification', cascade='all')
559 559 # notifications assigned to this user
560 560 user_created_notifications = relationship('Notification', cascade='all')
561 561 # comments created by this user
562 562 user_comments = relationship('ChangesetComment', cascade='all')
563 563 # user profile extra info
564 564 user_emails = relationship('UserEmailMap', cascade='all')
565 565 user_ip_map = relationship('UserIpMap', cascade='all')
566 566 user_auth_tokens = relationship('UserApiKeys', cascade='all')
567 567 user_ssh_keys = relationship('UserSshKeys', cascade='all')
568 568
569 569 # gists
570 570 user_gists = relationship('Gist', cascade='all')
571 571 # user pull requests
572 572 user_pull_requests = relationship('PullRequest', cascade='all')
573 573 # external identities
574 574 extenal_identities = relationship(
575 575 'ExternalIdentity',
576 576 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
577 577 cascade='all')
578 # review rules
579 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
578 580
579 581 def __unicode__(self):
580 582 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
581 583 self.user_id, self.username)
582 584
583 585 @hybrid_property
584 586 def email(self):
585 587 return self._email
586 588
587 589 @email.setter
588 590 def email(self, val):
589 591 self._email = val.lower() if val else None
590 592
591 593 @hybrid_property
592 594 def first_name(self):
593 595 from rhodecode.lib import helpers as h
594 596 if self.name:
595 597 return h.escape(self.name)
596 598 return self.name
597 599
598 600 @hybrid_property
599 601 def last_name(self):
600 602 from rhodecode.lib import helpers as h
601 603 if self.lastname:
602 604 return h.escape(self.lastname)
603 605 return self.lastname
604 606
605 607 @hybrid_property
606 608 def api_key(self):
607 609 """
608 610 Fetch if exist an auth-token with role ALL connected to this user
609 611 """
610 612 user_auth_token = UserApiKeys.query()\
611 613 .filter(UserApiKeys.user_id == self.user_id)\
612 614 .filter(or_(UserApiKeys.expires == -1,
613 615 UserApiKeys.expires >= time.time()))\
614 616 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
615 617 if user_auth_token:
616 618 user_auth_token = user_auth_token.api_key
617 619
618 620 return user_auth_token
619 621
620 622 @api_key.setter
621 623 def api_key(self, val):
622 624 # don't allow to set API key this is deprecated for now
623 625 self._api_key = None
624 626
625 627 @property
626 628 def reviewer_pull_requests(self):
627 629 return PullRequestReviewers.query() \
628 630 .options(joinedload(PullRequestReviewers.pull_request)) \
629 631 .filter(PullRequestReviewers.user_id == self.user_id) \
630 632 .all()
631 633
632 634 @property
633 635 def firstname(self):
634 636 # alias for future
635 637 return self.name
636 638
637 639 @property
638 640 def emails(self):
639 641 other = UserEmailMap.query()\
640 642 .filter(UserEmailMap.user == self) \
641 643 .order_by(UserEmailMap.email_id.asc()) \
642 644 .all()
643 645 return [self.email] + [x.email for x in other]
644 646
645 647 @property
646 648 def auth_tokens(self):
647 649 auth_tokens = self.get_auth_tokens()
648 650 return [x.api_key for x in auth_tokens]
649 651
650 652 def get_auth_tokens(self):
651 653 return UserApiKeys.query()\
652 654 .filter(UserApiKeys.user == self)\
653 655 .order_by(UserApiKeys.user_api_key_id.asc())\
654 656 .all()
655 657
656 658 @property
657 659 def feed_token(self):
658 660 return self.get_feed_token()
659 661
660 662 def get_feed_token(self):
661 663 feed_tokens = UserApiKeys.query()\
662 664 .filter(UserApiKeys.user == self)\
663 665 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
664 666 .all()
665 667 if feed_tokens:
666 668 return feed_tokens[0].api_key
667 669 return 'NO_FEED_TOKEN_AVAILABLE'
668 670
669 671 @classmethod
670 672 def extra_valid_auth_tokens(cls, user, role=None):
671 673 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
672 674 .filter(or_(UserApiKeys.expires == -1,
673 675 UserApiKeys.expires >= time.time()))
674 676 if role:
675 677 tokens = tokens.filter(or_(UserApiKeys.role == role,
676 678 UserApiKeys.role == UserApiKeys.ROLE_ALL))
677 679 return tokens.all()
678 680
679 681 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
680 682 from rhodecode.lib import auth
681 683
682 684 log.debug('Trying to authenticate user: %s via auth-token, '
683 685 'and roles: %s', self, roles)
684 686
685 687 if not auth_token:
686 688 return False
687 689
688 690 crypto_backend = auth.crypto_backend()
689 691
690 692 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
691 693 tokens_q = UserApiKeys.query()\
692 694 .filter(UserApiKeys.user_id == self.user_id)\
693 695 .filter(or_(UserApiKeys.expires == -1,
694 696 UserApiKeys.expires >= time.time()))
695 697
696 698 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
697 699
698 700 plain_tokens = []
699 701 hash_tokens = []
700 702
701 703 for token in tokens_q.all():
702 704 # verify scope first
703 705 if token.repo_id:
704 706 # token has a scope, we need to verify it
705 707 if scope_repo_id != token.repo_id:
706 708 log.debug(
707 709 'Scope mismatch: token has a set repo scope: %s, '
708 710 'and calling scope is:%s, skipping further checks',
709 711 token.repo, scope_repo_id)
710 712 # token has a scope, and it doesn't match, skip token
711 713 continue
712 714
713 715 if token.api_key.startswith(crypto_backend.ENC_PREF):
714 716 hash_tokens.append(token.api_key)
715 717 else:
716 718 plain_tokens.append(token.api_key)
717 719
718 720 is_plain_match = auth_token in plain_tokens
719 721 if is_plain_match:
720 722 return True
721 723
722 724 for hashed in hash_tokens:
723 725 # TODO(marcink): this is expensive to calculate, but most secure
724 726 match = crypto_backend.hash_check(auth_token, hashed)
725 727 if match:
726 728 return True
727 729
728 730 return False
729 731
730 732 @property
731 733 def ip_addresses(self):
732 734 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
733 735 return [x.ip_addr for x in ret]
734 736
735 737 @property
736 738 def username_and_name(self):
737 739 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
738 740
739 741 @property
740 742 def username_or_name_or_email(self):
741 743 full_name = self.full_name if self.full_name is not ' ' else None
742 744 return self.username or full_name or self.email
743 745
744 746 @property
745 747 def full_name(self):
746 748 return '%s %s' % (self.first_name, self.last_name)
747 749
748 750 @property
749 751 def full_name_or_username(self):
750 752 return ('%s %s' % (self.first_name, self.last_name)
751 753 if (self.first_name and self.last_name) else self.username)
752 754
753 755 @property
754 756 def full_contact(self):
755 757 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
756 758
757 759 @property
758 760 def short_contact(self):
759 761 return '%s %s' % (self.first_name, self.last_name)
760 762
761 763 @property
762 764 def is_admin(self):
763 765 return self.admin
764 766
765 767 def AuthUser(self, **kwargs):
766 768 """
767 769 Returns instance of AuthUser for this user
768 770 """
769 771 from rhodecode.lib.auth import AuthUser
770 772 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
771 773
772 774 @hybrid_property
773 775 def user_data(self):
774 776 if not self._user_data:
775 777 return {}
776 778
777 779 try:
778 780 return json.loads(self._user_data)
779 781 except TypeError:
780 782 return {}
781 783
782 784 @user_data.setter
783 785 def user_data(self, val):
784 786 if not isinstance(val, dict):
785 787 raise Exception('user_data must be dict, got %s' % type(val))
786 788 try:
787 789 self._user_data = json.dumps(val)
788 790 except Exception:
789 791 log.error(traceback.format_exc())
790 792
791 793 @classmethod
792 794 def get_by_username(cls, username, case_insensitive=False,
793 795 cache=False, identity_cache=False):
794 796 session = Session()
795 797
796 798 if case_insensitive:
797 799 q = cls.query().filter(
798 800 func.lower(cls.username) == func.lower(username))
799 801 else:
800 802 q = cls.query().filter(cls.username == username)
801 803
802 804 if cache:
803 805 if identity_cache:
804 806 val = cls.identity_cache(session, 'username', username)
805 807 if val:
806 808 return val
807 809 else:
808 810 cache_key = "get_user_by_name_%s" % _hash_key(username)
809 811 q = q.options(
810 812 FromCache("sql_cache_short", cache_key))
811 813
812 814 return q.scalar()
813 815
814 816 @classmethod
815 817 def get_by_auth_token(cls, auth_token, cache=False):
816 818 q = UserApiKeys.query()\
817 819 .filter(UserApiKeys.api_key == auth_token)\
818 820 .filter(or_(UserApiKeys.expires == -1,
819 821 UserApiKeys.expires >= time.time()))
820 822 if cache:
821 823 q = q.options(
822 824 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
823 825
824 826 match = q.first()
825 827 if match:
826 828 return match.user
827 829
828 830 @classmethod
829 831 def get_by_email(cls, email, case_insensitive=False, cache=False):
830 832
831 833 if case_insensitive:
832 834 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
833 835
834 836 else:
835 837 q = cls.query().filter(cls.email == email)
836 838
837 839 email_key = _hash_key(email)
838 840 if cache:
839 841 q = q.options(
840 842 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
841 843
842 844 ret = q.scalar()
843 845 if ret is None:
844 846 q = UserEmailMap.query()
845 847 # try fetching in alternate email map
846 848 if case_insensitive:
847 849 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
848 850 else:
849 851 q = q.filter(UserEmailMap.email == email)
850 852 q = q.options(joinedload(UserEmailMap.user))
851 853 if cache:
852 854 q = q.options(
853 855 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
854 856 ret = getattr(q.scalar(), 'user', None)
855 857
856 858 return ret
857 859
858 860 @classmethod
859 861 def get_from_cs_author(cls, author):
860 862 """
861 863 Tries to get User objects out of commit author string
862 864
863 865 :param author:
864 866 """
865 867 from rhodecode.lib.helpers import email, author_name
866 868 # Valid email in the attribute passed, see if they're in the system
867 869 _email = email(author)
868 870 if _email:
869 871 user = cls.get_by_email(_email, case_insensitive=True)
870 872 if user:
871 873 return user
872 874 # Maybe we can match by username?
873 875 _author = author_name(author)
874 876 user = cls.get_by_username(_author, case_insensitive=True)
875 877 if user:
876 878 return user
877 879
878 880 def update_userdata(self, **kwargs):
879 881 usr = self
880 882 old = usr.user_data
881 883 old.update(**kwargs)
882 884 usr.user_data = old
883 885 Session().add(usr)
884 886 log.debug('updated userdata with ', kwargs)
885 887
886 888 def update_lastlogin(self):
887 889 """Update user lastlogin"""
888 890 self.last_login = datetime.datetime.now()
889 891 Session().add(self)
890 892 log.debug('updated user %s lastlogin', self.username)
891 893
892 894 def update_lastactivity(self):
893 895 """Update user lastactivity"""
894 896 self.last_activity = datetime.datetime.now()
895 897 Session().add(self)
896 898 log.debug('updated user %s lastactivity', self.username)
897 899
898 900 def update_password(self, new_password):
899 901 from rhodecode.lib.auth import get_crypt_password
900 902
901 903 self.password = get_crypt_password(new_password)
902 904 Session().add(self)
903 905
904 906 @classmethod
905 907 def get_first_super_admin(cls):
906 908 user = User.query().filter(User.admin == true()).first()
907 909 if user is None:
908 910 raise Exception('FATAL: Missing administrative account!')
909 911 return user
910 912
911 913 @classmethod
912 914 def get_all_super_admins(cls):
913 915 """
914 916 Returns all admin accounts sorted by username
915 917 """
916 918 return User.query().filter(User.admin == true())\
917 919 .order_by(User.username.asc()).all()
918 920
919 921 @classmethod
920 922 def get_default_user(cls, cache=False, refresh=False):
921 923 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
922 924 if user is None:
923 925 raise Exception('FATAL: Missing default account!')
924 926 if refresh:
925 927 # The default user might be based on outdated state which
926 928 # has been loaded from the cache.
927 929 # A call to refresh() ensures that the
928 930 # latest state from the database is used.
929 931 Session().refresh(user)
930 932 return user
931 933
932 934 def _get_default_perms(self, user, suffix=''):
933 935 from rhodecode.model.permission import PermissionModel
934 936 return PermissionModel().get_default_perms(user.user_perms, suffix)
935 937
936 938 def get_default_perms(self, suffix=''):
937 939 return self._get_default_perms(self, suffix)
938 940
939 941 def get_api_data(self, include_secrets=False, details='full'):
940 942 """
941 943 Common function for generating user related data for API
942 944
943 945 :param include_secrets: By default secrets in the API data will be replaced
944 946 by a placeholder value to prevent exposing this data by accident. In case
945 947 this data shall be exposed, set this flag to ``True``.
946 948
947 949 :param details: details can be 'basic|full' basic gives only a subset of
948 950 the available user information that includes user_id, name and emails.
949 951 """
950 952 user = self
951 953 user_data = self.user_data
952 954 data = {
953 955 'user_id': user.user_id,
954 956 'username': user.username,
955 957 'firstname': user.name,
956 958 'lastname': user.lastname,
957 959 'email': user.email,
958 960 'emails': user.emails,
959 961 }
960 962 if details == 'basic':
961 963 return data
962 964
963 965 auth_token_length = 40
964 966 auth_token_replacement = '*' * auth_token_length
965 967
966 968 extras = {
967 969 'auth_tokens': [auth_token_replacement],
968 970 'active': user.active,
969 971 'admin': user.admin,
970 972 'extern_type': user.extern_type,
971 973 'extern_name': user.extern_name,
972 974 'last_login': user.last_login,
973 975 'last_activity': user.last_activity,
974 976 'ip_addresses': user.ip_addresses,
975 977 'language': user_data.get('language')
976 978 }
977 979 data.update(extras)
978 980
979 981 if include_secrets:
980 982 data['auth_tokens'] = user.auth_tokens
981 983 return data
982 984
983 985 def __json__(self):
984 986 data = {
985 987 'full_name': self.full_name,
986 988 'full_name_or_username': self.full_name_or_username,
987 989 'short_contact': self.short_contact,
988 990 'full_contact': self.full_contact,
989 991 }
990 992 data.update(self.get_api_data())
991 993 return data
992 994
993 995
994 996 class UserApiKeys(Base, BaseModel):
995 997 __tablename__ = 'user_api_keys'
996 998 __table_args__ = (
997 999 Index('uak_api_key_idx', 'api_key', unique=True),
998 1000 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
999 1001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1000 1002 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1001 1003 )
1002 1004 __mapper_args__ = {}
1003 1005
1004 1006 # ApiKey role
1005 1007 ROLE_ALL = 'token_role_all'
1006 1008 ROLE_HTTP = 'token_role_http'
1007 1009 ROLE_VCS = 'token_role_vcs'
1008 1010 ROLE_API = 'token_role_api'
1009 1011 ROLE_FEED = 'token_role_feed'
1010 1012 ROLE_PASSWORD_RESET = 'token_password_reset'
1011 1013
1012 1014 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1013 1015
1014 1016 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1015 1017 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1016 1018 api_key = Column("api_key", String(255), nullable=False, unique=True)
1017 1019 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1018 1020 expires = Column('expires', Float(53), nullable=False)
1019 1021 role = Column('role', String(255), nullable=True)
1020 1022 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1021 1023
1022 1024 # scope columns
1023 1025 repo_id = Column(
1024 1026 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1025 1027 nullable=True, unique=None, default=None)
1026 1028 repo = relationship('Repository', lazy='joined')
1027 1029
1028 1030 repo_group_id = Column(
1029 1031 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1030 1032 nullable=True, unique=None, default=None)
1031 1033 repo_group = relationship('RepoGroup', lazy='joined')
1032 1034
1033 1035 user = relationship('User', lazy='joined')
1034 1036
1035 1037 def __unicode__(self):
1036 1038 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1037 1039
1038 1040 def __json__(self):
1039 1041 data = {
1040 1042 'auth_token': self.api_key,
1041 1043 'role': self.role,
1042 1044 'scope': self.scope_humanized,
1043 1045 'expired': self.expired
1044 1046 }
1045 1047 return data
1046 1048
1047 1049 def get_api_data(self, include_secrets=False):
1048 1050 data = self.__json__()
1049 1051 if include_secrets:
1050 1052 return data
1051 1053 else:
1052 1054 data['auth_token'] = self.token_obfuscated
1053 1055 return data
1054 1056
1055 1057 @hybrid_property
1056 1058 def description_safe(self):
1057 1059 from rhodecode.lib import helpers as h
1058 1060 return h.escape(self.description)
1059 1061
1060 1062 @property
1061 1063 def expired(self):
1062 1064 if self.expires == -1:
1063 1065 return False
1064 1066 return time.time() > self.expires
1065 1067
1066 1068 @classmethod
1067 1069 def _get_role_name(cls, role):
1068 1070 return {
1069 1071 cls.ROLE_ALL: _('all'),
1070 1072 cls.ROLE_HTTP: _('http/web interface'),
1071 1073 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1072 1074 cls.ROLE_API: _('api calls'),
1073 1075 cls.ROLE_FEED: _('feed access'),
1074 1076 }.get(role, role)
1075 1077
1076 1078 @property
1077 1079 def role_humanized(self):
1078 1080 return self._get_role_name(self.role)
1079 1081
1080 1082 def _get_scope(self):
1081 1083 if self.repo:
1082 1084 return repr(self.repo)
1083 1085 if self.repo_group:
1084 1086 return repr(self.repo_group) + ' (recursive)'
1085 1087 return 'global'
1086 1088
1087 1089 @property
1088 1090 def scope_humanized(self):
1089 1091 return self._get_scope()
1090 1092
1091 1093 @property
1092 1094 def token_obfuscated(self):
1093 1095 if self.api_key:
1094 1096 return self.api_key[:4] + "****"
1095 1097
1096 1098
1097 1099 class UserEmailMap(Base, BaseModel):
1098 1100 __tablename__ = 'user_email_map'
1099 1101 __table_args__ = (
1100 1102 Index('uem_email_idx', 'email'),
1101 1103 UniqueConstraint('email'),
1102 1104 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1103 1105 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1104 1106 )
1105 1107 __mapper_args__ = {}
1106 1108
1107 1109 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1108 1110 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1109 1111 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1110 1112 user = relationship('User', lazy='joined')
1111 1113
1112 1114 @validates('_email')
1113 1115 def validate_email(self, key, email):
1114 1116 # check if this email is not main one
1115 1117 main_email = Session().query(User).filter(User.email == email).scalar()
1116 1118 if main_email is not None:
1117 1119 raise AttributeError('email %s is present is user table' % email)
1118 1120 return email
1119 1121
1120 1122 @hybrid_property
1121 1123 def email(self):
1122 1124 return self._email
1123 1125
1124 1126 @email.setter
1125 1127 def email(self, val):
1126 1128 self._email = val.lower() if val else None
1127 1129
1128 1130
1129 1131 class UserIpMap(Base, BaseModel):
1130 1132 __tablename__ = 'user_ip_map'
1131 1133 __table_args__ = (
1132 1134 UniqueConstraint('user_id', 'ip_addr'),
1133 1135 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1134 1136 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1135 1137 )
1136 1138 __mapper_args__ = {}
1137 1139
1138 1140 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1139 1141 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1140 1142 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1141 1143 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1142 1144 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1143 1145 user = relationship('User', lazy='joined')
1144 1146
1145 1147 @hybrid_property
1146 1148 def description_safe(self):
1147 1149 from rhodecode.lib import helpers as h
1148 1150 return h.escape(self.description)
1149 1151
1150 1152 @classmethod
1151 1153 def _get_ip_range(cls, ip_addr):
1152 1154 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1153 1155 return [str(net.network_address), str(net.broadcast_address)]
1154 1156
1155 1157 def __json__(self):
1156 1158 return {
1157 1159 'ip_addr': self.ip_addr,
1158 1160 'ip_range': self._get_ip_range(self.ip_addr),
1159 1161 }
1160 1162
1161 1163 def __unicode__(self):
1162 1164 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1163 1165 self.user_id, self.ip_addr)
1164 1166
1165 1167
1166 1168 class UserSshKeys(Base, BaseModel):
1167 1169 __tablename__ = 'user_ssh_keys'
1168 1170 __table_args__ = (
1169 1171 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1170 1172
1171 1173 UniqueConstraint('ssh_key_fingerprint'),
1172 1174
1173 1175 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1174 1176 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1175 1177 )
1176 1178 __mapper_args__ = {}
1177 1179
1178 1180 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1179 1181 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1180 1182 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1181 1183
1182 1184 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1183 1185
1184 1186 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1185 1187 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1186 1188 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1187 1189
1188 1190 user = relationship('User', lazy='joined')
1189 1191
1190 1192 def __json__(self):
1191 1193 data = {
1192 1194 'ssh_fingerprint': self.ssh_key_fingerprint,
1193 1195 'description': self.description,
1194 1196 'created_on': self.created_on
1195 1197 }
1196 1198 return data
1197 1199
1198 1200 def get_api_data(self):
1199 1201 data = self.__json__()
1200 1202 return data
1201 1203
1202 1204
1203 1205 class UserLog(Base, BaseModel):
1204 1206 __tablename__ = 'user_logs'
1205 1207 __table_args__ = (
1206 1208 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1207 1209 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1208 1210 )
1209 1211 VERSION_1 = 'v1'
1210 1212 VERSION_2 = 'v2'
1211 1213 VERSIONS = [VERSION_1, VERSION_2]
1212 1214
1213 1215 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1214 1216 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1215 1217 username = Column("username", String(255), nullable=True, unique=None, default=None)
1216 1218 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1217 1219 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1218 1220 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1219 1221 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1220 1222 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1221 1223
1222 1224 version = Column("version", String(255), nullable=True, default=VERSION_1)
1223 1225 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1224 1226 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1225 1227
1226 1228 def __unicode__(self):
1227 1229 return u"<%s('id:%s:%s')>" % (
1228 1230 self.__class__.__name__, self.repository_name, self.action)
1229 1231
1230 1232 def __json__(self):
1231 1233 return {
1232 1234 'user_id': self.user_id,
1233 1235 'username': self.username,
1234 1236 'repository_id': self.repository_id,
1235 1237 'repository_name': self.repository_name,
1236 1238 'user_ip': self.user_ip,
1237 1239 'action_date': self.action_date,
1238 1240 'action': self.action,
1239 1241 }
1240 1242
1241 1243 @property
1242 1244 def action_as_day(self):
1243 1245 return datetime.date(*self.action_date.timetuple()[:3])
1244 1246
1245 1247 user = relationship('User')
1246 1248 repository = relationship('Repository', cascade='')
1247 1249
1248 1250
1249 1251 class UserGroup(Base, BaseModel):
1250 1252 __tablename__ = 'users_groups'
1251 1253 __table_args__ = (
1252 1254 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1253 1255 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1254 1256 )
1255 1257
1256 1258 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1257 1259 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1258 1260 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1259 1261 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1260 1262 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1261 1263 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1262 1264 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1263 1265 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1264 1266
1265 1267 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1266 1268 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1267 1269 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1268 1270 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1269 1271 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1270 1272 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1271 1273
1274 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1272 1275 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1273 1276
1274 1277 @classmethod
1275 1278 def _load_group_data(cls, column):
1276 1279 if not column:
1277 1280 return {}
1278 1281
1279 1282 try:
1280 1283 return json.loads(column) or {}
1281 1284 except TypeError:
1282 1285 return {}
1283 1286
1284 1287 @hybrid_property
1285 1288 def description_safe(self):
1286 1289 from rhodecode.lib import helpers as h
1287 1290 return h.escape(self.description)
1288 1291
1289 1292 @hybrid_property
1290 1293 def group_data(self):
1291 1294 return self._load_group_data(self._group_data)
1292 1295
1293 1296 @group_data.expression
1294 1297 def group_data(self, **kwargs):
1295 1298 return self._group_data
1296 1299
1297 1300 @group_data.setter
1298 1301 def group_data(self, val):
1299 1302 try:
1300 1303 self._group_data = json.dumps(val)
1301 1304 except Exception:
1302 1305 log.error(traceback.format_exc())
1303 1306
1304 1307 def __unicode__(self):
1305 1308 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1306 1309 self.users_group_id,
1307 1310 self.users_group_name)
1308 1311
1309 1312 @classmethod
1310 1313 def get_by_group_name(cls, group_name, cache=False,
1311 1314 case_insensitive=False):
1312 1315 if case_insensitive:
1313 1316 q = cls.query().filter(func.lower(cls.users_group_name) ==
1314 1317 func.lower(group_name))
1315 1318
1316 1319 else:
1317 1320 q = cls.query().filter(cls.users_group_name == group_name)
1318 1321 if cache:
1319 1322 q = q.options(
1320 1323 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1321 1324 return q.scalar()
1322 1325
1323 1326 @classmethod
1324 1327 def get(cls, user_group_id, cache=False):
1325 1328 user_group = cls.query()
1326 1329 if cache:
1327 1330 user_group = user_group.options(
1328 1331 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1329 1332 return user_group.get(user_group_id)
1330 1333
1331 1334 def permissions(self, with_admins=True, with_owner=True):
1332 1335 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1333 1336 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1334 1337 joinedload(UserUserGroupToPerm.user),
1335 1338 joinedload(UserUserGroupToPerm.permission),)
1336 1339
1337 1340 # get owners and admins and permissions. We do a trick of re-writing
1338 1341 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1339 1342 # has a global reference and changing one object propagates to all
1340 1343 # others. This means if admin is also an owner admin_row that change
1341 1344 # would propagate to both objects
1342 1345 perm_rows = []
1343 1346 for _usr in q.all():
1344 1347 usr = AttributeDict(_usr.user.get_dict())
1345 1348 usr.permission = _usr.permission.permission_name
1346 1349 perm_rows.append(usr)
1347 1350
1348 1351 # filter the perm rows by 'default' first and then sort them by
1349 1352 # admin,write,read,none permissions sorted again alphabetically in
1350 1353 # each group
1351 1354 perm_rows = sorted(perm_rows, key=display_sort)
1352 1355
1353 1356 _admin_perm = 'usergroup.admin'
1354 1357 owner_row = []
1355 1358 if with_owner:
1356 1359 usr = AttributeDict(self.user.get_dict())
1357 1360 usr.owner_row = True
1358 1361 usr.permission = _admin_perm
1359 1362 owner_row.append(usr)
1360 1363
1361 1364 super_admin_rows = []
1362 1365 if with_admins:
1363 1366 for usr in User.get_all_super_admins():
1364 1367 # if this admin is also owner, don't double the record
1365 1368 if usr.user_id == owner_row[0].user_id:
1366 1369 owner_row[0].admin_row = True
1367 1370 else:
1368 1371 usr = AttributeDict(usr.get_dict())
1369 1372 usr.admin_row = True
1370 1373 usr.permission = _admin_perm
1371 1374 super_admin_rows.append(usr)
1372 1375
1373 1376 return super_admin_rows + owner_row + perm_rows
1374 1377
1375 1378 def permission_user_groups(self):
1376 1379 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1377 1380 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1378 1381 joinedload(UserGroupUserGroupToPerm.target_user_group),
1379 1382 joinedload(UserGroupUserGroupToPerm.permission),)
1380 1383
1381 1384 perm_rows = []
1382 1385 for _user_group in q.all():
1383 1386 usr = AttributeDict(_user_group.user_group.get_dict())
1384 1387 usr.permission = _user_group.permission.permission_name
1385 1388 perm_rows.append(usr)
1386 1389
1387 1390 return perm_rows
1388 1391
1389 1392 def _get_default_perms(self, user_group, suffix=''):
1390 1393 from rhodecode.model.permission import PermissionModel
1391 1394 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1392 1395
1393 1396 def get_default_perms(self, suffix=''):
1394 1397 return self._get_default_perms(self, suffix)
1395 1398
1396 1399 def get_api_data(self, with_group_members=True, include_secrets=False):
1397 1400 """
1398 1401 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1399 1402 basically forwarded.
1400 1403
1401 1404 """
1402 1405 user_group = self
1403 1406 data = {
1404 1407 'users_group_id': user_group.users_group_id,
1405 1408 'group_name': user_group.users_group_name,
1406 1409 'group_description': user_group.user_group_description,
1407 1410 'active': user_group.users_group_active,
1408 1411 'owner': user_group.user.username,
1409 1412 'owner_email': user_group.user.email,
1410 1413 }
1411 1414
1412 1415 if with_group_members:
1413 1416 users = []
1414 1417 for user in user_group.members:
1415 1418 user = user.user
1416 1419 users.append(user.get_api_data(include_secrets=include_secrets))
1417 1420 data['users'] = users
1418 1421
1419 1422 return data
1420 1423
1421 1424
1422 1425 class UserGroupMember(Base, BaseModel):
1423 1426 __tablename__ = 'users_groups_members'
1424 1427 __table_args__ = (
1425 1428 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1426 1429 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1427 1430 )
1428 1431
1429 1432 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1430 1433 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1431 1434 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1432 1435
1433 1436 user = relationship('User', lazy='joined')
1434 1437 users_group = relationship('UserGroup')
1435 1438
1436 1439 def __init__(self, gr_id='', u_id=''):
1437 1440 self.users_group_id = gr_id
1438 1441 self.user_id = u_id
1439 1442
1440 1443
1441 1444 class RepositoryField(Base, BaseModel):
1442 1445 __tablename__ = 'repositories_fields'
1443 1446 __table_args__ = (
1444 1447 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1445 1448 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1446 1449 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1447 1450 )
1448 1451 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1449 1452
1450 1453 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1451 1454 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1452 1455 field_key = Column("field_key", String(250))
1453 1456 field_label = Column("field_label", String(1024), nullable=False)
1454 1457 field_value = Column("field_value", String(10000), nullable=False)
1455 1458 field_desc = Column("field_desc", String(1024), nullable=False)
1456 1459 field_type = Column("field_type", String(255), nullable=False, unique=None)
1457 1460 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1458 1461
1459 1462 repository = relationship('Repository')
1460 1463
1461 1464 @property
1462 1465 def field_key_prefixed(self):
1463 1466 return 'ex_%s' % self.field_key
1464 1467
1465 1468 @classmethod
1466 1469 def un_prefix_key(cls, key):
1467 1470 if key.startswith(cls.PREFIX):
1468 1471 return key[len(cls.PREFIX):]
1469 1472 return key
1470 1473
1471 1474 @classmethod
1472 1475 def get_by_key_name(cls, key, repo):
1473 1476 row = cls.query()\
1474 1477 .filter(cls.repository == repo)\
1475 1478 .filter(cls.field_key == key).scalar()
1476 1479 return row
1477 1480
1478 1481
1479 1482 class Repository(Base, BaseModel):
1480 1483 __tablename__ = 'repositories'
1481 1484 __table_args__ = (
1482 1485 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1483 1486 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1484 1487 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1485 1488 )
1486 1489 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1487 1490 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1488 1491
1489 1492 STATE_CREATED = 'repo_state_created'
1490 1493 STATE_PENDING = 'repo_state_pending'
1491 1494 STATE_ERROR = 'repo_state_error'
1492 1495
1493 1496 LOCK_AUTOMATIC = 'lock_auto'
1494 1497 LOCK_API = 'lock_api'
1495 1498 LOCK_WEB = 'lock_web'
1496 1499 LOCK_PULL = 'lock_pull'
1497 1500
1498 1501 NAME_SEP = URL_SEP
1499 1502
1500 1503 repo_id = Column(
1501 1504 "repo_id", Integer(), nullable=False, unique=True, default=None,
1502 1505 primary_key=True)
1503 1506 _repo_name = Column(
1504 1507 "repo_name", Text(), nullable=False, default=None)
1505 1508 _repo_name_hash = Column(
1506 1509 "repo_name_hash", String(255), nullable=False, unique=True)
1507 1510 repo_state = Column("repo_state", String(255), nullable=True)
1508 1511
1509 1512 clone_uri = Column(
1510 1513 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1511 1514 default=None)
1512 1515 repo_type = Column(
1513 1516 "repo_type", String(255), nullable=False, unique=False, default=None)
1514 1517 user_id = Column(
1515 1518 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1516 1519 unique=False, default=None)
1517 1520 private = Column(
1518 1521 "private", Boolean(), nullable=True, unique=None, default=None)
1519 1522 enable_statistics = Column(
1520 1523 "statistics", Boolean(), nullable=True, unique=None, default=True)
1521 1524 enable_downloads = Column(
1522 1525 "downloads", Boolean(), nullable=True, unique=None, default=True)
1523 1526 description = Column(
1524 1527 "description", String(10000), nullable=True, unique=None, default=None)
1525 1528 created_on = Column(
1526 1529 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1527 1530 default=datetime.datetime.now)
1528 1531 updated_on = Column(
1529 1532 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1530 1533 default=datetime.datetime.now)
1531 1534 _landing_revision = Column(
1532 1535 "landing_revision", String(255), nullable=False, unique=False,
1533 1536 default=None)
1534 1537 enable_locking = Column(
1535 1538 "enable_locking", Boolean(), nullable=False, unique=None,
1536 1539 default=False)
1537 1540 _locked = Column(
1538 1541 "locked", String(255), nullable=True, unique=False, default=None)
1539 1542 _changeset_cache = Column(
1540 1543 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1541 1544
1542 1545 fork_id = Column(
1543 1546 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1544 1547 nullable=True, unique=False, default=None)
1545 1548 group_id = Column(
1546 1549 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1547 1550 unique=False, default=None)
1548 1551
1549 1552 user = relationship('User', lazy='joined')
1550 1553 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1551 1554 group = relationship('RepoGroup', lazy='joined')
1552 1555 repo_to_perm = relationship(
1553 1556 'UserRepoToPerm', cascade='all',
1554 1557 order_by='UserRepoToPerm.repo_to_perm_id')
1555 1558 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1556 1559 stats = relationship('Statistics', cascade='all', uselist=False)
1557 1560
1558 1561 followers = relationship(
1559 1562 'UserFollowing',
1560 1563 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1561 1564 cascade='all')
1562 1565 extra_fields = relationship(
1563 1566 'RepositoryField', cascade="all, delete, delete-orphan")
1564 1567 logs = relationship('UserLog')
1565 1568 comments = relationship(
1566 1569 'ChangesetComment', cascade="all, delete, delete-orphan")
1567 1570 pull_requests_source = relationship(
1568 1571 'PullRequest',
1569 1572 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1570 1573 cascade="all, delete, delete-orphan")
1571 1574 pull_requests_target = relationship(
1572 1575 'PullRequest',
1573 1576 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1574 1577 cascade="all, delete, delete-orphan")
1575 1578 ui = relationship('RepoRhodeCodeUi', cascade="all")
1576 1579 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1577 1580 integrations = relationship('Integration',
1578 1581 cascade="all, delete, delete-orphan")
1579 1582
1580 1583 def __unicode__(self):
1581 1584 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1582 1585 safe_unicode(self.repo_name))
1583 1586
1584 1587 @hybrid_property
1585 1588 def description_safe(self):
1586 1589 from rhodecode.lib import helpers as h
1587 1590 return h.escape(self.description)
1588 1591
1589 1592 @hybrid_property
1590 1593 def landing_rev(self):
1591 1594 # always should return [rev_type, rev]
1592 1595 if self._landing_revision:
1593 1596 _rev_info = self._landing_revision.split(':')
1594 1597 if len(_rev_info) < 2:
1595 1598 _rev_info.insert(0, 'rev')
1596 1599 return [_rev_info[0], _rev_info[1]]
1597 1600 return [None, None]
1598 1601
1599 1602 @landing_rev.setter
1600 1603 def landing_rev(self, val):
1601 1604 if ':' not in val:
1602 1605 raise ValueError('value must be delimited with `:` and consist '
1603 1606 'of <rev_type>:<rev>, got %s instead' % val)
1604 1607 self._landing_revision = val
1605 1608
1606 1609 @hybrid_property
1607 1610 def locked(self):
1608 1611 if self._locked:
1609 1612 user_id, timelocked, reason = self._locked.split(':')
1610 1613 lock_values = int(user_id), timelocked, reason
1611 1614 else:
1612 1615 lock_values = [None, None, None]
1613 1616 return lock_values
1614 1617
1615 1618 @locked.setter
1616 1619 def locked(self, val):
1617 1620 if val and isinstance(val, (list, tuple)):
1618 1621 self._locked = ':'.join(map(str, val))
1619 1622 else:
1620 1623 self._locked = None
1621 1624
1622 1625 @hybrid_property
1623 1626 def changeset_cache(self):
1624 1627 from rhodecode.lib.vcs.backends.base import EmptyCommit
1625 1628 dummy = EmptyCommit().__json__()
1626 1629 if not self._changeset_cache:
1627 1630 return dummy
1628 1631 try:
1629 1632 return json.loads(self._changeset_cache)
1630 1633 except TypeError:
1631 1634 return dummy
1632 1635 except Exception:
1633 1636 log.error(traceback.format_exc())
1634 1637 return dummy
1635 1638
1636 1639 @changeset_cache.setter
1637 1640 def changeset_cache(self, val):
1638 1641 try:
1639 1642 self._changeset_cache = json.dumps(val)
1640 1643 except Exception:
1641 1644 log.error(traceback.format_exc())
1642 1645
1643 1646 @hybrid_property
1644 1647 def repo_name(self):
1645 1648 return self._repo_name
1646 1649
1647 1650 @repo_name.setter
1648 1651 def repo_name(self, value):
1649 1652 self._repo_name = value
1650 1653 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1651 1654
1652 1655 @classmethod
1653 1656 def normalize_repo_name(cls, repo_name):
1654 1657 """
1655 1658 Normalizes os specific repo_name to the format internally stored inside
1656 1659 database using URL_SEP
1657 1660
1658 1661 :param cls:
1659 1662 :param repo_name:
1660 1663 """
1661 1664 return cls.NAME_SEP.join(repo_name.split(os.sep))
1662 1665
1663 1666 @classmethod
1664 1667 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1665 1668 session = Session()
1666 1669 q = session.query(cls).filter(cls.repo_name == repo_name)
1667 1670
1668 1671 if cache:
1669 1672 if identity_cache:
1670 1673 val = cls.identity_cache(session, 'repo_name', repo_name)
1671 1674 if val:
1672 1675 return val
1673 1676 else:
1674 1677 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1675 1678 q = q.options(
1676 1679 FromCache("sql_cache_short", cache_key))
1677 1680
1678 1681 return q.scalar()
1679 1682
1680 1683 @classmethod
1681 1684 def get_by_full_path(cls, repo_full_path):
1682 1685 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1683 1686 repo_name = cls.normalize_repo_name(repo_name)
1684 1687 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1685 1688
1686 1689 @classmethod
1687 1690 def get_repo_forks(cls, repo_id):
1688 1691 return cls.query().filter(Repository.fork_id == repo_id)
1689 1692
1690 1693 @classmethod
1691 1694 def base_path(cls):
1692 1695 """
1693 1696 Returns base path when all repos are stored
1694 1697
1695 1698 :param cls:
1696 1699 """
1697 1700 q = Session().query(RhodeCodeUi)\
1698 1701 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1699 1702 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1700 1703 return q.one().ui_value
1701 1704
1702 1705 @classmethod
1703 1706 def is_valid(cls, repo_name):
1704 1707 """
1705 1708 returns True if given repo name is a valid filesystem repository
1706 1709
1707 1710 :param cls:
1708 1711 :param repo_name:
1709 1712 """
1710 1713 from rhodecode.lib.utils import is_valid_repo
1711 1714
1712 1715 return is_valid_repo(repo_name, cls.base_path())
1713 1716
1714 1717 @classmethod
1715 1718 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1716 1719 case_insensitive=True):
1717 1720 q = Repository.query()
1718 1721
1719 1722 if not isinstance(user_id, Optional):
1720 1723 q = q.filter(Repository.user_id == user_id)
1721 1724
1722 1725 if not isinstance(group_id, Optional):
1723 1726 q = q.filter(Repository.group_id == group_id)
1724 1727
1725 1728 if case_insensitive:
1726 1729 q = q.order_by(func.lower(Repository.repo_name))
1727 1730 else:
1728 1731 q = q.order_by(Repository.repo_name)
1729 1732 return q.all()
1730 1733
1731 1734 @property
1732 1735 def forks(self):
1733 1736 """
1734 1737 Return forks of this repo
1735 1738 """
1736 1739 return Repository.get_repo_forks(self.repo_id)
1737 1740
1738 1741 @property
1739 1742 def parent(self):
1740 1743 """
1741 1744 Returns fork parent
1742 1745 """
1743 1746 return self.fork
1744 1747
1745 1748 @property
1746 1749 def just_name(self):
1747 1750 return self.repo_name.split(self.NAME_SEP)[-1]
1748 1751
1749 1752 @property
1750 1753 def groups_with_parents(self):
1751 1754 groups = []
1752 1755 if self.group is None:
1753 1756 return groups
1754 1757
1755 1758 cur_gr = self.group
1756 1759 groups.insert(0, cur_gr)
1757 1760 while 1:
1758 1761 gr = getattr(cur_gr, 'parent_group', None)
1759 1762 cur_gr = cur_gr.parent_group
1760 1763 if gr is None:
1761 1764 break
1762 1765 groups.insert(0, gr)
1763 1766
1764 1767 return groups
1765 1768
1766 1769 @property
1767 1770 def groups_and_repo(self):
1768 1771 return self.groups_with_parents, self
1769 1772
1770 1773 @LazyProperty
1771 1774 def repo_path(self):
1772 1775 """
1773 1776 Returns base full path for that repository means where it actually
1774 1777 exists on a filesystem
1775 1778 """
1776 1779 q = Session().query(RhodeCodeUi).filter(
1777 1780 RhodeCodeUi.ui_key == self.NAME_SEP)
1778 1781 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1779 1782 return q.one().ui_value
1780 1783
1781 1784 @property
1782 1785 def repo_full_path(self):
1783 1786 p = [self.repo_path]
1784 1787 # we need to split the name by / since this is how we store the
1785 1788 # names in the database, but that eventually needs to be converted
1786 1789 # into a valid system path
1787 1790 p += self.repo_name.split(self.NAME_SEP)
1788 1791 return os.path.join(*map(safe_unicode, p))
1789 1792
1790 1793 @property
1791 1794 def cache_keys(self):
1792 1795 """
1793 1796 Returns associated cache keys for that repo
1794 1797 """
1795 1798 return CacheKey.query()\
1796 1799 .filter(CacheKey.cache_args == self.repo_name)\
1797 1800 .order_by(CacheKey.cache_key)\
1798 1801 .all()
1799 1802
1800 1803 def get_new_name(self, repo_name):
1801 1804 """
1802 1805 returns new full repository name based on assigned group and new new
1803 1806
1804 1807 :param group_name:
1805 1808 """
1806 1809 path_prefix = self.group.full_path_splitted if self.group else []
1807 1810 return self.NAME_SEP.join(path_prefix + [repo_name])
1808 1811
1809 1812 @property
1810 1813 def _config(self):
1811 1814 """
1812 1815 Returns db based config object.
1813 1816 """
1814 1817 from rhodecode.lib.utils import make_db_config
1815 1818 return make_db_config(clear_session=False, repo=self)
1816 1819
1817 1820 def permissions(self, with_admins=True, with_owner=True):
1818 1821 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1819 1822 q = q.options(joinedload(UserRepoToPerm.repository),
1820 1823 joinedload(UserRepoToPerm.user),
1821 1824 joinedload(UserRepoToPerm.permission),)
1822 1825
1823 1826 # get owners and admins and permissions. We do a trick of re-writing
1824 1827 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1825 1828 # has a global reference and changing one object propagates to all
1826 1829 # others. This means if admin is also an owner admin_row that change
1827 1830 # would propagate to both objects
1828 1831 perm_rows = []
1829 1832 for _usr in q.all():
1830 1833 usr = AttributeDict(_usr.user.get_dict())
1831 1834 usr.permission = _usr.permission.permission_name
1832 1835 perm_rows.append(usr)
1833 1836
1834 1837 # filter the perm rows by 'default' first and then sort them by
1835 1838 # admin,write,read,none permissions sorted again alphabetically in
1836 1839 # each group
1837 1840 perm_rows = sorted(perm_rows, key=display_sort)
1838 1841
1839 1842 _admin_perm = 'repository.admin'
1840 1843 owner_row = []
1841 1844 if with_owner:
1842 1845 usr = AttributeDict(self.user.get_dict())
1843 1846 usr.owner_row = True
1844 1847 usr.permission = _admin_perm
1845 1848 owner_row.append(usr)
1846 1849
1847 1850 super_admin_rows = []
1848 1851 if with_admins:
1849 1852 for usr in User.get_all_super_admins():
1850 1853 # if this admin is also owner, don't double the record
1851 1854 if usr.user_id == owner_row[0].user_id:
1852 1855 owner_row[0].admin_row = True
1853 1856 else:
1854 1857 usr = AttributeDict(usr.get_dict())
1855 1858 usr.admin_row = True
1856 1859 usr.permission = _admin_perm
1857 1860 super_admin_rows.append(usr)
1858 1861
1859 1862 return super_admin_rows + owner_row + perm_rows
1860 1863
1861 1864 def permission_user_groups(self):
1862 1865 q = UserGroupRepoToPerm.query().filter(
1863 1866 UserGroupRepoToPerm.repository == self)
1864 1867 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1865 1868 joinedload(UserGroupRepoToPerm.users_group),
1866 1869 joinedload(UserGroupRepoToPerm.permission),)
1867 1870
1868 1871 perm_rows = []
1869 1872 for _user_group in q.all():
1870 1873 usr = AttributeDict(_user_group.users_group.get_dict())
1871 1874 usr.permission = _user_group.permission.permission_name
1872 1875 perm_rows.append(usr)
1873 1876
1874 1877 return perm_rows
1875 1878
1876 1879 def get_api_data(self, include_secrets=False):
1877 1880 """
1878 1881 Common function for generating repo api data
1879 1882
1880 1883 :param include_secrets: See :meth:`User.get_api_data`.
1881 1884
1882 1885 """
1883 1886 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1884 1887 # move this methods on models level.
1885 1888 from rhodecode.model.settings import SettingsModel
1886 1889 from rhodecode.model.repo import RepoModel
1887 1890
1888 1891 repo = self
1889 1892 _user_id, _time, _reason = self.locked
1890 1893
1891 1894 data = {
1892 1895 'repo_id': repo.repo_id,
1893 1896 'repo_name': repo.repo_name,
1894 1897 'repo_type': repo.repo_type,
1895 1898 'clone_uri': repo.clone_uri or '',
1896 1899 'url': RepoModel().get_url(self),
1897 1900 'private': repo.private,
1898 1901 'created_on': repo.created_on,
1899 1902 'description': repo.description_safe,
1900 1903 'landing_rev': repo.landing_rev,
1901 1904 'owner': repo.user.username,
1902 1905 'fork_of': repo.fork.repo_name if repo.fork else None,
1903 1906 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1904 1907 'enable_statistics': repo.enable_statistics,
1905 1908 'enable_locking': repo.enable_locking,
1906 1909 'enable_downloads': repo.enable_downloads,
1907 1910 'last_changeset': repo.changeset_cache,
1908 1911 'locked_by': User.get(_user_id).get_api_data(
1909 1912 include_secrets=include_secrets) if _user_id else None,
1910 1913 'locked_date': time_to_datetime(_time) if _time else None,
1911 1914 'lock_reason': _reason if _reason else None,
1912 1915 }
1913 1916
1914 1917 # TODO: mikhail: should be per-repo settings here
1915 1918 rc_config = SettingsModel().get_all_settings()
1916 1919 repository_fields = str2bool(
1917 1920 rc_config.get('rhodecode_repository_fields'))
1918 1921 if repository_fields:
1919 1922 for f in self.extra_fields:
1920 1923 data[f.field_key_prefixed] = f.field_value
1921 1924
1922 1925 return data
1923 1926
1924 1927 @classmethod
1925 1928 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1926 1929 if not lock_time:
1927 1930 lock_time = time.time()
1928 1931 if not lock_reason:
1929 1932 lock_reason = cls.LOCK_AUTOMATIC
1930 1933 repo.locked = [user_id, lock_time, lock_reason]
1931 1934 Session().add(repo)
1932 1935 Session().commit()
1933 1936
1934 1937 @classmethod
1935 1938 def unlock(cls, repo):
1936 1939 repo.locked = None
1937 1940 Session().add(repo)
1938 1941 Session().commit()
1939 1942
1940 1943 @classmethod
1941 1944 def getlock(cls, repo):
1942 1945 return repo.locked
1943 1946
1944 1947 def is_user_lock(self, user_id):
1945 1948 if self.lock[0]:
1946 1949 lock_user_id = safe_int(self.lock[0])
1947 1950 user_id = safe_int(user_id)
1948 1951 # both are ints, and they are equal
1949 1952 return all([lock_user_id, user_id]) and lock_user_id == user_id
1950 1953
1951 1954 return False
1952 1955
1953 1956 def get_locking_state(self, action, user_id, only_when_enabled=True):
1954 1957 """
1955 1958 Checks locking on this repository, if locking is enabled and lock is
1956 1959 present returns a tuple of make_lock, locked, locked_by.
1957 1960 make_lock can have 3 states None (do nothing) True, make lock
1958 1961 False release lock, This value is later propagated to hooks, which
1959 1962 do the locking. Think about this as signals passed to hooks what to do.
1960 1963
1961 1964 """
1962 1965 # TODO: johbo: This is part of the business logic and should be moved
1963 1966 # into the RepositoryModel.
1964 1967
1965 1968 if action not in ('push', 'pull'):
1966 1969 raise ValueError("Invalid action value: %s" % repr(action))
1967 1970
1968 1971 # defines if locked error should be thrown to user
1969 1972 currently_locked = False
1970 1973 # defines if new lock should be made, tri-state
1971 1974 make_lock = None
1972 1975 repo = self
1973 1976 user = User.get(user_id)
1974 1977
1975 1978 lock_info = repo.locked
1976 1979
1977 1980 if repo and (repo.enable_locking or not only_when_enabled):
1978 1981 if action == 'push':
1979 1982 # check if it's already locked !, if it is compare users
1980 1983 locked_by_user_id = lock_info[0]
1981 1984 if user.user_id == locked_by_user_id:
1982 1985 log.debug(
1983 1986 'Got `push` action from user %s, now unlocking', user)
1984 1987 # unlock if we have push from user who locked
1985 1988 make_lock = False
1986 1989 else:
1987 1990 # we're not the same user who locked, ban with
1988 1991 # code defined in settings (default is 423 HTTP Locked) !
1989 1992 log.debug('Repo %s is currently locked by %s', repo, user)
1990 1993 currently_locked = True
1991 1994 elif action == 'pull':
1992 1995 # [0] user [1] date
1993 1996 if lock_info[0] and lock_info[1]:
1994 1997 log.debug('Repo %s is currently locked by %s', repo, user)
1995 1998 currently_locked = True
1996 1999 else:
1997 2000 log.debug('Setting lock on repo %s by %s', repo, user)
1998 2001 make_lock = True
1999 2002
2000 2003 else:
2001 2004 log.debug('Repository %s do not have locking enabled', repo)
2002 2005
2003 2006 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2004 2007 make_lock, currently_locked, lock_info)
2005 2008
2006 2009 from rhodecode.lib.auth import HasRepoPermissionAny
2007 2010 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2008 2011 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2009 2012 # if we don't have at least write permission we cannot make a lock
2010 2013 log.debug('lock state reset back to FALSE due to lack '
2011 2014 'of at least read permission')
2012 2015 make_lock = False
2013 2016
2014 2017 return make_lock, currently_locked, lock_info
2015 2018
2016 2019 @property
2017 2020 def last_db_change(self):
2018 2021 return self.updated_on
2019 2022
2020 2023 @property
2021 2024 def clone_uri_hidden(self):
2022 2025 clone_uri = self.clone_uri
2023 2026 if clone_uri:
2024 2027 import urlobject
2025 2028 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2026 2029 if url_obj.password:
2027 2030 clone_uri = url_obj.with_password('*****')
2028 2031 return clone_uri
2029 2032
2030 2033 def clone_url(self, **override):
2031 2034 from rhodecode.model.settings import SettingsModel
2032 2035
2033 2036 uri_tmpl = None
2034 2037 if 'with_id' in override:
2035 2038 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2036 2039 del override['with_id']
2037 2040
2038 2041 if 'uri_tmpl' in override:
2039 2042 uri_tmpl = override['uri_tmpl']
2040 2043 del override['uri_tmpl']
2041 2044
2042 2045 # we didn't override our tmpl from **overrides
2043 2046 if not uri_tmpl:
2044 2047 rc_config = SettingsModel().get_all_settings(cache=True)
2045 2048 uri_tmpl = rc_config.get(
2046 2049 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2047 2050
2048 2051 request = get_current_request()
2049 2052 return get_clone_url(request=request,
2050 2053 uri_tmpl=uri_tmpl,
2051 2054 repo_name=self.repo_name,
2052 2055 repo_id=self.repo_id, **override)
2053 2056
2054 2057 def set_state(self, state):
2055 2058 self.repo_state = state
2056 2059 Session().add(self)
2057 2060 #==========================================================================
2058 2061 # SCM PROPERTIES
2059 2062 #==========================================================================
2060 2063
2061 2064 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2062 2065 return get_commit_safe(
2063 2066 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2064 2067
2065 2068 def get_changeset(self, rev=None, pre_load=None):
2066 2069 warnings.warn("Use get_commit", DeprecationWarning)
2067 2070 commit_id = None
2068 2071 commit_idx = None
2069 2072 if isinstance(rev, basestring):
2070 2073 commit_id = rev
2071 2074 else:
2072 2075 commit_idx = rev
2073 2076 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2074 2077 pre_load=pre_load)
2075 2078
2076 2079 def get_landing_commit(self):
2077 2080 """
2078 2081 Returns landing commit, or if that doesn't exist returns the tip
2079 2082 """
2080 2083 _rev_type, _rev = self.landing_rev
2081 2084 commit = self.get_commit(_rev)
2082 2085 if isinstance(commit, EmptyCommit):
2083 2086 return self.get_commit()
2084 2087 return commit
2085 2088
2086 2089 def update_commit_cache(self, cs_cache=None, config=None):
2087 2090 """
2088 2091 Update cache of last changeset for repository, keys should be::
2089 2092
2090 2093 short_id
2091 2094 raw_id
2092 2095 revision
2093 2096 parents
2094 2097 message
2095 2098 date
2096 2099 author
2097 2100
2098 2101 :param cs_cache:
2099 2102 """
2100 2103 from rhodecode.lib.vcs.backends.base import BaseChangeset
2101 2104 if cs_cache is None:
2102 2105 # use no-cache version here
2103 2106 scm_repo = self.scm_instance(cache=False, config=config)
2104 2107 if scm_repo:
2105 2108 cs_cache = scm_repo.get_commit(
2106 2109 pre_load=["author", "date", "message", "parents"])
2107 2110 else:
2108 2111 cs_cache = EmptyCommit()
2109 2112
2110 2113 if isinstance(cs_cache, BaseChangeset):
2111 2114 cs_cache = cs_cache.__json__()
2112 2115
2113 2116 def is_outdated(new_cs_cache):
2114 2117 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2115 2118 new_cs_cache['revision'] != self.changeset_cache['revision']):
2116 2119 return True
2117 2120 return False
2118 2121
2119 2122 # check if we have maybe already latest cached revision
2120 2123 if is_outdated(cs_cache) or not self.changeset_cache:
2121 2124 _default = datetime.datetime.fromtimestamp(0)
2122 2125 last_change = cs_cache.get('date') or _default
2123 2126 log.debug('updated repo %s with new cs cache %s',
2124 2127 self.repo_name, cs_cache)
2125 2128 self.updated_on = last_change
2126 2129 self.changeset_cache = cs_cache
2127 2130 Session().add(self)
2128 2131 Session().commit()
2129 2132 else:
2130 2133 log.debug('Skipping update_commit_cache for repo:`%s` '
2131 2134 'commit already with latest changes', self.repo_name)
2132 2135
2133 2136 @property
2134 2137 def tip(self):
2135 2138 return self.get_commit('tip')
2136 2139
2137 2140 @property
2138 2141 def author(self):
2139 2142 return self.tip.author
2140 2143
2141 2144 @property
2142 2145 def last_change(self):
2143 2146 return self.scm_instance().last_change
2144 2147
2145 2148 def get_comments(self, revisions=None):
2146 2149 """
2147 2150 Returns comments for this repository grouped by revisions
2148 2151
2149 2152 :param revisions: filter query by revisions only
2150 2153 """
2151 2154 cmts = ChangesetComment.query()\
2152 2155 .filter(ChangesetComment.repo == self)
2153 2156 if revisions:
2154 2157 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2155 2158 grouped = collections.defaultdict(list)
2156 2159 for cmt in cmts.all():
2157 2160 grouped[cmt.revision].append(cmt)
2158 2161 return grouped
2159 2162
2160 2163 def statuses(self, revisions=None):
2161 2164 """
2162 2165 Returns statuses for this repository
2163 2166
2164 2167 :param revisions: list of revisions to get statuses for
2165 2168 """
2166 2169 statuses = ChangesetStatus.query()\
2167 2170 .filter(ChangesetStatus.repo == self)\
2168 2171 .filter(ChangesetStatus.version == 0)
2169 2172
2170 2173 if revisions:
2171 2174 # Try doing the filtering in chunks to avoid hitting limits
2172 2175 size = 500
2173 2176 status_results = []
2174 2177 for chunk in xrange(0, len(revisions), size):
2175 2178 status_results += statuses.filter(
2176 2179 ChangesetStatus.revision.in_(
2177 2180 revisions[chunk: chunk+size])
2178 2181 ).all()
2179 2182 else:
2180 2183 status_results = statuses.all()
2181 2184
2182 2185 grouped = {}
2183 2186
2184 2187 # maybe we have open new pullrequest without a status?
2185 2188 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2186 2189 status_lbl = ChangesetStatus.get_status_lbl(stat)
2187 2190 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2188 2191 for rev in pr.revisions:
2189 2192 pr_id = pr.pull_request_id
2190 2193 pr_repo = pr.target_repo.repo_name
2191 2194 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2192 2195
2193 2196 for stat in status_results:
2194 2197 pr_id = pr_repo = None
2195 2198 if stat.pull_request:
2196 2199 pr_id = stat.pull_request.pull_request_id
2197 2200 pr_repo = stat.pull_request.target_repo.repo_name
2198 2201 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2199 2202 pr_id, pr_repo]
2200 2203 return grouped
2201 2204
2202 2205 # ==========================================================================
2203 2206 # SCM CACHE INSTANCE
2204 2207 # ==========================================================================
2205 2208
2206 2209 def scm_instance(self, **kwargs):
2207 2210 import rhodecode
2208 2211
2209 2212 # Passing a config will not hit the cache currently only used
2210 2213 # for repo2dbmapper
2211 2214 config = kwargs.pop('config', None)
2212 2215 cache = kwargs.pop('cache', None)
2213 2216 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2214 2217 # if cache is NOT defined use default global, else we have a full
2215 2218 # control over cache behaviour
2216 2219 if cache is None and full_cache and not config:
2217 2220 return self._get_instance_cached()
2218 2221 return self._get_instance(cache=bool(cache), config=config)
2219 2222
2220 2223 def _get_instance_cached(self):
2221 2224 @cache_region('long_term')
2222 2225 def _get_repo(cache_key):
2223 2226 return self._get_instance()
2224 2227
2225 2228 invalidator_context = CacheKey.repo_context_cache(
2226 2229 _get_repo, self.repo_name, None, thread_scoped=True)
2227 2230
2228 2231 with invalidator_context as context:
2229 2232 context.invalidate()
2230 2233 repo = context.compute()
2231 2234
2232 2235 return repo
2233 2236
2234 2237 def _get_instance(self, cache=True, config=None):
2235 2238 config = config or self._config
2236 2239 custom_wire = {
2237 2240 'cache': cache # controls the vcs.remote cache
2238 2241 }
2239 2242 repo = get_vcs_instance(
2240 2243 repo_path=safe_str(self.repo_full_path),
2241 2244 config=config,
2242 2245 with_wire=custom_wire,
2243 2246 create=False,
2244 2247 _vcs_alias=self.repo_type)
2245 2248
2246 2249 return repo
2247 2250
2248 2251 def __json__(self):
2249 2252 return {'landing_rev': self.landing_rev}
2250 2253
2251 2254 def get_dict(self):
2252 2255
2253 2256 # Since we transformed `repo_name` to a hybrid property, we need to
2254 2257 # keep compatibility with the code which uses `repo_name` field.
2255 2258
2256 2259 result = super(Repository, self).get_dict()
2257 2260 result['repo_name'] = result.pop('_repo_name', None)
2258 2261 return result
2259 2262
2260 2263
2261 2264 class RepoGroup(Base, BaseModel):
2262 2265 __tablename__ = 'groups'
2263 2266 __table_args__ = (
2264 2267 UniqueConstraint('group_name', 'group_parent_id'),
2265 2268 CheckConstraint('group_id != group_parent_id'),
2266 2269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2267 2270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2268 2271 )
2269 2272 __mapper_args__ = {'order_by': 'group_name'}
2270 2273
2271 2274 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2272 2275
2273 2276 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2274 2277 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2275 2278 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2276 2279 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2277 2280 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2278 2281 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2279 2282 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2280 2283 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2281 2284 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2282 2285
2283 2286 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2284 2287 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2285 2288 parent_group = relationship('RepoGroup', remote_side=group_id)
2286 2289 user = relationship('User')
2287 2290 integrations = relationship('Integration',
2288 2291 cascade="all, delete, delete-orphan")
2289 2292
2290 2293 def __init__(self, group_name='', parent_group=None):
2291 2294 self.group_name = group_name
2292 2295 self.parent_group = parent_group
2293 2296
2294 2297 def __unicode__(self):
2295 2298 return u"<%s('id:%s:%s')>" % (
2296 2299 self.__class__.__name__, self.group_id, self.group_name)
2297 2300
2298 2301 @hybrid_property
2299 2302 def description_safe(self):
2300 2303 from rhodecode.lib import helpers as h
2301 2304 return h.escape(self.group_description)
2302 2305
2303 2306 @classmethod
2304 2307 def _generate_choice(cls, repo_group):
2305 2308 from webhelpers.html import literal as _literal
2306 2309 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2307 2310 return repo_group.group_id, _name(repo_group.full_path_splitted)
2308 2311
2309 2312 @classmethod
2310 2313 def groups_choices(cls, groups=None, show_empty_group=True):
2311 2314 if not groups:
2312 2315 groups = cls.query().all()
2313 2316
2314 2317 repo_groups = []
2315 2318 if show_empty_group:
2316 2319 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2317 2320
2318 2321 repo_groups.extend([cls._generate_choice(x) for x in groups])
2319 2322
2320 2323 repo_groups = sorted(
2321 2324 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2322 2325 return repo_groups
2323 2326
2324 2327 @classmethod
2325 2328 def url_sep(cls):
2326 2329 return URL_SEP
2327 2330
2328 2331 @classmethod
2329 2332 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2330 2333 if case_insensitive:
2331 2334 gr = cls.query().filter(func.lower(cls.group_name)
2332 2335 == func.lower(group_name))
2333 2336 else:
2334 2337 gr = cls.query().filter(cls.group_name == group_name)
2335 2338 if cache:
2336 2339 name_key = _hash_key(group_name)
2337 2340 gr = gr.options(
2338 2341 FromCache("sql_cache_short", "get_group_%s" % name_key))
2339 2342 return gr.scalar()
2340 2343
2341 2344 @classmethod
2342 2345 def get_user_personal_repo_group(cls, user_id):
2343 2346 user = User.get(user_id)
2344 2347 if user.username == User.DEFAULT_USER:
2345 2348 return None
2346 2349
2347 2350 return cls.query()\
2348 2351 .filter(cls.personal == true()) \
2349 2352 .filter(cls.user == user).scalar()
2350 2353
2351 2354 @classmethod
2352 2355 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2353 2356 case_insensitive=True):
2354 2357 q = RepoGroup.query()
2355 2358
2356 2359 if not isinstance(user_id, Optional):
2357 2360 q = q.filter(RepoGroup.user_id == user_id)
2358 2361
2359 2362 if not isinstance(group_id, Optional):
2360 2363 q = q.filter(RepoGroup.group_parent_id == group_id)
2361 2364
2362 2365 if case_insensitive:
2363 2366 q = q.order_by(func.lower(RepoGroup.group_name))
2364 2367 else:
2365 2368 q = q.order_by(RepoGroup.group_name)
2366 2369 return q.all()
2367 2370
2368 2371 @property
2369 2372 def parents(self):
2370 2373 parents_recursion_limit = 10
2371 2374 groups = []
2372 2375 if self.parent_group is None:
2373 2376 return groups
2374 2377 cur_gr = self.parent_group
2375 2378 groups.insert(0, cur_gr)
2376 2379 cnt = 0
2377 2380 while 1:
2378 2381 cnt += 1
2379 2382 gr = getattr(cur_gr, 'parent_group', None)
2380 2383 cur_gr = cur_gr.parent_group
2381 2384 if gr is None:
2382 2385 break
2383 2386 if cnt == parents_recursion_limit:
2384 2387 # this will prevent accidental infinit loops
2385 2388 log.error(('more than %s parents found for group %s, stopping '
2386 2389 'recursive parent fetching' % (parents_recursion_limit, self)))
2387 2390 break
2388 2391
2389 2392 groups.insert(0, gr)
2390 2393 return groups
2391 2394
2392 2395 @property
2393 2396 def last_db_change(self):
2394 2397 return self.updated_on
2395 2398
2396 2399 @property
2397 2400 def children(self):
2398 2401 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2399 2402
2400 2403 @property
2401 2404 def name(self):
2402 2405 return self.group_name.split(RepoGroup.url_sep())[-1]
2403 2406
2404 2407 @property
2405 2408 def full_path(self):
2406 2409 return self.group_name
2407 2410
2408 2411 @property
2409 2412 def full_path_splitted(self):
2410 2413 return self.group_name.split(RepoGroup.url_sep())
2411 2414
2412 2415 @property
2413 2416 def repositories(self):
2414 2417 return Repository.query()\
2415 2418 .filter(Repository.group == self)\
2416 2419 .order_by(Repository.repo_name)
2417 2420
2418 2421 @property
2419 2422 def repositories_recursive_count(self):
2420 2423 cnt = self.repositories.count()
2421 2424
2422 2425 def children_count(group):
2423 2426 cnt = 0
2424 2427 for child in group.children:
2425 2428 cnt += child.repositories.count()
2426 2429 cnt += children_count(child)
2427 2430 return cnt
2428 2431
2429 2432 return cnt + children_count(self)
2430 2433
2431 2434 def _recursive_objects(self, include_repos=True):
2432 2435 all_ = []
2433 2436
2434 2437 def _get_members(root_gr):
2435 2438 if include_repos:
2436 2439 for r in root_gr.repositories:
2437 2440 all_.append(r)
2438 2441 childs = root_gr.children.all()
2439 2442 if childs:
2440 2443 for gr in childs:
2441 2444 all_.append(gr)
2442 2445 _get_members(gr)
2443 2446
2444 2447 _get_members(self)
2445 2448 return [self] + all_
2446 2449
2447 2450 def recursive_groups_and_repos(self):
2448 2451 """
2449 2452 Recursive return all groups, with repositories in those groups
2450 2453 """
2451 2454 return self._recursive_objects()
2452 2455
2453 2456 def recursive_groups(self):
2454 2457 """
2455 2458 Returns all children groups for this group including children of children
2456 2459 """
2457 2460 return self._recursive_objects(include_repos=False)
2458 2461
2459 2462 def get_new_name(self, group_name):
2460 2463 """
2461 2464 returns new full group name based on parent and new name
2462 2465
2463 2466 :param group_name:
2464 2467 """
2465 2468 path_prefix = (self.parent_group.full_path_splitted if
2466 2469 self.parent_group else [])
2467 2470 return RepoGroup.url_sep().join(path_prefix + [group_name])
2468 2471
2469 2472 def permissions(self, with_admins=True, with_owner=True):
2470 2473 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2471 2474 q = q.options(joinedload(UserRepoGroupToPerm.group),
2472 2475 joinedload(UserRepoGroupToPerm.user),
2473 2476 joinedload(UserRepoGroupToPerm.permission),)
2474 2477
2475 2478 # get owners and admins and permissions. We do a trick of re-writing
2476 2479 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2477 2480 # has a global reference and changing one object propagates to all
2478 2481 # others. This means if admin is also an owner admin_row that change
2479 2482 # would propagate to both objects
2480 2483 perm_rows = []
2481 2484 for _usr in q.all():
2482 2485 usr = AttributeDict(_usr.user.get_dict())
2483 2486 usr.permission = _usr.permission.permission_name
2484 2487 perm_rows.append(usr)
2485 2488
2486 2489 # filter the perm rows by 'default' first and then sort them by
2487 2490 # admin,write,read,none permissions sorted again alphabetically in
2488 2491 # each group
2489 2492 perm_rows = sorted(perm_rows, key=display_sort)
2490 2493
2491 2494 _admin_perm = 'group.admin'
2492 2495 owner_row = []
2493 2496 if with_owner:
2494 2497 usr = AttributeDict(self.user.get_dict())
2495 2498 usr.owner_row = True
2496 2499 usr.permission = _admin_perm
2497 2500 owner_row.append(usr)
2498 2501
2499 2502 super_admin_rows = []
2500 2503 if with_admins:
2501 2504 for usr in User.get_all_super_admins():
2502 2505 # if this admin is also owner, don't double the record
2503 2506 if usr.user_id == owner_row[0].user_id:
2504 2507 owner_row[0].admin_row = True
2505 2508 else:
2506 2509 usr = AttributeDict(usr.get_dict())
2507 2510 usr.admin_row = True
2508 2511 usr.permission = _admin_perm
2509 2512 super_admin_rows.append(usr)
2510 2513
2511 2514 return super_admin_rows + owner_row + perm_rows
2512 2515
2513 2516 def permission_user_groups(self):
2514 2517 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2515 2518 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2516 2519 joinedload(UserGroupRepoGroupToPerm.users_group),
2517 2520 joinedload(UserGroupRepoGroupToPerm.permission),)
2518 2521
2519 2522 perm_rows = []
2520 2523 for _user_group in q.all():
2521 2524 usr = AttributeDict(_user_group.users_group.get_dict())
2522 2525 usr.permission = _user_group.permission.permission_name
2523 2526 perm_rows.append(usr)
2524 2527
2525 2528 return perm_rows
2526 2529
2527 2530 def get_api_data(self):
2528 2531 """
2529 2532 Common function for generating api data
2530 2533
2531 2534 """
2532 2535 group = self
2533 2536 data = {
2534 2537 'group_id': group.group_id,
2535 2538 'group_name': group.group_name,
2536 2539 'group_description': group.description_safe,
2537 2540 'parent_group': group.parent_group.group_name if group.parent_group else None,
2538 2541 'repositories': [x.repo_name for x in group.repositories],
2539 2542 'owner': group.user.username,
2540 2543 }
2541 2544 return data
2542 2545
2543 2546
2544 2547 class Permission(Base, BaseModel):
2545 2548 __tablename__ = 'permissions'
2546 2549 __table_args__ = (
2547 2550 Index('p_perm_name_idx', 'permission_name'),
2548 2551 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2549 2552 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2550 2553 )
2551 2554 PERMS = [
2552 2555 ('hg.admin', _('RhodeCode Super Administrator')),
2553 2556
2554 2557 ('repository.none', _('Repository no access')),
2555 2558 ('repository.read', _('Repository read access')),
2556 2559 ('repository.write', _('Repository write access')),
2557 2560 ('repository.admin', _('Repository admin access')),
2558 2561
2559 2562 ('group.none', _('Repository group no access')),
2560 2563 ('group.read', _('Repository group read access')),
2561 2564 ('group.write', _('Repository group write access')),
2562 2565 ('group.admin', _('Repository group admin access')),
2563 2566
2564 2567 ('usergroup.none', _('User group no access')),
2565 2568 ('usergroup.read', _('User group read access')),
2566 2569 ('usergroup.write', _('User group write access')),
2567 2570 ('usergroup.admin', _('User group admin access')),
2568 2571
2569 2572 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2570 2573 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2571 2574
2572 2575 ('hg.usergroup.create.false', _('User Group creation disabled')),
2573 2576 ('hg.usergroup.create.true', _('User Group creation enabled')),
2574 2577
2575 2578 ('hg.create.none', _('Repository creation disabled')),
2576 2579 ('hg.create.repository', _('Repository creation enabled')),
2577 2580 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2578 2581 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2579 2582
2580 2583 ('hg.fork.none', _('Repository forking disabled')),
2581 2584 ('hg.fork.repository', _('Repository forking enabled')),
2582 2585
2583 2586 ('hg.register.none', _('Registration disabled')),
2584 2587 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2585 2588 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2586 2589
2587 2590 ('hg.password_reset.enabled', _('Password reset enabled')),
2588 2591 ('hg.password_reset.hidden', _('Password reset hidden')),
2589 2592 ('hg.password_reset.disabled', _('Password reset disabled')),
2590 2593
2591 2594 ('hg.extern_activate.manual', _('Manual activation of external account')),
2592 2595 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2593 2596
2594 2597 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2595 2598 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2596 2599 ]
2597 2600
2598 2601 # definition of system default permissions for DEFAULT user
2599 2602 DEFAULT_USER_PERMISSIONS = [
2600 2603 'repository.read',
2601 2604 'group.read',
2602 2605 'usergroup.read',
2603 2606 'hg.create.repository',
2604 2607 'hg.repogroup.create.false',
2605 2608 'hg.usergroup.create.false',
2606 2609 'hg.create.write_on_repogroup.true',
2607 2610 'hg.fork.repository',
2608 2611 'hg.register.manual_activate',
2609 2612 'hg.password_reset.enabled',
2610 2613 'hg.extern_activate.auto',
2611 2614 'hg.inherit_default_perms.true',
2612 2615 ]
2613 2616
2614 2617 # defines which permissions are more important higher the more important
2615 2618 # Weight defines which permissions are more important.
2616 2619 # The higher number the more important.
2617 2620 PERM_WEIGHTS = {
2618 2621 'repository.none': 0,
2619 2622 'repository.read': 1,
2620 2623 'repository.write': 3,
2621 2624 'repository.admin': 4,
2622 2625
2623 2626 'group.none': 0,
2624 2627 'group.read': 1,
2625 2628 'group.write': 3,
2626 2629 'group.admin': 4,
2627 2630
2628 2631 'usergroup.none': 0,
2629 2632 'usergroup.read': 1,
2630 2633 'usergroup.write': 3,
2631 2634 'usergroup.admin': 4,
2632 2635
2633 2636 'hg.repogroup.create.false': 0,
2634 2637 'hg.repogroup.create.true': 1,
2635 2638
2636 2639 'hg.usergroup.create.false': 0,
2637 2640 'hg.usergroup.create.true': 1,
2638 2641
2639 2642 'hg.fork.none': 0,
2640 2643 'hg.fork.repository': 1,
2641 2644 'hg.create.none': 0,
2642 2645 'hg.create.repository': 1
2643 2646 }
2644 2647
2645 2648 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2646 2649 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2647 2650 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2648 2651
2649 2652 def __unicode__(self):
2650 2653 return u"<%s('%s:%s')>" % (
2651 2654 self.__class__.__name__, self.permission_id, self.permission_name
2652 2655 )
2653 2656
2654 2657 @classmethod
2655 2658 def get_by_key(cls, key):
2656 2659 return cls.query().filter(cls.permission_name == key).scalar()
2657 2660
2658 2661 @classmethod
2659 2662 def get_default_repo_perms(cls, user_id, repo_id=None):
2660 2663 q = Session().query(UserRepoToPerm, Repository, Permission)\
2661 2664 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2662 2665 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2663 2666 .filter(UserRepoToPerm.user_id == user_id)
2664 2667 if repo_id:
2665 2668 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2666 2669 return q.all()
2667 2670
2668 2671 @classmethod
2669 2672 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2670 2673 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2671 2674 .join(
2672 2675 Permission,
2673 2676 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2674 2677 .join(
2675 2678 Repository,
2676 2679 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2677 2680 .join(
2678 2681 UserGroup,
2679 2682 UserGroupRepoToPerm.users_group_id ==
2680 2683 UserGroup.users_group_id)\
2681 2684 .join(
2682 2685 UserGroupMember,
2683 2686 UserGroupRepoToPerm.users_group_id ==
2684 2687 UserGroupMember.users_group_id)\
2685 2688 .filter(
2686 2689 UserGroupMember.user_id == user_id,
2687 2690 UserGroup.users_group_active == true())
2688 2691 if repo_id:
2689 2692 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2690 2693 return q.all()
2691 2694
2692 2695 @classmethod
2693 2696 def get_default_group_perms(cls, user_id, repo_group_id=None):
2694 2697 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2695 2698 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2696 2699 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2697 2700 .filter(UserRepoGroupToPerm.user_id == user_id)
2698 2701 if repo_group_id:
2699 2702 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2700 2703 return q.all()
2701 2704
2702 2705 @classmethod
2703 2706 def get_default_group_perms_from_user_group(
2704 2707 cls, user_id, repo_group_id=None):
2705 2708 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2706 2709 .join(
2707 2710 Permission,
2708 2711 UserGroupRepoGroupToPerm.permission_id ==
2709 2712 Permission.permission_id)\
2710 2713 .join(
2711 2714 RepoGroup,
2712 2715 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2713 2716 .join(
2714 2717 UserGroup,
2715 2718 UserGroupRepoGroupToPerm.users_group_id ==
2716 2719 UserGroup.users_group_id)\
2717 2720 .join(
2718 2721 UserGroupMember,
2719 2722 UserGroupRepoGroupToPerm.users_group_id ==
2720 2723 UserGroupMember.users_group_id)\
2721 2724 .filter(
2722 2725 UserGroupMember.user_id == user_id,
2723 2726 UserGroup.users_group_active == true())
2724 2727 if repo_group_id:
2725 2728 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2726 2729 return q.all()
2727 2730
2728 2731 @classmethod
2729 2732 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2730 2733 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2731 2734 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2732 2735 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2733 2736 .filter(UserUserGroupToPerm.user_id == user_id)
2734 2737 if user_group_id:
2735 2738 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2736 2739 return q.all()
2737 2740
2738 2741 @classmethod
2739 2742 def get_default_user_group_perms_from_user_group(
2740 2743 cls, user_id, user_group_id=None):
2741 2744 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2742 2745 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2743 2746 .join(
2744 2747 Permission,
2745 2748 UserGroupUserGroupToPerm.permission_id ==
2746 2749 Permission.permission_id)\
2747 2750 .join(
2748 2751 TargetUserGroup,
2749 2752 UserGroupUserGroupToPerm.target_user_group_id ==
2750 2753 TargetUserGroup.users_group_id)\
2751 2754 .join(
2752 2755 UserGroup,
2753 2756 UserGroupUserGroupToPerm.user_group_id ==
2754 2757 UserGroup.users_group_id)\
2755 2758 .join(
2756 2759 UserGroupMember,
2757 2760 UserGroupUserGroupToPerm.user_group_id ==
2758 2761 UserGroupMember.users_group_id)\
2759 2762 .filter(
2760 2763 UserGroupMember.user_id == user_id,
2761 2764 UserGroup.users_group_active == true())
2762 2765 if user_group_id:
2763 2766 q = q.filter(
2764 2767 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2765 2768
2766 2769 return q.all()
2767 2770
2768 2771
2769 2772 class UserRepoToPerm(Base, BaseModel):
2770 2773 __tablename__ = 'repo_to_perm'
2771 2774 __table_args__ = (
2772 2775 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2773 2776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2774 2777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2775 2778 )
2776 2779 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2777 2780 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2778 2781 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2779 2782 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2780 2783
2781 2784 user = relationship('User')
2782 2785 repository = relationship('Repository')
2783 2786 permission = relationship('Permission')
2784 2787
2785 2788 @classmethod
2786 2789 def create(cls, user, repository, permission):
2787 2790 n = cls()
2788 2791 n.user = user
2789 2792 n.repository = repository
2790 2793 n.permission = permission
2791 2794 Session().add(n)
2792 2795 return n
2793 2796
2794 2797 def __unicode__(self):
2795 2798 return u'<%s => %s >' % (self.user, self.repository)
2796 2799
2797 2800
2798 2801 class UserUserGroupToPerm(Base, BaseModel):
2799 2802 __tablename__ = 'user_user_group_to_perm'
2800 2803 __table_args__ = (
2801 2804 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2802 2805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2803 2806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2804 2807 )
2805 2808 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2806 2809 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2807 2810 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2808 2811 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2809 2812
2810 2813 user = relationship('User')
2811 2814 user_group = relationship('UserGroup')
2812 2815 permission = relationship('Permission')
2813 2816
2814 2817 @classmethod
2815 2818 def create(cls, user, user_group, permission):
2816 2819 n = cls()
2817 2820 n.user = user
2818 2821 n.user_group = user_group
2819 2822 n.permission = permission
2820 2823 Session().add(n)
2821 2824 return n
2822 2825
2823 2826 def __unicode__(self):
2824 2827 return u'<%s => %s >' % (self.user, self.user_group)
2825 2828
2826 2829
2827 2830 class UserToPerm(Base, BaseModel):
2828 2831 __tablename__ = 'user_to_perm'
2829 2832 __table_args__ = (
2830 2833 UniqueConstraint('user_id', 'permission_id'),
2831 2834 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2832 2835 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2833 2836 )
2834 2837 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2835 2838 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2836 2839 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2837 2840
2838 2841 user = relationship('User')
2839 2842 permission = relationship('Permission', lazy='joined')
2840 2843
2841 2844 def __unicode__(self):
2842 2845 return u'<%s => %s >' % (self.user, self.permission)
2843 2846
2844 2847
2845 2848 class UserGroupRepoToPerm(Base, BaseModel):
2846 2849 __tablename__ = 'users_group_repo_to_perm'
2847 2850 __table_args__ = (
2848 2851 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2849 2852 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2850 2853 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2851 2854 )
2852 2855 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2853 2856 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2854 2857 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2855 2858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2856 2859
2857 2860 users_group = relationship('UserGroup')
2858 2861 permission = relationship('Permission')
2859 2862 repository = relationship('Repository')
2860 2863
2861 2864 @classmethod
2862 2865 def create(cls, users_group, repository, permission):
2863 2866 n = cls()
2864 2867 n.users_group = users_group
2865 2868 n.repository = repository
2866 2869 n.permission = permission
2867 2870 Session().add(n)
2868 2871 return n
2869 2872
2870 2873 def __unicode__(self):
2871 2874 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2872 2875
2873 2876
2874 2877 class UserGroupUserGroupToPerm(Base, BaseModel):
2875 2878 __tablename__ = 'user_group_user_group_to_perm'
2876 2879 __table_args__ = (
2877 2880 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2878 2881 CheckConstraint('target_user_group_id != user_group_id'),
2879 2882 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2880 2883 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2881 2884 )
2882 2885 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2883 2886 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2884 2887 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2885 2888 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2886 2889
2887 2890 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2888 2891 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2889 2892 permission = relationship('Permission')
2890 2893
2891 2894 @classmethod
2892 2895 def create(cls, target_user_group, user_group, permission):
2893 2896 n = cls()
2894 2897 n.target_user_group = target_user_group
2895 2898 n.user_group = user_group
2896 2899 n.permission = permission
2897 2900 Session().add(n)
2898 2901 return n
2899 2902
2900 2903 def __unicode__(self):
2901 2904 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2902 2905
2903 2906
2904 2907 class UserGroupToPerm(Base, BaseModel):
2905 2908 __tablename__ = 'users_group_to_perm'
2906 2909 __table_args__ = (
2907 2910 UniqueConstraint('users_group_id', 'permission_id',),
2908 2911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2909 2912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2910 2913 )
2911 2914 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2912 2915 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2913 2916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2914 2917
2915 2918 users_group = relationship('UserGroup')
2916 2919 permission = relationship('Permission')
2917 2920
2918 2921
2919 2922 class UserRepoGroupToPerm(Base, BaseModel):
2920 2923 __tablename__ = 'user_repo_group_to_perm'
2921 2924 __table_args__ = (
2922 2925 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2923 2926 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2924 2927 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2925 2928 )
2926 2929
2927 2930 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2928 2931 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2929 2932 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2930 2933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2931 2934
2932 2935 user = relationship('User')
2933 2936 group = relationship('RepoGroup')
2934 2937 permission = relationship('Permission')
2935 2938
2936 2939 @classmethod
2937 2940 def create(cls, user, repository_group, permission):
2938 2941 n = cls()
2939 2942 n.user = user
2940 2943 n.group = repository_group
2941 2944 n.permission = permission
2942 2945 Session().add(n)
2943 2946 return n
2944 2947
2945 2948
2946 2949 class UserGroupRepoGroupToPerm(Base, BaseModel):
2947 2950 __tablename__ = 'users_group_repo_group_to_perm'
2948 2951 __table_args__ = (
2949 2952 UniqueConstraint('users_group_id', 'group_id'),
2950 2953 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2951 2954 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2952 2955 )
2953 2956
2954 2957 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2955 2958 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2956 2959 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2957 2960 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2958 2961
2959 2962 users_group = relationship('UserGroup')
2960 2963 permission = relationship('Permission')
2961 2964 group = relationship('RepoGroup')
2962 2965
2963 2966 @classmethod
2964 2967 def create(cls, user_group, repository_group, permission):
2965 2968 n = cls()
2966 2969 n.users_group = user_group
2967 2970 n.group = repository_group
2968 2971 n.permission = permission
2969 2972 Session().add(n)
2970 2973 return n
2971 2974
2972 2975 def __unicode__(self):
2973 2976 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2974 2977
2975 2978
2976 2979 class Statistics(Base, BaseModel):
2977 2980 __tablename__ = 'statistics'
2978 2981 __table_args__ = (
2979 2982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2980 2983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2981 2984 )
2982 2985 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2983 2986 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2984 2987 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2985 2988 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2986 2989 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2987 2990 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2988 2991
2989 2992 repository = relationship('Repository', single_parent=True)
2990 2993
2991 2994
2992 2995 class UserFollowing(Base, BaseModel):
2993 2996 __tablename__ = 'user_followings'
2994 2997 __table_args__ = (
2995 2998 UniqueConstraint('user_id', 'follows_repository_id'),
2996 2999 UniqueConstraint('user_id', 'follows_user_id'),
2997 3000 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2998 3001 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2999 3002 )
3000 3003
3001 3004 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3002 3005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3003 3006 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3004 3007 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3005 3008 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3006 3009
3007 3010 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3008 3011
3009 3012 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3010 3013 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3011 3014
3012 3015 @classmethod
3013 3016 def get_repo_followers(cls, repo_id):
3014 3017 return cls.query().filter(cls.follows_repo_id == repo_id)
3015 3018
3016 3019
3017 3020 class CacheKey(Base, BaseModel):
3018 3021 __tablename__ = 'cache_invalidation'
3019 3022 __table_args__ = (
3020 3023 UniqueConstraint('cache_key'),
3021 3024 Index('key_idx', 'cache_key'),
3022 3025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3023 3026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3024 3027 )
3025 3028 CACHE_TYPE_ATOM = 'ATOM'
3026 3029 CACHE_TYPE_RSS = 'RSS'
3027 3030 CACHE_TYPE_README = 'README'
3028 3031
3029 3032 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3030 3033 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3031 3034 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3032 3035 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3033 3036
3034 3037 def __init__(self, cache_key, cache_args=''):
3035 3038 self.cache_key = cache_key
3036 3039 self.cache_args = cache_args
3037 3040 self.cache_active = False
3038 3041
3039 3042 def __unicode__(self):
3040 3043 return u"<%s('%s:%s[%s]')>" % (
3041 3044 self.__class__.__name__,
3042 3045 self.cache_id, self.cache_key, self.cache_active)
3043 3046
3044 3047 def _cache_key_partition(self):
3045 3048 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3046 3049 return prefix, repo_name, suffix
3047 3050
3048 3051 def get_prefix(self):
3049 3052 """
3050 3053 Try to extract prefix from existing cache key. The key could consist
3051 3054 of prefix, repo_name, suffix
3052 3055 """
3053 3056 # this returns prefix, repo_name, suffix
3054 3057 return self._cache_key_partition()[0]
3055 3058
3056 3059 def get_suffix(self):
3057 3060 """
3058 3061 get suffix that might have been used in _get_cache_key to
3059 3062 generate self.cache_key. Only used for informational purposes
3060 3063 in repo_edit.mako.
3061 3064 """
3062 3065 # prefix, repo_name, suffix
3063 3066 return self._cache_key_partition()[2]
3064 3067
3065 3068 @classmethod
3066 3069 def delete_all_cache(cls):
3067 3070 """
3068 3071 Delete all cache keys from database.
3069 3072 Should only be run when all instances are down and all entries
3070 3073 thus stale.
3071 3074 """
3072 3075 cls.query().delete()
3073 3076 Session().commit()
3074 3077
3075 3078 @classmethod
3076 3079 def get_cache_key(cls, repo_name, cache_type):
3077 3080 """
3078 3081
3079 3082 Generate a cache key for this process of RhodeCode instance.
3080 3083 Prefix most likely will be process id or maybe explicitly set
3081 3084 instance_id from .ini file.
3082 3085 """
3083 3086 import rhodecode
3084 3087 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3085 3088
3086 3089 repo_as_unicode = safe_unicode(repo_name)
3087 3090 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3088 3091 if cache_type else repo_as_unicode
3089 3092
3090 3093 return u'{}{}'.format(prefix, key)
3091 3094
3092 3095 @classmethod
3093 3096 def set_invalidate(cls, repo_name, delete=False):
3094 3097 """
3095 3098 Mark all caches of a repo as invalid in the database.
3096 3099 """
3097 3100
3098 3101 try:
3099 3102 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3100 3103 if delete:
3101 3104 log.debug('cache objects deleted for repo %s',
3102 3105 safe_str(repo_name))
3103 3106 qry.delete()
3104 3107 else:
3105 3108 log.debug('cache objects marked as invalid for repo %s',
3106 3109 safe_str(repo_name))
3107 3110 qry.update({"cache_active": False})
3108 3111
3109 3112 Session().commit()
3110 3113 except Exception:
3111 3114 log.exception(
3112 3115 'Cache key invalidation failed for repository %s',
3113 3116 safe_str(repo_name))
3114 3117 Session().rollback()
3115 3118
3116 3119 @classmethod
3117 3120 def get_active_cache(cls, cache_key):
3118 3121 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3119 3122 if inv_obj:
3120 3123 return inv_obj
3121 3124 return None
3122 3125
3123 3126 @classmethod
3124 3127 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3125 3128 thread_scoped=False):
3126 3129 """
3127 3130 @cache_region('long_term')
3128 3131 def _heavy_calculation(cache_key):
3129 3132 return 'result'
3130 3133
3131 3134 cache_context = CacheKey.repo_context_cache(
3132 3135 _heavy_calculation, repo_name, cache_type)
3133 3136
3134 3137 with cache_context as context:
3135 3138 context.invalidate()
3136 3139 computed = context.compute()
3137 3140
3138 3141 assert computed == 'result'
3139 3142 """
3140 3143 from rhodecode.lib import caches
3141 3144 return caches.InvalidationContext(
3142 3145 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3143 3146
3144 3147
3145 3148 class ChangesetComment(Base, BaseModel):
3146 3149 __tablename__ = 'changeset_comments'
3147 3150 __table_args__ = (
3148 3151 Index('cc_revision_idx', 'revision'),
3149 3152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3150 3153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3151 3154 )
3152 3155
3153 3156 COMMENT_OUTDATED = u'comment_outdated'
3154 3157 COMMENT_TYPE_NOTE = u'note'
3155 3158 COMMENT_TYPE_TODO = u'todo'
3156 3159 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3157 3160
3158 3161 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3159 3162 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3160 3163 revision = Column('revision', String(40), nullable=True)
3161 3164 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3162 3165 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3163 3166 line_no = Column('line_no', Unicode(10), nullable=True)
3164 3167 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3165 3168 f_path = Column('f_path', Unicode(1000), nullable=True)
3166 3169 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3167 3170 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3168 3171 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3169 3172 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3170 3173 renderer = Column('renderer', Unicode(64), nullable=True)
3171 3174 display_state = Column('display_state', Unicode(128), nullable=True)
3172 3175
3173 3176 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3174 3177 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3175 3178 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3176 3179 author = relationship('User', lazy='joined')
3177 3180 repo = relationship('Repository')
3178 3181 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3179 3182 pull_request = relationship('PullRequest', lazy='joined')
3180 3183 pull_request_version = relationship('PullRequestVersion')
3181 3184
3182 3185 @classmethod
3183 3186 def get_users(cls, revision=None, pull_request_id=None):
3184 3187 """
3185 3188 Returns user associated with this ChangesetComment. ie those
3186 3189 who actually commented
3187 3190
3188 3191 :param cls:
3189 3192 :param revision:
3190 3193 """
3191 3194 q = Session().query(User)\
3192 3195 .join(ChangesetComment.author)
3193 3196 if revision:
3194 3197 q = q.filter(cls.revision == revision)
3195 3198 elif pull_request_id:
3196 3199 q = q.filter(cls.pull_request_id == pull_request_id)
3197 3200 return q.all()
3198 3201
3199 3202 @classmethod
3200 3203 def get_index_from_version(cls, pr_version, versions):
3201 3204 num_versions = [x.pull_request_version_id for x in versions]
3202 3205 try:
3203 3206 return num_versions.index(pr_version) +1
3204 3207 except (IndexError, ValueError):
3205 3208 return
3206 3209
3207 3210 @property
3208 3211 def outdated(self):
3209 3212 return self.display_state == self.COMMENT_OUTDATED
3210 3213
3211 3214 def outdated_at_version(self, version):
3212 3215 """
3213 3216 Checks if comment is outdated for given pull request version
3214 3217 """
3215 3218 return self.outdated and self.pull_request_version_id != version
3216 3219
3217 3220 def older_than_version(self, version):
3218 3221 """
3219 3222 Checks if comment is made from previous version than given
3220 3223 """
3221 3224 if version is None:
3222 3225 return self.pull_request_version_id is not None
3223 3226
3224 3227 return self.pull_request_version_id < version
3225 3228
3226 3229 @property
3227 3230 def resolved(self):
3228 3231 return self.resolved_by[0] if self.resolved_by else None
3229 3232
3230 3233 @property
3231 3234 def is_todo(self):
3232 3235 return self.comment_type == self.COMMENT_TYPE_TODO
3233 3236
3234 3237 @property
3235 3238 def is_inline(self):
3236 3239 return self.line_no and self.f_path
3237 3240
3238 3241 def get_index_version(self, versions):
3239 3242 return self.get_index_from_version(
3240 3243 self.pull_request_version_id, versions)
3241 3244
3242 3245 def __repr__(self):
3243 3246 if self.comment_id:
3244 3247 return '<DB:Comment #%s>' % self.comment_id
3245 3248 else:
3246 3249 return '<DB:Comment at %#x>' % id(self)
3247 3250
3248 3251 def get_api_data(self):
3249 3252 comment = self
3250 3253 data = {
3251 3254 'comment_id': comment.comment_id,
3252 3255 'comment_type': comment.comment_type,
3253 3256 'comment_text': comment.text,
3254 3257 'comment_status': comment.status_change,
3255 3258 'comment_f_path': comment.f_path,
3256 3259 'comment_lineno': comment.line_no,
3257 3260 'comment_author': comment.author,
3258 3261 'comment_created_on': comment.created_on
3259 3262 }
3260 3263 return data
3261 3264
3262 3265 def __json__(self):
3263 3266 data = dict()
3264 3267 data.update(self.get_api_data())
3265 3268 return data
3266 3269
3267 3270
3268 3271 class ChangesetStatus(Base, BaseModel):
3269 3272 __tablename__ = 'changeset_statuses'
3270 3273 __table_args__ = (
3271 3274 Index('cs_revision_idx', 'revision'),
3272 3275 Index('cs_version_idx', 'version'),
3273 3276 UniqueConstraint('repo_id', 'revision', 'version'),
3274 3277 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3275 3278 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3276 3279 )
3277 3280 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3278 3281 STATUS_APPROVED = 'approved'
3279 3282 STATUS_REJECTED = 'rejected'
3280 3283 STATUS_UNDER_REVIEW = 'under_review'
3281 3284
3282 3285 STATUSES = [
3283 3286 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3284 3287 (STATUS_APPROVED, _("Approved")),
3285 3288 (STATUS_REJECTED, _("Rejected")),
3286 3289 (STATUS_UNDER_REVIEW, _("Under Review")),
3287 3290 ]
3288 3291
3289 3292 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3290 3293 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3291 3294 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3292 3295 revision = Column('revision', String(40), nullable=False)
3293 3296 status = Column('status', String(128), nullable=False, default=DEFAULT)
3294 3297 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3295 3298 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3296 3299 version = Column('version', Integer(), nullable=False, default=0)
3297 3300 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3298 3301
3299 3302 author = relationship('User', lazy='joined')
3300 3303 repo = relationship('Repository')
3301 3304 comment = relationship('ChangesetComment', lazy='joined')
3302 3305 pull_request = relationship('PullRequest', lazy='joined')
3303 3306
3304 3307 def __unicode__(self):
3305 3308 return u"<%s('%s[v%s]:%s')>" % (
3306 3309 self.__class__.__name__,
3307 3310 self.status, self.version, self.author
3308 3311 )
3309 3312
3310 3313 @classmethod
3311 3314 def get_status_lbl(cls, value):
3312 3315 return dict(cls.STATUSES).get(value)
3313 3316
3314 3317 @property
3315 3318 def status_lbl(self):
3316 3319 return ChangesetStatus.get_status_lbl(self.status)
3317 3320
3318 3321 def get_api_data(self):
3319 3322 status = self
3320 3323 data = {
3321 3324 'status_id': status.changeset_status_id,
3322 3325 'status': status.status,
3323 3326 }
3324 3327 return data
3325 3328
3326 3329 def __json__(self):
3327 3330 data = dict()
3328 3331 data.update(self.get_api_data())
3329 3332 return data
3330 3333
3331 3334
3332 3335 class _PullRequestBase(BaseModel):
3333 3336 """
3334 3337 Common attributes of pull request and version entries.
3335 3338 """
3336 3339
3337 3340 # .status values
3338 3341 STATUS_NEW = u'new'
3339 3342 STATUS_OPEN = u'open'
3340 3343 STATUS_CLOSED = u'closed'
3341 3344
3342 3345 title = Column('title', Unicode(255), nullable=True)
3343 3346 description = Column(
3344 3347 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3345 3348 nullable=True)
3346 3349 # new/open/closed status of pull request (not approve/reject/etc)
3347 3350 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3348 3351 created_on = Column(
3349 3352 'created_on', DateTime(timezone=False), nullable=False,
3350 3353 default=datetime.datetime.now)
3351 3354 updated_on = Column(
3352 3355 'updated_on', DateTime(timezone=False), nullable=False,
3353 3356 default=datetime.datetime.now)
3354 3357
3355 3358 @declared_attr
3356 3359 def user_id(cls):
3357 3360 return Column(
3358 3361 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3359 3362 unique=None)
3360 3363
3361 3364 # 500 revisions max
3362 3365 _revisions = Column(
3363 3366 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3364 3367
3365 3368 @declared_attr
3366 3369 def source_repo_id(cls):
3367 3370 # TODO: dan: rename column to source_repo_id
3368 3371 return Column(
3369 3372 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3370 3373 nullable=False)
3371 3374
3372 3375 source_ref = Column('org_ref', Unicode(255), nullable=False)
3373 3376
3374 3377 @declared_attr
3375 3378 def target_repo_id(cls):
3376 3379 # TODO: dan: rename column to target_repo_id
3377 3380 return Column(
3378 3381 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3379 3382 nullable=False)
3380 3383
3381 3384 target_ref = Column('other_ref', Unicode(255), nullable=False)
3382 3385 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3383 3386
3384 3387 # TODO: dan: rename column to last_merge_source_rev
3385 3388 _last_merge_source_rev = Column(
3386 3389 'last_merge_org_rev', String(40), nullable=True)
3387 3390 # TODO: dan: rename column to last_merge_target_rev
3388 3391 _last_merge_target_rev = Column(
3389 3392 'last_merge_other_rev', String(40), nullable=True)
3390 3393 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3391 3394 merge_rev = Column('merge_rev', String(40), nullable=True)
3392 3395
3393 3396 reviewer_data = Column(
3394 3397 'reviewer_data_json', MutationObj.as_mutable(
3395 3398 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3396 3399
3397 3400 @property
3398 3401 def reviewer_data_json(self):
3399 3402 return json.dumps(self.reviewer_data)
3400 3403
3401 3404 @hybrid_property
3402 3405 def description_safe(self):
3403 3406 from rhodecode.lib import helpers as h
3404 3407 return h.escape(self.description)
3405 3408
3406 3409 @hybrid_property
3407 3410 def revisions(self):
3408 3411 return self._revisions.split(':') if self._revisions else []
3409 3412
3410 3413 @revisions.setter
3411 3414 def revisions(self, val):
3412 3415 self._revisions = ':'.join(val)
3413 3416
3414 3417 @hybrid_property
3415 3418 def last_merge_status(self):
3416 3419 return safe_int(self._last_merge_status)
3417 3420
3418 3421 @last_merge_status.setter
3419 3422 def last_merge_status(self, val):
3420 3423 self._last_merge_status = val
3421 3424
3422 3425 @declared_attr
3423 3426 def author(cls):
3424 3427 return relationship('User', lazy='joined')
3425 3428
3426 3429 @declared_attr
3427 3430 def source_repo(cls):
3428 3431 return relationship(
3429 3432 'Repository',
3430 3433 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3431 3434
3432 3435 @property
3433 3436 def source_ref_parts(self):
3434 3437 return self.unicode_to_reference(self.source_ref)
3435 3438
3436 3439 @declared_attr
3437 3440 def target_repo(cls):
3438 3441 return relationship(
3439 3442 'Repository',
3440 3443 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3441 3444
3442 3445 @property
3443 3446 def target_ref_parts(self):
3444 3447 return self.unicode_to_reference(self.target_ref)
3445 3448
3446 3449 @property
3447 3450 def shadow_merge_ref(self):
3448 3451 return self.unicode_to_reference(self._shadow_merge_ref)
3449 3452
3450 3453 @shadow_merge_ref.setter
3451 3454 def shadow_merge_ref(self, ref):
3452 3455 self._shadow_merge_ref = self.reference_to_unicode(ref)
3453 3456
3454 3457 def unicode_to_reference(self, raw):
3455 3458 """
3456 3459 Convert a unicode (or string) to a reference object.
3457 3460 If unicode evaluates to False it returns None.
3458 3461 """
3459 3462 if raw:
3460 3463 refs = raw.split(':')
3461 3464 return Reference(*refs)
3462 3465 else:
3463 3466 return None
3464 3467
3465 3468 def reference_to_unicode(self, ref):
3466 3469 """
3467 3470 Convert a reference object to unicode.
3468 3471 If reference is None it returns None.
3469 3472 """
3470 3473 if ref:
3471 3474 return u':'.join(ref)
3472 3475 else:
3473 3476 return None
3474 3477
3475 3478 def get_api_data(self, with_merge_state=True):
3476 3479 from rhodecode.model.pull_request import PullRequestModel
3477 3480
3478 3481 pull_request = self
3479 3482 if with_merge_state:
3480 3483 merge_status = PullRequestModel().merge_status(pull_request)
3481 3484 merge_state = {
3482 3485 'status': merge_status[0],
3483 3486 'message': safe_unicode(merge_status[1]),
3484 3487 }
3485 3488 else:
3486 3489 merge_state = {'status': 'not_available',
3487 3490 'message': 'not_available'}
3488 3491
3489 3492 merge_data = {
3490 3493 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3491 3494 'reference': (
3492 3495 pull_request.shadow_merge_ref._asdict()
3493 3496 if pull_request.shadow_merge_ref else None),
3494 3497 }
3495 3498
3496 3499 data = {
3497 3500 'pull_request_id': pull_request.pull_request_id,
3498 3501 'url': PullRequestModel().get_url(pull_request),
3499 3502 'title': pull_request.title,
3500 3503 'description': pull_request.description,
3501 3504 'status': pull_request.status,
3502 3505 'created_on': pull_request.created_on,
3503 3506 'updated_on': pull_request.updated_on,
3504 3507 'commit_ids': pull_request.revisions,
3505 3508 'review_status': pull_request.calculated_review_status(),
3506 3509 'mergeable': merge_state,
3507 3510 'source': {
3508 3511 'clone_url': pull_request.source_repo.clone_url(),
3509 3512 'repository': pull_request.source_repo.repo_name,
3510 3513 'reference': {
3511 3514 'name': pull_request.source_ref_parts.name,
3512 3515 'type': pull_request.source_ref_parts.type,
3513 3516 'commit_id': pull_request.source_ref_parts.commit_id,
3514 3517 },
3515 3518 },
3516 3519 'target': {
3517 3520 'clone_url': pull_request.target_repo.clone_url(),
3518 3521 'repository': pull_request.target_repo.repo_name,
3519 3522 'reference': {
3520 3523 'name': pull_request.target_ref_parts.name,
3521 3524 'type': pull_request.target_ref_parts.type,
3522 3525 'commit_id': pull_request.target_ref_parts.commit_id,
3523 3526 },
3524 3527 },
3525 3528 'merge': merge_data,
3526 3529 'author': pull_request.author.get_api_data(include_secrets=False,
3527 3530 details='basic'),
3528 3531 'reviewers': [
3529 3532 {
3530 3533 'user': reviewer.get_api_data(include_secrets=False,
3531 3534 details='basic'),
3532 3535 'reasons': reasons,
3533 3536 'review_status': st[0][1].status if st else 'not_reviewed',
3534 3537 }
3535 3538 for reviewer, reasons, mandatory, st in
3536 3539 pull_request.reviewers_statuses()
3537 3540 ]
3538 3541 }
3539 3542
3540 3543 return data
3541 3544
3542 3545
3543 3546 class PullRequest(Base, _PullRequestBase):
3544 3547 __tablename__ = 'pull_requests'
3545 3548 __table_args__ = (
3546 3549 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3547 3550 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3548 3551 )
3549 3552
3550 3553 pull_request_id = Column(
3551 3554 'pull_request_id', Integer(), nullable=False, primary_key=True)
3552 3555
3553 3556 def __repr__(self):
3554 3557 if self.pull_request_id:
3555 3558 return '<DB:PullRequest #%s>' % self.pull_request_id
3556 3559 else:
3557 3560 return '<DB:PullRequest at %#x>' % id(self)
3558 3561
3559 3562 reviewers = relationship('PullRequestReviewers',
3560 3563 cascade="all, delete, delete-orphan")
3561 3564 statuses = relationship('ChangesetStatus',
3562 3565 cascade="all, delete, delete-orphan")
3563 3566 comments = relationship('ChangesetComment',
3564 3567 cascade="all, delete, delete-orphan")
3565 3568 versions = relationship('PullRequestVersion',
3566 3569 cascade="all, delete, delete-orphan",
3567 3570 lazy='dynamic')
3568 3571
3569 3572 @classmethod
3570 3573 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3571 3574 internal_methods=None):
3572 3575
3573 3576 class PullRequestDisplay(object):
3574 3577 """
3575 3578 Special object wrapper for showing PullRequest data via Versions
3576 3579 It mimics PR object as close as possible. This is read only object
3577 3580 just for display
3578 3581 """
3579 3582
3580 3583 def __init__(self, attrs, internal=None):
3581 3584 self.attrs = attrs
3582 3585 # internal have priority over the given ones via attrs
3583 3586 self.internal = internal or ['versions']
3584 3587
3585 3588 def __getattr__(self, item):
3586 3589 if item in self.internal:
3587 3590 return getattr(self, item)
3588 3591 try:
3589 3592 return self.attrs[item]
3590 3593 except KeyError:
3591 3594 raise AttributeError(
3592 3595 '%s object has no attribute %s' % (self, item))
3593 3596
3594 3597 def __repr__(self):
3595 3598 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3596 3599
3597 3600 def versions(self):
3598 3601 return pull_request_obj.versions.order_by(
3599 3602 PullRequestVersion.pull_request_version_id).all()
3600 3603
3601 3604 def is_closed(self):
3602 3605 return pull_request_obj.is_closed()
3603 3606
3604 3607 @property
3605 3608 def pull_request_version_id(self):
3606 3609 return getattr(pull_request_obj, 'pull_request_version_id', None)
3607 3610
3608 3611 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3609 3612
3610 3613 attrs.author = StrictAttributeDict(
3611 3614 pull_request_obj.author.get_api_data())
3612 3615 if pull_request_obj.target_repo:
3613 3616 attrs.target_repo = StrictAttributeDict(
3614 3617 pull_request_obj.target_repo.get_api_data())
3615 3618 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3616 3619
3617 3620 if pull_request_obj.source_repo:
3618 3621 attrs.source_repo = StrictAttributeDict(
3619 3622 pull_request_obj.source_repo.get_api_data())
3620 3623 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3621 3624
3622 3625 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3623 3626 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3624 3627 attrs.revisions = pull_request_obj.revisions
3625 3628
3626 3629 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3627 3630 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3628 3631 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3629 3632
3630 3633 return PullRequestDisplay(attrs, internal=internal_methods)
3631 3634
3632 3635 def is_closed(self):
3633 3636 return self.status == self.STATUS_CLOSED
3634 3637
3635 3638 def __json__(self):
3636 3639 return {
3637 3640 'revisions': self.revisions,
3638 3641 }
3639 3642
3640 3643 def calculated_review_status(self):
3641 3644 from rhodecode.model.changeset_status import ChangesetStatusModel
3642 3645 return ChangesetStatusModel().calculated_review_status(self)
3643 3646
3644 3647 def reviewers_statuses(self):
3645 3648 from rhodecode.model.changeset_status import ChangesetStatusModel
3646 3649 return ChangesetStatusModel().reviewers_statuses(self)
3647 3650
3648 3651 @property
3649 3652 def workspace_id(self):
3650 3653 from rhodecode.model.pull_request import PullRequestModel
3651 3654 return PullRequestModel()._workspace_id(self)
3652 3655
3653 3656 def get_shadow_repo(self):
3654 3657 workspace_id = self.workspace_id
3655 3658 vcs_obj = self.target_repo.scm_instance()
3656 3659 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3657 3660 workspace_id)
3658 3661 return vcs_obj._get_shadow_instance(shadow_repository_path)
3659 3662
3660 3663
3661 3664 class PullRequestVersion(Base, _PullRequestBase):
3662 3665 __tablename__ = 'pull_request_versions'
3663 3666 __table_args__ = (
3664 3667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3665 3668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3666 3669 )
3667 3670
3668 3671 pull_request_version_id = Column(
3669 3672 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3670 3673 pull_request_id = Column(
3671 3674 'pull_request_id', Integer(),
3672 3675 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3673 3676 pull_request = relationship('PullRequest')
3674 3677
3675 3678 def __repr__(self):
3676 3679 if self.pull_request_version_id:
3677 3680 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3678 3681 else:
3679 3682 return '<DB:PullRequestVersion at %#x>' % id(self)
3680 3683
3681 3684 @property
3682 3685 def reviewers(self):
3683 3686 return self.pull_request.reviewers
3684 3687
3685 3688 @property
3686 3689 def versions(self):
3687 3690 return self.pull_request.versions
3688 3691
3689 3692 def is_closed(self):
3690 3693 # calculate from original
3691 3694 return self.pull_request.status == self.STATUS_CLOSED
3692 3695
3693 3696 def calculated_review_status(self):
3694 3697 return self.pull_request.calculated_review_status()
3695 3698
3696 3699 def reviewers_statuses(self):
3697 3700 return self.pull_request.reviewers_statuses()
3698 3701
3699 3702
3700 3703 class PullRequestReviewers(Base, BaseModel):
3701 3704 __tablename__ = 'pull_request_reviewers'
3702 3705 __table_args__ = (
3703 3706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3704 3707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3705 3708 )
3706 3709
3707 3710 @hybrid_property
3708 3711 def reasons(self):
3709 3712 if not self._reasons:
3710 3713 return []
3711 3714 return self._reasons
3712 3715
3713 3716 @reasons.setter
3714 3717 def reasons(self, val):
3715 3718 val = val or []
3716 3719 if any(not isinstance(x, basestring) for x in val):
3717 3720 raise Exception('invalid reasons type, must be list of strings')
3718 3721 self._reasons = val
3719 3722
3720 3723 pull_requests_reviewers_id = Column(
3721 3724 'pull_requests_reviewers_id', Integer(), nullable=False,
3722 3725 primary_key=True)
3723 3726 pull_request_id = Column(
3724 3727 "pull_request_id", Integer(),
3725 3728 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3726 3729 user_id = Column(
3727 3730 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3728 3731 _reasons = Column(
3729 3732 'reason', MutationList.as_mutable(
3730 3733 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3731 3734 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3732 3735 user = relationship('User')
3733 3736 pull_request = relationship('PullRequest')
3734 3737
3735 3738
3736 3739 class Notification(Base, BaseModel):
3737 3740 __tablename__ = 'notifications'
3738 3741 __table_args__ = (
3739 3742 Index('notification_type_idx', 'type'),
3740 3743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3741 3744 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3742 3745 )
3743 3746
3744 3747 TYPE_CHANGESET_COMMENT = u'cs_comment'
3745 3748 TYPE_MESSAGE = u'message'
3746 3749 TYPE_MENTION = u'mention'
3747 3750 TYPE_REGISTRATION = u'registration'
3748 3751 TYPE_PULL_REQUEST = u'pull_request'
3749 3752 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3750 3753
3751 3754 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3752 3755 subject = Column('subject', Unicode(512), nullable=True)
3753 3756 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3754 3757 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3755 3758 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3756 3759 type_ = Column('type', Unicode(255))
3757 3760
3758 3761 created_by_user = relationship('User')
3759 3762 notifications_to_users = relationship('UserNotification', lazy='joined',
3760 3763 cascade="all, delete, delete-orphan")
3761 3764
3762 3765 @property
3763 3766 def recipients(self):
3764 3767 return [x.user for x in UserNotification.query()\
3765 3768 .filter(UserNotification.notification == self)\
3766 3769 .order_by(UserNotification.user_id.asc()).all()]
3767 3770
3768 3771 @classmethod
3769 3772 def create(cls, created_by, subject, body, recipients, type_=None):
3770 3773 if type_ is None:
3771 3774 type_ = Notification.TYPE_MESSAGE
3772 3775
3773 3776 notification = cls()
3774 3777 notification.created_by_user = created_by
3775 3778 notification.subject = subject
3776 3779 notification.body = body
3777 3780 notification.type_ = type_
3778 3781 notification.created_on = datetime.datetime.now()
3779 3782
3780 3783 for u in recipients:
3781 3784 assoc = UserNotification()
3782 3785 assoc.notification = notification
3783 3786
3784 3787 # if created_by is inside recipients mark his notification
3785 3788 # as read
3786 3789 if u.user_id == created_by.user_id:
3787 3790 assoc.read = True
3788 3791
3789 3792 u.notifications.append(assoc)
3790 3793 Session().add(notification)
3791 3794
3792 3795 return notification
3793 3796
3794 3797
3795 3798 class UserNotification(Base, BaseModel):
3796 3799 __tablename__ = 'user_to_notification'
3797 3800 __table_args__ = (
3798 3801 UniqueConstraint('user_id', 'notification_id'),
3799 3802 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3800 3803 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3801 3804 )
3802 3805 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3803 3806 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3804 3807 read = Column('read', Boolean, default=False)
3805 3808 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3806 3809
3807 3810 user = relationship('User', lazy="joined")
3808 3811 notification = relationship('Notification', lazy="joined",
3809 3812 order_by=lambda: Notification.created_on.desc(),)
3810 3813
3811 3814 def mark_as_read(self):
3812 3815 self.read = True
3813 3816 Session().add(self)
3814 3817
3815 3818
3816 3819 class Gist(Base, BaseModel):
3817 3820 __tablename__ = 'gists'
3818 3821 __table_args__ = (
3819 3822 Index('g_gist_access_id_idx', 'gist_access_id'),
3820 3823 Index('g_created_on_idx', 'created_on'),
3821 3824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3822 3825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3823 3826 )
3824 3827 GIST_PUBLIC = u'public'
3825 3828 GIST_PRIVATE = u'private'
3826 3829 DEFAULT_FILENAME = u'gistfile1.txt'
3827 3830
3828 3831 ACL_LEVEL_PUBLIC = u'acl_public'
3829 3832 ACL_LEVEL_PRIVATE = u'acl_private'
3830 3833
3831 3834 gist_id = Column('gist_id', Integer(), primary_key=True)
3832 3835 gist_access_id = Column('gist_access_id', Unicode(250))
3833 3836 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3834 3837 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3835 3838 gist_expires = Column('gist_expires', Float(53), nullable=False)
3836 3839 gist_type = Column('gist_type', Unicode(128), nullable=False)
3837 3840 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3838 3841 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3839 3842 acl_level = Column('acl_level', Unicode(128), nullable=True)
3840 3843
3841 3844 owner = relationship('User')
3842 3845
3843 3846 def __repr__(self):
3844 3847 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3845 3848
3846 3849 @hybrid_property
3847 3850 def description_safe(self):
3848 3851 from rhodecode.lib import helpers as h
3849 3852 return h.escape(self.gist_description)
3850 3853
3851 3854 @classmethod
3852 3855 def get_or_404(cls, id_):
3853 3856 from pyramid.httpexceptions import HTTPNotFound
3854 3857
3855 3858 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3856 3859 if not res:
3857 3860 raise HTTPNotFound()
3858 3861 return res
3859 3862
3860 3863 @classmethod
3861 3864 def get_by_access_id(cls, gist_access_id):
3862 3865 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3863 3866
3864 3867 def gist_url(self):
3865 3868 from rhodecode.model.gist import GistModel
3866 3869 return GistModel().get_url(self)
3867 3870
3868 3871 @classmethod
3869 3872 def base_path(cls):
3870 3873 """
3871 3874 Returns base path when all gists are stored
3872 3875
3873 3876 :param cls:
3874 3877 """
3875 3878 from rhodecode.model.gist import GIST_STORE_LOC
3876 3879 q = Session().query(RhodeCodeUi)\
3877 3880 .filter(RhodeCodeUi.ui_key == URL_SEP)
3878 3881 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3879 3882 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3880 3883
3881 3884 def get_api_data(self):
3882 3885 """
3883 3886 Common function for generating gist related data for API
3884 3887 """
3885 3888 gist = self
3886 3889 data = {
3887 3890 'gist_id': gist.gist_id,
3888 3891 'type': gist.gist_type,
3889 3892 'access_id': gist.gist_access_id,
3890 3893 'description': gist.gist_description,
3891 3894 'url': gist.gist_url(),
3892 3895 'expires': gist.gist_expires,
3893 3896 'created_on': gist.created_on,
3894 3897 'modified_at': gist.modified_at,
3895 3898 'content': None,
3896 3899 'acl_level': gist.acl_level,
3897 3900 }
3898 3901 return data
3899 3902
3900 3903 def __json__(self):
3901 3904 data = dict(
3902 3905 )
3903 3906 data.update(self.get_api_data())
3904 3907 return data
3905 3908 # SCM functions
3906 3909
3907 3910 def scm_instance(self, **kwargs):
3908 3911 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3909 3912 return get_vcs_instance(
3910 3913 repo_path=safe_str(full_repo_path), create=False)
3911 3914
3912 3915
3913 3916 class ExternalIdentity(Base, BaseModel):
3914 3917 __tablename__ = 'external_identities'
3915 3918 __table_args__ = (
3916 3919 Index('local_user_id_idx', 'local_user_id'),
3917 3920 Index('external_id_idx', 'external_id'),
3918 3921 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3919 3922 'mysql_charset': 'utf8'})
3920 3923
3921 3924 external_id = Column('external_id', Unicode(255), default=u'',
3922 3925 primary_key=True)
3923 3926 external_username = Column('external_username', Unicode(1024), default=u'')
3924 3927 local_user_id = Column('local_user_id', Integer(),
3925 3928 ForeignKey('users.user_id'), primary_key=True)
3926 3929 provider_name = Column('provider_name', Unicode(255), default=u'',
3927 3930 primary_key=True)
3928 3931 access_token = Column('access_token', String(1024), default=u'')
3929 3932 alt_token = Column('alt_token', String(1024), default=u'')
3930 3933 token_secret = Column('token_secret', String(1024), default=u'')
3931 3934
3932 3935 @classmethod
3933 3936 def by_external_id_and_provider(cls, external_id, provider_name,
3934 3937 local_user_id=None):
3935 3938 """
3936 3939 Returns ExternalIdentity instance based on search params
3937 3940
3938 3941 :param external_id:
3939 3942 :param provider_name:
3940 3943 :return: ExternalIdentity
3941 3944 """
3942 3945 query = cls.query()
3943 3946 query = query.filter(cls.external_id == external_id)
3944 3947 query = query.filter(cls.provider_name == provider_name)
3945 3948 if local_user_id:
3946 3949 query = query.filter(cls.local_user_id == local_user_id)
3947 3950 return query.first()
3948 3951
3949 3952 @classmethod
3950 3953 def user_by_external_id_and_provider(cls, external_id, provider_name):
3951 3954 """
3952 3955 Returns User instance based on search params
3953 3956
3954 3957 :param external_id:
3955 3958 :param provider_name:
3956 3959 :return: User
3957 3960 """
3958 3961 query = User.query()
3959 3962 query = query.filter(cls.external_id == external_id)
3960 3963 query = query.filter(cls.provider_name == provider_name)
3961 3964 query = query.filter(User.user_id == cls.local_user_id)
3962 3965 return query.first()
3963 3966
3964 3967 @classmethod
3965 3968 def by_local_user_id(cls, local_user_id):
3966 3969 """
3967 3970 Returns all tokens for user
3968 3971
3969 3972 :param local_user_id:
3970 3973 :return: ExternalIdentity
3971 3974 """
3972 3975 query = cls.query()
3973 3976 query = query.filter(cls.local_user_id == local_user_id)
3974 3977 return query
3975 3978
3976 3979
3977 3980 class Integration(Base, BaseModel):
3978 3981 __tablename__ = 'integrations'
3979 3982 __table_args__ = (
3980 3983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3981 3984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3982 3985 )
3983 3986
3984 3987 integration_id = Column('integration_id', Integer(), primary_key=True)
3985 3988 integration_type = Column('integration_type', String(255))
3986 3989 enabled = Column('enabled', Boolean(), nullable=False)
3987 3990 name = Column('name', String(255), nullable=False)
3988 3991 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3989 3992 default=False)
3990 3993
3991 3994 settings = Column(
3992 3995 'settings_json', MutationObj.as_mutable(
3993 3996 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3994 3997 repo_id = Column(
3995 3998 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3996 3999 nullable=True, unique=None, default=None)
3997 4000 repo = relationship('Repository', lazy='joined')
3998 4001
3999 4002 repo_group_id = Column(
4000 4003 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4001 4004 nullable=True, unique=None, default=None)
4002 4005 repo_group = relationship('RepoGroup', lazy='joined')
4003 4006
4004 4007 @property
4005 4008 def scope(self):
4006 4009 if self.repo:
4007 4010 return repr(self.repo)
4008 4011 if self.repo_group:
4009 4012 if self.child_repos_only:
4010 4013 return repr(self.repo_group) + ' (child repos only)'
4011 4014 else:
4012 4015 return repr(self.repo_group) + ' (recursive)'
4013 4016 if self.child_repos_only:
4014 4017 return 'root_repos'
4015 4018 return 'global'
4016 4019
4017 4020 def __repr__(self):
4018 4021 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4019 4022
4020 4023
4021 4024 class RepoReviewRuleUser(Base, BaseModel):
4022 4025 __tablename__ = 'repo_review_rules_users'
4023 4026 __table_args__ = (
4024 4027 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4025 4028 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4026 4029 )
4027 4030 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4028 4031 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4029 4032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4030 4033 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4031 4034 user = relationship('User')
4032 4035
4033 4036 def rule_data(self):
4034 4037 return {
4035 4038 'mandatory': self.mandatory
4036 4039 }
4037 4040
4038 4041
4039 4042 class RepoReviewRuleUserGroup(Base, BaseModel):
4040 4043 __tablename__ = 'repo_review_rules_users_groups'
4041 4044 __table_args__ = (
4042 4045 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4043 4046 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4044 4047 )
4045 4048 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4046 4049 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4047 4050 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4048 4051 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4049 4052 users_group = relationship('UserGroup')
4050 4053
4051 4054 def rule_data(self):
4052 4055 return {
4053 4056 'mandatory': self.mandatory
4054 4057 }
4055 4058
4056 4059
4057 4060 class RepoReviewRule(Base, BaseModel):
4058 4061 __tablename__ = 'repo_review_rules'
4059 4062 __table_args__ = (
4060 4063 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4061 4064 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4062 4065 )
4063 4066
4064 4067 repo_review_rule_id = Column(
4065 4068 'repo_review_rule_id', Integer(), primary_key=True)
4066 4069 repo_id = Column(
4067 4070 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4068 4071 repo = relationship('Repository', backref='review_rules')
4069 4072
4070 4073 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4071 4074 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4072 4075
4073 4076 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4074 4077 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4075 4078 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4076 4079 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4077 4080
4078 4081 rule_users = relationship('RepoReviewRuleUser')
4079 4082 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4080 4083
4081 4084 @hybrid_property
4082 4085 def branch_pattern(self):
4083 4086 return self._branch_pattern or '*'
4084 4087
4085 4088 def _validate_glob(self, value):
4086 4089 re.compile('^' + glob2re(value) + '$')
4087 4090
4088 4091 @branch_pattern.setter
4089 4092 def branch_pattern(self, value):
4090 4093 self._validate_glob(value)
4091 4094 self._branch_pattern = value or '*'
4092 4095
4093 4096 @hybrid_property
4094 4097 def file_pattern(self):
4095 4098 return self._file_pattern or '*'
4096 4099
4097 4100 @file_pattern.setter
4098 4101 def file_pattern(self, value):
4099 4102 self._validate_glob(value)
4100 4103 self._file_pattern = value or '*'
4101 4104
4102 4105 def matches(self, branch, files_changed):
4103 4106 """
4104 4107 Check if this review rule matches a branch/files in a pull request
4105 4108
4106 4109 :param branch: branch name for the commit
4107 4110 :param files_changed: list of file paths changed in the pull request
4108 4111 """
4109 4112
4110 4113 branch = branch or ''
4111 4114 files_changed = files_changed or []
4112 4115
4113 4116 branch_matches = True
4114 4117 if branch:
4115 4118 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4116 4119 branch_matches = bool(branch_regex.search(branch))
4117 4120
4118 4121 files_matches = True
4119 4122 if self.file_pattern != '*':
4120 4123 files_matches = False
4121 4124 file_regex = re.compile(glob2re(self.file_pattern))
4122 4125 for filename in files_changed:
4123 4126 if file_regex.search(filename):
4124 4127 files_matches = True
4125 4128 break
4126 4129
4127 4130 return branch_matches and files_matches
4128 4131
4129 4132 @property
4130 4133 def review_users(self):
4131 4134 """ Returns the users which this rule applies to """
4132 4135
4133 4136 users = collections.OrderedDict()
4134 4137
4135 4138 for rule_user in self.rule_users:
4136 4139 if rule_user.user.active:
4137 4140 if rule_user.user not in users:
4138 4141 users[rule_user.user.username] = {
4139 4142 'user': rule_user.user,
4140 4143 'source': 'user',
4141 4144 'source_data': {},
4142 4145 'data': rule_user.rule_data()
4143 4146 }
4144 4147
4145 4148 for rule_user_group in self.rule_user_groups:
4146 4149 source_data = {
4147 4150 'name': rule_user_group.users_group.users_group_name,
4148 4151 'members': len(rule_user_group.users_group.members)
4149 4152 }
4150 4153 for member in rule_user_group.users_group.members:
4151 4154 if member.user.active:
4152 4155 users[member.user.username] = {
4153 4156 'user': member.user,
4154 4157 'source': 'user_group',
4155 4158 'source_data': source_data,
4156 4159 'data': rule_user_group.rule_data()
4157 4160 }
4158 4161
4159 4162 return users
4160 4163
4161 4164 def __repr__(self):
4162 4165 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4163 4166 self.repo_review_rule_id, self.repo)
4164 4167
4165 4168
4166 4169 class DbMigrateVersion(Base, BaseModel):
4167 4170 __tablename__ = 'db_migrate_version'
4168 4171 __table_args__ = (
4169 4172 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4170 4173 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4171 4174 )
4172 4175 repository_id = Column('repository_id', String(250), primary_key=True)
4173 4176 repository_path = Column('repository_path', Text)
4174 4177 version = Column('version', Integer)
4175 4178
4176 4179
4177 4180 class DbSession(Base, BaseModel):
4178 4181 __tablename__ = 'db_session'
4179 4182 __table_args__ = (
4180 4183 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4181 4184 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4182 4185 )
4183 4186
4184 4187 def __repr__(self):
4185 4188 return '<DB:DbSession({})>'.format(self.id)
4186 4189
4187 4190 id = Column('id', Integer())
4188 4191 namespace = Column('namespace', String(255), primary_key=True)
4189 4192 accessed = Column('accessed', DateTime, nullable=False)
4190 4193 created = Column('created', DateTime, nullable=False)
4191 4194 data = Column('data', PickleType, nullable=False)
@@ -1,83 +1,84 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Owner'), lambda:base.gravatar_with_user(c.user_group.user.email), '', ''),
6 6 (_('Created on'), h.format_date(c.user_group.created_on), '', '',),
7 7
8 8 (_('Members'), len(c.group_members_obj),'', [x for x in c.group_members_obj]),
9 9 (_('Automatic member sync'), 'Yes' if c.user_group.group_data.get('extern_type') else 'No', '', '',),
10 10
11 11 (_('Assigned to repositories'), len(c.group_to_repos),'', [x for x in c.group_to_repos]),
12 12 (_('Assigned to repo groups'), len(c.group_to_repo_groups), '', [x for x in c.group_to_repo_groups]),
13 13
14 (_('Assigned to review rules'), len(c.group_to_review_rules), '', [x for x in c.group_to_review_rules]),
14 15 ]
15 16 %>
16 17
17 18 <div class="panel panel-default">
18 19 <div class="panel-heading">
19 20 <h3 class="panel-title">${_('User Group: %s') % c.user_group.users_group_name}</h3>
20 21 </div>
21 22 <div class="panel-body">
22 23 ${base.dt_info_panel(elems)}
23 24 </div>
24 25
25 26 </div>
26 27
27 28 <div class="panel panel-default">
28 29 <div class="panel-heading">
29 30 <h3 class="panel-title">${_('Group members sync')}</h3>
30 31 </div>
31 32 <div class="panel-body">
32 33 <% sync_type = c.user_group.group_data.get('extern_type') %>
33 34
34 35 % if sync_type:
35 36 <p>
36 37 ${_('This group is set to be automatically synchronised.')}<br/>
37 38 ${_('This group synchronization was set by')}: <strong>${sync_type}</strong>
38 39 </p>
39 40 % else:
40 41 <p>
41 42 ${_('This group is not set to be automatically synchronised')}
42 43 </p>
43 44 % endif
44 45
45 46 <div>
46 47 ${h.secure_form(h.url('edit_user_group_advanced_sync', user_group_id=c.user_group.users_group_id), method='post')}
47 48 <div class="field">
48 49 <button class="btn btn-default" type="submit">
49 50 %if sync_type:
50 51 ${_('Disable synchronization')}
51 52 %else:
52 53 ${_('Enable synchronization')}
53 54 %endif
54 55 </button>
55 56 </div>
56 57 <div class="field">
57 58 <span class="help-block">
58 59 ${_('Users will be added or removed from this group when they authenticate with RhodeCode system, based on LDAP group membership. '
59 60 'This requires `LDAP+User group` authentication plugin to be configured and enabled. (EE only feature)')}
60 61 </span>
61 62 </div>
62 63 ${h.end_form()}
63 64 </div>
64 65
65 66 </div>
66 67 </div>
67 68
68 69
69 70 <div class="panel panel-danger">
70 71 <div class="panel-heading">
71 72 <h3 class="panel-title">${_('Delete User Group')}</h3>
72 73 </div>
73 74 <div class="panel-body">
74 75 ${h.secure_form(h.url('delete_users_group', user_group_id=c.user_group.users_group_id),method='delete')}
75 76 ${h.hidden('force', 1)}
76 77 <button class="btn btn-small btn-danger" type="submit"
77 78 onclick="return confirm('${_('Confirm to delete user group `%(ugroup)s` with all permission assignments') % {'ugroup': c.user_group.users_group_name}}');">
78 79 <i class="icon-remove-sign"></i>
79 80 ${_('Delete This User Group')}
80 81 </button>
81 82 ${h.end_form()}
82 83 </div>
83 84 </div>
@@ -1,159 +1,161 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 6 (_('Source of Record'), c.user.extern_type, '', ''),
7 7
8 8 (_('Last login'), c.user.last_login or '-', '', ''),
9 9 (_('Last activity'), c.user.last_activity, '', ''),
10 10
11 11 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
12 12 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 13 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 14
15 15 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
16 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
17
16 18 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
17 19 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
18 20 ]
19 21 %>
20 22
21 23 <div class="panel panel-default">
22 24 <div class="panel-heading">
23 25 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
24 26 </div>
25 27 <div class="panel-body">
26 28 ${base.dt_info_panel(elems)}
27 29 </div>
28 30 </div>
29 31
30 32 <div class="panel panel-default">
31 33 <div class="panel-heading">
32 34 <h3 class="panel-title">${_('Force Password Reset')}</h3>
33 35 </div>
34 36 <div class="panel-body">
35 37 ${h.secure_form(h.url('force_password_reset_user', user_id=c.user.user_id), method='post')}
36 38 <div class="field">
37 39 <button class="btn btn-default" type="submit">
38 40 <i class="icon-lock"></i>
39 41 %if c.user.user_data.get('force_password_change'):
40 42 ${_('Disable forced password reset')}
41 43 %else:
42 44 ${_('Enable forced password reset')}
43 45 %endif
44 46 </button>
45 47 </div>
46 48 <div class="field">
47 49 <span class="help-block">
48 50 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
49 51 </span>
50 52 </div>
51 53 ${h.end_form()}
52 54 </div>
53 55 </div>
54 56
55 57 <div class="panel panel-default">
56 58 <div class="panel-heading">
57 59 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
58 60 </div>
59 61 <div class="panel-body">
60 62 ${h.secure_form(h.url('create_personal_repo_group', user_id=c.user.user_id), method='post')}
61 63
62 64 %if c.personal_repo_group:
63 65 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
64 66 %else:
65 67 <div class="panel-body-title-text">
66 68 ${_('This user currently does not have a personal repository group')}
67 69 <br/>
68 70 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
69 71 </div>
70 72 %endif
71 73 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
72 74 <i class="icon-folder-close"></i>
73 75 ${_('Create personal repository group')}
74 76 </button>
75 77 ${h.end_form()}
76 78 </div>
77 79 </div>
78 80
79 81
80 82 <div class="panel panel-danger">
81 83 <div class="panel-heading">
82 84 <h3 class="panel-title">${_('Delete User')}</h3>
83 85 </div>
84 86 <div class="panel-body">
85 87 ${h.secure_form(h.url('delete_user', user_id=c.user.user_id), method='delete')}
86 88
87 89 <table class="display">
88 90 <tr>
89 91 <td>
90 92 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
91 93 </td>
92 94 <td>
93 95 %if len(c.user.repositories) > 0:
94 96 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked"/> <label for="user_repos_1">${_('Detach repositories')}</label>
95 97 %endif
96 98 </td>
97 99 <td>
98 100 %if len(c.user.repositories) > 0:
99 101 <input type="radio" id="user_repos_2" name="user_repos" value="delete" /> <label for="user_repos_2">${_('Delete repositories')}</label>
100 102 %endif
101 103 </td>
102 104 </tr>
103 105
104 106 <tr>
105 107 <td>
106 108 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
107 109 </td>
108 110 <td>
109 111 %if len(c.user.repository_groups) > 0:
110 112 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked"/> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
111 113 %endif
112 114 </td>
113 115 <td>
114 116 %if len(c.user.repository_groups) > 0:
115 117 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" /> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
116 118 %endif
117 119 </td>
118 120 </tr>
119 121
120 122 <tr>
121 123 <td>
122 124 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
123 125 </td>
124 126 <td>
125 127 %if len(c.user.user_groups) > 0:
126 128 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked"/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
127 129 %endif
128 130 </td>
129 131 <td>
130 132 %if len(c.user.user_groups) > 0:
131 133 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" /> <label for="user_user_groups_2">${_('Delete repositories')}</label>
132 134 %endif
133 135 </td>
134 136 </tr>
135 137 </table>
136 138 <div style="margin: 0 0 20px 0" class="fake-space"></div>
137 139
138 140 <div class="field">
139 141 <button class="btn btn-small btn-danger" type="submit"
140 142 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
141 143 ${"disabled" if not c.can_delete_user else ""}>
142 144 ${_('Delete this user')}
143 145 </button>
144 146 </div>
145 147 % if c.can_delete_user_message:
146 148 <p class="help-block pre-formatting">${c.can_delete_user_message}</p>
147 149 % endif
148 150
149 151 <div class="field">
150 152 <span class="help-block">
151 153 %if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
152 154 <p class="help-block">${_("When selecting the detach option, the depending objects owned by this user will be assigned to the `%s` super admin in the system. The delete option will delete the user's repositories!") % (c.first_admin.full_name)}</p>
153 155 %endif
154 156 </span>
155 157 </div>
156 158
157 159 ${h.end_form()}
158 160 </div>
159 161 </div>
General Comments 0
You need to be logged in to leave comments. Login now