##// END OF EJS Templates
fix(caching): fixed problems with Cache query for users....
super-admin -
r5365:ae8a165b default
parent child Browse files
Show More
@@ -1,811 +1,818 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import datetime
21 21 import string
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import peppercorn
26 26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27 27
28 28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 29 from rhodecode import forms
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib import ext_json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired,
35 35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 36 from rhodecode.lib.channelstream import (
37 37 channelstream_request, ChannelstreamException)
38 38 from rhodecode.lib.hash_utils import md5_safe
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 IntegrityError, or_, in_filter_generator,
43 IntegrityError, or_, in_filter_generator, select,
44 44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.user_group import UserGroupModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @NotAnonymous()
70 70 def my_account_profile(self):
71 71 c = self.load_default_context()
72 72 c.active = 'profile'
73 73 c.extern_type = c.user.extern_type
74 74 return self._get_template_context(c)
75 75
76 76 @LoginRequired()
77 77 @NotAnonymous()
78 78 def my_account_edit(self):
79 79 c = self.load_default_context()
80 80 c.active = 'profile_edit'
81 81 c.extern_type = c.user.extern_type
82 82 c.extern_name = c.user.extern_name
83 83
84 84 schema = user_schema.UserProfileSchema().bind(
85 85 username=c.user.username, user_emails=c.user.emails)
86 86 appstruct = {
87 87 'username': c.user.username,
88 88 'email': c.user.email,
89 89 'firstname': c.user.firstname,
90 90 'lastname': c.user.lastname,
91 91 'description': c.user.description,
92 92 }
93 93 c.form = forms.RcForm(
94 94 schema, appstruct=appstruct,
95 95 action=h.route_path('my_account_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 return self._get_template_context(c)
99 99
100 100 @LoginRequired()
101 101 @NotAnonymous()
102 102 @CSRFRequired()
103 103 def my_account_update(self):
104 104 _ = self.request.translate
105 105 c = self.load_default_context()
106 106 c.active = 'profile_edit'
107 107 c.perm_user = c.auth_user
108 108 c.extern_type = c.user.extern_type
109 109 c.extern_name = c.user.extern_name
110 110
111 111 schema = user_schema.UserProfileSchema().bind(
112 112 username=c.user.username, user_emails=c.user.emails)
113 113 form = forms.RcForm(
114 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 115
116 116 controls = list(self.request.POST.items())
117 117 try:
118 118 valid_data = form.validate(controls)
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 if c.extern_type != "rhodecode":
122 122 # forbid updating username for external accounts
123 123 skip_attrs.append('username')
124 124 old_email = c.user.email
125 125 UserModel().update_user(
126 126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 127 **valid_data)
128 128 if old_email != valid_data['email']:
129 129 old = UserEmailMap.query() \
130 130 .filter(UserEmailMap.user == c.user)\
131 131 .filter(UserEmailMap.email == valid_data['email'])\
132 132 .first()
133 133 old.email = old_email
134 134 h.flash(_('Your account was updated successfully'), category='success')
135 135 Session().commit()
136 136 except forms.ValidationFailure as e:
137 137 c.form = e
138 138 return self._get_template_context(c)
139 139
140 140 except Exception:
141 141 log.exception("Exception updating user")
142 142 h.flash(_('Error occurred during update of user'),
143 143 category='error')
144 144 raise HTTPFound(h.route_path('my_account_profile'))
145 145
146 146 @LoginRequired()
147 147 @NotAnonymous()
148 148 def my_account_password(self):
149 149 c = self.load_default_context()
150 150 c.active = 'password'
151 151 c.extern_type = c.user.extern_type
152 152
153 153 schema = user_schema.ChangePasswordSchema().bind(
154 154 username=c.user.username)
155 155
156 156 form = forms.Form(
157 157 schema,
158 158 action=h.route_path('my_account_password_update'),
159 159 buttons=(forms.buttons.save, forms.buttons.reset))
160 160
161 161 c.form = form
162 162 return self._get_template_context(c)
163 163
164 164 @LoginRequired()
165 165 @NotAnonymous()
166 166 @CSRFRequired()
167 167 def my_account_password_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'password'
171 171 c.extern_type = c.user.extern_type
172 172
173 173 schema = user_schema.ChangePasswordSchema().bind(
174 174 username=c.user.username)
175 175
176 176 form = forms.Form(
177 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 178
179 179 if c.extern_type != 'rhodecode':
180 180 raise HTTPFound(self.request.route_path('my_account_password'))
181 181
182 182 controls = list(self.request.POST.items())
183 183 try:
184 184 valid_data = form.validate(controls)
185 185 UserModel().update_user(c.user.user_id, **valid_data)
186 186 c.user.update_userdata(force_password_change=False)
187 187 Session().commit()
188 188 except forms.ValidationFailure as e:
189 189 c.form = e
190 190 return self._get_template_context(c)
191 191
192 192 except Exception:
193 193 log.exception("Exception updating password")
194 194 h.flash(_('Error occurred during update of user password'),
195 195 category='error')
196 196 else:
197 197 instance = c.auth_user.get_instance()
198 198 self.session.setdefault('rhodecode_user', {}).update(
199 199 {'password': md5_safe(instance.password)})
200 200 self.session.save()
201 201 h.flash(_("Successfully updated password"), category='success')
202 202
203 203 raise HTTPFound(self.request.route_path('my_account_password'))
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 def my_account_2fa(self):
208 208 _ = self.request.translate
209 209 c = self.load_default_context()
210 210 c.active = '2fa'
211 211 from rhodecode.model.settings import SettingsModel
212 212 user_instance = self._rhodecode_db_user
213 213 locked_by_admin = user_instance.has_forced_2fa
214 214 c.state_of_2fa = user_instance.has_enabled_2fa
215 215 c.locked_2fa = str2bool(locked_by_admin)
216 216 return self._get_template_context(c)
217 217
218 218 @LoginRequired()
219 219 @NotAnonymous()
220 220 @CSRFRequired()
221 221 def my_account_2fa_configure(self):
222 222 state = self.request.POST.get('state')
223 223 self._rhodecode_db_user.has_enabled_2fa = state
224 224 return {'state_of_2fa': state}
225 225
226 226 @LoginRequired()
227 227 @NotAnonymous()
228 228 @CSRFRequired()
229 229 def my_account_2fa_regenerate_recovery_codes(self):
230 230 return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()}
231 231
232 232 @LoginRequired()
233 233 @NotAnonymous()
234 234 def my_account_auth_tokens(self):
235 235 _ = self.request.translate
236 236
237 237 c = self.load_default_context()
238 238 c.active = 'auth_tokens'
239 239 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
240 240 c.role_values = [
241 241 (x, AuthTokenModel.cls._get_role_name(x))
242 242 for x in AuthTokenModel.cls.ROLES]
243 243 c.role_options = [(c.role_values, _("Role"))]
244 244 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
245 245 c.user.user_id, show_expired=True)
246 246 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
247 247 return self._get_template_context(c)
248 248
249 249 @LoginRequired()
250 250 @NotAnonymous()
251 251 @CSRFRequired()
252 252 def my_account_auth_tokens_view(self):
253 253 _ = self.request.translate
254 254 c = self.load_default_context()
255 255
256 256 auth_token_id = self.request.POST.get('auth_token_id')
257 257
258 258 if auth_token_id:
259 259 token = UserApiKeys.get_or_404(auth_token_id)
260 260 if token.user.user_id != c.user.user_id:
261 261 raise HTTPNotFound()
262 262
263 263 return {
264 264 'auth_token': token.api_key
265 265 }
266 266
267 267 def maybe_attach_token_scope(self, token):
268 268 # implemented in EE edition
269 269 pass
270 270
271 271 @LoginRequired()
272 272 @NotAnonymous()
273 273 @CSRFRequired()
274 274 def my_account_auth_tokens_add(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 278 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
279 279 description = self.request.POST.get('description')
280 280 role = self.request.POST.get('role')
281 281
282 282 token = UserModel().add_auth_token(
283 283 user=c.user.user_id,
284 284 lifetime_minutes=lifetime, role=role, description=description,
285 285 scope_callback=self.maybe_attach_token_scope)
286 286 token_data = token.get_api_data()
287 287
288 288 audit_logger.store_web(
289 289 'user.edit.token.add', action_data={
290 290 'data': {'token': token_data, 'user': 'self'}},
291 291 user=self._rhodecode_user, )
292 292 Session().commit()
293 293
294 294 h.flash(_("Auth token successfully created"), category='success')
295 295 return HTTPFound(h.route_path('my_account_auth_tokens'))
296 296
297 297 @LoginRequired()
298 298 @NotAnonymous()
299 299 @CSRFRequired()
300 300 def my_account_auth_tokens_delete(self):
301 301 _ = self.request.translate
302 302 c = self.load_default_context()
303 303
304 304 del_auth_token = self.request.POST.get('del_auth_token')
305 305
306 306 if del_auth_token:
307 307 token = UserApiKeys.get_or_404(del_auth_token)
308 308 token_data = token.get_api_data()
309 309
310 310 AuthTokenModel().delete(del_auth_token, c.user.user_id)
311 311 audit_logger.store_web(
312 312 'user.edit.token.delete', action_data={
313 313 'data': {'token': token_data, 'user': 'self'}},
314 314 user=self._rhodecode_user,)
315 315 Session().commit()
316 316 h.flash(_("Auth token successfully deleted"), category='success')
317 317
318 318 return HTTPFound(h.route_path('my_account_auth_tokens'))
319 319
320 320 @LoginRequired()
321 321 @NotAnonymous()
322 322 def my_account_emails(self):
323 323 _ = self.request.translate
324 324
325 325 c = self.load_default_context()
326 326 c.active = 'emails'
327 327
328 328 c.user_email_map = UserEmailMap.query()\
329 329 .filter(UserEmailMap.user == c.user).all()
330 330
331 331 schema = user_schema.AddEmailSchema().bind(
332 332 username=c.user.username, user_emails=c.user.emails)
333 333
334 334 form = forms.RcForm(schema,
335 335 action=h.route_path('my_account_emails_add'),
336 336 buttons=(forms.buttons.save, forms.buttons.reset))
337 337
338 338 c.form = form
339 339 return self._get_template_context(c)
340 340
341 341 @LoginRequired()
342 342 @NotAnonymous()
343 343 @CSRFRequired()
344 344 def my_account_emails_add(self):
345 345 _ = self.request.translate
346 346 c = self.load_default_context()
347 347 c.active = 'emails'
348 348
349 349 schema = user_schema.AddEmailSchema().bind(
350 350 username=c.user.username, user_emails=c.user.emails)
351 351
352 352 form = forms.RcForm(
353 353 schema, action=h.route_path('my_account_emails_add'),
354 354 buttons=(forms.buttons.save, forms.buttons.reset))
355 355
356 356 controls = list(self.request.POST.items())
357 357 try:
358 358 valid_data = form.validate(controls)
359 359 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
360 360 audit_logger.store_web(
361 361 'user.edit.email.add', action_data={
362 362 'data': {'email': valid_data['email'], 'user': 'self'}},
363 363 user=self._rhodecode_user,)
364 364 Session().commit()
365 365 except formencode.Invalid as error:
366 366 h.flash(h.escape(error.error_dict['email']), category='error')
367 367 except forms.ValidationFailure as e:
368 368 c.user_email_map = UserEmailMap.query() \
369 369 .filter(UserEmailMap.user == c.user).all()
370 370 c.form = e
371 371 return self._get_template_context(c)
372 372 except Exception:
373 373 log.exception("Exception adding email")
374 374 h.flash(_('Error occurred during adding email'),
375 375 category='error')
376 376 else:
377 377 h.flash(_("Successfully added email"), category='success')
378 378
379 379 raise HTTPFound(self.request.route_path('my_account_emails'))
380 380
381 381 @LoginRequired()
382 382 @NotAnonymous()
383 383 @CSRFRequired()
384 384 def my_account_emails_delete(self):
385 385 _ = self.request.translate
386 386 c = self.load_default_context()
387 387
388 388 del_email_id = self.request.POST.get('del_email_id')
389 389 if del_email_id:
390 390 email = UserEmailMap.get_or_404(del_email_id).email
391 391 UserModel().delete_extra_email(c.user.user_id, del_email_id)
392 392 audit_logger.store_web(
393 393 'user.edit.email.delete', action_data={
394 394 'data': {'email': email, 'user': 'self'}},
395 395 user=self._rhodecode_user,)
396 396 Session().commit()
397 397 h.flash(_("Email successfully deleted"),
398 398 category='success')
399 399 return HTTPFound(h.route_path('my_account_emails'))
400 400
401 401 @LoginRequired()
402 402 @NotAnonymous()
403 403 @CSRFRequired()
404 404 def my_account_notifications_test_channelstream(self):
405 405 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
406 406 self._rhodecode_user.username, datetime.datetime.now())
407 407 payload = {
408 408 # 'channel': 'broadcast',
409 409 'type': 'message',
410 410 'timestamp': datetime.datetime.utcnow(),
411 411 'user': 'system',
412 412 'pm_users': [self._rhodecode_user.username],
413 413 'message': {
414 414 'message': message,
415 415 'level': 'info',
416 416 'topic': '/notifications'
417 417 }
418 418 }
419 419
420 420 registry = self.request.registry
421 421 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
422 422 channelstream_config = rhodecode_plugins.get('channelstream', {})
423 423
424 424 try:
425 425 channelstream_request(channelstream_config, [payload], '/message')
426 426 except ChannelstreamException as e:
427 427 log.exception('Failed to send channelstream data')
428 428 return {"response": f'ERROR: {e.__class__.__name__}'}
429 429 return {"response": 'Channelstream data sent. '
430 430 'You should see a new live message now.'}
431 431
432 432 def _load_my_repos_data(self, watched=False):
433 433
434 434 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
435 435
436 436 if watched:
437 437 # repos user watch
438 438 repo_list = Session().query(
439 439 Repository
440 440 ) \
441 441 .join(
442 442 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
443 443 ) \
444 444 .filter(
445 445 UserFollowing.user_id == self._rhodecode_user.user_id
446 446 ) \
447 447 .filter(or_(
448 448 # generate multiple IN to fix limitation problems
449 449 *in_filter_generator(Repository.repo_id, allowed_ids))
450 450 ) \
451 451 .order_by(Repository.repo_name) \
452 452 .all()
453 453
454 454 else:
455 455 # repos user is owner of
456 456 repo_list = Session().query(
457 457 Repository
458 458 ) \
459 459 .filter(
460 460 Repository.user_id == self._rhodecode_user.user_id
461 461 ) \
462 462 .filter(or_(
463 463 # generate multiple IN to fix limitation problems
464 464 *in_filter_generator(Repository.repo_id, allowed_ids))
465 465 ) \
466 466 .order_by(Repository.repo_name) \
467 467 .all()
468 468
469 469 _render = self.request.get_partial_renderer(
470 470 'rhodecode:templates/data_table/_dt_elements.mako')
471 471
472 472 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
473 473 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
474 474 short_name=False, admin=False)
475 475
476 476 repos_data = []
477 477 for repo in repo_list:
478 478 row = {
479 479 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
480 480 repo.private, repo.archived, repo.fork),
481 481 "name_raw": repo.repo_name.lower(),
482 482 }
483 483
484 484 repos_data.append(row)
485 485
486 486 # json used to render the grid
487 487 return ext_json.str_json(repos_data)
488 488
489 489 @LoginRequired()
490 490 @NotAnonymous()
491 491 def my_account_repos(self):
492 492 c = self.load_default_context()
493 493 c.active = 'repos'
494 494
495 495 # json used to render the grid
496 496 c.data = self._load_my_repos_data()
497 497 return self._get_template_context(c)
498 498
499 499 @LoginRequired()
500 500 @NotAnonymous()
501 501 def my_account_watched(self):
502 502 c = self.load_default_context()
503 503 c.active = 'watched'
504 504
505 505 # json used to render the grid
506 506 c.data = self._load_my_repos_data(watched=True)
507 507 return self._get_template_context(c)
508 508
509 509 @LoginRequired()
510 510 @NotAnonymous()
511 511 def my_account_bookmarks(self):
512 512 c = self.load_default_context()
513 513 c.active = 'bookmarks'
514 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
515 self._rhodecode_db_user.user_id, cache=False)
514
515 user_bookmarks = \
516 select(UserBookmark, Repository, RepoGroup) \
517 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
518 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
519 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
520 .order_by(UserBookmark.position.asc())
521
522 c.user_bookmark_items = Session().execute(user_bookmarks).all()
516 523 return self._get_template_context(c)
517 524
518 525 def _process_bookmark_entry(self, entry, user_id):
519 526 position = safe_int(entry.get('position'))
520 527 cur_position = safe_int(entry.get('cur_position'))
521 528 if position is None:
522 529 return
523 530
524 531 # check if this is an existing entry
525 532 is_new = False
526 533 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
527 534
528 535 if db_entry and str2bool(entry.get('remove')):
529 536 log.debug('Marked bookmark %s for deletion', db_entry)
530 537 Session().delete(db_entry)
531 538 return
532 539
533 540 if not db_entry:
534 541 # new
535 542 db_entry = UserBookmark()
536 543 is_new = True
537 544
538 545 should_save = False
539 546 default_redirect_url = ''
540 547
541 548 # save repo
542 549 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
543 550 repo = Repository.get(entry['bookmark_repo'])
544 551 perm_check = HasRepoPermissionAny(
545 552 'repository.read', 'repository.write', 'repository.admin')
546 553 if repo and perm_check(repo_name=repo.repo_name):
547 554 db_entry.repository = repo
548 555 should_save = True
549 556 default_redirect_url = '${repo_url}'
550 557 # save repo group
551 558 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
552 559 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
553 560 perm_check = HasRepoGroupPermissionAny(
554 561 'group.read', 'group.write', 'group.admin')
555 562
556 563 if repo_group and perm_check(group_name=repo_group.group_name):
557 564 db_entry.repository_group = repo_group
558 565 should_save = True
559 566 default_redirect_url = '${repo_group_url}'
560 567 # save generic info
561 568 elif entry.get('title') and entry.get('redirect_url'):
562 569 should_save = True
563 570
564 571 if should_save:
565 572 # mark user and position
566 573 db_entry.user_id = user_id
567 574 db_entry.position = position
568 575 db_entry.title = entry.get('title')
569 576 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
570 577 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
571 578
572 579 Session().add(db_entry)
573 580
574 581 @LoginRequired()
575 582 @NotAnonymous()
576 583 @CSRFRequired()
577 584 def my_account_bookmarks_update(self):
578 585 _ = self.request.translate
579 586 c = self.load_default_context()
580 587 c.active = 'bookmarks'
581 588
582 589 controls = peppercorn.parse(self.request.POST.items())
583 590 user_id = c.user.user_id
584 591
585 592 # validate positions
586 593 positions = {}
587 594 for entry in controls.get('bookmarks', []):
588 595 position = safe_int(entry['position'])
589 596 if position is None:
590 597 continue
591 598
592 599 if position in positions:
593 600 h.flash(_("Position {} is defined twice. "
594 601 "Please correct this error.").format(position), category='error')
595 602 return HTTPFound(h.route_path('my_account_bookmarks'))
596 603
597 604 entry['position'] = position
598 605 entry['cur_position'] = safe_int(entry.get('cur_position'))
599 606 positions[position] = entry
600 607
601 608 try:
602 609 for entry in positions.values():
603 610 self._process_bookmark_entry(entry, user_id)
604 611
605 612 Session().commit()
606 613 h.flash(_("Update Bookmarks"), category='success')
607 614 except IntegrityError:
608 615 h.flash(_("Failed to update bookmarks. "
609 616 "Make sure an unique position is used."), category='error')
610 617
611 618 return HTTPFound(h.route_path('my_account_bookmarks'))
612 619
613 620 @LoginRequired()
614 621 @NotAnonymous()
615 622 def my_account_goto_bookmark(self):
616 623
617 624 bookmark_id = self.request.matchdict['bookmark_id']
618 625 user_bookmark = UserBookmark().query()\
619 626 .filter(UserBookmark.user_id == self.request.user.user_id) \
620 627 .filter(UserBookmark.position == bookmark_id).scalar()
621 628
622 629 redirect_url = h.route_path('my_account_bookmarks')
623 630 if not user_bookmark:
624 631 raise HTTPFound(redirect_url)
625 632
626 633 # repository set
627 634 if user_bookmark.repository:
628 635 repo_name = user_bookmark.repository.repo_name
629 636 base_redirect_url = h.route_path(
630 637 'repo_summary', repo_name=repo_name)
631 638 if user_bookmark.redirect_url and \
632 639 '${repo_url}' in user_bookmark.redirect_url:
633 640 redirect_url = string.Template(user_bookmark.redirect_url)\
634 641 .safe_substitute({'repo_url': base_redirect_url})
635 642 else:
636 643 redirect_url = base_redirect_url
637 644 # repository group set
638 645 elif user_bookmark.repository_group:
639 646 repo_group_name = user_bookmark.repository_group.group_name
640 647 base_redirect_url = h.route_path(
641 648 'repo_group_home', repo_group_name=repo_group_name)
642 649 if user_bookmark.redirect_url and \
643 650 '${repo_group_url}' in user_bookmark.redirect_url:
644 651 redirect_url = string.Template(user_bookmark.redirect_url)\
645 652 .safe_substitute({'repo_group_url': base_redirect_url})
646 653 else:
647 654 redirect_url = base_redirect_url
648 655 # custom URL set
649 656 elif user_bookmark.redirect_url:
650 657 server_url = h.route_url('home').rstrip('/')
651 658 redirect_url = string.Template(user_bookmark.redirect_url) \
652 659 .safe_substitute({'server_url': server_url})
653 660
654 661 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
655 662 raise HTTPFound(redirect_url)
656 663
657 664 @LoginRequired()
658 665 @NotAnonymous()
659 666 def my_account_perms(self):
660 667 c = self.load_default_context()
661 668 c.active = 'perms'
662 669
663 670 c.perm_user = c.auth_user
664 671 return self._get_template_context(c)
665 672
666 673 @LoginRequired()
667 674 @NotAnonymous()
668 675 def my_notifications(self):
669 676 c = self.load_default_context()
670 677 c.active = 'notifications'
671 678
672 679 return self._get_template_context(c)
673 680
674 681 @LoginRequired()
675 682 @NotAnonymous()
676 683 @CSRFRequired()
677 684 def my_notifications_toggle_visibility(self):
678 685 user = self._rhodecode_db_user
679 686 new_status = not user.user_data.get('notification_status', True)
680 687 user.update_userdata(notification_status=new_status)
681 688 Session().commit()
682 689 return user.user_data['notification_status']
683 690
684 691 def _get_pull_requests_list(self, statuses, filter_type=None):
685 692 draw, start, limit = self._extract_chunk(self.request)
686 693 search_q, order_by, order_dir = self._extract_ordering(self.request)
687 694
688 695 _render = self.request.get_partial_renderer(
689 696 'rhodecode:templates/data_table/_dt_elements.mako')
690 697
691 698 if filter_type == 'awaiting_my_review':
692 699 pull_requests = PullRequestModel().get_im_participating_in_for_review(
693 700 user_id=self._rhodecode_user.user_id,
694 701 statuses=statuses, query=search_q,
695 702 offset=start, length=limit, order_by=order_by,
696 703 order_dir=order_dir)
697 704
698 705 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
699 706 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
700 707 else:
701 708 pull_requests = PullRequestModel().get_im_participating_in(
702 709 user_id=self._rhodecode_user.user_id,
703 710 statuses=statuses, query=search_q,
704 711 offset=start, length=limit, order_by=order_by,
705 712 order_dir=order_dir)
706 713
707 714 pull_requests_total_count = PullRequestModel().count_im_participating_in(
708 715 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
709 716
710 717 data = []
711 718 comments_model = CommentsModel()
712 719 for pr in pull_requests:
713 720 repo_id = pr.target_repo_id
714 721 comments_count = comments_model.get_all_comments(
715 722 repo_id, pull_request=pr, include_drafts=False, count_only=True)
716 723 owned = pr.user_id == self._rhodecode_user.user_id
717 724
718 725 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
719 726 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
720 727 if review_statuses and review_statuses[4]:
721 728 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
722 729 my_review_status = statuses[0][1].status
723 730
724 731 data.append({
725 732 'target_repo': _render('pullrequest_target_repo',
726 733 pr.target_repo.repo_name),
727 734 'name': _render('pullrequest_name',
728 735 pr.pull_request_id, pr.pull_request_state,
729 736 pr.work_in_progress, pr.target_repo.repo_name,
730 737 short=True),
731 738 'name_raw': pr.pull_request_id,
732 739 'status': _render('pullrequest_status',
733 740 pr.calculated_review_status()),
734 741 'my_status': _render('pullrequest_status',
735 742 my_review_status),
736 743 'title': _render('pullrequest_title', pr.title, pr.description),
737 744 'pr_flow': _render('pullrequest_commit_flow', pr),
738 745 'description': h.escape(pr.description),
739 746 'updated_on': _render('pullrequest_updated_on',
740 747 h.datetime_to_time(pr.updated_on),
741 748 pr.versions_count),
742 749 'updated_on_raw': h.datetime_to_time(pr.updated_on),
743 750 'created_on': _render('pullrequest_updated_on',
744 751 h.datetime_to_time(pr.created_on)),
745 752 'created_on_raw': h.datetime_to_time(pr.created_on),
746 753 'state': pr.pull_request_state,
747 754 'author': _render('pullrequest_author',
748 755 pr.author.full_contact, ),
749 756 'author_raw': pr.author.full_name,
750 757 'comments': _render('pullrequest_comments', comments_count),
751 758 'comments_raw': comments_count,
752 759 'closed': pr.is_closed(),
753 760 'owned': owned
754 761 })
755 762
756 763 # json used to render the grid
757 764 data = ({
758 765 'draw': draw,
759 766 'data': data,
760 767 'recordsTotal': pull_requests_total_count,
761 768 'recordsFiltered': pull_requests_total_count,
762 769 })
763 770 return data
764 771
765 772 @LoginRequired()
766 773 @NotAnonymous()
767 774 def my_account_pullrequests(self):
768 775 c = self.load_default_context()
769 776 c.active = 'pullrequests'
770 777 req_get = self.request.GET
771 778
772 779 c.closed = str2bool(req_get.get('closed'))
773 780 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
774 781
775 782 c.selected_filter = 'all'
776 783 if c.closed:
777 784 c.selected_filter = 'all_closed'
778 785 if c.awaiting_my_review:
779 786 c.selected_filter = 'awaiting_my_review'
780 787
781 788 return self._get_template_context(c)
782 789
783 790 @LoginRequired()
784 791 @NotAnonymous()
785 792 def my_account_pullrequests_data(self):
786 793 self.load_default_context()
787 794 req_get = self.request.GET
788 795
789 796 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
790 797 closed = str2bool(req_get.get('closed'))
791 798
792 799 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
793 800 if closed:
794 801 statuses += [PullRequest.STATUS_CLOSED]
795 802
796 803 filter_type = \
797 804 'awaiting_my_review' if awaiting_my_review \
798 805 else None
799 806
800 807 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
801 808 return data
802 809
803 810 @LoginRequired()
804 811 @NotAnonymous()
805 812 def my_account_user_group_membership(self):
806 813 c = self.load_default_context()
807 814 c.active = 'user_group_membership'
808 815 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
809 816 for group in self._rhodecode_db_user.group_member]
810 817 c.user_groups = ext_json.str_json(groups)
811 818 return self._get_template_context(c)
@@ -1,5981 +1,5986 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 36 import pyotp
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 39 true, false, null, union_all,
40 40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 42 Text, Float, PickleType, BigInteger)
43 43 from sqlalchemy.sql.expression import case
44 44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 45 from sqlalchemy.orm import (
46 46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 47 from sqlalchemy.ext.declarative import declared_attr
48 48 from sqlalchemy.ext.hybrid import hybrid_property
49 49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 50 from sqlalchemy.dialects.mysql import LONGTEXT
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52 from pyramid.threadlocal import get_current_request
53 53 from webhelpers2.text import remove_formatting
54 54
55 55 from rhodecode import ConfigGet
56 56 from rhodecode.lib.str_utils import safe_bytes
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import (
60 60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 61 from rhodecode.lib.utils2 import (
62 62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 65 from rhodecode.lib.jsonalchemy import (
66 66 MutationObj, MutationList, JsonType, JsonRaw)
67 67 from rhodecode.lib.hash_utils import sha1
68 68 from rhodecode.lib import ext_json
69 69 from rhodecode.lib import enc_utils
70 70 from rhodecode.lib.ext_json import json, str_json
71 71 from rhodecode.lib.caching_query import FromCache
72 72 from rhodecode.lib.exceptions import (
73 73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 74 from rhodecode.model.meta import Base, Session
75 75
76 76 URL_SEP = '/'
77 77 log = logging.getLogger(__name__)
78 78
79 79 # =============================================================================
80 80 # BASE CLASSES
81 81 # =============================================================================
82 82
83 83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 84 # beaker.session.secret if first is not set.
85 85 # and initialized at environment.py
86 86 ENCRYPTION_KEY: bytes = b''
87 87
88 88 # used to sort permissions by types, '#' used here is not allowed to be in
89 89 # usernames, and it's very early in sorted string.printable table.
90 90 PERMISSION_TYPE_SORT = {
91 91 'admin': '####',
92 92 'write': '###',
93 93 'read': '##',
94 94 'none': '#',
95 95 }
96 96
97 97
98 98 def display_user_sort(obj):
99 99 """
100 100 Sort function used to sort permissions in .permissions() function of
101 101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 102 of all other resources
103 103 """
104 104
105 105 if obj.username == User.DEFAULT_USER:
106 106 return '#####'
107 107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 108 extra_sort_num = '1' # default
109 109
110 110 # NOTE(dan): inactive duplicates goes last
111 111 if getattr(obj, 'duplicate_perm', None):
112 112 extra_sort_num = '9'
113 113 return prefix + extra_sort_num + obj.username
114 114
115 115
116 116 def display_user_group_sort(obj):
117 117 """
118 118 Sort function used to sort permissions in .permissions() function of
119 119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 120 of all other resources
121 121 """
122 122
123 123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 124 return prefix + obj.users_group_name
125 125
126 126
127 127 def _hash_key(k):
128 128 return sha1_safe(k)
129 129
130 130
131 131 def in_filter_generator(qry, items, limit=500):
132 132 """
133 133 Splits IN() into multiple with OR
134 134 e.g.::
135 135 cnt = Repository.query().filter(
136 136 or_(
137 137 *in_filter_generator(Repository.repo_id, range(100000))
138 138 )).count()
139 139 """
140 140 if not items:
141 141 # empty list will cause empty query which might cause security issues
142 142 # this can lead to hidden unpleasant results
143 143 items = [-1]
144 144
145 145 parts = []
146 146 for chunk in range(0, len(items), limit):
147 147 parts.append(
148 148 qry.in_(items[chunk: chunk + limit])
149 149 )
150 150
151 151 return parts
152 152
153 153
154 154 base_table_args = {
155 155 'extend_existing': True,
156 156 'mysql_engine': 'InnoDB',
157 157 'mysql_charset': 'utf8',
158 158 'sqlite_autoincrement': True
159 159 }
160 160
161 161
162 162 class EncryptedTextValue(TypeDecorator):
163 163 """
164 164 Special column for encrypted long text data, use like::
165 165
166 166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
167 167
168 168 This column is intelligent so if value is in unencrypted form it return
169 169 unencrypted form, but on save it always encrypts
170 170 """
171 171 cache_ok = True
172 172 impl = Text
173 173
174 174 def process_bind_param(self, value, dialect):
175 175 """
176 176 Setter for storing value
177 177 """
178 178 import rhodecode
179 179 if not value:
180 180 return value
181 181
182 182 # protect against double encrypting if values is already encrypted
183 183 if value.startswith('enc$aes$') \
184 184 or value.startswith('enc$aes_hmac$') \
185 185 or value.startswith('enc2$'):
186 186 raise ValueError('value needs to be in unencrypted format, '
187 187 'ie. not starting with enc$ or enc2$')
188 188
189 189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
190 190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
191 191 return safe_str(bytes_val)
192 192
193 193 def process_result_value(self, value, dialect):
194 194 """
195 195 Getter for retrieving value
196 196 """
197 197
198 198 import rhodecode
199 199 if not value:
200 200 return value
201 201
202 202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
203 203
204 204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
205 205
206 206 return safe_str(bytes_val)
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.items():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def select(cls, custom_cls=None):
260 260 """
261 261 stmt = cls.select().where(cls.user_id==1)
262 262 # optionally
263 263 stmt = cls.select(User.user_id).where(cls.user_id==1)
264 264 result = cls.execute(stmt) | cls.scalars(stmt)
265 265 """
266 266
267 267 if custom_cls:
268 268 stmt = select(custom_cls)
269 269 else:
270 270 stmt = select(cls)
271 271 return stmt
272 272
273 273 @classmethod
274 274 def execute(cls, stmt):
275 275 return Session().execute(stmt)
276 276
277 277 @classmethod
278 278 def scalars(cls, stmt):
279 279 return Session().scalars(stmt)
280 280
281 281 @classmethod
282 282 def get(cls, id_):
283 283 if id_:
284 284 return cls.query().get(id_)
285 285
286 286 @classmethod
287 287 def get_or_404(cls, id_):
288 288 from pyramid.httpexceptions import HTTPNotFound
289 289
290 290 try:
291 291 id_ = int(id_)
292 292 except (TypeError, ValueError):
293 293 raise HTTPNotFound()
294 294
295 295 res = cls.query().get(id_)
296 296 if not res:
297 297 raise HTTPNotFound()
298 298 return res
299 299
300 300 @classmethod
301 301 def getAll(cls):
302 302 # deprecated and left for backward compatibility
303 303 return cls.get_all()
304 304
305 305 @classmethod
306 306 def get_all(cls):
307 307 return cls.query().all()
308 308
309 309 @classmethod
310 310 def delete(cls, id_):
311 311 obj = cls.query().get(id_)
312 312 Session().delete(obj)
313 313
314 314 @classmethod
315 315 def identity_cache(cls, session, attr_name, value):
316 316 exist_in_session = []
317 317 for (item_cls, pkey), instance in session.identity_map.items():
318 318 if cls == item_cls and getattr(instance, attr_name) == value:
319 319 exist_in_session.append(instance)
320 320 if exist_in_session:
321 321 if len(exist_in_session) == 1:
322 322 return exist_in_session[0]
323 323 log.exception(
324 324 'multiple objects with attr %s and '
325 325 'value %s found with same name: %r',
326 326 attr_name, value, exist_in_session)
327 327
328 328 @property
329 329 def cls_name(self):
330 330 return self.__class__.__name__
331 331
332 332 def __repr__(self):
333 333 return f'<DB:{self.cls_name}>'
334 334
335 335
336 336 class RhodeCodeSetting(Base, BaseModel):
337 337 __tablename__ = 'rhodecode_settings'
338 338 __table_args__ = (
339 339 UniqueConstraint('app_settings_name'),
340 340 base_table_args
341 341 )
342 342
343 343 SETTINGS_TYPES = {
344 344 'str': safe_str,
345 345 'int': safe_int,
346 346 'unicode': safe_str,
347 347 'bool': str2bool,
348 348 'list': functools.partial(aslist, sep=',')
349 349 }
350 350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
351 351 GLOBAL_CONF_KEY = 'app_settings'
352 352
353 353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
354 354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
355 355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
356 356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
357 357
358 358 def __init__(self, key='', val='', type='unicode'):
359 359 self.app_settings_name = key
360 360 self.app_settings_type = type
361 361 self.app_settings_value = val
362 362
363 363 @validates('_app_settings_value')
364 364 def validate_settings_value(self, key, val):
365 365 assert type(val) == str
366 366 return val
367 367
368 368 @hybrid_property
369 369 def app_settings_value(self):
370 370 v = self._app_settings_value
371 371 _type = self.app_settings_type
372 372 if _type:
373 373 _type = self.app_settings_type.split('.')[0]
374 374 # decode the encrypted value
375 375 if 'encrypted' in self.app_settings_type:
376 376 cipher = EncryptedTextValue()
377 377 v = safe_str(cipher.process_result_value(v, None))
378 378
379 379 converter = self.SETTINGS_TYPES.get(_type) or \
380 380 self.SETTINGS_TYPES['unicode']
381 381 return converter(v)
382 382
383 383 @app_settings_value.setter
384 384 def app_settings_value(self, val):
385 385 """
386 386 Setter that will always make sure we use unicode in app_settings_value
387 387
388 388 :param val:
389 389 """
390 390 val = safe_str(val)
391 391 # encode the encrypted value
392 392 if 'encrypted' in self.app_settings_type:
393 393 cipher = EncryptedTextValue()
394 394 val = safe_str(cipher.process_bind_param(val, None))
395 395 self._app_settings_value = val
396 396
397 397 @hybrid_property
398 398 def app_settings_type(self):
399 399 return self._app_settings_type
400 400
401 401 @app_settings_type.setter
402 402 def app_settings_type(self, val):
403 403 if val.split('.')[0] not in self.SETTINGS_TYPES:
404 404 raise Exception('type must be one of %s got %s'
405 405 % (self.SETTINGS_TYPES.keys(), val))
406 406 self._app_settings_type = val
407 407
408 408 @classmethod
409 409 def get_by_prefix(cls, prefix):
410 410 return RhodeCodeSetting.query()\
411 411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
412 412 .all()
413 413
414 414 def __repr__(self):
415 415 return "<%s('%s:%s[%s]')>" % (
416 416 self.cls_name,
417 417 self.app_settings_name, self.app_settings_value,
418 418 self.app_settings_type
419 419 )
420 420
421 421
422 422 class RhodeCodeUi(Base, BaseModel):
423 423 __tablename__ = 'rhodecode_ui'
424 424 __table_args__ = (
425 425 UniqueConstraint('ui_key'),
426 426 base_table_args
427 427 )
428 428 # Sync those values with vcsserver.config.hooks
429 429
430 430 HOOK_REPO_SIZE = 'changegroup.repo_size'
431 431 # HG
432 432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
433 433 HOOK_PULL = 'outgoing.pull_logger'
434 434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
435 435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
436 436 HOOK_PUSH = 'changegroup.push_logger'
437 437 HOOK_PUSH_KEY = 'pushkey.key_push'
438 438
439 439 HOOKS_BUILTIN = [
440 440 HOOK_PRE_PULL,
441 441 HOOK_PULL,
442 442 HOOK_PRE_PUSH,
443 443 HOOK_PRETX_PUSH,
444 444 HOOK_PUSH,
445 445 HOOK_PUSH_KEY,
446 446 ]
447 447
448 448 # TODO: johbo: Unify way how hooks are configured for git and hg,
449 449 # git part is currently hardcoded.
450 450
451 451 # SVN PATTERNS
452 452 SVN_BRANCH_ID = 'vcs_svn_branch'
453 453 SVN_TAG_ID = 'vcs_svn_tag'
454 454
455 455 ui_id = Column(
456 456 "ui_id", Integer(), nullable=False, unique=True, default=None,
457 457 primary_key=True)
458 458 ui_section = Column(
459 459 "ui_section", String(255), nullable=True, unique=None, default=None)
460 460 ui_key = Column(
461 461 "ui_key", String(255), nullable=True, unique=None, default=None)
462 462 ui_value = Column(
463 463 "ui_value", String(255), nullable=True, unique=None, default=None)
464 464 ui_active = Column(
465 465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
466 466
467 467 def __repr__(self):
468 468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
469 469 self.ui_key, self.ui_value)
470 470
471 471
472 472 class RepoRhodeCodeSetting(Base, BaseModel):
473 473 __tablename__ = 'repo_rhodecode_settings'
474 474 __table_args__ = (
475 475 UniqueConstraint(
476 476 'app_settings_name', 'repository_id',
477 477 name='uq_repo_rhodecode_setting_name_repo_id'),
478 478 base_table_args
479 479 )
480 480
481 481 repository_id = Column(
482 482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
483 483 nullable=False)
484 484 app_settings_id = Column(
485 485 "app_settings_id", Integer(), nullable=False, unique=True,
486 486 default=None, primary_key=True)
487 487 app_settings_name = Column(
488 488 "app_settings_name", String(255), nullable=True, unique=None,
489 489 default=None)
490 490 _app_settings_value = Column(
491 491 "app_settings_value", String(4096), nullable=True, unique=None,
492 492 default=None)
493 493 _app_settings_type = Column(
494 494 "app_settings_type", String(255), nullable=True, unique=None,
495 495 default=None)
496 496
497 497 repository = relationship('Repository', viewonly=True)
498 498
499 499 def __init__(self, repository_id, key='', val='', type='unicode'):
500 500 self.repository_id = repository_id
501 501 self.app_settings_name = key
502 502 self.app_settings_type = type
503 503 self.app_settings_value = val
504 504
505 505 @validates('_app_settings_value')
506 506 def validate_settings_value(self, key, val):
507 507 assert type(val) == str
508 508 return val
509 509
510 510 @hybrid_property
511 511 def app_settings_value(self):
512 512 v = self._app_settings_value
513 513 type_ = self.app_settings_type
514 514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
515 515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
516 516 return converter(v)
517 517
518 518 @app_settings_value.setter
519 519 def app_settings_value(self, val):
520 520 """
521 521 Setter that will always make sure we use unicode in app_settings_value
522 522
523 523 :param val:
524 524 """
525 525 self._app_settings_value = safe_str(val)
526 526
527 527 @hybrid_property
528 528 def app_settings_type(self):
529 529 return self._app_settings_type
530 530
531 531 @app_settings_type.setter
532 532 def app_settings_type(self, val):
533 533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
534 534 if val not in SETTINGS_TYPES:
535 535 raise Exception('type must be one of %s got %s'
536 536 % (SETTINGS_TYPES.keys(), val))
537 537 self._app_settings_type = val
538 538
539 539 def __repr__(self):
540 540 return "<%s('%s:%s:%s[%s]')>" % (
541 541 self.cls_name, self.repository.repo_name,
542 542 self.app_settings_name, self.app_settings_value,
543 543 self.app_settings_type
544 544 )
545 545
546 546
547 547 class RepoRhodeCodeUi(Base, BaseModel):
548 548 __tablename__ = 'repo_rhodecode_ui'
549 549 __table_args__ = (
550 550 UniqueConstraint(
551 551 'repository_id', 'ui_section', 'ui_key',
552 552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
553 553 base_table_args
554 554 )
555 555
556 556 repository_id = Column(
557 557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
558 558 nullable=False)
559 559 ui_id = Column(
560 560 "ui_id", Integer(), nullable=False, unique=True, default=None,
561 561 primary_key=True)
562 562 ui_section = Column(
563 563 "ui_section", String(255), nullable=True, unique=None, default=None)
564 564 ui_key = Column(
565 565 "ui_key", String(255), nullable=True, unique=None, default=None)
566 566 ui_value = Column(
567 567 "ui_value", String(255), nullable=True, unique=None, default=None)
568 568 ui_active = Column(
569 569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
570 570
571 571 repository = relationship('Repository', viewonly=True)
572 572
573 573 def __repr__(self):
574 574 return '<%s[%s:%s]%s=>%s]>' % (
575 575 self.cls_name, self.repository.repo_name,
576 576 self.ui_section, self.ui_key, self.ui_value)
577 577
578 578
579 579 class User(Base, BaseModel):
580 580 __tablename__ = 'users'
581 581 __table_args__ = (
582 582 UniqueConstraint('username'), UniqueConstraint('email'),
583 583 Index('u_username_idx', 'username'),
584 584 Index('u_email_idx', 'email'),
585 585 base_table_args
586 586 )
587 587
588 588 DEFAULT_USER = 'default'
589 589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
590 590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 591 RECOVERY_CODES_COUNT = 10
592 592
593 593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
594 594 username = Column("username", String(255), nullable=True, unique=None, default=None)
595 595 password = Column("password", String(255), nullable=True, unique=None, default=None)
596 596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
597 597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
598 598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
599 599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
600 600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
601 601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
602 602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
603 603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
604 604
605 605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
606 606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
607 607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
608 608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
609 609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
610 610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
611 611
612 612 user_log = relationship('UserLog', back_populates='user')
613 613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
614 614
615 615 repositories = relationship('Repository', back_populates='user')
616 616 repository_groups = relationship('RepoGroup', back_populates='user')
617 617 user_groups = relationship('UserGroup', back_populates='user')
618 618
619 619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
620 620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
621 621
622 622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
623 623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
625 625
626 626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
627 627
628 628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
629 629 # notifications assigned to this user
630 630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
631 631 # comments created by this user
632 632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
633 633 # user profile extra info
634 634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
635 635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
636 636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
637 637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
638 638
639 639 # gists
640 640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
641 641 # user pull requests
642 642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
643 643
644 644 # external identities
645 645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
646 646 # review rules
647 647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
648 648
649 649 # artifacts owned
650 650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
651 651
652 652 # no cascade, set NULL
653 653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
654 654
655 655 def __repr__(self):
656 656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
657 657
658 658 @hybrid_property
659 659 def email(self):
660 660 return self._email
661 661
662 662 @email.setter
663 663 def email(self, val):
664 664 self._email = val.lower() if val else None
665 665
666 666 @hybrid_property
667 667 def first_name(self):
668 668 from rhodecode.lib import helpers as h
669 669 if self.name:
670 670 return h.escape(self.name)
671 671 return self.name
672 672
673 673 @hybrid_property
674 674 def last_name(self):
675 675 from rhodecode.lib import helpers as h
676 676 if self.lastname:
677 677 return h.escape(self.lastname)
678 678 return self.lastname
679 679
680 680 @hybrid_property
681 681 def api_key(self):
682 682 """
683 683 Fetch if exist an auth-token with role ALL connected to this user
684 684 """
685 685 user_auth_token = UserApiKeys.query()\
686 686 .filter(UserApiKeys.user_id == self.user_id)\
687 687 .filter(or_(UserApiKeys.expires == -1,
688 688 UserApiKeys.expires >= time.time()))\
689 689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
690 690 if user_auth_token:
691 691 user_auth_token = user_auth_token.api_key
692 692
693 693 return user_auth_token
694 694
695 695 @api_key.setter
696 696 def api_key(self, val):
697 697 # don't allow to set API key this is deprecated for now
698 698 self._api_key = None
699 699
700 700 @property
701 701 def reviewer_pull_requests(self):
702 702 return PullRequestReviewers.query() \
703 703 .options(joinedload(PullRequestReviewers.pull_request)) \
704 704 .filter(PullRequestReviewers.user_id == self.user_id) \
705 705 .all()
706 706
707 707 @property
708 708 def firstname(self):
709 709 # alias for future
710 710 return self.name
711 711
712 712 @property
713 713 def emails(self):
714 714 other = UserEmailMap.query()\
715 715 .filter(UserEmailMap.user == self) \
716 716 .order_by(UserEmailMap.email_id.asc()) \
717 717 .all()
718 718 return [self.email] + [x.email for x in other]
719 719
720 720 def emails_cached(self):
721 721 emails = []
722 722 if self.user_id != self.get_default_user_id():
723 723 emails = UserEmailMap.query()\
724 724 .filter(UserEmailMap.user == self) \
725 725 .order_by(UserEmailMap.email_id.asc())
726 726
727 727 emails = emails.options(
728 728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
729 729 )
730 730
731 731 return [self.email] + [x.email for x in emails]
732 732
733 733 @property
734 734 def auth_tokens(self):
735 735 auth_tokens = self.get_auth_tokens()
736 736 return [x.api_key for x in auth_tokens]
737 737
738 738 def get_auth_tokens(self):
739 739 return UserApiKeys.query()\
740 740 .filter(UserApiKeys.user == self)\
741 741 .order_by(UserApiKeys.user_api_key_id.asc())\
742 742 .all()
743 743
744 744 @LazyProperty
745 745 def feed_token(self):
746 746 return self.get_feed_token()
747 747
748 748 def get_feed_token(self, cache=True):
749 749 feed_tokens = UserApiKeys.query()\
750 750 .filter(UserApiKeys.user == self)\
751 751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
752 752 if cache:
753 753 feed_tokens = feed_tokens.options(
754 754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
755 755
756 756 feed_tokens = feed_tokens.all()
757 757 if feed_tokens:
758 758 return feed_tokens[0].api_key
759 759 return 'NO_FEED_TOKEN_AVAILABLE'
760 760
761 761 @LazyProperty
762 762 def artifact_token(self):
763 763 return self.get_artifact_token()
764 764
765 765 def get_artifact_token(self, cache=True):
766 766 artifacts_tokens = UserApiKeys.query()\
767 767 .filter(UserApiKeys.user == self) \
768 768 .filter(or_(UserApiKeys.expires == -1,
769 769 UserApiKeys.expires >= time.time())) \
770 770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 771
772 772 if cache:
773 773 artifacts_tokens = artifacts_tokens.options(
774 774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
775 775
776 776 artifacts_tokens = artifacts_tokens.all()
777 777 if artifacts_tokens:
778 778 return artifacts_tokens[0].api_key
779 779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
780 780
781 781 def get_or_create_artifact_token(self):
782 782 artifacts_tokens = UserApiKeys.query()\
783 783 .filter(UserApiKeys.user == self) \
784 784 .filter(or_(UserApiKeys.expires == -1,
785 785 UserApiKeys.expires >= time.time())) \
786 786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
787 787
788 788 artifacts_tokens = artifacts_tokens.all()
789 789 if artifacts_tokens:
790 790 return artifacts_tokens[0].api_key
791 791 else:
792 792 from rhodecode.model.auth_token import AuthTokenModel
793 793 artifact_token = AuthTokenModel().create(
794 794 self, 'auto-generated-artifact-token',
795 795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
796 796 Session.commit()
797 797 return artifact_token.api_key
798 798
799 799 @hybrid_property
800 800 def secret_2fa(self):
801 801 if not self.user_data.get('secret_2fa'):
802 802 secret = pyotp.random_base32()
803 803 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY)))
804 804 return secret
805 805 return safe_str(
806 806 enc_utils.decrypt_value(self.user_data['secret_2fa'],
807 807 enc_key=ENCRYPTION_KEY,
808 808 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict',
809 809 missing=True)
810 810 )
811 811 )
812 812
813 813 def is_totp_valid(self, received_code):
814 814 totp = pyotp.TOTP(self.secret_2fa)
815 815 return totp.verify(received_code)
816 816
817 817 def is_2fa_recovery_code_valid(self, received_code):
818 818 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
819 819 recovery_codes = list(map(
820 820 lambda x: safe_str(
821 821 enc_utils.decrypt_value(
822 822 x,
823 823 enc_key=ENCRYPTION_KEY,
824 824 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
825 825 )),
826 826 encrypted_recovery_codes))
827 827 if received_code in recovery_codes:
828 828 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
829 829 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
830 830 return True
831 831 return False
832 832
833 833 @hybrid_property
834 834 def has_forced_2fa(self):
835 835 """
836 836 Checks if 2fa was forced for ALL users (including current one)
837 837 """
838 838 from rhodecode.model.settings import SettingsModel
839 839 # So now we're supporting only auth_rhodecode_global_2f
840 840 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
841 841 return value.app_settings_value
842 842 return False
843 843
844 844 @hybrid_property
845 845 def has_enabled_2fa(self):
846 846 """
847 847 Checks if 2fa was enabled by user
848 848 """
849 849 if value := self.has_forced_2fa:
850 850 return value
851 851 return self.user_data.get('enabled_2fa', False)
852 852
853 853 @has_enabled_2fa.setter
854 854 def has_enabled_2fa(self, val):
855 855 val = str2bool(val)
856 856 self.update_userdata(enabled_2fa=str2bool(val))
857 857 if not val:
858 858 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[])
859 859 Session().commit()
860 860
861 861 def get_2fa_recovery_codes(self):
862 862 """
863 863 Creates 2fa recovery codes
864 864 """
865 865 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
866 866 encrypted_codes = []
867 867 if not recovery_codes:
868 868 for _ in range(self.RECOVERY_CODES_COUNT):
869 869 recovery_code = pyotp.random_base32()
870 870 recovery_codes.append(recovery_code)
871 871 encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY)))
872 872 self.update_userdata(recovery_codes_2fa=encrypted_codes)
873 873 return recovery_codes
874 874 # User should not check the same recovery codes more than once
875 875 return []
876 876
877 877 def regenerate_2fa_recovery_codes(self):
878 878 """
879 879 Regenerates 2fa recovery codes upon request
880 880 """
881 881 self.update_userdata(recovery_codes_2fa=[])
882 882 Session().flush()
883 883 new_recovery_codes = self.get_2fa_recovery_codes()
884 884 Session().commit()
885 885 return new_recovery_codes
886 886
887 887 @classmethod
888 def get(cls, user_id, cache=False):
889 if not user_id:
890 return
891
892 user = cls.query()
893 if cache:
894 user = user.options(
895 FromCache("sql_cache_short", f"get_users_{user_id}"))
896 return user.get(user_id)
897
898 @classmethod
899 888 def extra_valid_auth_tokens(cls, user, role=None):
900 889 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
901 890 .filter(or_(UserApiKeys.expires == -1,
902 891 UserApiKeys.expires >= time.time()))
903 892 if role:
904 893 tokens = tokens.filter(or_(UserApiKeys.role == role,
905 894 UserApiKeys.role == UserApiKeys.ROLE_ALL))
906 895 return tokens.all()
907 896
908 897 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
909 898 from rhodecode.lib import auth
910 899
911 900 log.debug('Trying to authenticate user: %s via auth-token, '
912 901 'and roles: %s', self, roles)
913 902
914 903 if not auth_token:
915 904 return False
916 905
917 906 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
918 907 tokens_q = UserApiKeys.query()\
919 908 .filter(UserApiKeys.user_id == self.user_id)\
920 909 .filter(or_(UserApiKeys.expires == -1,
921 910 UserApiKeys.expires >= time.time()))
922 911
923 912 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
924 913
925 914 crypto_backend = auth.crypto_backend()
926 915 enc_token_map = {}
927 916 plain_token_map = {}
928 917 for token in tokens_q:
929 918 if token.api_key.startswith(crypto_backend.ENC_PREF):
930 919 enc_token_map[token.api_key] = token
931 920 else:
932 921 plain_token_map[token.api_key] = token
933 922 log.debug(
934 923 'Found %s plain and %s encrypted tokens to check for authentication for this user',
935 924 len(plain_token_map), len(enc_token_map))
936 925
937 926 # plain token match comes first
938 927 match = plain_token_map.get(auth_token)
939 928
940 929 # check encrypted tokens now
941 930 if not match:
942 931 for token_hash, token in enc_token_map.items():
943 932 # NOTE(marcink): this is expensive to calculate, but most secure
944 933 if crypto_backend.hash_check(auth_token, token_hash):
945 934 match = token
946 935 break
947 936
948 937 if match:
949 938 log.debug('Found matching token %s', match)
950 939 if match.repo_id:
951 940 log.debug('Found scope, checking for scope match of token %s', match)
952 941 if match.repo_id == scope_repo_id:
953 942 return True
954 943 else:
955 944 log.debug(
956 945 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
957 946 'and calling scope is:%s, skipping further checks',
958 947 match.repo, scope_repo_id)
959 948 return False
960 949 else:
961 950 return True
962 951
963 952 return False
964 953
965 954 @property
966 955 def ip_addresses(self):
967 956 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
968 957 return [x.ip_addr for x in ret]
969 958
970 959 @property
971 960 def username_and_name(self):
972 961 return f'{self.username} ({self.first_name} {self.last_name})'
973 962
974 963 @property
975 964 def username_or_name_or_email(self):
976 965 full_name = self.full_name if self.full_name != ' ' else None
977 966 return self.username or full_name or self.email
978 967
979 968 @property
980 969 def full_name(self):
981 970 return f'{self.first_name} {self.last_name}'
982 971
983 972 @property
984 973 def full_name_or_username(self):
985 974 return (f'{self.first_name} {self.last_name}'
986 975 if (self.first_name and self.last_name) else self.username)
987 976
988 977 @property
989 978 def full_contact(self):
990 979 return f'{self.first_name} {self.last_name} <{self.email}>'
991 980
992 981 @property
993 982 def short_contact(self):
994 983 return f'{self.first_name} {self.last_name}'
995 984
996 985 @property
997 986 def is_admin(self):
998 987 return self.admin
999 988
1000 989 @property
1001 990 def language(self):
1002 991 return self.user_data.get('language')
1003 992
1004 993 def AuthUser(self, **kwargs):
1005 994 """
1006 995 Returns instance of AuthUser for this user
1007 996 """
1008 997 from rhodecode.lib.auth import AuthUser
1009 998 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1010 999
1011 1000 @hybrid_property
1012 1001 def user_data(self):
1013 1002 if not self._user_data:
1014 1003 return {}
1015 1004
1016 1005 try:
1017 1006 return json.loads(self._user_data) or {}
1018 1007 except TypeError:
1019 1008 return {}
1020 1009
1021 1010 @user_data.setter
1022 1011 def user_data(self, val):
1023 1012 if not isinstance(val, dict):
1024 raise Exception('user_data must be dict, got %s' % type(val))
1013 raise Exception(f'user_data must be dict, got {type(val)}')
1025 1014 try:
1026 1015 self._user_data = safe_bytes(json.dumps(val))
1027 1016 except Exception:
1028 1017 log.error(traceback.format_exc())
1029 1018
1030 1019 @classmethod
1020 def get(cls, user_id, cache=False):
1021 if not user_id:
1022 return
1023
1024 user = cls.query()
1025 if cache:
1026 user = user.options(
1027 FromCache("sql_cache_short", f"get_users_{user_id}"))
1028 return user.get(user_id)
1029
1030 @classmethod
1031 1031 def get_by_username(cls, username, case_insensitive=False,
1032 1032 cache=False):
1033 1033
1034 1034 if case_insensitive:
1035 1035 q = cls.select().where(
1036 1036 func.lower(cls.username) == func.lower(username))
1037 1037 else:
1038 1038 q = cls.select().where(cls.username == username)
1039 1039
1040 1040 if cache:
1041 1041 hash_key = _hash_key(username)
1042 1042 q = q.options(
1043 1043 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1044 1044
1045 1045 return cls.execute(q).scalar_one_or_none()
1046 1046
1047 1047 @classmethod
1048 1048 def get_by_username_or_primary_email(cls, user_identifier):
1049 1049 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1050 1050 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1051 1051 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1052 1052
1053 1053 @classmethod
1054 1054 def get_by_auth_token(cls, auth_token, cache=False):
1055 1055
1056 1056 q = cls.select(User)\
1057 1057 .join(UserApiKeys)\
1058 1058 .where(UserApiKeys.api_key == auth_token)\
1059 1059 .where(or_(UserApiKeys.expires == -1,
1060 1060 UserApiKeys.expires >= time.time()))
1061 1061
1062 1062 if cache:
1063 1063 q = q.options(
1064 1064 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1065 1065
1066 1066 matched_user = cls.execute(q).scalar_one_or_none()
1067 1067
1068 1068 return matched_user
1069 1069
1070 1070 @classmethod
1071 1071 def get_by_email(cls, email, case_insensitive=False, cache=False):
1072 1072
1073 1073 if case_insensitive:
1074 1074 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1075 1075 else:
1076 1076 q = cls.select().where(cls.email == email)
1077 1077
1078 1078 if cache:
1079 1079 email_key = _hash_key(email)
1080 1080 q = q.options(
1081 1081 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1082 1082
1083 1083 ret = cls.execute(q).scalar_one_or_none()
1084 1084
1085 1085 if ret is None:
1086 1086 q = cls.select(UserEmailMap)
1087 1087 # try fetching in alternate email map
1088 1088 if case_insensitive:
1089 1089 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1090 1090 else:
1091 1091 q = q.where(UserEmailMap.email == email)
1092 1092 q = q.options(joinedload(UserEmailMap.user))
1093 1093 if cache:
1094 1094 q = q.options(
1095 1095 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1096 1096
1097 1097 result = cls.execute(q).scalar_one_or_none()
1098 1098 ret = getattr(result, 'user', None)
1099 1099
1100 1100 return ret
1101 1101
1102 1102 @classmethod
1103 1103 def get_from_cs_author(cls, author):
1104 1104 """
1105 1105 Tries to get User objects out of commit author string
1106 1106
1107 1107 :param author:
1108 1108 """
1109 1109 from rhodecode.lib.helpers import email, author_name
1110 1110 # Valid email in the attribute passed, see if they're in the system
1111 1111 _email = email(author)
1112 1112 if _email:
1113 1113 user = cls.get_by_email(_email, case_insensitive=True)
1114 1114 if user:
1115 1115 return user
1116 1116 # Maybe we can match by username?
1117 1117 _author = author_name(author)
1118 1118 user = cls.get_by_username(_author, case_insensitive=True)
1119 1119 if user:
1120 1120 return user
1121 1121
1122 1122 def update_userdata(self, **kwargs):
1123 1123 usr = self
1124 1124 old = usr.user_data
1125 1125 old.update(**kwargs)
1126 1126 usr.user_data = old
1127 1127 Session().add(usr)
1128 1128 log.debug('updated userdata with %s', kwargs)
1129 1129
1130 1130 def update_lastlogin(self):
1131 1131 """Update user lastlogin"""
1132 1132 self.last_login = datetime.datetime.now()
1133 1133 Session().add(self)
1134 1134 log.debug('updated user %s lastlogin', self.username)
1135 1135
1136 1136 def update_password(self, new_password):
1137 1137 from rhodecode.lib.auth import get_crypt_password
1138 1138
1139 1139 self.password = get_crypt_password(new_password)
1140 1140 Session().add(self)
1141 1141
1142 1142 @classmethod
1143 1143 def get_first_super_admin(cls):
1144 1144 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1145 1145 user = cls.scalars(stmt).first()
1146 1146
1147 1147 if user is None:
1148 1148 raise Exception('FATAL: Missing administrative account!')
1149 1149 return user
1150 1150
1151 1151 @classmethod
1152 1152 def get_all_super_admins(cls, only_active=False):
1153 1153 """
1154 1154 Returns all admin accounts sorted by username
1155 1155 """
1156 1156 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1157 1157 if only_active:
1158 1158 qry = qry.filter(User.active == true())
1159 1159 return qry.all()
1160 1160
1161 1161 @classmethod
1162 1162 def get_all_user_ids(cls, only_active=True):
1163 1163 """
1164 1164 Returns all users IDs
1165 1165 """
1166 1166 qry = Session().query(User.user_id)
1167 1167
1168 1168 if only_active:
1169 1169 qry = qry.filter(User.active == true())
1170 1170 return [x.user_id for x in qry]
1171 1171
1172 1172 @classmethod
1173 1173 def get_default_user(cls, cache=False, refresh=False):
1174 1174 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1175 1175 if user is None:
1176 1176 raise Exception('FATAL: Missing default account!')
1177 1177 if refresh:
1178 1178 # The default user might be based on outdated state which
1179 1179 # has been loaded from the cache.
1180 1180 # A call to refresh() ensures that the
1181 1181 # latest state from the database is used.
1182 1182 Session().refresh(user)
1183 1183
1184 1184 return user
1185 1185
1186 1186 @classmethod
1187 1187 def get_default_user_id(cls):
1188 1188 import rhodecode
1189 1189 return rhodecode.CONFIG['default_user_id']
1190 1190
1191 1191 def _get_default_perms(self, user, suffix=''):
1192 1192 from rhodecode.model.permission import PermissionModel
1193 1193 return PermissionModel().get_default_perms(user.user_perms, suffix)
1194 1194
1195 1195 def get_default_perms(self, suffix=''):
1196 1196 return self._get_default_perms(self, suffix)
1197 1197
1198 1198 def get_api_data(self, include_secrets=False, details='full'):
1199 1199 """
1200 1200 Common function for generating user related data for API
1201 1201
1202 1202 :param include_secrets: By default secrets in the API data will be replaced
1203 1203 by a placeholder value to prevent exposing this data by accident. In case
1204 1204 this data shall be exposed, set this flag to ``True``.
1205 1205
1206 1206 :param details: details can be 'basic|full' basic gives only a subset of
1207 1207 the available user information that includes user_id, name and emails.
1208 1208 """
1209 1209 user = self
1210 1210 user_data = self.user_data
1211 1211 data = {
1212 1212 'user_id': user.user_id,
1213 1213 'username': user.username,
1214 1214 'firstname': user.name,
1215 1215 'lastname': user.lastname,
1216 1216 'description': user.description,
1217 1217 'email': user.email,
1218 1218 'emails': user.emails,
1219 1219 }
1220 1220 if details == 'basic':
1221 1221 return data
1222 1222
1223 1223 auth_token_length = 40
1224 1224 auth_token_replacement = '*' * auth_token_length
1225 1225
1226 1226 extras = {
1227 1227 'auth_tokens': [auth_token_replacement],
1228 1228 'active': user.active,
1229 1229 'admin': user.admin,
1230 1230 'extern_type': user.extern_type,
1231 1231 'extern_name': user.extern_name,
1232 1232 'last_login': user.last_login,
1233 1233 'last_activity': user.last_activity,
1234 1234 'ip_addresses': user.ip_addresses,
1235 1235 'language': user_data.get('language')
1236 1236 }
1237 1237 data.update(extras)
1238 1238
1239 1239 if include_secrets:
1240 1240 data['auth_tokens'] = user.auth_tokens
1241 1241 return data
1242 1242
1243 1243 def __json__(self):
1244 1244 data = {
1245 1245 'full_name': self.full_name,
1246 1246 'full_name_or_username': self.full_name_or_username,
1247 1247 'short_contact': self.short_contact,
1248 1248 'full_contact': self.full_contact,
1249 1249 }
1250 1250 data.update(self.get_api_data())
1251 1251 return data
1252 1252
1253 1253
1254 1254 class UserApiKeys(Base, BaseModel):
1255 1255 __tablename__ = 'user_api_keys'
1256 1256 __table_args__ = (
1257 1257 Index('uak_api_key_idx', 'api_key'),
1258 1258 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1259 1259 base_table_args
1260 1260 )
1261 1261
1262 1262 # ApiKey role
1263 1263 ROLE_ALL = 'token_role_all'
1264 1264 ROLE_VCS = 'token_role_vcs'
1265 1265 ROLE_API = 'token_role_api'
1266 1266 ROLE_HTTP = 'token_role_http'
1267 1267 ROLE_FEED = 'token_role_feed'
1268 1268 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1269 1269 # The last one is ignored in the list as we only
1270 1270 # use it for one action, and cannot be created by users
1271 1271 ROLE_PASSWORD_RESET = 'token_password_reset'
1272 1272
1273 1273 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1274 1274
1275 1275 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1276 1276 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1277 1277 api_key = Column("api_key", String(255), nullable=False, unique=True)
1278 1278 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1279 1279 expires = Column('expires', Float(53), nullable=False)
1280 1280 role = Column('role', String(255), nullable=True)
1281 1281 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1282 1282
1283 1283 # scope columns
1284 1284 repo_id = Column(
1285 1285 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1286 1286 nullable=True, unique=None, default=None)
1287 1287 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1288 1288
1289 1289 repo_group_id = Column(
1290 1290 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1291 1291 nullable=True, unique=None, default=None)
1292 1292 repo_group = relationship('RepoGroup', lazy='joined')
1293 1293
1294 1294 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1295 1295
1296 1296 def __repr__(self):
1297 1297 return f"<{self.cls_name}('{self.role}')>"
1298 1298
1299 1299 def __json__(self):
1300 1300 data = {
1301 1301 'auth_token': self.api_key,
1302 1302 'role': self.role,
1303 1303 'scope': self.scope_humanized,
1304 1304 'expired': self.expired
1305 1305 }
1306 1306 return data
1307 1307
1308 1308 def get_api_data(self, include_secrets=False):
1309 1309 data = self.__json__()
1310 1310 if include_secrets:
1311 1311 return data
1312 1312 else:
1313 1313 data['auth_token'] = self.token_obfuscated
1314 1314 return data
1315 1315
1316 1316 @hybrid_property
1317 1317 def description_safe(self):
1318 1318 from rhodecode.lib import helpers as h
1319 1319 return h.escape(self.description)
1320 1320
1321 1321 @property
1322 1322 def expired(self):
1323 1323 if self.expires == -1:
1324 1324 return False
1325 1325 return time.time() > self.expires
1326 1326
1327 1327 @classmethod
1328 1328 def _get_role_name(cls, role):
1329 1329 return {
1330 1330 cls.ROLE_ALL: _('all'),
1331 1331 cls.ROLE_HTTP: _('http/web interface'),
1332 1332 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1333 1333 cls.ROLE_API: _('api calls'),
1334 1334 cls.ROLE_FEED: _('feed access'),
1335 1335 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1336 1336 }.get(role, role)
1337 1337
1338 1338 @classmethod
1339 1339 def _get_role_description(cls, role):
1340 1340 return {
1341 1341 cls.ROLE_ALL: _('Token for all actions.'),
1342 1342 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1343 1343 'login using `api_access_controllers_whitelist` functionality.'),
1344 1344 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1345 1345 'Requires auth_token authentication plugin to be active. <br/>'
1346 1346 'Such Token should be used then instead of a password to '
1347 1347 'interact with a repository, and additionally can be '
1348 1348 'limited to single repository using repo scope.'),
1349 1349 cls.ROLE_API: _('Token limited to api calls.'),
1350 1350 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1351 1351 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1352 1352 }.get(role, role)
1353 1353
1354 1354 @property
1355 1355 def role_humanized(self):
1356 1356 return self._get_role_name(self.role)
1357 1357
1358 1358 def _get_scope(self):
1359 1359 if self.repo:
1360 1360 return 'Repository: {}'.format(self.repo.repo_name)
1361 1361 if self.repo_group:
1362 1362 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1363 1363 return 'Global'
1364 1364
1365 1365 @property
1366 1366 def scope_humanized(self):
1367 1367 return self._get_scope()
1368 1368
1369 1369 @property
1370 1370 def token_obfuscated(self):
1371 1371 if self.api_key:
1372 1372 return self.api_key[:4] + "****"
1373 1373
1374 1374
1375 1375 class UserEmailMap(Base, BaseModel):
1376 1376 __tablename__ = 'user_email_map'
1377 1377 __table_args__ = (
1378 1378 Index('uem_email_idx', 'email'),
1379 1379 Index('uem_user_id_idx', 'user_id'),
1380 1380 UniqueConstraint('email'),
1381 1381 base_table_args
1382 1382 )
1383 1383
1384 1384 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1385 1385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1386 1386 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1387 1387 user = relationship('User', lazy='joined', back_populates='user_emails')
1388 1388
1389 1389 @validates('_email')
1390 1390 def validate_email(self, key, email):
1391 1391 # check if this email is not main one
1392 1392 main_email = Session().query(User).filter(User.email == email).scalar()
1393 1393 if main_email is not None:
1394 1394 raise AttributeError('email %s is present is user table' % email)
1395 1395 return email
1396 1396
1397 1397 @hybrid_property
1398 1398 def email(self):
1399 1399 return self._email
1400 1400
1401 1401 @email.setter
1402 1402 def email(self, val):
1403 1403 self._email = val.lower() if val else None
1404 1404
1405 1405
1406 1406 class UserIpMap(Base, BaseModel):
1407 1407 __tablename__ = 'user_ip_map'
1408 1408 __table_args__ = (
1409 1409 UniqueConstraint('user_id', 'ip_addr'),
1410 1410 base_table_args
1411 1411 )
1412 1412
1413 1413 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1414 1414 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1415 1415 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1416 1416 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1417 1417 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1418 1418 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1419 1419
1420 1420 @hybrid_property
1421 1421 def description_safe(self):
1422 1422 from rhodecode.lib import helpers as h
1423 1423 return h.escape(self.description)
1424 1424
1425 1425 @classmethod
1426 1426 def _get_ip_range(cls, ip_addr):
1427 1427 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1428 1428 return [str(net.network_address), str(net.broadcast_address)]
1429 1429
1430 1430 def __json__(self):
1431 1431 return {
1432 1432 'ip_addr': self.ip_addr,
1433 1433 'ip_range': self._get_ip_range(self.ip_addr),
1434 1434 }
1435 1435
1436 1436 def __repr__(self):
1437 1437 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1438 1438
1439 1439
1440 1440 class UserSshKeys(Base, BaseModel):
1441 1441 __tablename__ = 'user_ssh_keys'
1442 1442 __table_args__ = (
1443 1443 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1444 1444
1445 1445 UniqueConstraint('ssh_key_fingerprint'),
1446 1446
1447 1447 base_table_args
1448 1448 )
1449 1449
1450 1450 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1451 1451 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1452 1452 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1453 1453
1454 1454 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1455 1455
1456 1456 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1457 1457 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1458 1458 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1459 1459
1460 1460 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1461 1461
1462 1462 def __json__(self):
1463 1463 data = {
1464 1464 'ssh_fingerprint': self.ssh_key_fingerprint,
1465 1465 'description': self.description,
1466 1466 'created_on': self.created_on
1467 1467 }
1468 1468 return data
1469 1469
1470 1470 def get_api_data(self):
1471 1471 data = self.__json__()
1472 1472 return data
1473 1473
1474 1474
1475 1475 class UserLog(Base, BaseModel):
1476 1476 __tablename__ = 'user_logs'
1477 1477 __table_args__ = (
1478 1478 base_table_args,
1479 1479 )
1480 1480
1481 1481 VERSION_1 = 'v1'
1482 1482 VERSION_2 = 'v2'
1483 1483 VERSIONS = [VERSION_1, VERSION_2]
1484 1484
1485 1485 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1486 1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1487 1487 username = Column("username", String(255), nullable=True, unique=None, default=None)
1488 1488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1489 1489 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1490 1490 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1491 1491 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1492 1492 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1493 1493
1494 1494 version = Column("version", String(255), nullable=True, default=VERSION_1)
1495 1495 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1496 1496 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1497 1497 user = relationship('User', cascade='', back_populates='user_log')
1498 1498 repository = relationship('Repository', cascade='', back_populates='logs')
1499 1499
1500 1500 def __repr__(self):
1501 1501 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1502 1502
1503 1503 def __json__(self):
1504 1504 return {
1505 1505 'user_id': self.user_id,
1506 1506 'username': self.username,
1507 1507 'repository_id': self.repository_id,
1508 1508 'repository_name': self.repository_name,
1509 1509 'user_ip': self.user_ip,
1510 1510 'action_date': self.action_date,
1511 1511 'action': self.action,
1512 1512 }
1513 1513
1514 1514 @hybrid_property
1515 1515 def entry_id(self):
1516 1516 return self.user_log_id
1517 1517
1518 1518 @property
1519 1519 def action_as_day(self):
1520 1520 return datetime.date(*self.action_date.timetuple()[:3])
1521 1521
1522 1522
1523 1523 class UserGroup(Base, BaseModel):
1524 1524 __tablename__ = 'users_groups'
1525 1525 __table_args__ = (
1526 1526 base_table_args,
1527 1527 )
1528 1528
1529 1529 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1530 1530 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1531 1531 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1532 1532 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1533 1533 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1534 1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1535 1535 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1536 1536 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1537 1537
1538 1538 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1539 1539 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1540 1540 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1541 1541 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1542 1542 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1543 1543
1544 1544 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1545 1545
1546 1546 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1547 1547 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1548 1548
1549 1549 @classmethod
1550 1550 def _load_group_data(cls, column):
1551 1551 if not column:
1552 1552 return {}
1553 1553
1554 1554 try:
1555 1555 return json.loads(column) or {}
1556 1556 except TypeError:
1557 1557 return {}
1558 1558
1559 1559 @hybrid_property
1560 1560 def description_safe(self):
1561 1561 from rhodecode.lib import helpers as h
1562 1562 return h.escape(self.user_group_description)
1563 1563
1564 1564 @hybrid_property
1565 1565 def group_data(self):
1566 1566 return self._load_group_data(self._group_data)
1567 1567
1568 1568 @group_data.expression
1569 1569 def group_data(self, **kwargs):
1570 1570 return self._group_data
1571 1571
1572 1572 @group_data.setter
1573 1573 def group_data(self, val):
1574 1574 try:
1575 1575 self._group_data = json.dumps(val)
1576 1576 except Exception:
1577 1577 log.error(traceback.format_exc())
1578 1578
1579 1579 @classmethod
1580 1580 def _load_sync(cls, group_data):
1581 1581 if group_data:
1582 1582 return group_data.get('extern_type')
1583 1583
1584 1584 @property
1585 1585 def sync(self):
1586 1586 return self._load_sync(self.group_data)
1587 1587
1588 1588 def __repr__(self):
1589 1589 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1590 1590
1591 1591 @classmethod
1592 1592 def get_by_group_name(cls, group_name, cache=False,
1593 1593 case_insensitive=False):
1594 1594 if case_insensitive:
1595 1595 q = cls.query().filter(func.lower(cls.users_group_name) ==
1596 1596 func.lower(group_name))
1597 1597
1598 1598 else:
1599 1599 q = cls.query().filter(cls.users_group_name == group_name)
1600 1600 if cache:
1601 1601 name_key = _hash_key(group_name)
1602 1602 q = q.options(
1603 1603 FromCache("sql_cache_short", f"get_group_{name_key}"))
1604 1604 return q.scalar()
1605 1605
1606 1606 @classmethod
1607 1607 def get(cls, user_group_id, cache=False):
1608 1608 if not user_group_id:
1609 1609 return
1610 1610
1611 1611 user_group = cls.query()
1612 1612 if cache:
1613 1613 user_group = user_group.options(
1614 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1614 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1615 1615 return user_group.get(user_group_id)
1616 1616
1617 1617 def permissions(self, with_admins=True, with_owner=True,
1618 1618 expand_from_user_groups=False):
1619 1619 """
1620 1620 Permissions for user groups
1621 1621 """
1622 1622 _admin_perm = 'usergroup.admin'
1623 1623
1624 1624 owner_row = []
1625 1625 if with_owner:
1626 1626 usr = AttributeDict(self.user.get_dict())
1627 1627 usr.owner_row = True
1628 1628 usr.permission = _admin_perm
1629 1629 owner_row.append(usr)
1630 1630
1631 1631 super_admin_ids = []
1632 1632 super_admin_rows = []
1633 1633 if with_admins:
1634 1634 for usr in User.get_all_super_admins():
1635 1635 super_admin_ids.append(usr.user_id)
1636 1636 # if this admin is also owner, don't double the record
1637 1637 if usr.user_id == owner_row[0].user_id:
1638 1638 owner_row[0].admin_row = True
1639 1639 else:
1640 1640 usr = AttributeDict(usr.get_dict())
1641 1641 usr.admin_row = True
1642 1642 usr.permission = _admin_perm
1643 1643 super_admin_rows.append(usr)
1644 1644
1645 1645 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1646 1646 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1647 1647 joinedload(UserUserGroupToPerm.user),
1648 1648 joinedload(UserUserGroupToPerm.permission),)
1649 1649
1650 1650 # get owners and admins and permissions. We do a trick of re-writing
1651 1651 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1652 1652 # has a global reference and changing one object propagates to all
1653 1653 # others. This means if admin is also an owner admin_row that change
1654 1654 # would propagate to both objects
1655 1655 perm_rows = []
1656 1656 for _usr in q.all():
1657 1657 usr = AttributeDict(_usr.user.get_dict())
1658 1658 # if this user is also owner/admin, mark as duplicate record
1659 1659 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1660 1660 usr.duplicate_perm = True
1661 1661 usr.permission = _usr.permission.permission_name
1662 1662 perm_rows.append(usr)
1663 1663
1664 1664 # filter the perm rows by 'default' first and then sort them by
1665 1665 # admin,write,read,none permissions sorted again alphabetically in
1666 1666 # each group
1667 1667 perm_rows = sorted(perm_rows, key=display_user_sort)
1668 1668
1669 1669 user_groups_rows = []
1670 1670 if expand_from_user_groups:
1671 1671 for ug in self.permission_user_groups(with_members=True):
1672 1672 for user_data in ug.members:
1673 1673 user_groups_rows.append(user_data)
1674 1674
1675 1675 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1676 1676
1677 1677 def permission_user_groups(self, with_members=False):
1678 1678 q = UserGroupUserGroupToPerm.query()\
1679 1679 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1680 1680 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1681 1681 joinedload(UserGroupUserGroupToPerm.target_user_group),
1682 1682 joinedload(UserGroupUserGroupToPerm.permission),)
1683 1683
1684 1684 perm_rows = []
1685 1685 for _user_group in q.all():
1686 1686 entry = AttributeDict(_user_group.user_group.get_dict())
1687 1687 entry.permission = _user_group.permission.permission_name
1688 1688 if with_members:
1689 1689 entry.members = [x.user.get_dict()
1690 1690 for x in _user_group.user_group.members]
1691 1691 perm_rows.append(entry)
1692 1692
1693 1693 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1694 1694 return perm_rows
1695 1695
1696 1696 def _get_default_perms(self, user_group, suffix=''):
1697 1697 from rhodecode.model.permission import PermissionModel
1698 1698 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1699 1699
1700 1700 def get_default_perms(self, suffix=''):
1701 1701 return self._get_default_perms(self, suffix)
1702 1702
1703 1703 def get_api_data(self, with_group_members=True, include_secrets=False):
1704 1704 """
1705 1705 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1706 1706 basically forwarded.
1707 1707
1708 1708 """
1709 1709 user_group = self
1710 1710 data = {
1711 1711 'users_group_id': user_group.users_group_id,
1712 1712 'group_name': user_group.users_group_name,
1713 1713 'group_description': user_group.user_group_description,
1714 1714 'active': user_group.users_group_active,
1715 1715 'owner': user_group.user.username,
1716 1716 'sync': user_group.sync,
1717 1717 'owner_email': user_group.user.email,
1718 1718 }
1719 1719
1720 1720 if with_group_members:
1721 1721 users = []
1722 1722 for user in user_group.members:
1723 1723 user = user.user
1724 1724 users.append(user.get_api_data(include_secrets=include_secrets))
1725 1725 data['users'] = users
1726 1726
1727 1727 return data
1728 1728
1729 1729
1730 1730 class UserGroupMember(Base, BaseModel):
1731 1731 __tablename__ = 'users_groups_members'
1732 1732 __table_args__ = (
1733 1733 base_table_args,
1734 1734 )
1735 1735
1736 1736 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1737 1737 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1738 1738 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1739 1739
1740 1740 user = relationship('User', lazy='joined', back_populates='group_member')
1741 1741 users_group = relationship('UserGroup', back_populates='members')
1742 1742
1743 1743 def __init__(self, gr_id='', u_id=''):
1744 1744 self.users_group_id = gr_id
1745 1745 self.user_id = u_id
1746 1746
1747 1747
1748 1748 class RepositoryField(Base, BaseModel):
1749 1749 __tablename__ = 'repositories_fields'
1750 1750 __table_args__ = (
1751 1751 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1752 1752 base_table_args,
1753 1753 )
1754 1754
1755 1755 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1756 1756
1757 1757 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1758 1758 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1759 1759 field_key = Column("field_key", String(250))
1760 1760 field_label = Column("field_label", String(1024), nullable=False)
1761 1761 field_value = Column("field_value", String(10000), nullable=False)
1762 1762 field_desc = Column("field_desc", String(1024), nullable=False)
1763 1763 field_type = Column("field_type", String(255), nullable=False, unique=None)
1764 1764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1765 1765
1766 1766 repository = relationship('Repository', back_populates='extra_fields')
1767 1767
1768 1768 @property
1769 1769 def field_key_prefixed(self):
1770 1770 return 'ex_%s' % self.field_key
1771 1771
1772 1772 @classmethod
1773 1773 def un_prefix_key(cls, key):
1774 1774 if key.startswith(cls.PREFIX):
1775 1775 return key[len(cls.PREFIX):]
1776 1776 return key
1777 1777
1778 1778 @classmethod
1779 1779 def get_by_key_name(cls, key, repo):
1780 1780 row = cls.query()\
1781 1781 .filter(cls.repository == repo)\
1782 1782 .filter(cls.field_key == key).scalar()
1783 1783 return row
1784 1784
1785 1785
1786 1786 class Repository(Base, BaseModel):
1787 1787 __tablename__ = 'repositories'
1788 1788 __table_args__ = (
1789 1789 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1790 1790 base_table_args,
1791 1791 )
1792 1792 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1793 1793 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1794 1794 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1795 1795
1796 1796 STATE_CREATED = 'repo_state_created'
1797 1797 STATE_PENDING = 'repo_state_pending'
1798 1798 STATE_ERROR = 'repo_state_error'
1799 1799
1800 1800 LOCK_AUTOMATIC = 'lock_auto'
1801 1801 LOCK_API = 'lock_api'
1802 1802 LOCK_WEB = 'lock_web'
1803 1803 LOCK_PULL = 'lock_pull'
1804 1804
1805 1805 NAME_SEP = URL_SEP
1806 1806
1807 1807 repo_id = Column(
1808 1808 "repo_id", Integer(), nullable=False, unique=True, default=None,
1809 1809 primary_key=True)
1810 1810 _repo_name = Column(
1811 1811 "repo_name", Text(), nullable=False, default=None)
1812 1812 repo_name_hash = Column(
1813 1813 "repo_name_hash", String(255), nullable=False, unique=True)
1814 1814 repo_state = Column("repo_state", String(255), nullable=True)
1815 1815
1816 1816 clone_uri = Column(
1817 1817 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1818 1818 default=None)
1819 1819 push_uri = Column(
1820 1820 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1821 1821 default=None)
1822 1822 repo_type = Column(
1823 1823 "repo_type", String(255), nullable=False, unique=False, default=None)
1824 1824 user_id = Column(
1825 1825 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1826 1826 unique=False, default=None)
1827 1827 private = Column(
1828 1828 "private", Boolean(), nullable=True, unique=None, default=None)
1829 1829 archived = Column(
1830 1830 "archived", Boolean(), nullable=True, unique=None, default=None)
1831 1831 enable_statistics = Column(
1832 1832 "statistics", Boolean(), nullable=True, unique=None, default=True)
1833 1833 enable_downloads = Column(
1834 1834 "downloads", Boolean(), nullable=True, unique=None, default=True)
1835 1835 description = Column(
1836 1836 "description", String(10000), nullable=True, unique=None, default=None)
1837 1837 created_on = Column(
1838 1838 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1839 1839 default=datetime.datetime.now)
1840 1840 updated_on = Column(
1841 1841 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1842 1842 default=datetime.datetime.now)
1843 1843 _landing_revision = Column(
1844 1844 "landing_revision", String(255), nullable=False, unique=False,
1845 1845 default=None)
1846 1846 enable_locking = Column(
1847 1847 "enable_locking", Boolean(), nullable=False, unique=None,
1848 1848 default=False)
1849 1849 _locked = Column(
1850 1850 "locked", String(255), nullable=True, unique=False, default=None)
1851 1851 _changeset_cache = Column(
1852 1852 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1853 1853
1854 1854 fork_id = Column(
1855 1855 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1856 1856 nullable=True, unique=False, default=None)
1857 1857 group_id = Column(
1858 1858 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1859 1859 unique=False, default=None)
1860 1860
1861 1861 user = relationship('User', lazy='joined', back_populates='repositories')
1862 1862 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1863 1863 group = relationship('RepoGroup', lazy='joined')
1864 1864 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1865 1865 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1866 1866 stats = relationship('Statistics', cascade='all', uselist=False)
1867 1867
1868 1868 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1869 1869 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1870 1870
1871 1871 logs = relationship('UserLog', back_populates='repository')
1872 1872
1873 1873 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1874 1874
1875 1875 pull_requests_source = relationship(
1876 1876 'PullRequest',
1877 1877 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1878 1878 cascade="all, delete-orphan",
1879 1879 overlaps="source_repo"
1880 1880 )
1881 1881 pull_requests_target = relationship(
1882 1882 'PullRequest',
1883 1883 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1884 1884 cascade="all, delete-orphan",
1885 1885 overlaps="target_repo"
1886 1886 )
1887 1887
1888 1888 ui = relationship('RepoRhodeCodeUi', cascade="all")
1889 1889 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1890 1890 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1891 1891
1892 1892 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1893 1893
1894 1894 # no cascade, set NULL
1895 1895 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1896 1896
1897 1897 review_rules = relationship('RepoReviewRule')
1898 1898 user_branch_perms = relationship('UserToRepoBranchPermission')
1899 1899 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1900 1900
1901 1901 def __repr__(self):
1902 1902 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1903 1903
1904 1904 @hybrid_property
1905 1905 def description_safe(self):
1906 1906 from rhodecode.lib import helpers as h
1907 1907 return h.escape(self.description)
1908 1908
1909 1909 @hybrid_property
1910 1910 def landing_rev(self):
1911 1911 # always should return [rev_type, rev], e.g ['branch', 'master']
1912 1912 if self._landing_revision:
1913 1913 _rev_info = self._landing_revision.split(':')
1914 1914 if len(_rev_info) < 2:
1915 1915 _rev_info.insert(0, 'rev')
1916 1916 return [_rev_info[0], _rev_info[1]]
1917 1917 return [None, None]
1918 1918
1919 1919 @property
1920 1920 def landing_ref_type(self):
1921 1921 return self.landing_rev[0]
1922 1922
1923 1923 @property
1924 1924 def landing_ref_name(self):
1925 1925 return self.landing_rev[1]
1926 1926
1927 1927 @landing_rev.setter
1928 1928 def landing_rev(self, val):
1929 1929 if ':' not in val:
1930 1930 raise ValueError('value must be delimited with `:` and consist '
1931 1931 'of <rev_type>:<rev>, got %s instead' % val)
1932 1932 self._landing_revision = val
1933 1933
1934 1934 @hybrid_property
1935 1935 def locked(self):
1936 1936 if self._locked:
1937 1937 user_id, timelocked, reason = self._locked.split(':')
1938 1938 lock_values = int(user_id), timelocked, reason
1939 1939 else:
1940 1940 lock_values = [None, None, None]
1941 1941 return lock_values
1942 1942
1943 1943 @locked.setter
1944 1944 def locked(self, val):
1945 1945 if val and isinstance(val, (list, tuple)):
1946 1946 self._locked = ':'.join(map(str, val))
1947 1947 else:
1948 1948 self._locked = None
1949 1949
1950 1950 @classmethod
1951 1951 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1952 1952 from rhodecode.lib.vcs.backends.base import EmptyCommit
1953 1953 dummy = EmptyCommit().__json__()
1954 1954 if not changeset_cache_raw:
1955 1955 dummy['source_repo_id'] = repo_id
1956 1956 return json.loads(json.dumps(dummy))
1957 1957
1958 1958 try:
1959 1959 return json.loads(changeset_cache_raw)
1960 1960 except TypeError:
1961 1961 return dummy
1962 1962 except Exception:
1963 1963 log.error(traceback.format_exc())
1964 1964 return dummy
1965 1965
1966 1966 @hybrid_property
1967 1967 def changeset_cache(self):
1968 1968 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1969 1969
1970 1970 @changeset_cache.setter
1971 1971 def changeset_cache(self, val):
1972 1972 try:
1973 1973 self._changeset_cache = json.dumps(val)
1974 1974 except Exception:
1975 1975 log.error(traceback.format_exc())
1976 1976
1977 1977 @hybrid_property
1978 1978 def repo_name(self):
1979 1979 return self._repo_name
1980 1980
1981 1981 @repo_name.setter
1982 1982 def repo_name(self, value):
1983 1983 self._repo_name = value
1984 1984 self.repo_name_hash = sha1(safe_bytes(value))
1985 1985
1986 1986 @classmethod
1987 1987 def normalize_repo_name(cls, repo_name):
1988 1988 """
1989 1989 Normalizes os specific repo_name to the format internally stored inside
1990 1990 database using URL_SEP
1991 1991
1992 1992 :param cls:
1993 1993 :param repo_name:
1994 1994 """
1995 1995 return cls.NAME_SEP.join(repo_name.split(os.sep))
1996 1996
1997 1997 @classmethod
1998 1998 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1999 1999 session = Session()
2000 2000 q = session.query(cls).filter(cls.repo_name == repo_name)
2001 2001
2002 2002 if cache:
2003 2003 if identity_cache:
2004 2004 val = cls.identity_cache(session, 'repo_name', repo_name)
2005 2005 if val:
2006 2006 return val
2007 2007 else:
2008 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
2008 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2009 2009 q = q.options(
2010 2010 FromCache("sql_cache_short", cache_key))
2011 2011
2012 2012 return q.scalar()
2013 2013
2014 2014 @classmethod
2015 2015 def get_by_id_or_repo_name(cls, repoid):
2016 2016 if isinstance(repoid, int):
2017 2017 try:
2018 2018 repo = cls.get(repoid)
2019 2019 except ValueError:
2020 2020 repo = None
2021 2021 else:
2022 2022 repo = cls.get_by_repo_name(repoid)
2023 2023 return repo
2024 2024
2025 2025 @classmethod
2026 2026 def get_by_full_path(cls, repo_full_path):
2027 2027 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2028 2028 repo_name = cls.normalize_repo_name(repo_name)
2029 2029 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2030 2030
2031 2031 @classmethod
2032 2032 def get_repo_forks(cls, repo_id):
2033 2033 return cls.query().filter(Repository.fork_id == repo_id)
2034 2034
2035 2035 @classmethod
2036 2036 def base_path(cls):
2037 2037 """
2038 2038 Returns base path when all repos are stored
2039 2039
2040 2040 :param cls:
2041 2041 """
2042 2042 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2043 2043 return get_rhodecode_repo_store_path()
2044 2044
2045 2045 @classmethod
2046 2046 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2047 2047 case_insensitive=True, archived=False):
2048 2048 q = Repository.query()
2049 2049
2050 2050 if not archived:
2051 2051 q = q.filter(Repository.archived.isnot(true()))
2052 2052
2053 2053 if not isinstance(user_id, Optional):
2054 2054 q = q.filter(Repository.user_id == user_id)
2055 2055
2056 2056 if not isinstance(group_id, Optional):
2057 2057 q = q.filter(Repository.group_id == group_id)
2058 2058
2059 2059 if case_insensitive:
2060 2060 q = q.order_by(func.lower(Repository.repo_name))
2061 2061 else:
2062 2062 q = q.order_by(Repository.repo_name)
2063 2063
2064 2064 return q.all()
2065 2065
2066 2066 @property
2067 2067 def repo_uid(self):
2068 2068 return '_{}'.format(self.repo_id)
2069 2069
2070 2070 @property
2071 2071 def forks(self):
2072 2072 """
2073 2073 Return forks of this repo
2074 2074 """
2075 2075 return Repository.get_repo_forks(self.repo_id)
2076 2076
2077 2077 @property
2078 2078 def parent(self):
2079 2079 """
2080 2080 Returns fork parent
2081 2081 """
2082 2082 return self.fork
2083 2083
2084 2084 @property
2085 2085 def just_name(self):
2086 2086 return self.repo_name.split(self.NAME_SEP)[-1]
2087 2087
2088 2088 @property
2089 2089 def groups_with_parents(self):
2090 2090 groups = []
2091 2091 if self.group is None:
2092 2092 return groups
2093 2093
2094 2094 cur_gr = self.group
2095 2095 groups.insert(0, cur_gr)
2096 2096 while 1:
2097 2097 gr = getattr(cur_gr, 'parent_group', None)
2098 2098 cur_gr = cur_gr.parent_group
2099 2099 if gr is None:
2100 2100 break
2101 2101 groups.insert(0, gr)
2102 2102
2103 2103 return groups
2104 2104
2105 2105 @property
2106 2106 def groups_and_repo(self):
2107 2107 return self.groups_with_parents, self
2108 2108
2109 2109 @property
2110 2110 def repo_path(self):
2111 2111 """
2112 2112 Returns base full path for that repository means where it actually
2113 2113 exists on a filesystem
2114 2114 """
2115 2115 return self.base_path()
2116 2116
2117 2117 @property
2118 2118 def repo_full_path(self):
2119 2119 p = [self.repo_path]
2120 2120 # we need to split the name by / since this is how we store the
2121 2121 # names in the database, but that eventually needs to be converted
2122 2122 # into a valid system path
2123 2123 p += self.repo_name.split(self.NAME_SEP)
2124 2124 return os.path.join(*map(safe_str, p))
2125 2125
2126 2126 @property
2127 2127 def cache_keys(self):
2128 2128 """
2129 2129 Returns associated cache keys for that repo
2130 2130 """
2131 2131 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2132 2132 return CacheKey.query()\
2133 2133 .filter(CacheKey.cache_key == repo_namespace_key)\
2134 2134 .order_by(CacheKey.cache_key)\
2135 2135 .all()
2136 2136
2137 2137 @property
2138 2138 def cached_diffs_relative_dir(self):
2139 2139 """
2140 2140 Return a relative to the repository store path of cached diffs
2141 2141 used for safe display for users, who shouldn't know the absolute store
2142 2142 path
2143 2143 """
2144 2144 return os.path.join(
2145 2145 os.path.dirname(self.repo_name),
2146 2146 self.cached_diffs_dir.split(os.path.sep)[-1])
2147 2147
2148 2148 @property
2149 2149 def cached_diffs_dir(self):
2150 2150 path = self.repo_full_path
2151 2151 return os.path.join(
2152 2152 os.path.dirname(path),
2153 2153 f'.__shadow_diff_cache_repo_{self.repo_id}')
2154 2154
2155 2155 def cached_diffs(self):
2156 2156 diff_cache_dir = self.cached_diffs_dir
2157 2157 if os.path.isdir(diff_cache_dir):
2158 2158 return os.listdir(diff_cache_dir)
2159 2159 return []
2160 2160
2161 2161 def shadow_repos(self):
2162 2162 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2163 2163 return [
2164 2164 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2165 2165 if x.startswith(shadow_repos_pattern)
2166 2166 ]
2167 2167
2168 2168 def get_new_name(self, repo_name):
2169 2169 """
2170 2170 returns new full repository name based on assigned group and new new
2171 2171
2172 2172 :param repo_name:
2173 2173 """
2174 2174 path_prefix = self.group.full_path_splitted if self.group else []
2175 2175 return self.NAME_SEP.join(path_prefix + [repo_name])
2176 2176
2177 2177 @property
2178 2178 def _config(self):
2179 2179 """
2180 2180 Returns db based config object.
2181 2181 """
2182 2182 from rhodecode.lib.utils import make_db_config
2183 2183 return make_db_config(clear_session=False, repo=self)
2184 2184
2185 2185 def permissions(self, with_admins=True, with_owner=True,
2186 2186 expand_from_user_groups=False):
2187 2187 """
2188 2188 Permissions for repositories
2189 2189 """
2190 2190 _admin_perm = 'repository.admin'
2191 2191
2192 2192 owner_row = []
2193 2193 if with_owner:
2194 2194 usr = AttributeDict(self.user.get_dict())
2195 2195 usr.owner_row = True
2196 2196 usr.permission = _admin_perm
2197 2197 usr.permission_id = None
2198 2198 owner_row.append(usr)
2199 2199
2200 2200 super_admin_ids = []
2201 2201 super_admin_rows = []
2202 2202 if with_admins:
2203 2203 for usr in User.get_all_super_admins():
2204 2204 super_admin_ids.append(usr.user_id)
2205 2205 # if this admin is also owner, don't double the record
2206 2206 if usr.user_id == owner_row[0].user_id:
2207 2207 owner_row[0].admin_row = True
2208 2208 else:
2209 2209 usr = AttributeDict(usr.get_dict())
2210 2210 usr.admin_row = True
2211 2211 usr.permission = _admin_perm
2212 2212 usr.permission_id = None
2213 2213 super_admin_rows.append(usr)
2214 2214
2215 2215 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2216 2216 q = q.options(joinedload(UserRepoToPerm.repository),
2217 2217 joinedload(UserRepoToPerm.user),
2218 2218 joinedload(UserRepoToPerm.permission),)
2219 2219
2220 2220 # get owners and admins and permissions. We do a trick of re-writing
2221 2221 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2222 2222 # has a global reference and changing one object propagates to all
2223 2223 # others. This means if admin is also an owner admin_row that change
2224 2224 # would propagate to both objects
2225 2225 perm_rows = []
2226 2226 for _usr in q.all():
2227 2227 usr = AttributeDict(_usr.user.get_dict())
2228 2228 # if this user is also owner/admin, mark as duplicate record
2229 2229 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2230 2230 usr.duplicate_perm = True
2231 2231 # also check if this permission is maybe used by branch_permissions
2232 2232 if _usr.branch_perm_entry:
2233 2233 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2234 2234
2235 2235 usr.permission = _usr.permission.permission_name
2236 2236 usr.permission_id = _usr.repo_to_perm_id
2237 2237 perm_rows.append(usr)
2238 2238
2239 2239 # filter the perm rows by 'default' first and then sort them by
2240 2240 # admin,write,read,none permissions sorted again alphabetically in
2241 2241 # each group
2242 2242 perm_rows = sorted(perm_rows, key=display_user_sort)
2243 2243
2244 2244 user_groups_rows = []
2245 2245 if expand_from_user_groups:
2246 2246 for ug in self.permission_user_groups(with_members=True):
2247 2247 for user_data in ug.members:
2248 2248 user_groups_rows.append(user_data)
2249 2249
2250 2250 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2251 2251
2252 2252 def permission_user_groups(self, with_members=True):
2253 2253 q = UserGroupRepoToPerm.query()\
2254 2254 .filter(UserGroupRepoToPerm.repository == self)
2255 2255 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2256 2256 joinedload(UserGroupRepoToPerm.users_group),
2257 2257 joinedload(UserGroupRepoToPerm.permission),)
2258 2258
2259 2259 perm_rows = []
2260 2260 for _user_group in q.all():
2261 2261 entry = AttributeDict(_user_group.users_group.get_dict())
2262 2262 entry.permission = _user_group.permission.permission_name
2263 2263 if with_members:
2264 2264 entry.members = [x.user.get_dict()
2265 2265 for x in _user_group.users_group.members]
2266 2266 perm_rows.append(entry)
2267 2267
2268 2268 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2269 2269 return perm_rows
2270 2270
2271 2271 def get_api_data(self, include_secrets=False):
2272 2272 """
2273 2273 Common function for generating repo api data
2274 2274
2275 2275 :param include_secrets: See :meth:`User.get_api_data`.
2276 2276
2277 2277 """
2278 2278 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2279 2279 # move this methods on models level.
2280 2280 from rhodecode.model.settings import SettingsModel
2281 2281 from rhodecode.model.repo import RepoModel
2282 2282
2283 2283 repo = self
2284 2284 _user_id, _time, _reason = self.locked
2285 2285
2286 2286 data = {
2287 2287 'repo_id': repo.repo_id,
2288 2288 'repo_name': repo.repo_name,
2289 2289 'repo_type': repo.repo_type,
2290 2290 'clone_uri': repo.clone_uri or '',
2291 2291 'push_uri': repo.push_uri or '',
2292 2292 'url': RepoModel().get_url(self),
2293 2293 'private': repo.private,
2294 2294 'created_on': repo.created_on,
2295 2295 'description': repo.description_safe,
2296 2296 'landing_rev': repo.landing_rev,
2297 2297 'owner': repo.user.username,
2298 2298 'fork_of': repo.fork.repo_name if repo.fork else None,
2299 2299 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2300 2300 'enable_statistics': repo.enable_statistics,
2301 2301 'enable_locking': repo.enable_locking,
2302 2302 'enable_downloads': repo.enable_downloads,
2303 2303 'last_changeset': repo.changeset_cache,
2304 2304 'locked_by': User.get(_user_id).get_api_data(
2305 2305 include_secrets=include_secrets) if _user_id else None,
2306 2306 'locked_date': time_to_datetime(_time) if _time else None,
2307 2307 'lock_reason': _reason if _reason else None,
2308 2308 }
2309 2309
2310 2310 # TODO: mikhail: should be per-repo settings here
2311 2311 rc_config = SettingsModel().get_all_settings()
2312 2312 repository_fields = str2bool(
2313 2313 rc_config.get('rhodecode_repository_fields'))
2314 2314 if repository_fields:
2315 2315 for f in self.extra_fields:
2316 2316 data[f.field_key_prefixed] = f.field_value
2317 2317
2318 2318 return data
2319 2319
2320 2320 @classmethod
2321 2321 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2322 2322 if not lock_time:
2323 2323 lock_time = time.time()
2324 2324 if not lock_reason:
2325 2325 lock_reason = cls.LOCK_AUTOMATIC
2326 2326 repo.locked = [user_id, lock_time, lock_reason]
2327 2327 Session().add(repo)
2328 2328 Session().commit()
2329 2329
2330 2330 @classmethod
2331 2331 def unlock(cls, repo):
2332 2332 repo.locked = None
2333 2333 Session().add(repo)
2334 2334 Session().commit()
2335 2335
2336 2336 @classmethod
2337 2337 def getlock(cls, repo):
2338 2338 return repo.locked
2339 2339
2340 2340 def get_locking_state(self, action, user_id, only_when_enabled=True):
2341 2341 """
2342 2342 Checks locking on this repository, if locking is enabled and lock is
2343 2343 present returns a tuple of make_lock, locked, locked_by.
2344 2344 make_lock can have 3 states None (do nothing) True, make lock
2345 2345 False release lock, This value is later propagated to hooks, which
2346 2346 do the locking. Think about this as signals passed to hooks what to do.
2347 2347
2348 2348 """
2349 2349 # TODO: johbo: This is part of the business logic and should be moved
2350 2350 # into the RepositoryModel.
2351 2351
2352 2352 if action not in ('push', 'pull'):
2353 2353 raise ValueError("Invalid action value: %s" % repr(action))
2354 2354
2355 2355 # defines if locked error should be thrown to user
2356 2356 currently_locked = False
2357 2357 # defines if new lock should be made, tri-state
2358 2358 make_lock = None
2359 2359 repo = self
2360 2360 user = User.get(user_id)
2361 2361
2362 2362 lock_info = repo.locked
2363 2363
2364 2364 if repo and (repo.enable_locking or not only_when_enabled):
2365 2365 if action == 'push':
2366 2366 # check if it's already locked !, if it is compare users
2367 2367 locked_by_user_id = lock_info[0]
2368 2368 if user.user_id == locked_by_user_id:
2369 2369 log.debug(
2370 2370 'Got `push` action from user %s, now unlocking', user)
2371 2371 # unlock if we have push from user who locked
2372 2372 make_lock = False
2373 2373 else:
2374 2374 # we're not the same user who locked, ban with
2375 2375 # code defined in settings (default is 423 HTTP Locked) !
2376 2376 log.debug('Repo %s is currently locked by %s', repo, user)
2377 2377 currently_locked = True
2378 2378 elif action == 'pull':
2379 2379 # [0] user [1] date
2380 2380 if lock_info[0] and lock_info[1]:
2381 2381 log.debug('Repo %s is currently locked by %s', repo, user)
2382 2382 currently_locked = True
2383 2383 else:
2384 2384 log.debug('Setting lock on repo %s by %s', repo, user)
2385 2385 make_lock = True
2386 2386
2387 2387 else:
2388 2388 log.debug('Repository %s do not have locking enabled', repo)
2389 2389
2390 2390 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2391 2391 make_lock, currently_locked, lock_info)
2392 2392
2393 2393 from rhodecode.lib.auth import HasRepoPermissionAny
2394 2394 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2395 2395 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2396 2396 # if we don't have at least write permission we cannot make a lock
2397 2397 log.debug('lock state reset back to FALSE due to lack '
2398 2398 'of at least read permission')
2399 2399 make_lock = False
2400 2400
2401 2401 return make_lock, currently_locked, lock_info
2402 2402
2403 2403 @property
2404 2404 def last_commit_cache_update_diff(self):
2405 2405 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2406 2406
2407 2407 @classmethod
2408 2408 def _load_commit_change(cls, last_commit_cache):
2409 2409 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2410 2410 empty_date = datetime.datetime.fromtimestamp(0)
2411 2411 date_latest = last_commit_cache.get('date', empty_date)
2412 2412 try:
2413 2413 return parse_datetime(date_latest)
2414 2414 except Exception:
2415 2415 return empty_date
2416 2416
2417 2417 @property
2418 2418 def last_commit_change(self):
2419 2419 return self._load_commit_change(self.changeset_cache)
2420 2420
2421 2421 @property
2422 2422 def last_db_change(self):
2423 2423 return self.updated_on
2424 2424
2425 2425 @property
2426 2426 def clone_uri_hidden(self):
2427 2427 clone_uri = self.clone_uri
2428 2428 if clone_uri:
2429 2429 import urlobject
2430 2430 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2431 2431 if url_obj.password:
2432 2432 clone_uri = url_obj.with_password('*****')
2433 2433 return clone_uri
2434 2434
2435 2435 @property
2436 2436 def push_uri_hidden(self):
2437 2437 push_uri = self.push_uri
2438 2438 if push_uri:
2439 2439 import urlobject
2440 2440 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2441 2441 if url_obj.password:
2442 2442 push_uri = url_obj.with_password('*****')
2443 2443 return push_uri
2444 2444
2445 2445 def clone_url(self, **override):
2446 2446 from rhodecode.model.settings import SettingsModel
2447 2447
2448 2448 uri_tmpl = None
2449 2449 if 'with_id' in override:
2450 2450 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2451 2451 del override['with_id']
2452 2452
2453 2453 if 'uri_tmpl' in override:
2454 2454 uri_tmpl = override['uri_tmpl']
2455 2455 del override['uri_tmpl']
2456 2456
2457 2457 ssh = False
2458 2458 if 'ssh' in override:
2459 2459 ssh = True
2460 2460 del override['ssh']
2461 2461
2462 2462 # we didn't override our tmpl from **overrides
2463 2463 request = get_current_request()
2464 2464 if not uri_tmpl:
2465 2465 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2466 2466 rc_config = request.call_context.rc_config
2467 2467 else:
2468 2468 rc_config = SettingsModel().get_all_settings(cache=True)
2469 2469
2470 2470 if ssh:
2471 2471 uri_tmpl = rc_config.get(
2472 2472 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2473 2473
2474 2474 else:
2475 2475 uri_tmpl = rc_config.get(
2476 2476 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2477 2477
2478 2478 return get_clone_url(request=request,
2479 2479 uri_tmpl=uri_tmpl,
2480 2480 repo_name=self.repo_name,
2481 2481 repo_id=self.repo_id,
2482 2482 repo_type=self.repo_type,
2483 2483 **override)
2484 2484
2485 2485 def set_state(self, state):
2486 2486 self.repo_state = state
2487 2487 Session().add(self)
2488 2488 #==========================================================================
2489 2489 # SCM PROPERTIES
2490 2490 #==========================================================================
2491 2491
2492 2492 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2493 2493 return get_commit_safe(
2494 2494 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2495 2495 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2496 2496
2497 2497 def get_changeset(self, rev=None, pre_load=None):
2498 2498 warnings.warn("Use get_commit", DeprecationWarning)
2499 2499 commit_id = None
2500 2500 commit_idx = None
2501 2501 if isinstance(rev, str):
2502 2502 commit_id = rev
2503 2503 else:
2504 2504 commit_idx = rev
2505 2505 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2506 2506 pre_load=pre_load)
2507 2507
2508 2508 def get_landing_commit(self):
2509 2509 """
2510 2510 Returns landing commit, or if that doesn't exist returns the tip
2511 2511 """
2512 2512 _rev_type, _rev = self.landing_rev
2513 2513 commit = self.get_commit(_rev)
2514 2514 if isinstance(commit, EmptyCommit):
2515 2515 return self.get_commit()
2516 2516 return commit
2517 2517
2518 2518 def flush_commit_cache(self):
2519 2519 self.update_commit_cache(cs_cache={'raw_id':'0'})
2520 2520 self.update_commit_cache()
2521 2521
2522 2522 def update_commit_cache(self, cs_cache=None, config=None):
2523 2523 """
2524 2524 Update cache of last commit for repository
2525 2525 cache_keys should be::
2526 2526
2527 2527 source_repo_id
2528 2528 short_id
2529 2529 raw_id
2530 2530 revision
2531 2531 parents
2532 2532 message
2533 2533 date
2534 2534 author
2535 2535 updated_on
2536 2536
2537 2537 """
2538 2538 from rhodecode.lib.vcs.backends.base import BaseCommit
2539 2539 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2540 2540 empty_date = datetime.datetime.fromtimestamp(0)
2541 2541 repo_commit_count = 0
2542 2542
2543 2543 if cs_cache is None:
2544 2544 # use no-cache version here
2545 2545 try:
2546 2546 scm_repo = self.scm_instance(cache=False, config=config)
2547 2547 except VCSError:
2548 2548 scm_repo = None
2549 2549 empty = scm_repo is None or scm_repo.is_empty()
2550 2550
2551 2551 if not empty:
2552 2552 cs_cache = scm_repo.get_commit(
2553 2553 pre_load=["author", "date", "message", "parents", "branch"])
2554 2554 repo_commit_count = scm_repo.count()
2555 2555 else:
2556 2556 cs_cache = EmptyCommit()
2557 2557
2558 2558 if isinstance(cs_cache, BaseCommit):
2559 2559 cs_cache = cs_cache.__json__()
2560 2560
2561 2561 def is_outdated(new_cs_cache):
2562 2562 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2563 2563 new_cs_cache['revision'] != self.changeset_cache['revision']):
2564 2564 return True
2565 2565 return False
2566 2566
2567 2567 # check if we have maybe already latest cached revision
2568 2568 if is_outdated(cs_cache) or not self.changeset_cache:
2569 2569 _current_datetime = datetime.datetime.utcnow()
2570 2570 last_change = cs_cache.get('date') or _current_datetime
2571 2571 # we check if last update is newer than the new value
2572 2572 # if yes, we use the current timestamp instead. Imagine you get
2573 2573 # old commit pushed 1y ago, we'd set last update 1y to ago.
2574 2574 last_change_timestamp = datetime_to_time(last_change)
2575 2575 current_timestamp = datetime_to_time(last_change)
2576 2576 if last_change_timestamp > current_timestamp and not empty:
2577 2577 cs_cache['date'] = _current_datetime
2578 2578
2579 2579 # also store size of repo
2580 2580 cs_cache['repo_commit_count'] = repo_commit_count
2581 2581
2582 2582 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2583 2583 cs_cache['updated_on'] = time.time()
2584 2584 self.changeset_cache = cs_cache
2585 2585 self.updated_on = last_change
2586 2586 Session().add(self)
2587 2587 Session().commit()
2588 2588
2589 2589 else:
2590 2590 if empty:
2591 2591 cs_cache = EmptyCommit().__json__()
2592 2592 else:
2593 2593 cs_cache = self.changeset_cache
2594 2594
2595 2595 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2596 2596
2597 2597 cs_cache['updated_on'] = time.time()
2598 2598 self.changeset_cache = cs_cache
2599 2599 self.updated_on = _date_latest
2600 2600 Session().add(self)
2601 2601 Session().commit()
2602 2602
2603 2603 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2604 2604 self.repo_name, cs_cache, _date_latest)
2605 2605
2606 2606 @property
2607 2607 def tip(self):
2608 2608 return self.get_commit('tip')
2609 2609
2610 2610 @property
2611 2611 def author(self):
2612 2612 return self.tip.author
2613 2613
2614 2614 @property
2615 2615 def last_change(self):
2616 2616 return self.scm_instance().last_change
2617 2617
2618 2618 def get_comments(self, revisions=None):
2619 2619 """
2620 2620 Returns comments for this repository grouped by revisions
2621 2621
2622 2622 :param revisions: filter query by revisions only
2623 2623 """
2624 2624 cmts = ChangesetComment.query()\
2625 2625 .filter(ChangesetComment.repo == self)
2626 2626 if revisions:
2627 2627 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2628 2628 grouped = collections.defaultdict(list)
2629 2629 for cmt in cmts.all():
2630 2630 grouped[cmt.revision].append(cmt)
2631 2631 return grouped
2632 2632
2633 2633 def statuses(self, revisions=None):
2634 2634 """
2635 2635 Returns statuses for this repository
2636 2636
2637 2637 :param revisions: list of revisions to get statuses for
2638 2638 """
2639 2639 statuses = ChangesetStatus.query()\
2640 2640 .filter(ChangesetStatus.repo == self)\
2641 2641 .filter(ChangesetStatus.version == 0)
2642 2642
2643 2643 if revisions:
2644 2644 # Try doing the filtering in chunks to avoid hitting limits
2645 2645 size = 500
2646 2646 status_results = []
2647 2647 for chunk in range(0, len(revisions), size):
2648 2648 status_results += statuses.filter(
2649 2649 ChangesetStatus.revision.in_(
2650 2650 revisions[chunk: chunk+size])
2651 2651 ).all()
2652 2652 else:
2653 2653 status_results = statuses.all()
2654 2654
2655 2655 grouped = {}
2656 2656
2657 2657 # maybe we have open new pullrequest without a status?
2658 2658 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2659 2659 status_lbl = ChangesetStatus.get_status_lbl(stat)
2660 2660 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2661 2661 for rev in pr.revisions:
2662 2662 pr_id = pr.pull_request_id
2663 2663 pr_repo = pr.target_repo.repo_name
2664 2664 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2665 2665
2666 2666 for stat in status_results:
2667 2667 pr_id = pr_repo = None
2668 2668 if stat.pull_request:
2669 2669 pr_id = stat.pull_request.pull_request_id
2670 2670 pr_repo = stat.pull_request.target_repo.repo_name
2671 2671 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2672 2672 pr_id, pr_repo]
2673 2673 return grouped
2674 2674
2675 2675 # ==========================================================================
2676 2676 # SCM CACHE INSTANCE
2677 2677 # ==========================================================================
2678 2678
2679 2679 def scm_instance(self, **kwargs):
2680 2680 import rhodecode
2681 2681
2682 2682 # Passing a config will not hit the cache currently only used
2683 2683 # for repo2dbmapper
2684 2684 config = kwargs.pop('config', None)
2685 2685 cache = kwargs.pop('cache', None)
2686 2686 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2687 2687 if vcs_full_cache is not None:
2688 2688 # allows override global config
2689 2689 full_cache = vcs_full_cache
2690 2690 else:
2691 2691 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2692 2692 # if cache is NOT defined use default global, else we have a full
2693 2693 # control over cache behaviour
2694 2694 if cache is None and full_cache and not config:
2695 2695 log.debug('Initializing pure cached instance for %s', self.repo_path)
2696 2696 return self._get_instance_cached()
2697 2697
2698 2698 # cache here is sent to the "vcs server"
2699 2699 return self._get_instance(cache=bool(cache), config=config)
2700 2700
2701 2701 def _get_instance_cached(self):
2702 2702 from rhodecode.lib import rc_cache
2703 2703
2704 2704 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2705 2705 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2706 2706
2707 2707 # we must use thread scoped cache here,
2708 2708 # because each thread of gevent needs it's own not shared connection and cache
2709 2709 # we also alter `args` so the cache key is individual for every green thread.
2710 2710 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2711 2711 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2712 2712
2713 2713 # our wrapped caching function that takes state_uid to save the previous state in
2714 2714 def cache_generator(_state_uid):
2715 2715
2716 2716 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2717 2717 def get_instance_cached(_repo_id, _process_context_id):
2718 2718 # we save in cached func the generation state so we can detect a change and invalidate caches
2719 2719 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2720 2720
2721 2721 return get_instance_cached
2722 2722
2723 2723 with inv_context_manager as invalidation_context:
2724 2724 cache_state_uid = invalidation_context.state_uid
2725 2725 cache_func = cache_generator(cache_state_uid)
2726 2726
2727 2727 args = self.repo_id, inv_context_manager.proc_key
2728 2728
2729 2729 previous_state_uid, instance = cache_func(*args)
2730 2730
2731 2731 # now compare keys, the "cache" state vs expected state.
2732 2732 if previous_state_uid != cache_state_uid:
2733 2733 log.warning('Cached state uid %s is different than current state uid %s',
2734 2734 previous_state_uid, cache_state_uid)
2735 2735 _, instance = cache_func.refresh(*args)
2736 2736
2737 2737 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2738 2738 return instance
2739 2739
2740 2740 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2741 2741 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2742 2742 self.repo_type, self.repo_path, cache)
2743 2743 config = config or self._config
2744 2744 custom_wire = {
2745 2745 'cache': cache, # controls the vcs.remote cache
2746 2746 'repo_state_uid': repo_state_uid
2747 2747 }
2748 2748
2749 2749 repo = get_vcs_instance(
2750 2750 repo_path=safe_str(self.repo_full_path),
2751 2751 config=config,
2752 2752 with_wire=custom_wire,
2753 2753 create=False,
2754 2754 _vcs_alias=self.repo_type)
2755 2755 if repo is not None:
2756 2756 repo.count() # cache rebuild
2757 2757
2758 2758 return repo
2759 2759
2760 2760 def get_shadow_repository_path(self, workspace_id):
2761 2761 from rhodecode.lib.vcs.backends.base import BaseRepository
2762 2762 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2763 2763 self.repo_full_path, self.repo_id, workspace_id)
2764 2764 return shadow_repo_path
2765 2765
2766 2766 def __json__(self):
2767 2767 return {'landing_rev': self.landing_rev}
2768 2768
2769 2769 def get_dict(self):
2770 2770
2771 2771 # Since we transformed `repo_name` to a hybrid property, we need to
2772 2772 # keep compatibility with the code which uses `repo_name` field.
2773 2773
2774 2774 result = super(Repository, self).get_dict()
2775 2775 result['repo_name'] = result.pop('_repo_name', None)
2776 2776 result.pop('_changeset_cache', '')
2777 2777 return result
2778 2778
2779 2779
2780 2780 class RepoGroup(Base, BaseModel):
2781 2781 __tablename__ = 'groups'
2782 2782 __table_args__ = (
2783 2783 UniqueConstraint('group_name', 'group_parent_id'),
2784 2784 base_table_args,
2785 2785 )
2786 2786
2787 2787 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2788 2788
2789 2789 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2790 2790 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2791 2791 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2792 2792 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2793 2793 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2794 2794 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2795 2795 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2796 2796 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2797 2797 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2798 2798 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2799 2799 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2800 2800
2801 2801 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2802 2802 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2803 2803 parent_group = relationship('RepoGroup', remote_side=group_id)
2804 2804 user = relationship('User', back_populates='repository_groups')
2805 2805 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2806 2806
2807 2807 # no cascade, set NULL
2808 2808 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2809 2809
2810 2810 def __init__(self, group_name='', parent_group=None):
2811 2811 self.group_name = group_name
2812 2812 self.parent_group = parent_group
2813 2813
2814 2814 def __repr__(self):
2815 2815 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2816 2816
2817 2817 @hybrid_property
2818 2818 def group_name(self):
2819 2819 return self._group_name
2820 2820
2821 2821 @group_name.setter
2822 2822 def group_name(self, value):
2823 2823 self._group_name = value
2824 2824 self.group_name_hash = self.hash_repo_group_name(value)
2825 2825
2826 2826 @classmethod
2827 2827 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2828 2828 from rhodecode.lib.vcs.backends.base import EmptyCommit
2829 2829 dummy = EmptyCommit().__json__()
2830 2830 if not changeset_cache_raw:
2831 2831 dummy['source_repo_id'] = repo_id
2832 2832 return json.loads(json.dumps(dummy))
2833 2833
2834 2834 try:
2835 2835 return json.loads(changeset_cache_raw)
2836 2836 except TypeError:
2837 2837 return dummy
2838 2838 except Exception:
2839 2839 log.error(traceback.format_exc())
2840 2840 return dummy
2841 2841
2842 2842 @hybrid_property
2843 2843 def changeset_cache(self):
2844 2844 return self._load_changeset_cache('', self._changeset_cache)
2845 2845
2846 2846 @changeset_cache.setter
2847 2847 def changeset_cache(self, val):
2848 2848 try:
2849 2849 self._changeset_cache = json.dumps(val)
2850 2850 except Exception:
2851 2851 log.error(traceback.format_exc())
2852 2852
2853 2853 @validates('group_parent_id')
2854 2854 def validate_group_parent_id(self, key, val):
2855 2855 """
2856 2856 Check cycle references for a parent group to self
2857 2857 """
2858 2858 if self.group_id and val:
2859 2859 assert val != self.group_id
2860 2860
2861 2861 return val
2862 2862
2863 2863 @hybrid_property
2864 2864 def description_safe(self):
2865 2865 from rhodecode.lib import helpers as h
2866 2866 return h.escape(self.group_description)
2867 2867
2868 2868 @classmethod
2869 2869 def hash_repo_group_name(cls, repo_group_name):
2870 2870 val = remove_formatting(repo_group_name)
2871 2871 val = safe_str(val).lower()
2872 2872 chars = []
2873 2873 for c in val:
2874 2874 if c not in string.ascii_letters:
2875 2875 c = str(ord(c))
2876 2876 chars.append(c)
2877 2877
2878 2878 return ''.join(chars)
2879 2879
2880 2880 @classmethod
2881 2881 def _generate_choice(cls, repo_group):
2882 2882 from webhelpers2.html import literal as _literal
2883 2883
2884 2884 def _name(k):
2885 2885 return _literal(cls.CHOICES_SEPARATOR.join(k))
2886 2886
2887 2887 return repo_group.group_id, _name(repo_group.full_path_splitted)
2888 2888
2889 2889 @classmethod
2890 2890 def groups_choices(cls, groups=None, show_empty_group=True):
2891 2891 if not groups:
2892 2892 groups = cls.query().all()
2893 2893
2894 2894 repo_groups = []
2895 2895 if show_empty_group:
2896 2896 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2897 2897
2898 2898 repo_groups.extend([cls._generate_choice(x) for x in groups])
2899 2899
2900 2900 repo_groups = sorted(
2901 2901 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2902 2902 return repo_groups
2903 2903
2904 2904 @classmethod
2905 2905 def url_sep(cls):
2906 2906 return URL_SEP
2907 2907
2908 2908 @classmethod
2909 2909 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2910 2910 if case_insensitive:
2911 2911 gr = cls.query().filter(func.lower(cls.group_name)
2912 2912 == func.lower(group_name))
2913 2913 else:
2914 2914 gr = cls.query().filter(cls.group_name == group_name)
2915 2915 if cache:
2916 2916 name_key = _hash_key(group_name)
2917 2917 gr = gr.options(
2918 2918 FromCache("sql_cache_short", f"get_group_{name_key}"))
2919 2919 return gr.scalar()
2920 2920
2921 2921 @classmethod
2922 2922 def get_user_personal_repo_group(cls, user_id):
2923 2923 user = User.get(user_id)
2924 2924 if user.username == User.DEFAULT_USER:
2925 2925 return None
2926 2926
2927 2927 return cls.query()\
2928 2928 .filter(cls.personal == true()) \
2929 2929 .filter(cls.user == user) \
2930 2930 .order_by(cls.group_id.asc()) \
2931 2931 .first()
2932 2932
2933 2933 @classmethod
2934 2934 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2935 2935 case_insensitive=True):
2936 2936 q = RepoGroup.query()
2937 2937
2938 2938 if not isinstance(user_id, Optional):
2939 2939 q = q.filter(RepoGroup.user_id == user_id)
2940 2940
2941 2941 if not isinstance(group_id, Optional):
2942 2942 q = q.filter(RepoGroup.group_parent_id == group_id)
2943 2943
2944 2944 if case_insensitive:
2945 2945 q = q.order_by(func.lower(RepoGroup.group_name))
2946 2946 else:
2947 2947 q = q.order_by(RepoGroup.group_name)
2948 2948 return q.all()
2949 2949
2950 2950 @property
2951 2951 def parents(self, parents_recursion_limit=10):
2952 2952 groups = []
2953 2953 if self.parent_group is None:
2954 2954 return groups
2955 2955 cur_gr = self.parent_group
2956 2956 groups.insert(0, cur_gr)
2957 2957 cnt = 0
2958 2958 while 1:
2959 2959 cnt += 1
2960 2960 gr = getattr(cur_gr, 'parent_group', None)
2961 2961 cur_gr = cur_gr.parent_group
2962 2962 if gr is None:
2963 2963 break
2964 2964 if cnt == parents_recursion_limit:
2965 2965 # this will prevent accidental infinit loops
2966 2966 log.error('more than %s parents found for group %s, stopping '
2967 2967 'recursive parent fetching', parents_recursion_limit, self)
2968 2968 break
2969 2969
2970 2970 groups.insert(0, gr)
2971 2971 return groups
2972 2972
2973 2973 @property
2974 2974 def last_commit_cache_update_diff(self):
2975 2975 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2976 2976
2977 2977 @classmethod
2978 2978 def _load_commit_change(cls, last_commit_cache):
2979 2979 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2980 2980 empty_date = datetime.datetime.fromtimestamp(0)
2981 2981 date_latest = last_commit_cache.get('date', empty_date)
2982 2982 try:
2983 2983 return parse_datetime(date_latest)
2984 2984 except Exception:
2985 2985 return empty_date
2986 2986
2987 2987 @property
2988 2988 def last_commit_change(self):
2989 2989 return self._load_commit_change(self.changeset_cache)
2990 2990
2991 2991 @property
2992 2992 def last_db_change(self):
2993 2993 return self.updated_on
2994 2994
2995 2995 @property
2996 2996 def children(self):
2997 2997 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2998 2998
2999 2999 @property
3000 3000 def name(self):
3001 3001 return self.group_name.split(RepoGroup.url_sep())[-1]
3002 3002
3003 3003 @property
3004 3004 def full_path(self):
3005 3005 return self.group_name
3006 3006
3007 3007 @property
3008 3008 def full_path_splitted(self):
3009 3009 return self.group_name.split(RepoGroup.url_sep())
3010 3010
3011 3011 @property
3012 3012 def repositories(self):
3013 3013 return Repository.query()\
3014 3014 .filter(Repository.group == self)\
3015 3015 .order_by(Repository.repo_name)
3016 3016
3017 3017 @property
3018 3018 def repositories_recursive_count(self):
3019 3019 cnt = self.repositories.count()
3020 3020
3021 3021 def children_count(group):
3022 3022 cnt = 0
3023 3023 for child in group.children:
3024 3024 cnt += child.repositories.count()
3025 3025 cnt += children_count(child)
3026 3026 return cnt
3027 3027
3028 3028 return cnt + children_count(self)
3029 3029
3030 3030 def _recursive_objects(self, include_repos=True, include_groups=True):
3031 3031 all_ = []
3032 3032
3033 3033 def _get_members(root_gr):
3034 3034 if include_repos:
3035 3035 for r in root_gr.repositories:
3036 3036 all_.append(r)
3037 3037 childs = root_gr.children.all()
3038 3038 if childs:
3039 3039 for gr in childs:
3040 3040 if include_groups:
3041 3041 all_.append(gr)
3042 3042 _get_members(gr)
3043 3043
3044 3044 root_group = []
3045 3045 if include_groups:
3046 3046 root_group = [self]
3047 3047
3048 3048 _get_members(self)
3049 3049 return root_group + all_
3050 3050
3051 3051 def recursive_groups_and_repos(self):
3052 3052 """
3053 3053 Recursive return all groups, with repositories in those groups
3054 3054 """
3055 3055 return self._recursive_objects()
3056 3056
3057 3057 def recursive_groups(self):
3058 3058 """
3059 3059 Returns all children groups for this group including children of children
3060 3060 """
3061 3061 return self._recursive_objects(include_repos=False)
3062 3062
3063 3063 def recursive_repos(self):
3064 3064 """
3065 3065 Returns all children repositories for this group
3066 3066 """
3067 3067 return self._recursive_objects(include_groups=False)
3068 3068
3069 3069 def get_new_name(self, group_name):
3070 3070 """
3071 3071 returns new full group name based on parent and new name
3072 3072
3073 3073 :param group_name:
3074 3074 """
3075 3075 path_prefix = (self.parent_group.full_path_splitted if
3076 3076 self.parent_group else [])
3077 3077 return RepoGroup.url_sep().join(path_prefix + [group_name])
3078 3078
3079 3079 def update_commit_cache(self, config=None):
3080 3080 """
3081 3081 Update cache of last commit for newest repository inside this repository group.
3082 3082 cache_keys should be::
3083 3083
3084 3084 source_repo_id
3085 3085 short_id
3086 3086 raw_id
3087 3087 revision
3088 3088 parents
3089 3089 message
3090 3090 date
3091 3091 author
3092 3092
3093 3093 """
3094 3094 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3095 3095 empty_date = datetime.datetime.fromtimestamp(0)
3096 3096
3097 3097 def repo_groups_and_repos(root_gr):
3098 3098 for _repo in root_gr.repositories:
3099 3099 yield _repo
3100 3100 for child_group in root_gr.children.all():
3101 3101 yield child_group
3102 3102
3103 3103 latest_repo_cs_cache = {}
3104 3104 for obj in repo_groups_and_repos(self):
3105 3105 repo_cs_cache = obj.changeset_cache
3106 3106 date_latest = latest_repo_cs_cache.get('date', empty_date)
3107 3107 date_current = repo_cs_cache.get('date', empty_date)
3108 3108 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3109 3109 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3110 3110 latest_repo_cs_cache = repo_cs_cache
3111 3111 if hasattr(obj, 'repo_id'):
3112 3112 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3113 3113 else:
3114 3114 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3115 3115
3116 3116 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3117 3117
3118 3118 latest_repo_cs_cache['updated_on'] = time.time()
3119 3119 self.changeset_cache = latest_repo_cs_cache
3120 3120 self.updated_on = _date_latest
3121 3121 Session().add(self)
3122 3122 Session().commit()
3123 3123
3124 3124 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3125 3125 self.group_name, latest_repo_cs_cache, _date_latest)
3126 3126
3127 3127 def permissions(self, with_admins=True, with_owner=True,
3128 3128 expand_from_user_groups=False):
3129 3129 """
3130 3130 Permissions for repository groups
3131 3131 """
3132 3132 _admin_perm = 'group.admin'
3133 3133
3134 3134 owner_row = []
3135 3135 if with_owner:
3136 3136 usr = AttributeDict(self.user.get_dict())
3137 3137 usr.owner_row = True
3138 3138 usr.permission = _admin_perm
3139 3139 owner_row.append(usr)
3140 3140
3141 3141 super_admin_ids = []
3142 3142 super_admin_rows = []
3143 3143 if with_admins:
3144 3144 for usr in User.get_all_super_admins():
3145 3145 super_admin_ids.append(usr.user_id)
3146 3146 # if this admin is also owner, don't double the record
3147 3147 if usr.user_id == owner_row[0].user_id:
3148 3148 owner_row[0].admin_row = True
3149 3149 else:
3150 3150 usr = AttributeDict(usr.get_dict())
3151 3151 usr.admin_row = True
3152 3152 usr.permission = _admin_perm
3153 3153 super_admin_rows.append(usr)
3154 3154
3155 3155 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3156 3156 q = q.options(joinedload(UserRepoGroupToPerm.group),
3157 3157 joinedload(UserRepoGroupToPerm.user),
3158 3158 joinedload(UserRepoGroupToPerm.permission),)
3159 3159
3160 3160 # get owners and admins and permissions. We do a trick of re-writing
3161 3161 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3162 3162 # has a global reference and changing one object propagates to all
3163 3163 # others. This means if admin is also an owner admin_row that change
3164 3164 # would propagate to both objects
3165 3165 perm_rows = []
3166 3166 for _usr in q.all():
3167 3167 usr = AttributeDict(_usr.user.get_dict())
3168 3168 # if this user is also owner/admin, mark as duplicate record
3169 3169 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3170 3170 usr.duplicate_perm = True
3171 3171 usr.permission = _usr.permission.permission_name
3172 3172 perm_rows.append(usr)
3173 3173
3174 3174 # filter the perm rows by 'default' first and then sort them by
3175 3175 # admin,write,read,none permissions sorted again alphabetically in
3176 3176 # each group
3177 3177 perm_rows = sorted(perm_rows, key=display_user_sort)
3178 3178
3179 3179 user_groups_rows = []
3180 3180 if expand_from_user_groups:
3181 3181 for ug in self.permission_user_groups(with_members=True):
3182 3182 for user_data in ug.members:
3183 3183 user_groups_rows.append(user_data)
3184 3184
3185 3185 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3186 3186
3187 3187 def permission_user_groups(self, with_members=False):
3188 3188 q = UserGroupRepoGroupToPerm.query()\
3189 3189 .filter(UserGroupRepoGroupToPerm.group == self)
3190 3190 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3191 3191 joinedload(UserGroupRepoGroupToPerm.users_group),
3192 3192 joinedload(UserGroupRepoGroupToPerm.permission),)
3193 3193
3194 3194 perm_rows = []
3195 3195 for _user_group in q.all():
3196 3196 entry = AttributeDict(_user_group.users_group.get_dict())
3197 3197 entry.permission = _user_group.permission.permission_name
3198 3198 if with_members:
3199 3199 entry.members = [x.user.get_dict()
3200 3200 for x in _user_group.users_group.members]
3201 3201 perm_rows.append(entry)
3202 3202
3203 3203 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3204 3204 return perm_rows
3205 3205
3206 3206 def get_api_data(self):
3207 3207 """
3208 3208 Common function for generating api data
3209 3209
3210 3210 """
3211 3211 group = self
3212 3212 data = {
3213 3213 'group_id': group.group_id,
3214 3214 'group_name': group.group_name,
3215 3215 'group_description': group.description_safe,
3216 3216 'parent_group': group.parent_group.group_name if group.parent_group else None,
3217 3217 'repositories': [x.repo_name for x in group.repositories],
3218 3218 'owner': group.user.username,
3219 3219 }
3220 3220 return data
3221 3221
3222 3222 def get_dict(self):
3223 3223 # Since we transformed `group_name` to a hybrid property, we need to
3224 3224 # keep compatibility with the code which uses `group_name` field.
3225 3225 result = super(RepoGroup, self).get_dict()
3226 3226 result['group_name'] = result.pop('_group_name', None)
3227 3227 result.pop('_changeset_cache', '')
3228 3228 return result
3229 3229
3230 3230
3231 3231 class Permission(Base, BaseModel):
3232 3232 __tablename__ = 'permissions'
3233 3233 __table_args__ = (
3234 3234 Index('p_perm_name_idx', 'permission_name'),
3235 3235 base_table_args,
3236 3236 )
3237 3237
3238 3238 PERMS = [
3239 3239 ('hg.admin', _('RhodeCode Super Administrator')),
3240 3240
3241 3241 ('repository.none', _('Repository no access')),
3242 3242 ('repository.read', _('Repository read access')),
3243 3243 ('repository.write', _('Repository write access')),
3244 3244 ('repository.admin', _('Repository admin access')),
3245 3245
3246 3246 ('group.none', _('Repository group no access')),
3247 3247 ('group.read', _('Repository group read access')),
3248 3248 ('group.write', _('Repository group write access')),
3249 3249 ('group.admin', _('Repository group admin access')),
3250 3250
3251 3251 ('usergroup.none', _('User group no access')),
3252 3252 ('usergroup.read', _('User group read access')),
3253 3253 ('usergroup.write', _('User group write access')),
3254 3254 ('usergroup.admin', _('User group admin access')),
3255 3255
3256 3256 ('branch.none', _('Branch no permissions')),
3257 3257 ('branch.merge', _('Branch access by web merge')),
3258 3258 ('branch.push', _('Branch access by push')),
3259 3259 ('branch.push_force', _('Branch access by push with force')),
3260 3260
3261 3261 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3262 3262 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3263 3263
3264 3264 ('hg.usergroup.create.false', _('User Group creation disabled')),
3265 3265 ('hg.usergroup.create.true', _('User Group creation enabled')),
3266 3266
3267 3267 ('hg.create.none', _('Repository creation disabled')),
3268 3268 ('hg.create.repository', _('Repository creation enabled')),
3269 3269 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3270 3270 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3271 3271
3272 3272 ('hg.fork.none', _('Repository forking disabled')),
3273 3273 ('hg.fork.repository', _('Repository forking enabled')),
3274 3274
3275 3275 ('hg.register.none', _('Registration disabled')),
3276 3276 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3277 3277 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3278 3278
3279 3279 ('hg.password_reset.enabled', _('Password reset enabled')),
3280 3280 ('hg.password_reset.hidden', _('Password reset hidden')),
3281 3281 ('hg.password_reset.disabled', _('Password reset disabled')),
3282 3282
3283 3283 ('hg.extern_activate.manual', _('Manual activation of external account')),
3284 3284 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3285 3285
3286 3286 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3287 3287 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3288 3288 ]
3289 3289
3290 3290 # definition of system default permissions for DEFAULT user, created on
3291 3291 # system setup
3292 3292 DEFAULT_USER_PERMISSIONS = [
3293 3293 # object perms
3294 3294 'repository.read',
3295 3295 'group.read',
3296 3296 'usergroup.read',
3297 3297 # branch, for backward compat we need same value as before so forced pushed
3298 3298 'branch.push_force',
3299 3299 # global
3300 3300 'hg.create.repository',
3301 3301 'hg.repogroup.create.false',
3302 3302 'hg.usergroup.create.false',
3303 3303 'hg.create.write_on_repogroup.true',
3304 3304 'hg.fork.repository',
3305 3305 'hg.register.manual_activate',
3306 3306 'hg.password_reset.enabled',
3307 3307 'hg.extern_activate.auto',
3308 3308 'hg.inherit_default_perms.true',
3309 3309 ]
3310 3310
3311 3311 # defines which permissions are more important higher the more important
3312 3312 # Weight defines which permissions are more important.
3313 3313 # The higher number the more important.
3314 3314 PERM_WEIGHTS = {
3315 3315 'repository.none': 0,
3316 3316 'repository.read': 1,
3317 3317 'repository.write': 3,
3318 3318 'repository.admin': 4,
3319 3319
3320 3320 'group.none': 0,
3321 3321 'group.read': 1,
3322 3322 'group.write': 3,
3323 3323 'group.admin': 4,
3324 3324
3325 3325 'usergroup.none': 0,
3326 3326 'usergroup.read': 1,
3327 3327 'usergroup.write': 3,
3328 3328 'usergroup.admin': 4,
3329 3329
3330 3330 'branch.none': 0,
3331 3331 'branch.merge': 1,
3332 3332 'branch.push': 3,
3333 3333 'branch.push_force': 4,
3334 3334
3335 3335 'hg.repogroup.create.false': 0,
3336 3336 'hg.repogroup.create.true': 1,
3337 3337
3338 3338 'hg.usergroup.create.false': 0,
3339 3339 'hg.usergroup.create.true': 1,
3340 3340
3341 3341 'hg.fork.none': 0,
3342 3342 'hg.fork.repository': 1,
3343 3343 'hg.create.none': 0,
3344 3344 'hg.create.repository': 1
3345 3345 }
3346 3346
3347 3347 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3348 3348 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3349 3349 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3350 3350
3351 3351 def __repr__(self):
3352 3352 return "<%s('%s:%s')>" % (
3353 3353 self.cls_name, self.permission_id, self.permission_name
3354 3354 )
3355 3355
3356 3356 @classmethod
3357 3357 def get_by_key(cls, key):
3358 3358 return cls.query().filter(cls.permission_name == key).scalar()
3359 3359
3360 3360 @classmethod
3361 3361 def get_default_repo_perms(cls, user_id, repo_id=None):
3362 3362 q = Session().query(UserRepoToPerm, Repository, Permission)\
3363 3363 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3364 3364 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3365 3365 .filter(UserRepoToPerm.user_id == user_id)
3366 3366 if repo_id:
3367 3367 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3368 3368 return q.all()
3369 3369
3370 3370 @classmethod
3371 3371 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3372 3372 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3373 3373 .join(
3374 3374 Permission,
3375 3375 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3376 3376 .join(
3377 3377 UserRepoToPerm,
3378 3378 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3379 3379 .filter(UserRepoToPerm.user_id == user_id)
3380 3380
3381 3381 if repo_id:
3382 3382 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3383 3383 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3384 3384
3385 3385 @classmethod
3386 3386 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3387 3387 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3388 3388 .join(
3389 3389 Permission,
3390 3390 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3391 3391 .join(
3392 3392 Repository,
3393 3393 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3394 3394 .join(
3395 3395 UserGroup,
3396 3396 UserGroupRepoToPerm.users_group_id ==
3397 3397 UserGroup.users_group_id)\
3398 3398 .join(
3399 3399 UserGroupMember,
3400 3400 UserGroupRepoToPerm.users_group_id ==
3401 3401 UserGroupMember.users_group_id)\
3402 3402 .filter(
3403 3403 UserGroupMember.user_id == user_id,
3404 3404 UserGroup.users_group_active == true())
3405 3405 if repo_id:
3406 3406 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3407 3407 return q.all()
3408 3408
3409 3409 @classmethod
3410 3410 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3411 3411 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3412 3412 .join(
3413 3413 Permission,
3414 3414 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3415 3415 .join(
3416 3416 UserGroupRepoToPerm,
3417 3417 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3418 3418 .join(
3419 3419 UserGroup,
3420 3420 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3421 3421 .join(
3422 3422 UserGroupMember,
3423 3423 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3424 3424 .filter(
3425 3425 UserGroupMember.user_id == user_id,
3426 3426 UserGroup.users_group_active == true())
3427 3427
3428 3428 if repo_id:
3429 3429 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3430 3430 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3431 3431
3432 3432 @classmethod
3433 3433 def get_default_group_perms(cls, user_id, repo_group_id=None):
3434 3434 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3435 3435 .join(
3436 3436 Permission,
3437 3437 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3438 3438 .join(
3439 3439 RepoGroup,
3440 3440 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3441 3441 .filter(UserRepoGroupToPerm.user_id == user_id)
3442 3442 if repo_group_id:
3443 3443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3444 3444 return q.all()
3445 3445
3446 3446 @classmethod
3447 3447 def get_default_group_perms_from_user_group(
3448 3448 cls, user_id, repo_group_id=None):
3449 3449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3450 3450 .join(
3451 3451 Permission,
3452 3452 UserGroupRepoGroupToPerm.permission_id ==
3453 3453 Permission.permission_id)\
3454 3454 .join(
3455 3455 RepoGroup,
3456 3456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3457 3457 .join(
3458 3458 UserGroup,
3459 3459 UserGroupRepoGroupToPerm.users_group_id ==
3460 3460 UserGroup.users_group_id)\
3461 3461 .join(
3462 3462 UserGroupMember,
3463 3463 UserGroupRepoGroupToPerm.users_group_id ==
3464 3464 UserGroupMember.users_group_id)\
3465 3465 .filter(
3466 3466 UserGroupMember.user_id == user_id,
3467 3467 UserGroup.users_group_active == true())
3468 3468 if repo_group_id:
3469 3469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3470 3470 return q.all()
3471 3471
3472 3472 @classmethod
3473 3473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3474 3474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3475 3475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3476 3476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3477 3477 .filter(UserUserGroupToPerm.user_id == user_id)
3478 3478 if user_group_id:
3479 3479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3480 3480 return q.all()
3481 3481
3482 3482 @classmethod
3483 3483 def get_default_user_group_perms_from_user_group(
3484 3484 cls, user_id, user_group_id=None):
3485 3485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3486 3486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3487 3487 .join(
3488 3488 Permission,
3489 3489 UserGroupUserGroupToPerm.permission_id ==
3490 3490 Permission.permission_id)\
3491 3491 .join(
3492 3492 TargetUserGroup,
3493 3493 UserGroupUserGroupToPerm.target_user_group_id ==
3494 3494 TargetUserGroup.users_group_id)\
3495 3495 .join(
3496 3496 UserGroup,
3497 3497 UserGroupUserGroupToPerm.user_group_id ==
3498 3498 UserGroup.users_group_id)\
3499 3499 .join(
3500 3500 UserGroupMember,
3501 3501 UserGroupUserGroupToPerm.user_group_id ==
3502 3502 UserGroupMember.users_group_id)\
3503 3503 .filter(
3504 3504 UserGroupMember.user_id == user_id,
3505 3505 UserGroup.users_group_active == true())
3506 3506 if user_group_id:
3507 3507 q = q.filter(
3508 3508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3509 3509
3510 3510 return q.all()
3511 3511
3512 3512
3513 3513 class UserRepoToPerm(Base, BaseModel):
3514 3514 __tablename__ = 'repo_to_perm'
3515 3515 __table_args__ = (
3516 3516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3517 3517 base_table_args
3518 3518 )
3519 3519
3520 3520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3521 3521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3522 3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 3523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3524 3524
3525 3525 user = relationship('User', back_populates="repo_to_perm")
3526 3526 repository = relationship('Repository', back_populates="repo_to_perm")
3527 3527 permission = relationship('Permission')
3528 3528
3529 3529 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3530 3530
3531 3531 @classmethod
3532 3532 def create(cls, user, repository, permission):
3533 3533 n = cls()
3534 3534 n.user = user
3535 3535 n.repository = repository
3536 3536 n.permission = permission
3537 3537 Session().add(n)
3538 3538 return n
3539 3539
3540 3540 def __repr__(self):
3541 3541 return f'<{self.user} => {self.repository} >'
3542 3542
3543 3543
3544 3544 class UserUserGroupToPerm(Base, BaseModel):
3545 3545 __tablename__ = 'user_user_group_to_perm'
3546 3546 __table_args__ = (
3547 3547 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3548 3548 base_table_args
3549 3549 )
3550 3550
3551 3551 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3552 3552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3553 3553 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3554 3554 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3555 3555
3556 3556 user = relationship('User', back_populates='user_group_to_perm')
3557 3557 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3558 3558 permission = relationship('Permission')
3559 3559
3560 3560 @classmethod
3561 3561 def create(cls, user, user_group, permission):
3562 3562 n = cls()
3563 3563 n.user = user
3564 3564 n.user_group = user_group
3565 3565 n.permission = permission
3566 3566 Session().add(n)
3567 3567 return n
3568 3568
3569 3569 def __repr__(self):
3570 3570 return f'<{self.user} => {self.user_group} >'
3571 3571
3572 3572
3573 3573 class UserToPerm(Base, BaseModel):
3574 3574 __tablename__ = 'user_to_perm'
3575 3575 __table_args__ = (
3576 3576 UniqueConstraint('user_id', 'permission_id'),
3577 3577 base_table_args
3578 3578 )
3579 3579
3580 3580 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3581 3581 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3582 3582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3583 3583
3584 3584 user = relationship('User', back_populates='user_perms')
3585 3585 permission = relationship('Permission', lazy='joined')
3586 3586
3587 3587 def __repr__(self):
3588 3588 return f'<{self.user} => {self.permission} >'
3589 3589
3590 3590
3591 3591 class UserGroupRepoToPerm(Base, BaseModel):
3592 3592 __tablename__ = 'users_group_repo_to_perm'
3593 3593 __table_args__ = (
3594 3594 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3595 3595 base_table_args
3596 3596 )
3597 3597
3598 3598 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3599 3599 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3600 3600 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3601 3601 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3602 3602
3603 3603 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3604 3604 permission = relationship('Permission')
3605 3605 repository = relationship('Repository', back_populates='users_group_to_perm')
3606 3606 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3607 3607
3608 3608 @classmethod
3609 3609 def create(cls, users_group, repository, permission):
3610 3610 n = cls()
3611 3611 n.users_group = users_group
3612 3612 n.repository = repository
3613 3613 n.permission = permission
3614 3614 Session().add(n)
3615 3615 return n
3616 3616
3617 3617 def __repr__(self):
3618 3618 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3619 3619
3620 3620
3621 3621 class UserGroupUserGroupToPerm(Base, BaseModel):
3622 3622 __tablename__ = 'user_group_user_group_to_perm'
3623 3623 __table_args__ = (
3624 3624 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3625 3625 CheckConstraint('target_user_group_id != user_group_id'),
3626 3626 base_table_args
3627 3627 )
3628 3628
3629 3629 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)
3630 3630 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3631 3631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3632 3632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3633 3633
3634 3634 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3635 3635 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3636 3636 permission = relationship('Permission')
3637 3637
3638 3638 @classmethod
3639 3639 def create(cls, target_user_group, user_group, permission):
3640 3640 n = cls()
3641 3641 n.target_user_group = target_user_group
3642 3642 n.user_group = user_group
3643 3643 n.permission = permission
3644 3644 Session().add(n)
3645 3645 return n
3646 3646
3647 3647 def __repr__(self):
3648 3648 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3649 3649
3650 3650
3651 3651 class UserGroupToPerm(Base, BaseModel):
3652 3652 __tablename__ = 'users_group_to_perm'
3653 3653 __table_args__ = (
3654 3654 UniqueConstraint('users_group_id', 'permission_id',),
3655 3655 base_table_args
3656 3656 )
3657 3657
3658 3658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3659 3659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3660 3660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3661 3661
3662 3662 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3663 3663 permission = relationship('Permission')
3664 3664
3665 3665
3666 3666 class UserRepoGroupToPerm(Base, BaseModel):
3667 3667 __tablename__ = 'user_repo_group_to_perm'
3668 3668 __table_args__ = (
3669 3669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3670 3670 base_table_args
3671 3671 )
3672 3672
3673 3673 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3674 3674 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3675 3675 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3676 3676 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3677 3677
3678 3678 user = relationship('User', back_populates='repo_group_to_perm')
3679 3679 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3680 3680 permission = relationship('Permission')
3681 3681
3682 3682 @classmethod
3683 3683 def create(cls, user, repository_group, permission):
3684 3684 n = cls()
3685 3685 n.user = user
3686 3686 n.group = repository_group
3687 3687 n.permission = permission
3688 3688 Session().add(n)
3689 3689 return n
3690 3690
3691 3691
3692 3692 class UserGroupRepoGroupToPerm(Base, BaseModel):
3693 3693 __tablename__ = 'users_group_repo_group_to_perm'
3694 3694 __table_args__ = (
3695 3695 UniqueConstraint('users_group_id', 'group_id'),
3696 3696 base_table_args
3697 3697 )
3698 3698
3699 3699 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)
3700 3700 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3701 3701 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3702 3702 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3703 3703
3704 3704 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3705 3705 permission = relationship('Permission')
3706 3706 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3707 3707
3708 3708 @classmethod
3709 3709 def create(cls, user_group, repository_group, permission):
3710 3710 n = cls()
3711 3711 n.users_group = user_group
3712 3712 n.group = repository_group
3713 3713 n.permission = permission
3714 3714 Session().add(n)
3715 3715 return n
3716 3716
3717 3717 def __repr__(self):
3718 3718 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3719 3719
3720 3720
3721 3721 class Statistics(Base, BaseModel):
3722 3722 __tablename__ = 'statistics'
3723 3723 __table_args__ = (
3724 3724 base_table_args
3725 3725 )
3726 3726
3727 3727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3728 3728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3729 3729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3730 3730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3731 3731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3732 3732 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3733 3733
3734 3734 repository = relationship('Repository', single_parent=True, viewonly=True)
3735 3735
3736 3736
3737 3737 class UserFollowing(Base, BaseModel):
3738 3738 __tablename__ = 'user_followings'
3739 3739 __table_args__ = (
3740 3740 UniqueConstraint('user_id', 'follows_repository_id'),
3741 3741 UniqueConstraint('user_id', 'follows_user_id'),
3742 3742 base_table_args
3743 3743 )
3744 3744
3745 3745 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3746 3746 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3747 3747 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3748 3748 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3749 3749 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3750 3750
3751 3751 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3752 3752
3753 3753 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3754 3754 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3755 3755
3756 3756 @classmethod
3757 3757 def get_repo_followers(cls, repo_id):
3758 3758 return cls.query().filter(cls.follows_repo_id == repo_id)
3759 3759
3760 3760
3761 3761 class CacheKey(Base, BaseModel):
3762 3762 __tablename__ = 'cache_invalidation'
3763 3763 __table_args__ = (
3764 3764 UniqueConstraint('cache_key'),
3765 3765 Index('key_idx', 'cache_key'),
3766 3766 Index('cache_args_idx', 'cache_args'),
3767 3767 base_table_args,
3768 3768 )
3769 3769
3770 3770 CACHE_TYPE_FEED = 'FEED'
3771 3771
3772 3772 # namespaces used to register process/thread aware caches
3773 3773 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3774 3774
3775 3775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3776 3776 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3777 3777 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3778 3778 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3779 3779 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3780 3780
3781 3781 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3782 3782 self.cache_key = cache_key
3783 3783 self.cache_args = cache_args
3784 3784 self.cache_active = cache_active
3785 3785 # first key should be same for all entries, since all workers should share it
3786 3786 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3787 3787
3788 3788 def __repr__(self):
3789 3789 return "<%s('%s:%s[%s]')>" % (
3790 3790 self.cls_name,
3791 3791 self.cache_id, self.cache_key, self.cache_active)
3792 3792
3793 3793 def _cache_key_partition(self):
3794 3794 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3795 3795 return prefix, repo_name, suffix
3796 3796
3797 3797 def get_prefix(self):
3798 3798 """
3799 3799 Try to extract prefix from existing cache key. The key could consist
3800 3800 of prefix, repo_name, suffix
3801 3801 """
3802 3802 # this returns prefix, repo_name, suffix
3803 3803 return self._cache_key_partition()[0]
3804 3804
3805 3805 def get_suffix(self):
3806 3806 """
3807 3807 get suffix that might have been used in _get_cache_key to
3808 3808 generate self.cache_key. Only used for informational purposes
3809 3809 in repo_edit.mako.
3810 3810 """
3811 3811 # prefix, repo_name, suffix
3812 3812 return self._cache_key_partition()[2]
3813 3813
3814 3814 @classmethod
3815 3815 def generate_new_state_uid(cls, based_on=None):
3816 3816 if based_on:
3817 3817 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3818 3818 else:
3819 3819 return str(uuid.uuid4())
3820 3820
3821 3821 @classmethod
3822 3822 def delete_all_cache(cls):
3823 3823 """
3824 3824 Delete all cache keys from database.
3825 3825 Should only be run when all instances are down and all entries
3826 3826 thus stale.
3827 3827 """
3828 3828 cls.query().delete()
3829 3829 Session().commit()
3830 3830
3831 3831 @classmethod
3832 3832 def set_invalidate(cls, cache_uid, delete=False):
3833 3833 """
3834 3834 Mark all caches of a repo as invalid in the database.
3835 3835 """
3836 3836 try:
3837 3837 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3838 3838 if delete:
3839 3839 qry.delete()
3840 3840 log.debug('cache objects deleted for cache args %s',
3841 3841 safe_str(cache_uid))
3842 3842 else:
3843 3843 new_uid = cls.generate_new_state_uid()
3844 3844 qry.update({"cache_state_uid": new_uid,
3845 3845 "cache_args": f"repo_state:{time.time()}"})
3846 3846 log.debug('cache object %s set new UID %s',
3847 3847 safe_str(cache_uid), new_uid)
3848 3848
3849 3849 Session().commit()
3850 3850 except Exception:
3851 3851 log.exception(
3852 3852 'Cache key invalidation failed for cache args %s',
3853 3853 safe_str(cache_uid))
3854 3854 Session().rollback()
3855 3855
3856 3856 @classmethod
3857 3857 def get_active_cache(cls, cache_key):
3858 3858 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3859 3859 if inv_obj:
3860 3860 return inv_obj
3861 3861 return None
3862 3862
3863 3863 @classmethod
3864 3864 def get_namespace_map(cls, namespace):
3865 3865 return {
3866 3866 x.cache_key: x
3867 3867 for x in cls.query().filter(cls.cache_args == namespace)}
3868 3868
3869 3869
3870 3870 class ChangesetComment(Base, BaseModel):
3871 3871 __tablename__ = 'changeset_comments'
3872 3872 __table_args__ = (
3873 3873 Index('cc_revision_idx', 'revision'),
3874 3874 base_table_args,
3875 3875 )
3876 3876
3877 3877 COMMENT_OUTDATED = 'comment_outdated'
3878 3878 COMMENT_TYPE_NOTE = 'note'
3879 3879 COMMENT_TYPE_TODO = 'todo'
3880 3880 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3881 3881
3882 3882 OP_IMMUTABLE = 'immutable'
3883 3883 OP_CHANGEABLE = 'changeable'
3884 3884
3885 3885 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3886 3886 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3887 3887 revision = Column('revision', String(40), nullable=True)
3888 3888 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3889 3889 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3890 3890 line_no = Column('line_no', Unicode(10), nullable=True)
3891 3891 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3892 3892 f_path = Column('f_path', Unicode(1000), nullable=True)
3893 3893 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3894 3894 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3895 3895 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3896 3896 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3897 3897 renderer = Column('renderer', Unicode(64), nullable=True)
3898 3898 display_state = Column('display_state', Unicode(128), nullable=True)
3899 3899 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3900 3900 draft = Column('draft', Boolean(), nullable=True, default=False)
3901 3901
3902 3902 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3903 3903 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3904 3904
3905 3905 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3906 3906 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3907 3907
3908 3908 author = relationship('User', lazy='select', back_populates='user_comments')
3909 3909 repo = relationship('Repository', back_populates='comments')
3910 3910 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3911 3911 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3912 3912 pull_request_version = relationship('PullRequestVersion', lazy='select')
3913 3913 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3914 3914
3915 3915 @classmethod
3916 3916 def get_users(cls, revision=None, pull_request_id=None):
3917 3917 """
3918 3918 Returns user associated with this ChangesetComment. ie those
3919 3919 who actually commented
3920 3920
3921 3921 :param cls:
3922 3922 :param revision:
3923 3923 """
3924 3924 q = Session().query(User).join(ChangesetComment.author)
3925 3925 if revision:
3926 3926 q = q.filter(cls.revision == revision)
3927 3927 elif pull_request_id:
3928 3928 q = q.filter(cls.pull_request_id == pull_request_id)
3929 3929 return q.all()
3930 3930
3931 3931 @classmethod
3932 3932 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3933 3933 if pr_version is None:
3934 3934 return 0
3935 3935
3936 3936 if versions is not None:
3937 3937 num_versions = [x.pull_request_version_id for x in versions]
3938 3938
3939 3939 num_versions = num_versions or []
3940 3940 try:
3941 3941 return num_versions.index(pr_version) + 1
3942 3942 except (IndexError, ValueError):
3943 3943 return 0
3944 3944
3945 3945 @property
3946 3946 def outdated(self):
3947 3947 return self.display_state == self.COMMENT_OUTDATED
3948 3948
3949 3949 @property
3950 3950 def outdated_js(self):
3951 3951 return str_json(self.display_state == self.COMMENT_OUTDATED)
3952 3952
3953 3953 @property
3954 3954 def immutable(self):
3955 3955 return self.immutable_state == self.OP_IMMUTABLE
3956 3956
3957 3957 def outdated_at_version(self, version: int) -> bool:
3958 3958 """
3959 3959 Checks if comment is outdated for given pull request version
3960 3960 """
3961 3961
3962 3962 def version_check():
3963 3963 return self.pull_request_version_id and self.pull_request_version_id != version
3964 3964
3965 3965 if self.is_inline:
3966 3966 return self.outdated and version_check()
3967 3967 else:
3968 3968 # general comments don't have .outdated set, also latest don't have a version
3969 3969 return version_check()
3970 3970
3971 3971 def outdated_at_version_js(self, version):
3972 3972 """
3973 3973 Checks if comment is outdated for given pull request version
3974 3974 """
3975 3975 return str_json(self.outdated_at_version(version))
3976 3976
3977 3977 def older_than_version(self, version: int) -> bool:
3978 3978 """
3979 3979 Checks if comment is made from a previous version than given.
3980 3980 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3981 3981 """
3982 3982
3983 3983 # If version is None, return False as the current version cannot be less than None
3984 3984 if version is None:
3985 3985 return False
3986 3986
3987 3987 # Ensure that the version is an integer to prevent TypeError on comparison
3988 3988 if not isinstance(version, int):
3989 3989 raise ValueError("The provided version must be an integer.")
3990 3990
3991 3991 # Initialize current version to 0 or pull_request_version_id if it's available
3992 3992 cur_ver = 0
3993 3993 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3994 3994 cur_ver = self.pull_request_version.pull_request_version_id
3995 3995
3996 3996 # Return True if the current version is less than the given version
3997 3997 return cur_ver < version
3998 3998
3999 3999 def older_than_version_js(self, version):
4000 4000 """
4001 4001 Checks if comment is made from previous version than given
4002 4002 """
4003 4003 return str_json(self.older_than_version(version))
4004 4004
4005 4005 @property
4006 4006 def commit_id(self):
4007 4007 """New style naming to stop using .revision"""
4008 4008 return self.revision
4009 4009
4010 4010 @property
4011 4011 def resolved(self):
4012 4012 return self.resolved_by[0] if self.resolved_by else None
4013 4013
4014 4014 @property
4015 4015 def is_todo(self):
4016 4016 return self.comment_type == self.COMMENT_TYPE_TODO
4017 4017
4018 4018 @property
4019 4019 def is_inline(self):
4020 4020 if self.line_no and self.f_path:
4021 4021 return True
4022 4022 return False
4023 4023
4024 4024 @property
4025 4025 def last_version(self):
4026 4026 version = 0
4027 4027 if self.history:
4028 4028 version = self.history[-1].version
4029 4029 return version
4030 4030
4031 4031 def get_index_version(self, versions):
4032 4032 return self.get_index_from_version(
4033 4033 self.pull_request_version_id, versions)
4034 4034
4035 4035 @property
4036 4036 def review_status(self):
4037 4037 if self.status_change:
4038 4038 return self.status_change[0].status
4039 4039
4040 4040 @property
4041 4041 def review_status_lbl(self):
4042 4042 if self.status_change:
4043 4043 return self.status_change[0].status_lbl
4044 4044
4045 4045 def __repr__(self):
4046 4046 if self.comment_id:
4047 4047 return f'<DB:Comment #{self.comment_id}>'
4048 4048 else:
4049 4049 return f'<DB:Comment at {id(self)!r}>'
4050 4050
4051 4051 def get_api_data(self):
4052 4052 comment = self
4053 4053
4054 4054 data = {
4055 4055 'comment_id': comment.comment_id,
4056 4056 'comment_type': comment.comment_type,
4057 4057 'comment_text': comment.text,
4058 4058 'comment_status': comment.status_change,
4059 4059 'comment_f_path': comment.f_path,
4060 4060 'comment_lineno': comment.line_no,
4061 4061 'comment_author': comment.author,
4062 4062 'comment_created_on': comment.created_on,
4063 4063 'comment_resolved_by': self.resolved,
4064 4064 'comment_commit_id': comment.revision,
4065 4065 'comment_pull_request_id': comment.pull_request_id,
4066 4066 'comment_last_version': self.last_version
4067 4067 }
4068 4068 return data
4069 4069
4070 4070 def __json__(self):
4071 4071 data = dict()
4072 4072 data.update(self.get_api_data())
4073 4073 return data
4074 4074
4075 4075
4076 4076 class ChangesetCommentHistory(Base, BaseModel):
4077 4077 __tablename__ = 'changeset_comments_history'
4078 4078 __table_args__ = (
4079 4079 Index('cch_comment_id_idx', 'comment_id'),
4080 4080 base_table_args,
4081 4081 )
4082 4082
4083 4083 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4084 4084 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4085 4085 version = Column("version", Integer(), nullable=False, default=0)
4086 4086 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4087 4087 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4088 4088 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4089 4089 deleted = Column('deleted', Boolean(), default=False)
4090 4090
4091 4091 author = relationship('User', lazy='joined')
4092 4092 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4093 4093
4094 4094 @classmethod
4095 4095 def get_version(cls, comment_id):
4096 4096 q = Session().query(ChangesetCommentHistory).filter(
4097 4097 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4098 4098 if q.count() == 0:
4099 4099 return 1
4100 4100 elif q.count() >= q[0].version:
4101 4101 return q.count() + 1
4102 4102 else:
4103 4103 return q[0].version + 1
4104 4104
4105 4105
4106 4106 class ChangesetStatus(Base, BaseModel):
4107 4107 __tablename__ = 'changeset_statuses'
4108 4108 __table_args__ = (
4109 4109 Index('cs_revision_idx', 'revision'),
4110 4110 Index('cs_version_idx', 'version'),
4111 4111 UniqueConstraint('repo_id', 'revision', 'version'),
4112 4112 base_table_args
4113 4113 )
4114 4114
4115 4115 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4116 4116 STATUS_APPROVED = 'approved'
4117 4117 STATUS_REJECTED = 'rejected'
4118 4118 STATUS_UNDER_REVIEW = 'under_review'
4119 4119
4120 4120 STATUSES = [
4121 4121 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4122 4122 (STATUS_APPROVED, _("Approved")),
4123 4123 (STATUS_REJECTED, _("Rejected")),
4124 4124 (STATUS_UNDER_REVIEW, _("Under Review")),
4125 4125 ]
4126 4126
4127 4127 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4128 4128 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4129 4129 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4130 4130 revision = Column('revision', String(40), nullable=False)
4131 4131 status = Column('status', String(128), nullable=False, default=DEFAULT)
4132 4132 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4133 4133 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4134 4134 version = Column('version', Integer(), nullable=False, default=0)
4135 4135 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4136 4136
4137 4137 author = relationship('User', lazy='select')
4138 4138 repo = relationship('Repository', lazy='select')
4139 4139 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4140 4140 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4141 4141
4142 4142 def __repr__(self):
4143 4143 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4144 4144
4145 4145 @classmethod
4146 4146 def get_status_lbl(cls, value):
4147 4147 return dict(cls.STATUSES).get(value)
4148 4148
4149 4149 @property
4150 4150 def status_lbl(self):
4151 4151 return ChangesetStatus.get_status_lbl(self.status)
4152 4152
4153 4153 def get_api_data(self):
4154 4154 status = self
4155 4155 data = {
4156 4156 'status_id': status.changeset_status_id,
4157 4157 'status': status.status,
4158 4158 }
4159 4159 return data
4160 4160
4161 4161 def __json__(self):
4162 4162 data = dict()
4163 4163 data.update(self.get_api_data())
4164 4164 return data
4165 4165
4166 4166
4167 4167 class _SetState(object):
4168 4168 """
4169 4169 Context processor allowing changing state for sensitive operation such as
4170 4170 pull request update or merge
4171 4171 """
4172 4172
4173 4173 def __init__(self, pull_request, pr_state, back_state=None):
4174 4174 self._pr = pull_request
4175 4175 self._org_state = back_state or pull_request.pull_request_state
4176 4176 self._pr_state = pr_state
4177 4177 self._current_state = None
4178 4178
4179 4179 def __enter__(self):
4180 4180 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4181 4181 self._pr, self._pr_state)
4182 4182 self.set_pr_state(self._pr_state)
4183 4183 return self
4184 4184
4185 4185 def __exit__(self, exc_type, exc_val, exc_tb):
4186 4186 if exc_val is not None or exc_type is not None:
4187 4187 log.error(traceback.format_tb(exc_tb))
4188 4188 return None
4189 4189
4190 4190 self.set_pr_state(self._org_state)
4191 4191 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4192 4192 self._pr, self._org_state)
4193 4193
4194 4194 @property
4195 4195 def state(self):
4196 4196 return self._current_state
4197 4197
4198 4198 def set_pr_state(self, pr_state):
4199 4199 try:
4200 4200 self._pr.pull_request_state = pr_state
4201 4201 Session().add(self._pr)
4202 4202 Session().commit()
4203 4203 self._current_state = pr_state
4204 4204 except Exception:
4205 4205 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4206 4206 raise
4207 4207
4208 4208
4209 4209 class _PullRequestBase(BaseModel):
4210 4210 """
4211 4211 Common attributes of pull request and version entries.
4212 4212 """
4213 4213
4214 4214 # .status values
4215 4215 STATUS_NEW = 'new'
4216 4216 STATUS_OPEN = 'open'
4217 4217 STATUS_CLOSED = 'closed'
4218 4218
4219 4219 # available states
4220 4220 STATE_CREATING = 'creating'
4221 4221 STATE_UPDATING = 'updating'
4222 4222 STATE_MERGING = 'merging'
4223 4223 STATE_CREATED = 'created'
4224 4224
4225 4225 title = Column('title', Unicode(255), nullable=True)
4226 4226 description = Column(
4227 4227 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4228 4228 nullable=True)
4229 4229 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4230 4230
4231 4231 # new/open/closed status of pull request (not approve/reject/etc)
4232 4232 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4233 4233 created_on = Column(
4234 4234 'created_on', DateTime(timezone=False), nullable=False,
4235 4235 default=datetime.datetime.now)
4236 4236 updated_on = Column(
4237 4237 'updated_on', DateTime(timezone=False), nullable=False,
4238 4238 default=datetime.datetime.now)
4239 4239
4240 4240 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4241 4241
4242 4242 @declared_attr
4243 4243 def user_id(cls):
4244 4244 return Column(
4245 4245 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4246 4246 unique=None)
4247 4247
4248 4248 # 500 revisions max
4249 4249 _revisions = Column(
4250 4250 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4251 4251
4252 4252 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4253 4253
4254 4254 @declared_attr
4255 4255 def source_repo_id(cls):
4256 4256 # TODO: dan: rename column to source_repo_id
4257 4257 return Column(
4258 4258 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4259 4259 nullable=False)
4260 4260
4261 4261 @declared_attr
4262 4262 def pr_source(cls):
4263 4263 return relationship(
4264 4264 'Repository',
4265 4265 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4266 4266 overlaps="pull_requests_source"
4267 4267 )
4268 4268
4269 4269 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4270 4270
4271 4271 @hybrid_property
4272 4272 def source_ref(self):
4273 4273 return self._source_ref
4274 4274
4275 4275 @source_ref.setter
4276 4276 def source_ref(self, val):
4277 4277 parts = (val or '').split(':')
4278 4278 if len(parts) != 3:
4279 4279 raise ValueError(
4280 4280 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4281 4281 self._source_ref = safe_str(val)
4282 4282
4283 4283 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4284 4284
4285 4285 @hybrid_property
4286 4286 def target_ref(self):
4287 4287 return self._target_ref
4288 4288
4289 4289 @target_ref.setter
4290 4290 def target_ref(self, val):
4291 4291 parts = (val or '').split(':')
4292 4292 if len(parts) != 3:
4293 4293 raise ValueError(
4294 4294 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4295 4295 self._target_ref = safe_str(val)
4296 4296
4297 4297 @declared_attr
4298 4298 def target_repo_id(cls):
4299 4299 # TODO: dan: rename column to target_repo_id
4300 4300 return Column(
4301 4301 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4302 4302 nullable=False)
4303 4303
4304 4304 @declared_attr
4305 4305 def pr_target(cls):
4306 4306 return relationship(
4307 4307 'Repository',
4308 4308 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4309 4309 overlaps="pull_requests_target"
4310 4310 )
4311 4311
4312 4312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4313 4313
4314 4314 # TODO: dan: rename column to last_merge_source_rev
4315 4315 _last_merge_source_rev = Column(
4316 4316 'last_merge_org_rev', String(40), nullable=True)
4317 4317 # TODO: dan: rename column to last_merge_target_rev
4318 4318 _last_merge_target_rev = Column(
4319 4319 'last_merge_other_rev', String(40), nullable=True)
4320 4320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4321 4321 last_merge_metadata = Column(
4322 4322 'last_merge_metadata', MutationObj.as_mutable(
4323 4323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4324 4324
4325 4325 merge_rev = Column('merge_rev', String(40), nullable=True)
4326 4326
4327 4327 reviewer_data = Column(
4328 4328 'reviewer_data_json', MutationObj.as_mutable(
4329 4329 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4330 4330
4331 4331 @property
4332 4332 def reviewer_data_json(self):
4333 4333 return str_json(self.reviewer_data)
4334 4334
4335 4335 @property
4336 4336 def last_merge_metadata_parsed(self):
4337 4337 metadata = {}
4338 4338 if not self.last_merge_metadata:
4339 4339 return metadata
4340 4340
4341 4341 if hasattr(self.last_merge_metadata, 'de_coerce'):
4342 4342 for k, v in self.last_merge_metadata.de_coerce().items():
4343 4343 if k in ['target_ref', 'source_ref']:
4344 4344 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4345 4345 else:
4346 4346 if hasattr(v, 'de_coerce'):
4347 4347 metadata[k] = v.de_coerce()
4348 4348 else:
4349 4349 metadata[k] = v
4350 4350 return metadata
4351 4351
4352 4352 @property
4353 4353 def work_in_progress(self):
4354 4354 """checks if pull request is work in progress by checking the title"""
4355 4355 title = self.title.upper()
4356 4356 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4357 4357 return True
4358 4358 return False
4359 4359
4360 4360 @property
4361 4361 def title_safe(self):
4362 4362 return self.title\
4363 4363 .replace('{', '{{')\
4364 4364 .replace('}', '}}')
4365 4365
4366 4366 @hybrid_property
4367 4367 def description_safe(self):
4368 4368 from rhodecode.lib import helpers as h
4369 4369 return h.escape(self.description)
4370 4370
4371 4371 @hybrid_property
4372 4372 def revisions(self):
4373 4373 return self._revisions.split(':') if self._revisions else []
4374 4374
4375 4375 @revisions.setter
4376 4376 def revisions(self, val):
4377 4377 self._revisions = ':'.join(val)
4378 4378
4379 4379 @hybrid_property
4380 4380 def last_merge_status(self):
4381 4381 return safe_int(self._last_merge_status)
4382 4382
4383 4383 @last_merge_status.setter
4384 4384 def last_merge_status(self, val):
4385 4385 self._last_merge_status = val
4386 4386
4387 4387 @declared_attr
4388 4388 def author(cls):
4389 4389 return relationship(
4390 4390 'User', lazy='joined',
4391 4391 #TODO, problem that is somehow :?
4392 4392 #back_populates='user_pull_requests'
4393 4393 )
4394 4394
4395 4395 @declared_attr
4396 4396 def source_repo(cls):
4397 4397 return relationship(
4398 4398 'Repository',
4399 4399 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4400 4400 overlaps="pr_source"
4401 4401 )
4402 4402
4403 4403 @property
4404 4404 def source_ref_parts(self):
4405 4405 return self.unicode_to_reference(self.source_ref)
4406 4406
4407 4407 @declared_attr
4408 4408 def target_repo(cls):
4409 4409 return relationship(
4410 4410 'Repository',
4411 4411 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4412 4412 overlaps="pr_target"
4413 4413 )
4414 4414
4415 4415 @property
4416 4416 def target_ref_parts(self):
4417 4417 return self.unicode_to_reference(self.target_ref)
4418 4418
4419 4419 @property
4420 4420 def shadow_merge_ref(self):
4421 4421 return self.unicode_to_reference(self._shadow_merge_ref)
4422 4422
4423 4423 @shadow_merge_ref.setter
4424 4424 def shadow_merge_ref(self, ref):
4425 4425 self._shadow_merge_ref = self.reference_to_unicode(ref)
4426 4426
4427 4427 @staticmethod
4428 4428 def unicode_to_reference(raw):
4429 4429 return unicode_to_reference(raw)
4430 4430
4431 4431 @staticmethod
4432 4432 def reference_to_unicode(ref):
4433 4433 return reference_to_unicode(ref)
4434 4434
4435 4435 def get_api_data(self, with_merge_state=True):
4436 4436 from rhodecode.model.pull_request import PullRequestModel
4437 4437
4438 4438 pull_request = self
4439 4439 if with_merge_state:
4440 4440 merge_response, merge_status, msg = \
4441 4441 PullRequestModel().merge_status(pull_request)
4442 4442 merge_state = {
4443 4443 'status': merge_status,
4444 4444 'message': safe_str(msg),
4445 4445 }
4446 4446 else:
4447 4447 merge_state = {'status': 'not_available',
4448 4448 'message': 'not_available'}
4449 4449
4450 4450 merge_data = {
4451 4451 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4452 4452 'reference': (
4453 4453 pull_request.shadow_merge_ref.asdict()
4454 4454 if pull_request.shadow_merge_ref else None),
4455 4455 }
4456 4456
4457 4457 data = {
4458 4458 'pull_request_id': pull_request.pull_request_id,
4459 4459 'url': PullRequestModel().get_url(pull_request),
4460 4460 'title': pull_request.title,
4461 4461 'description': pull_request.description,
4462 4462 'status': pull_request.status,
4463 4463 'state': pull_request.pull_request_state,
4464 4464 'created_on': pull_request.created_on,
4465 4465 'updated_on': pull_request.updated_on,
4466 4466 'commit_ids': pull_request.revisions,
4467 4467 'review_status': pull_request.calculated_review_status(),
4468 4468 'mergeable': merge_state,
4469 4469 'source': {
4470 4470 'clone_url': pull_request.source_repo.clone_url(),
4471 4471 'repository': pull_request.source_repo.repo_name,
4472 4472 'reference': {
4473 4473 'name': pull_request.source_ref_parts.name,
4474 4474 'type': pull_request.source_ref_parts.type,
4475 4475 'commit_id': pull_request.source_ref_parts.commit_id,
4476 4476 },
4477 4477 },
4478 4478 'target': {
4479 4479 'clone_url': pull_request.target_repo.clone_url(),
4480 4480 'repository': pull_request.target_repo.repo_name,
4481 4481 'reference': {
4482 4482 'name': pull_request.target_ref_parts.name,
4483 4483 'type': pull_request.target_ref_parts.type,
4484 4484 'commit_id': pull_request.target_ref_parts.commit_id,
4485 4485 },
4486 4486 },
4487 4487 'merge': merge_data,
4488 4488 'author': pull_request.author.get_api_data(include_secrets=False,
4489 4489 details='basic'),
4490 4490 'reviewers': [
4491 4491 {
4492 4492 'user': reviewer.get_api_data(include_secrets=False,
4493 4493 details='basic'),
4494 4494 'reasons': reasons,
4495 4495 'review_status': st[0][1].status if st else 'not_reviewed',
4496 4496 }
4497 4497 for obj, reviewer, reasons, mandatory, st in
4498 4498 pull_request.reviewers_statuses()
4499 4499 ]
4500 4500 }
4501 4501
4502 4502 return data
4503 4503
4504 4504 def set_state(self, pull_request_state, final_state=None):
4505 4505 """
4506 4506 # goes from initial state to updating to initial state.
4507 4507 # initial state can be changed by specifying back_state=
4508 4508 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4509 4509 pull_request.merge()
4510 4510
4511 4511 :param pull_request_state:
4512 4512 :param final_state:
4513 4513
4514 4514 """
4515 4515
4516 4516 return _SetState(self, pull_request_state, back_state=final_state)
4517 4517
4518 4518
4519 4519 class PullRequest(Base, _PullRequestBase):
4520 4520 __tablename__ = 'pull_requests'
4521 4521 __table_args__ = (
4522 4522 base_table_args,
4523 4523 )
4524 4524 LATEST_VER = 'latest'
4525 4525
4526 4526 pull_request_id = Column(
4527 4527 'pull_request_id', Integer(), nullable=False, primary_key=True)
4528 4528
4529 4529 def __repr__(self):
4530 4530 if self.pull_request_id:
4531 4531 return f'<DB:PullRequest #{self.pull_request_id}>'
4532 4532 else:
4533 4533 return f'<DB:PullRequest at {id(self)!r}>'
4534 4534
4535 4535 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4536 4536 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4537 4537 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4538 4538 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4539 4539
4540 4540 @classmethod
4541 4541 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4542 4542 internal_methods=None):
4543 4543
4544 4544 class PullRequestDisplay(object):
4545 4545 """
4546 4546 Special object wrapper for showing PullRequest data via Versions
4547 4547 It mimics PR object as close as possible. This is read only object
4548 4548 just for display
4549 4549 """
4550 4550
4551 4551 def __init__(self, attrs, internal=None):
4552 4552 self.attrs = attrs
4553 4553 # internal have priority over the given ones via attrs
4554 4554 self.internal = internal or ['versions']
4555 4555
4556 4556 def __getattr__(self, item):
4557 4557 if item in self.internal:
4558 4558 return getattr(self, item)
4559 4559 try:
4560 4560 return self.attrs[item]
4561 4561 except KeyError:
4562 4562 raise AttributeError(
4563 4563 '%s object has no attribute %s' % (self, item))
4564 4564
4565 4565 def __repr__(self):
4566 4566 pr_id = self.attrs.get('pull_request_id')
4567 4567 return f'<DB:PullRequestDisplay #{pr_id}>'
4568 4568
4569 4569 def versions(self):
4570 4570 return pull_request_obj.versions.order_by(
4571 4571 PullRequestVersion.pull_request_version_id).all()
4572 4572
4573 4573 def is_closed(self):
4574 4574 return pull_request_obj.is_closed()
4575 4575
4576 4576 def is_state_changing(self):
4577 4577 return pull_request_obj.is_state_changing()
4578 4578
4579 4579 @property
4580 4580 def pull_request_version_id(self):
4581 4581 return getattr(pull_request_obj, 'pull_request_version_id', None)
4582 4582
4583 4583 @property
4584 4584 def pull_request_last_version(self):
4585 4585 return pull_request_obj.pull_request_last_version
4586 4586
4587 4587 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4588 4588
4589 4589 attrs.author = StrictAttributeDict(
4590 4590 pull_request_obj.author.get_api_data())
4591 4591 if pull_request_obj.target_repo:
4592 4592 attrs.target_repo = StrictAttributeDict(
4593 4593 pull_request_obj.target_repo.get_api_data())
4594 4594 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4595 4595
4596 4596 if pull_request_obj.source_repo:
4597 4597 attrs.source_repo = StrictAttributeDict(
4598 4598 pull_request_obj.source_repo.get_api_data())
4599 4599 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4600 4600
4601 4601 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4602 4602 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4603 4603 attrs.revisions = pull_request_obj.revisions
4604 4604 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4605 4605 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4606 4606 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4607 4607 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4608 4608
4609 4609 return PullRequestDisplay(attrs, internal=internal_methods)
4610 4610
4611 4611 def is_closed(self):
4612 4612 return self.status == self.STATUS_CLOSED
4613 4613
4614 4614 def is_state_changing(self):
4615 4615 return self.pull_request_state != PullRequest.STATE_CREATED
4616 4616
4617 4617 def __json__(self):
4618 4618 return {
4619 4619 'revisions': self.revisions,
4620 4620 'versions': self.versions_count
4621 4621 }
4622 4622
4623 4623 def calculated_review_status(self):
4624 4624 from rhodecode.model.changeset_status import ChangesetStatusModel
4625 4625 return ChangesetStatusModel().calculated_review_status(self)
4626 4626
4627 4627 def reviewers_statuses(self, user=None):
4628 4628 from rhodecode.model.changeset_status import ChangesetStatusModel
4629 4629 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4630 4630
4631 4631 def get_pull_request_reviewers(self, role=None):
4632 4632 qry = PullRequestReviewers.query()\
4633 4633 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4634 4634 if role:
4635 4635 qry = qry.filter(PullRequestReviewers.role == role)
4636 4636
4637 4637 return qry.all()
4638 4638
4639 4639 @property
4640 4640 def reviewers_count(self):
4641 4641 qry = PullRequestReviewers.query()\
4642 4642 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4643 4643 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4644 4644 return qry.count()
4645 4645
4646 4646 @property
4647 4647 def observers_count(self):
4648 4648 qry = PullRequestReviewers.query()\
4649 4649 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4650 4650 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4651 4651 return qry.count()
4652 4652
4653 4653 def observers(self):
4654 4654 qry = PullRequestReviewers.query()\
4655 4655 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4656 4656 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4657 4657 .all()
4658 4658
4659 4659 for entry in qry:
4660 4660 yield entry, entry.user
4661 4661
4662 4662 @property
4663 4663 def workspace_id(self):
4664 4664 from rhodecode.model.pull_request import PullRequestModel
4665 4665 return PullRequestModel()._workspace_id(self)
4666 4666
4667 4667 def get_shadow_repo(self):
4668 4668 workspace_id = self.workspace_id
4669 4669 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4670 4670 if os.path.isdir(shadow_repository_path):
4671 4671 vcs_obj = self.target_repo.scm_instance()
4672 4672 return vcs_obj.get_shadow_instance(shadow_repository_path)
4673 4673
4674 4674 @property
4675 4675 def versions_count(self):
4676 4676 """
4677 4677 return number of versions this PR have, e.g a PR that once been
4678 4678 updated will have 2 versions
4679 4679 """
4680 4680 return self.versions.count() + 1
4681 4681
4682 4682 @property
4683 4683 def pull_request_last_version(self):
4684 4684 return self.versions_count
4685 4685
4686 4686
4687 4687 class PullRequestVersion(Base, _PullRequestBase):
4688 4688 __tablename__ = 'pull_request_versions'
4689 4689 __table_args__ = (
4690 4690 base_table_args,
4691 4691 )
4692 4692
4693 4693 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4694 4694 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4695 4695 pull_request = relationship('PullRequest', back_populates='versions')
4696 4696
4697 4697 def __repr__(self):
4698 4698 if self.pull_request_version_id:
4699 4699 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4700 4700 else:
4701 4701 return f'<DB:PullRequestVersion at {id(self)!r}>'
4702 4702
4703 4703 @property
4704 4704 def reviewers(self):
4705 4705 return self.pull_request.reviewers
4706 4706
4707 4707 @property
4708 4708 def versions(self):
4709 4709 return self.pull_request.versions
4710 4710
4711 4711 def is_closed(self):
4712 4712 # calculate from original
4713 4713 return self.pull_request.status == self.STATUS_CLOSED
4714 4714
4715 4715 def is_state_changing(self):
4716 4716 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4717 4717
4718 4718 def calculated_review_status(self):
4719 4719 return self.pull_request.calculated_review_status()
4720 4720
4721 4721 def reviewers_statuses(self):
4722 4722 return self.pull_request.reviewers_statuses()
4723 4723
4724 4724 def observers(self):
4725 4725 return self.pull_request.observers()
4726 4726
4727 4727
4728 4728 class PullRequestReviewers(Base, BaseModel):
4729 4729 __tablename__ = 'pull_request_reviewers'
4730 4730 __table_args__ = (
4731 4731 base_table_args,
4732 4732 )
4733 4733 ROLE_REVIEWER = 'reviewer'
4734 4734 ROLE_OBSERVER = 'observer'
4735 4735 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4736 4736
4737 4737 @hybrid_property
4738 4738 def reasons(self):
4739 4739 if not self._reasons:
4740 4740 return []
4741 4741 return self._reasons
4742 4742
4743 4743 @reasons.setter
4744 4744 def reasons(self, val):
4745 4745 val = val or []
4746 4746 if any(not isinstance(x, str) for x in val):
4747 4747 raise Exception('invalid reasons type, must be list of strings')
4748 4748 self._reasons = val
4749 4749
4750 4750 pull_requests_reviewers_id = Column(
4751 4751 'pull_requests_reviewers_id', Integer(), nullable=False,
4752 4752 primary_key=True)
4753 4753 pull_request_id = Column(
4754 4754 "pull_request_id", Integer(),
4755 4755 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4756 4756 user_id = Column(
4757 4757 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4758 4758 _reasons = Column(
4759 4759 'reason', MutationList.as_mutable(
4760 4760 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4761 4761
4762 4762 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4763 4763 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4764 4764
4765 4765 user = relationship('User')
4766 4766 pull_request = relationship('PullRequest', back_populates='reviewers')
4767 4767
4768 4768 rule_data = Column(
4769 4769 'rule_data_json',
4770 4770 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4771 4771
4772 4772 def rule_user_group_data(self):
4773 4773 """
4774 4774 Returns the voting user group rule data for this reviewer
4775 4775 """
4776 4776
4777 4777 if self.rule_data and 'vote_rule' in self.rule_data:
4778 4778 user_group_data = {}
4779 4779 if 'rule_user_group_entry_id' in self.rule_data:
4780 4780 # means a group with voting rules !
4781 4781 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4782 4782 user_group_data['name'] = self.rule_data['rule_name']
4783 4783 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4784 4784
4785 4785 return user_group_data
4786 4786
4787 4787 @classmethod
4788 4788 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4789 4789 qry = PullRequestReviewers.query()\
4790 4790 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4791 4791 if role:
4792 4792 qry = qry.filter(PullRequestReviewers.role == role)
4793 4793
4794 4794 return qry.all()
4795 4795
4796 4796 def __repr__(self):
4797 4797 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4798 4798
4799 4799
4800 4800 class Notification(Base, BaseModel):
4801 4801 __tablename__ = 'notifications'
4802 4802 __table_args__ = (
4803 4803 Index('notification_type_idx', 'type'),
4804 4804 base_table_args,
4805 4805 )
4806 4806
4807 4807 TYPE_CHANGESET_COMMENT = 'cs_comment'
4808 4808 TYPE_MESSAGE = 'message'
4809 4809 TYPE_MENTION = 'mention'
4810 4810 TYPE_REGISTRATION = 'registration'
4811 4811 TYPE_PULL_REQUEST = 'pull_request'
4812 4812 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4813 4813 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4814 4814
4815 4815 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4816 4816 subject = Column('subject', Unicode(512), nullable=True)
4817 4817 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4818 4818 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4819 4819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4820 4820 type_ = Column('type', Unicode(255))
4821 4821
4822 4822 created_by_user = relationship('User', back_populates='user_created_notifications')
4823 4823 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4824 4824
4825 4825 @property
4826 4826 def recipients(self):
4827 4827 return [x.user for x in UserNotification.query()\
4828 4828 .filter(UserNotification.notification == self)\
4829 4829 .order_by(UserNotification.user_id.asc()).all()]
4830 4830
4831 4831 @classmethod
4832 4832 def create(cls, created_by, subject, body, recipients, type_=None):
4833 4833 if type_ is None:
4834 4834 type_ = Notification.TYPE_MESSAGE
4835 4835
4836 4836 notification = cls()
4837 4837 notification.created_by_user = created_by
4838 4838 notification.subject = subject
4839 4839 notification.body = body
4840 4840 notification.type_ = type_
4841 4841 notification.created_on = datetime.datetime.now()
4842 4842
4843 4843 # For each recipient link the created notification to his account
4844 4844 for u in recipients:
4845 4845 assoc = UserNotification()
4846 4846 assoc.user_id = u.user_id
4847 4847 assoc.notification = notification
4848 4848
4849 4849 # if created_by is inside recipients mark his notification
4850 4850 # as read
4851 4851 if u.user_id == created_by.user_id:
4852 4852 assoc.read = True
4853 4853 Session().add(assoc)
4854 4854
4855 4855 Session().add(notification)
4856 4856
4857 4857 return notification
4858 4858
4859 4859
4860 4860 class UserNotification(Base, BaseModel):
4861 4861 __tablename__ = 'user_to_notification'
4862 4862 __table_args__ = (
4863 4863 UniqueConstraint('user_id', 'notification_id'),
4864 4864 base_table_args
4865 4865 )
4866 4866
4867 4867 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4868 4868 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4869 4869 read = Column('read', Boolean, default=False)
4870 4870 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4871 4871
4872 4872 user = relationship('User', lazy="joined", back_populates='notifications')
4873 4873 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4874 4874
4875 4875 def mark_as_read(self):
4876 4876 self.read = True
4877 4877 Session().add(self)
4878 4878
4879 4879
4880 4880 class UserNotice(Base, BaseModel):
4881 4881 __tablename__ = 'user_notices'
4882 4882 __table_args__ = (
4883 4883 base_table_args
4884 4884 )
4885 4885
4886 4886 NOTIFICATION_TYPE_MESSAGE = 'message'
4887 4887 NOTIFICATION_TYPE_NOTICE = 'notice'
4888 4888
4889 4889 NOTIFICATION_LEVEL_INFO = 'info'
4890 4890 NOTIFICATION_LEVEL_WARNING = 'warning'
4891 4891 NOTIFICATION_LEVEL_ERROR = 'error'
4892 4892
4893 4893 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4894 4894
4895 4895 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4896 4896 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4897 4897
4898 4898 notice_read = Column('notice_read', Boolean, default=False)
4899 4899
4900 4900 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4901 4901 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4902 4902
4903 4903 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4904 4904 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4905 4905
4906 4906 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4907 4907 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4908 4908
4909 4909 @classmethod
4910 4910 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4911 4911
4912 4912 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4913 4913 cls.NOTIFICATION_LEVEL_WARNING,
4914 4914 cls.NOTIFICATION_LEVEL_INFO]:
4915 4915 return
4916 4916
4917 4917 from rhodecode.model.user import UserModel
4918 4918 user = UserModel().get_user(user)
4919 4919
4920 4920 new_notice = UserNotice()
4921 4921 if not allow_duplicate:
4922 4922 existing_msg = UserNotice().query() \
4923 4923 .filter(UserNotice.user == user) \
4924 4924 .filter(UserNotice.notice_body == body) \
4925 4925 .filter(UserNotice.notice_read == false()) \
4926 4926 .scalar()
4927 4927 if existing_msg:
4928 4928 log.warning('Ignoring duplicate notice for user %s', user)
4929 4929 return
4930 4930
4931 4931 new_notice.user = user
4932 4932 new_notice.notice_subject = subject
4933 4933 new_notice.notice_body = body
4934 4934 new_notice.notification_level = notice_level
4935 4935 Session().add(new_notice)
4936 4936 Session().commit()
4937 4937
4938 4938
4939 4939 class Gist(Base, BaseModel):
4940 4940 __tablename__ = 'gists'
4941 4941 __table_args__ = (
4942 4942 Index('g_gist_access_id_idx', 'gist_access_id'),
4943 4943 Index('g_created_on_idx', 'created_on'),
4944 4944 base_table_args
4945 4945 )
4946 4946
4947 4947 GIST_PUBLIC = 'public'
4948 4948 GIST_PRIVATE = 'private'
4949 4949 DEFAULT_FILENAME = 'gistfile1.txt'
4950 4950
4951 4951 ACL_LEVEL_PUBLIC = 'acl_public'
4952 4952 ACL_LEVEL_PRIVATE = 'acl_private'
4953 4953
4954 4954 gist_id = Column('gist_id', Integer(), primary_key=True)
4955 4955 gist_access_id = Column('gist_access_id', Unicode(250))
4956 4956 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4957 4957 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4958 4958 gist_expires = Column('gist_expires', Float(53), nullable=False)
4959 4959 gist_type = Column('gist_type', Unicode(128), nullable=False)
4960 4960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4961 4961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4962 4962 acl_level = Column('acl_level', Unicode(128), nullable=True)
4963 4963
4964 4964 owner = relationship('User', back_populates='user_gists')
4965 4965
4966 4966 def __repr__(self):
4967 4967 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4968 4968
4969 4969 @hybrid_property
4970 4970 def description_safe(self):
4971 4971 from rhodecode.lib import helpers as h
4972 4972 return h.escape(self.gist_description)
4973 4973
4974 4974 @classmethod
4975 4975 def get_or_404(cls, id_):
4976 4976 from pyramid.httpexceptions import HTTPNotFound
4977 4977
4978 4978 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4979 4979 if not res:
4980 4980 log.debug('WARN: No DB entry with id %s', id_)
4981 4981 raise HTTPNotFound()
4982 4982 return res
4983 4983
4984 4984 @classmethod
4985 4985 def get_by_access_id(cls, gist_access_id):
4986 4986 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4987 4987
4988 4988 def gist_url(self):
4989 4989 from rhodecode.model.gist import GistModel
4990 4990 return GistModel().get_url(self)
4991 4991
4992 4992 @classmethod
4993 4993 def base_path(cls):
4994 4994 """
4995 4995 Returns base path when all gists are stored
4996 4996
4997 4997 :param cls:
4998 4998 """
4999 4999 from rhodecode.model.gist import GIST_STORE_LOC
5000 5000 q = Session().query(RhodeCodeUi)\
5001 5001 .filter(RhodeCodeUi.ui_key == URL_SEP)
5002 5002 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
5003 5003 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
5004 5004
5005 5005 def get_api_data(self):
5006 5006 """
5007 5007 Common function for generating gist related data for API
5008 5008 """
5009 5009 gist = self
5010 5010 data = {
5011 5011 'gist_id': gist.gist_id,
5012 5012 'type': gist.gist_type,
5013 5013 'access_id': gist.gist_access_id,
5014 5014 'description': gist.gist_description,
5015 5015 'url': gist.gist_url(),
5016 5016 'expires': gist.gist_expires,
5017 5017 'created_on': gist.created_on,
5018 5018 'modified_at': gist.modified_at,
5019 5019 'content': None,
5020 5020 'acl_level': gist.acl_level,
5021 5021 }
5022 5022 return data
5023 5023
5024 5024 def __json__(self):
5025 5025 data = dict(
5026 5026 )
5027 5027 data.update(self.get_api_data())
5028 5028 return data
5029 5029 # SCM functions
5030 5030
5031 5031 def scm_instance(self, **kwargs):
5032 5032 """
5033 5033 Get an instance of VCS Repository
5034 5034
5035 5035 :param kwargs:
5036 5036 """
5037 5037 from rhodecode.model.gist import GistModel
5038 5038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5039 5039 return get_vcs_instance(
5040 5040 repo_path=safe_str(full_repo_path), create=False,
5041 5041 _vcs_alias=GistModel.vcs_backend)
5042 5042
5043 5043
5044 5044 class ExternalIdentity(Base, BaseModel):
5045 5045 __tablename__ = 'external_identities'
5046 5046 __table_args__ = (
5047 5047 Index('local_user_id_idx', 'local_user_id'),
5048 5048 Index('external_id_idx', 'external_id'),
5049 5049 base_table_args
5050 5050 )
5051 5051
5052 5052 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5053 5053 external_username = Column('external_username', Unicode(1024), default='')
5054 5054 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5055 5055 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5056 5056 access_token = Column('access_token', String(1024), default='')
5057 5057 alt_token = Column('alt_token', String(1024), default='')
5058 5058 token_secret = Column('token_secret', String(1024), default='')
5059 5059
5060 5060 @classmethod
5061 5061 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5062 5062 """
5063 5063 Returns ExternalIdentity instance based on search params
5064 5064
5065 5065 :param external_id:
5066 5066 :param provider_name:
5067 5067 :return: ExternalIdentity
5068 5068 """
5069 5069 query = cls.query()
5070 5070 query = query.filter(cls.external_id == external_id)
5071 5071 query = query.filter(cls.provider_name == provider_name)
5072 5072 if local_user_id:
5073 5073 query = query.filter(cls.local_user_id == local_user_id)
5074 5074 return query.first()
5075 5075
5076 5076 @classmethod
5077 5077 def user_by_external_id_and_provider(cls, external_id, provider_name):
5078 5078 """
5079 5079 Returns User instance based on search params
5080 5080
5081 5081 :param external_id:
5082 5082 :param provider_name:
5083 5083 :return: User
5084 5084 """
5085 5085 query = User.query()
5086 5086 query = query.filter(cls.external_id == external_id)
5087 5087 query = query.filter(cls.provider_name == provider_name)
5088 5088 query = query.filter(User.user_id == cls.local_user_id)
5089 5089 return query.first()
5090 5090
5091 5091 @classmethod
5092 5092 def by_local_user_id(cls, local_user_id):
5093 5093 """
5094 5094 Returns all tokens for user
5095 5095
5096 5096 :param local_user_id:
5097 5097 :return: ExternalIdentity
5098 5098 """
5099 5099 query = cls.query()
5100 5100 query = query.filter(cls.local_user_id == local_user_id)
5101 5101 return query
5102 5102
5103 5103 @classmethod
5104 5104 def load_provider_plugin(cls, plugin_id):
5105 5105 from rhodecode.authentication.base import loadplugin
5106 5106 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5107 5107 auth_plugin = loadplugin(_plugin_id)
5108 5108 return auth_plugin
5109 5109
5110 5110
5111 5111 class Integration(Base, BaseModel):
5112 5112 __tablename__ = 'integrations'
5113 5113 __table_args__ = (
5114 5114 base_table_args
5115 5115 )
5116 5116
5117 5117 integration_id = Column('integration_id', Integer(), primary_key=True)
5118 5118 integration_type = Column('integration_type', String(255))
5119 5119 enabled = Column('enabled', Boolean(), nullable=False)
5120 5120 name = Column('name', String(255), nullable=False)
5121 5121 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5122 5122
5123 5123 settings = Column(
5124 5124 'settings_json', MutationObj.as_mutable(
5125 5125 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5126 5126 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5127 5127 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5128 5128
5129 5129 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5130 5130 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5131 5131
5132 5132 @property
5133 5133 def scope(self):
5134 5134 if self.repo:
5135 5135 return repr(self.repo)
5136 5136 if self.repo_group:
5137 5137 if self.child_repos_only:
5138 5138 return repr(self.repo_group) + ' (child repos only)'
5139 5139 else:
5140 5140 return repr(self.repo_group) + ' (recursive)'
5141 5141 if self.child_repos_only:
5142 5142 return 'root_repos'
5143 5143 return 'global'
5144 5144
5145 5145 def __repr__(self):
5146 5146 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5147 5147
5148 5148
5149 5149 class RepoReviewRuleUser(Base, BaseModel):
5150 5150 __tablename__ = 'repo_review_rules_users'
5151 5151 __table_args__ = (
5152 5152 base_table_args
5153 5153 )
5154 5154 ROLE_REVIEWER = 'reviewer'
5155 5155 ROLE_OBSERVER = 'observer'
5156 5156 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5157 5157
5158 5158 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5159 5159 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5160 5160 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5161 5161 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5162 5162 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5163 5163 user = relationship('User', back_populates='user_review_rules')
5164 5164
5165 5165 def rule_data(self):
5166 5166 return {
5167 5167 'mandatory': self.mandatory,
5168 5168 'role': self.role,
5169 5169 }
5170 5170
5171 5171
5172 5172 class RepoReviewRuleUserGroup(Base, BaseModel):
5173 5173 __tablename__ = 'repo_review_rules_users_groups'
5174 5174 __table_args__ = (
5175 5175 base_table_args
5176 5176 )
5177 5177
5178 5178 VOTE_RULE_ALL = -1
5179 5179 ROLE_REVIEWER = 'reviewer'
5180 5180 ROLE_OBSERVER = 'observer'
5181 5181 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5182 5182
5183 5183 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5184 5184 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5185 5185 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5186 5186 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5187 5187 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5188 5188 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5189 5189 users_group = relationship('UserGroup')
5190 5190
5191 5191 def rule_data(self):
5192 5192 return {
5193 5193 'mandatory': self.mandatory,
5194 5194 'role': self.role,
5195 5195 'vote_rule': self.vote_rule
5196 5196 }
5197 5197
5198 5198 @property
5199 5199 def vote_rule_label(self):
5200 5200 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5201 5201 return 'all must vote'
5202 5202 else:
5203 5203 return 'min. vote {}'.format(self.vote_rule)
5204 5204
5205 5205
5206 5206 class RepoReviewRule(Base, BaseModel):
5207 5207 __tablename__ = 'repo_review_rules'
5208 5208 __table_args__ = (
5209 5209 base_table_args
5210 5210 )
5211 5211
5212 5212 repo_review_rule_id = Column(
5213 5213 'repo_review_rule_id', Integer(), primary_key=True)
5214 5214 repo_id = Column(
5215 5215 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5216 5216 repo = relationship('Repository', back_populates='review_rules')
5217 5217
5218 5218 review_rule_name = Column('review_rule_name', String(255))
5219 5219 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5220 5220 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5221 5221 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5222 5222
5223 5223 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5224 5224
5225 5225 # Legacy fields, just for backward compat
5226 5226 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5227 5227 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5228 5228
5229 5229 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5230 5230 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5231 5231
5232 5232 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5233 5233
5234 5234 rule_users = relationship('RepoReviewRuleUser')
5235 5235 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5236 5236
5237 5237 def _validate_pattern(self, value):
5238 5238 re.compile('^' + glob2re(value) + '$')
5239 5239
5240 5240 @hybrid_property
5241 5241 def source_branch_pattern(self):
5242 5242 return self._branch_pattern or '*'
5243 5243
5244 5244 @source_branch_pattern.setter
5245 5245 def source_branch_pattern(self, value):
5246 5246 self._validate_pattern(value)
5247 5247 self._branch_pattern = value or '*'
5248 5248
5249 5249 @hybrid_property
5250 5250 def target_branch_pattern(self):
5251 5251 return self._target_branch_pattern or '*'
5252 5252
5253 5253 @target_branch_pattern.setter
5254 5254 def target_branch_pattern(self, value):
5255 5255 self._validate_pattern(value)
5256 5256 self._target_branch_pattern = value or '*'
5257 5257
5258 5258 @hybrid_property
5259 5259 def file_pattern(self):
5260 5260 return self._file_pattern or '*'
5261 5261
5262 5262 @file_pattern.setter
5263 5263 def file_pattern(self, value):
5264 5264 self._validate_pattern(value)
5265 5265 self._file_pattern = value or '*'
5266 5266
5267 5267 @hybrid_property
5268 5268 def forbid_pr_author_to_review(self):
5269 5269 return self.pr_author == 'forbid_pr_author'
5270 5270
5271 5271 @hybrid_property
5272 5272 def include_pr_author_to_review(self):
5273 5273 return self.pr_author == 'include_pr_author'
5274 5274
5275 5275 @hybrid_property
5276 5276 def forbid_commit_author_to_review(self):
5277 5277 return self.commit_author == 'forbid_commit_author'
5278 5278
5279 5279 @hybrid_property
5280 5280 def include_commit_author_to_review(self):
5281 5281 return self.commit_author == 'include_commit_author'
5282 5282
5283 5283 def matches(self, source_branch, target_branch, files_changed):
5284 5284 """
5285 5285 Check if this review rule matches a branch/files in a pull request
5286 5286
5287 5287 :param source_branch: source branch name for the commit
5288 5288 :param target_branch: target branch name for the commit
5289 5289 :param files_changed: list of file paths changed in the pull request
5290 5290 """
5291 5291
5292 5292 source_branch = source_branch or ''
5293 5293 target_branch = target_branch or ''
5294 5294 files_changed = files_changed or []
5295 5295
5296 5296 branch_matches = True
5297 5297 if source_branch or target_branch:
5298 5298 if self.source_branch_pattern == '*':
5299 5299 source_branch_match = True
5300 5300 else:
5301 5301 if self.source_branch_pattern.startswith('re:'):
5302 5302 source_pattern = self.source_branch_pattern[3:]
5303 5303 else:
5304 5304 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5305 5305 source_branch_regex = re.compile(source_pattern)
5306 5306 source_branch_match = bool(source_branch_regex.search(source_branch))
5307 5307 if self.target_branch_pattern == '*':
5308 5308 target_branch_match = True
5309 5309 else:
5310 5310 if self.target_branch_pattern.startswith('re:'):
5311 5311 target_pattern = self.target_branch_pattern[3:]
5312 5312 else:
5313 5313 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5314 5314 target_branch_regex = re.compile(target_pattern)
5315 5315 target_branch_match = bool(target_branch_regex.search(target_branch))
5316 5316
5317 5317 branch_matches = source_branch_match and target_branch_match
5318 5318
5319 5319 files_matches = True
5320 5320 if self.file_pattern != '*':
5321 5321 files_matches = False
5322 5322 if self.file_pattern.startswith('re:'):
5323 5323 file_pattern = self.file_pattern[3:]
5324 5324 else:
5325 5325 file_pattern = glob2re(self.file_pattern)
5326 5326 file_regex = re.compile(file_pattern)
5327 5327 for file_data in files_changed:
5328 5328 filename = file_data.get('filename')
5329 5329
5330 5330 if file_regex.search(filename):
5331 5331 files_matches = True
5332 5332 break
5333 5333
5334 5334 return branch_matches and files_matches
5335 5335
5336 5336 @property
5337 5337 def review_users(self):
5338 5338 """ Returns the users which this rule applies to """
5339 5339
5340 5340 users = collections.OrderedDict()
5341 5341
5342 5342 for rule_user in self.rule_users:
5343 5343 if rule_user.user.active:
5344 5344 if rule_user.user not in users:
5345 5345 users[rule_user.user.username] = {
5346 5346 'user': rule_user.user,
5347 5347 'source': 'user',
5348 5348 'source_data': {},
5349 5349 'data': rule_user.rule_data()
5350 5350 }
5351 5351
5352 5352 for rule_user_group in self.rule_user_groups:
5353 5353 source_data = {
5354 5354 'user_group_id': rule_user_group.users_group.users_group_id,
5355 5355 'name': rule_user_group.users_group.users_group_name,
5356 5356 'members': len(rule_user_group.users_group.members)
5357 5357 }
5358 5358 for member in rule_user_group.users_group.members:
5359 5359 if member.user.active:
5360 5360 key = member.user.username
5361 5361 if key in users:
5362 5362 # skip this member as we have him already
5363 5363 # this prevents from override the "first" matched
5364 5364 # users with duplicates in multiple groups
5365 5365 continue
5366 5366
5367 5367 users[key] = {
5368 5368 'user': member.user,
5369 5369 'source': 'user_group',
5370 5370 'source_data': source_data,
5371 5371 'data': rule_user_group.rule_data()
5372 5372 }
5373 5373
5374 5374 return users
5375 5375
5376 5376 def user_group_vote_rule(self, user_id):
5377 5377
5378 5378 rules = []
5379 5379 if not self.rule_user_groups:
5380 5380 return rules
5381 5381
5382 5382 for user_group in self.rule_user_groups:
5383 5383 user_group_members = [x.user_id for x in user_group.users_group.members]
5384 5384 if user_id in user_group_members:
5385 5385 rules.append(user_group)
5386 5386 return rules
5387 5387
5388 5388 def __repr__(self):
5389 5389 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5390 5390
5391 5391
5392 5392 class ScheduleEntry(Base, BaseModel):
5393 5393 __tablename__ = 'schedule_entries'
5394 5394 __table_args__ = (
5395 5395 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5396 5396 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5397 5397 base_table_args,
5398 5398 )
5399 5399 SCHEDULE_TYPE_INTEGER = "integer"
5400 5400 SCHEDULE_TYPE_CRONTAB = "crontab"
5401 5401
5402 5402 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5403 5403 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5404 5404
5405 5405 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5406 5406 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5407 5407 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5408 5408
5409 5409 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5410 5410 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5411 5411
5412 5412 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5413 5413 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5414 5414
5415 5415 # task
5416 5416 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5417 5417 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5418 5418 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5419 5419 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5420 5420
5421 5421 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5422 5422 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5423 5423
5424 5424 @hybrid_property
5425 5425 def schedule_type(self):
5426 5426 return self._schedule_type
5427 5427
5428 5428 @schedule_type.setter
5429 5429 def schedule_type(self, val):
5430 5430 if val not in self.schedule_types:
5431 5431 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5432 5432 val, self.schedule_type))
5433 5433
5434 5434 self._schedule_type = val
5435 5435
5436 5436 @classmethod
5437 5437 def get_uid(cls, obj):
5438 5438 args = obj.task_args
5439 5439 kwargs = obj.task_kwargs
5440 5440 if isinstance(args, JsonRaw):
5441 5441 try:
5442 5442 args = json.loads(args)
5443 5443 except ValueError:
5444 5444 args = tuple()
5445 5445
5446 5446 if isinstance(kwargs, JsonRaw):
5447 5447 try:
5448 5448 kwargs = json.loads(kwargs)
5449 5449 except ValueError:
5450 5450 kwargs = dict()
5451 5451
5452 5452 dot_notation = obj.task_dot_notation
5453 5453 val = '.'.join(map(safe_str, [
5454 5454 sorted(dot_notation), args, sorted(kwargs.items())]))
5455 5455 return sha1(safe_bytes(val))
5456 5456
5457 5457 @classmethod
5458 5458 def get_by_schedule_name(cls, schedule_name):
5459 5459 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5460 5460
5461 5461 @classmethod
5462 5462 def get_by_schedule_id(cls, schedule_id):
5463 5463 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5464 5464
5465 5465 @property
5466 5466 def task(self):
5467 5467 return self.task_dot_notation
5468 5468
5469 5469 @property
5470 5470 def schedule(self):
5471 5471 from rhodecode.lib.celerylib.utils import raw_2_schedule
5472 5472 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5473 5473 return schedule
5474 5474
5475 5475 @property
5476 5476 def args(self):
5477 5477 try:
5478 5478 return list(self.task_args or [])
5479 5479 except ValueError:
5480 5480 return list()
5481 5481
5482 5482 @property
5483 5483 def kwargs(self):
5484 5484 try:
5485 5485 return dict(self.task_kwargs or {})
5486 5486 except ValueError:
5487 5487 return dict()
5488 5488
5489 5489 def _as_raw(self, val, indent=False):
5490 5490 if hasattr(val, 'de_coerce'):
5491 5491 val = val.de_coerce()
5492 5492 if val:
5493 5493 if indent:
5494 5494 val = ext_json.formatted_str_json(val)
5495 5495 else:
5496 5496 val = ext_json.str_json(val)
5497 5497
5498 5498 return val
5499 5499
5500 5500 @property
5501 5501 def schedule_definition_raw(self):
5502 5502 return self._as_raw(self.schedule_definition)
5503 5503
5504 5504 def args_raw(self, indent=False):
5505 5505 return self._as_raw(self.task_args, indent)
5506 5506
5507 5507 def kwargs_raw(self, indent=False):
5508 5508 return self._as_raw(self.task_kwargs, indent)
5509 5509
5510 5510 def __repr__(self):
5511 5511 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5512 5512
5513 5513
5514 5514 @event.listens_for(ScheduleEntry, 'before_update')
5515 5515 def update_task_uid(mapper, connection, target):
5516 5516 target.task_uid = ScheduleEntry.get_uid(target)
5517 5517
5518 5518
5519 5519 @event.listens_for(ScheduleEntry, 'before_insert')
5520 5520 def set_task_uid(mapper, connection, target):
5521 5521 target.task_uid = ScheduleEntry.get_uid(target)
5522 5522
5523 5523
5524 5524 class _BaseBranchPerms(BaseModel):
5525 5525 @classmethod
5526 5526 def compute_hash(cls, value):
5527 5527 return sha1_safe(value)
5528 5528
5529 5529 @hybrid_property
5530 5530 def branch_pattern(self):
5531 5531 return self._branch_pattern or '*'
5532 5532
5533 5533 @hybrid_property
5534 5534 def branch_hash(self):
5535 5535 return self._branch_hash
5536 5536
5537 5537 def _validate_glob(self, value):
5538 5538 re.compile('^' + glob2re(value) + '$')
5539 5539
5540 5540 @branch_pattern.setter
5541 5541 def branch_pattern(self, value):
5542 5542 self._validate_glob(value)
5543 5543 self._branch_pattern = value or '*'
5544 5544 # set the Hash when setting the branch pattern
5545 5545 self._branch_hash = self.compute_hash(self._branch_pattern)
5546 5546
5547 5547 def matches(self, branch):
5548 5548 """
5549 5549 Check if this the branch matches entry
5550 5550
5551 5551 :param branch: branch name for the commit
5552 5552 """
5553 5553
5554 5554 branch = branch or ''
5555 5555
5556 5556 branch_matches = True
5557 5557 if branch:
5558 5558 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5559 5559 branch_matches = bool(branch_regex.search(branch))
5560 5560
5561 5561 return branch_matches
5562 5562
5563 5563
5564 5564 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5565 5565 __tablename__ = 'user_to_repo_branch_permissions'
5566 5566 __table_args__ = (
5567 5567 base_table_args
5568 5568 )
5569 5569
5570 5570 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5571 5571
5572 5572 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5573 5573 repo = relationship('Repository', back_populates='user_branch_perms')
5574 5574
5575 5575 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5576 5576 permission = relationship('Permission')
5577 5577
5578 5578 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5579 5579 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5580 5580
5581 5581 rule_order = Column('rule_order', Integer(), nullable=False)
5582 5582 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5583 5583 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5584 5584
5585 5585 def __repr__(self):
5586 5586 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5587 5587
5588 5588
5589 5589 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5590 5590 __tablename__ = 'user_group_to_repo_branch_permissions'
5591 5591 __table_args__ = (
5592 5592 base_table_args
5593 5593 )
5594 5594
5595 5595 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5596 5596
5597 5597 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5598 5598 repo = relationship('Repository', back_populates='user_group_branch_perms')
5599 5599
5600 5600 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5601 5601 permission = relationship('Permission')
5602 5602
5603 5603 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5604 5604 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5605 5605
5606 5606 rule_order = Column('rule_order', Integer(), nullable=False)
5607 5607 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5608 5608 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5609 5609
5610 5610 def __repr__(self):
5611 5611 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5612 5612
5613 5613
5614 5614 class UserBookmark(Base, BaseModel):
5615 5615 __tablename__ = 'user_bookmarks'
5616 5616 __table_args__ = (
5617 5617 UniqueConstraint('user_id', 'bookmark_repo_id'),
5618 5618 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5619 5619 UniqueConstraint('user_id', 'bookmark_position'),
5620 5620 base_table_args
5621 5621 )
5622 5622
5623 5623 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5624 5624 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5625 5625 position = Column("bookmark_position", Integer(), nullable=False)
5626 5626 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5627 5627 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5628 5628 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5629 5629
5630 5630 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5631 5631 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5632 5632
5633 5633 user = relationship("User")
5634 5634
5635 5635 repository = relationship("Repository")
5636 5636 repository_group = relationship("RepoGroup")
5637 5637
5638 5638 @classmethod
5639 5639 def get_by_position_for_user(cls, position, user_id):
5640 5640 return cls.query() \
5641 5641 .filter(UserBookmark.user_id == user_id) \
5642 5642 .filter(UserBookmark.position == position).scalar()
5643 5643
5644 5644 @classmethod
5645 5645 def get_bookmarks_for_user(cls, user_id, cache=True):
5646 bookmarks = cls.query() \
5647 .filter(UserBookmark.user_id == user_id) \
5648 .options(joinedload(UserBookmark.repository)) \
5649 .options(joinedload(UserBookmark.repository_group)) \
5646 bookmarks = select(
5647 UserBookmark.title,
5648 UserBookmark.position,
5649 ) \
5650 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5651 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5652 .where(UserBookmark.user_id == user_id) \
5653 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5654 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5650 5655 .order_by(UserBookmark.position.asc())
5651 5656
5652 5657 if cache:
5653 5658 bookmarks = bookmarks.options(
5654 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5659 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5655 5660 )
5656 5661
5657 return bookmarks.all()
5662 return Session().execute(bookmarks).all()
5658 5663
5659 5664 def __repr__(self):
5660 5665 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5661 5666
5662 5667
5663 5668 class FileStore(Base, BaseModel):
5664 5669 __tablename__ = 'file_store'
5665 5670 __table_args__ = (
5666 5671 base_table_args
5667 5672 )
5668 5673
5669 5674 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5670 5675 file_uid = Column('file_uid', String(1024), nullable=False)
5671 5676 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5672 5677 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5673 5678 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5674 5679
5675 5680 # sha256 hash
5676 5681 file_hash = Column('file_hash', String(512), nullable=False)
5677 5682 file_size = Column('file_size', BigInteger(), nullable=False)
5678 5683
5679 5684 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5680 5685 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5681 5686 accessed_count = Column('accessed_count', Integer(), default=0)
5682 5687
5683 5688 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5684 5689
5685 5690 # if repo/repo_group reference is set, check for permissions
5686 5691 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5687 5692
5688 5693 # hidden defines an attachment that should be hidden from showing in artifact listing
5689 5694 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5690 5695
5691 5696 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5692 5697 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5693 5698
5694 5699 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5695 5700
5696 5701 # scope limited to user, which requester have access to
5697 5702 scope_user_id = Column(
5698 5703 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5699 5704 nullable=True, unique=None, default=None)
5700 5705 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5701 5706
5702 5707 # scope limited to user group, which requester have access to
5703 5708 scope_user_group_id = Column(
5704 5709 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5705 5710 nullable=True, unique=None, default=None)
5706 5711 user_group = relationship('UserGroup', lazy='joined')
5707 5712
5708 5713 # scope limited to repo, which requester have access to
5709 5714 scope_repo_id = Column(
5710 5715 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5711 5716 nullable=True, unique=None, default=None)
5712 5717 repo = relationship('Repository', lazy='joined')
5713 5718
5714 5719 # scope limited to repo group, which requester have access to
5715 5720 scope_repo_group_id = Column(
5716 5721 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5717 5722 nullable=True, unique=None, default=None)
5718 5723 repo_group = relationship('RepoGroup', lazy='joined')
5719 5724
5720 5725 @classmethod
5721 5726 def get_scope(cls, scope_type, scope_id):
5722 5727 if scope_type == 'repo':
5723 5728 return f'repo:{scope_id}'
5724 5729 elif scope_type == 'repo-group':
5725 5730 return f'repo-group:{scope_id}'
5726 5731 elif scope_type == 'user':
5727 5732 return f'user:{scope_id}'
5728 5733 elif scope_type == 'user-group':
5729 5734 return f'user-group:{scope_id}'
5730 5735 else:
5731 5736 return scope_type
5732 5737
5733 5738 @classmethod
5734 5739 def get_by_store_uid(cls, file_store_uid, safe=False):
5735 5740 if safe:
5736 5741 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5737 5742 else:
5738 5743 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5739 5744
5740 5745 @classmethod
5741 5746 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5742 5747 file_description='', enabled=True, hidden=False, check_acl=True,
5743 5748 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5744 5749
5745 5750 store_entry = FileStore()
5746 5751 store_entry.file_uid = file_uid
5747 5752 store_entry.file_display_name = file_display_name
5748 5753 store_entry.file_org_name = filename
5749 5754 store_entry.file_size = file_size
5750 5755 store_entry.file_hash = file_hash
5751 5756 store_entry.file_description = file_description
5752 5757
5753 5758 store_entry.check_acl = check_acl
5754 5759 store_entry.enabled = enabled
5755 5760 store_entry.hidden = hidden
5756 5761
5757 5762 store_entry.user_id = user_id
5758 5763 store_entry.scope_user_id = scope_user_id
5759 5764 store_entry.scope_repo_id = scope_repo_id
5760 5765 store_entry.scope_repo_group_id = scope_repo_group_id
5761 5766
5762 5767 return store_entry
5763 5768
5764 5769 @classmethod
5765 5770 def store_metadata(cls, file_store_id, args, commit=True):
5766 5771 file_store = FileStore.get(file_store_id)
5767 5772 if file_store is None:
5768 5773 return
5769 5774
5770 5775 for section, key, value, value_type in args:
5771 5776 has_key = FileStoreMetadata().query() \
5772 5777 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5773 5778 .filter(FileStoreMetadata.file_store_meta_section == section) \
5774 5779 .filter(FileStoreMetadata.file_store_meta_key == key) \
5775 5780 .scalar()
5776 5781 if has_key:
5777 5782 msg = 'key `{}` already defined under section `{}` for this file.'\
5778 5783 .format(key, section)
5779 5784 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5780 5785
5781 5786 # NOTE(marcink): raises ArtifactMetadataBadValueType
5782 5787 FileStoreMetadata.valid_value_type(value_type)
5783 5788
5784 5789 meta_entry = FileStoreMetadata()
5785 5790 meta_entry.file_store = file_store
5786 5791 meta_entry.file_store_meta_section = section
5787 5792 meta_entry.file_store_meta_key = key
5788 5793 meta_entry.file_store_meta_value_type = value_type
5789 5794 meta_entry.file_store_meta_value = value
5790 5795
5791 5796 Session().add(meta_entry)
5792 5797
5793 5798 try:
5794 5799 if commit:
5795 5800 Session().commit()
5796 5801 except IntegrityError:
5797 5802 Session().rollback()
5798 5803 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5799 5804
5800 5805 @classmethod
5801 5806 def bump_access_counter(cls, file_uid, commit=True):
5802 5807 FileStore().query()\
5803 5808 .filter(FileStore.file_uid == file_uid)\
5804 5809 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5805 5810 FileStore.accessed_on: datetime.datetime.now()})
5806 5811 if commit:
5807 5812 Session().commit()
5808 5813
5809 5814 def __json__(self):
5810 5815 data = {
5811 5816 'filename': self.file_display_name,
5812 5817 'filename_org': self.file_org_name,
5813 5818 'file_uid': self.file_uid,
5814 5819 'description': self.file_description,
5815 5820 'hidden': self.hidden,
5816 5821 'size': self.file_size,
5817 5822 'created_on': self.created_on,
5818 5823 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5819 5824 'downloaded_times': self.accessed_count,
5820 5825 'sha256': self.file_hash,
5821 5826 'metadata': self.file_metadata,
5822 5827 }
5823 5828
5824 5829 return data
5825 5830
5826 5831 def __repr__(self):
5827 5832 return f'<FileStore({self.file_store_id})>'
5828 5833
5829 5834
5830 5835 class FileStoreMetadata(Base, BaseModel):
5831 5836 __tablename__ = 'file_store_metadata'
5832 5837 __table_args__ = (
5833 5838 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5834 5839 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5835 5840 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5836 5841 base_table_args
5837 5842 )
5838 5843 SETTINGS_TYPES = {
5839 5844 'str': safe_str,
5840 5845 'int': safe_int,
5841 5846 'unicode': safe_str,
5842 5847 'bool': str2bool,
5843 5848 'list': functools.partial(aslist, sep=',')
5844 5849 }
5845 5850
5846 5851 file_store_meta_id = Column(
5847 5852 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5848 5853 primary_key=True)
5849 5854 _file_store_meta_section = Column(
5850 5855 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5851 5856 nullable=True, unique=None, default=None)
5852 5857 _file_store_meta_section_hash = Column(
5853 5858 "file_store_meta_section_hash", String(255),
5854 5859 nullable=True, unique=None, default=None)
5855 5860 _file_store_meta_key = Column(
5856 5861 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5857 5862 nullable=True, unique=None, default=None)
5858 5863 _file_store_meta_key_hash = Column(
5859 5864 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5860 5865 _file_store_meta_value = Column(
5861 5866 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5862 5867 nullable=True, unique=None, default=None)
5863 5868 _file_store_meta_value_type = Column(
5864 5869 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5865 5870 default='unicode')
5866 5871
5867 5872 file_store_id = Column(
5868 5873 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5869 5874 nullable=True, unique=None, default=None)
5870 5875
5871 5876 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5872 5877
5873 5878 @classmethod
5874 5879 def valid_value_type(cls, value):
5875 5880 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5876 5881 raise ArtifactMetadataBadValueType(
5877 5882 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5878 5883
5879 5884 @hybrid_property
5880 5885 def file_store_meta_section(self):
5881 5886 return self._file_store_meta_section
5882 5887
5883 5888 @file_store_meta_section.setter
5884 5889 def file_store_meta_section(self, value):
5885 5890 self._file_store_meta_section = value
5886 5891 self._file_store_meta_section_hash = _hash_key(value)
5887 5892
5888 5893 @hybrid_property
5889 5894 def file_store_meta_key(self):
5890 5895 return self._file_store_meta_key
5891 5896
5892 5897 @file_store_meta_key.setter
5893 5898 def file_store_meta_key(self, value):
5894 5899 self._file_store_meta_key = value
5895 5900 self._file_store_meta_key_hash = _hash_key(value)
5896 5901
5897 5902 @hybrid_property
5898 5903 def file_store_meta_value(self):
5899 5904 val = self._file_store_meta_value
5900 5905
5901 5906 if self._file_store_meta_value_type:
5902 5907 # e.g unicode.encrypted == unicode
5903 5908 _type = self._file_store_meta_value_type.split('.')[0]
5904 5909 # decode the encrypted value if it's encrypted field type
5905 5910 if '.encrypted' in self._file_store_meta_value_type:
5906 5911 cipher = EncryptedTextValue()
5907 5912 val = safe_str(cipher.process_result_value(val, None))
5908 5913 # do final type conversion
5909 5914 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5910 5915 val = converter(val)
5911 5916
5912 5917 return val
5913 5918
5914 5919 @file_store_meta_value.setter
5915 5920 def file_store_meta_value(self, val):
5916 5921 val = safe_str(val)
5917 5922 # encode the encrypted value
5918 5923 if '.encrypted' in self.file_store_meta_value_type:
5919 5924 cipher = EncryptedTextValue()
5920 5925 val = safe_str(cipher.process_bind_param(val, None))
5921 5926 self._file_store_meta_value = val
5922 5927
5923 5928 @hybrid_property
5924 5929 def file_store_meta_value_type(self):
5925 5930 return self._file_store_meta_value_type
5926 5931
5927 5932 @file_store_meta_value_type.setter
5928 5933 def file_store_meta_value_type(self, val):
5929 5934 # e.g unicode.encrypted
5930 5935 self.valid_value_type(val)
5931 5936 self._file_store_meta_value_type = val
5932 5937
5933 5938 def __json__(self):
5934 5939 data = {
5935 5940 'artifact': self.file_store.file_uid,
5936 5941 'section': self.file_store_meta_section,
5937 5942 'key': self.file_store_meta_key,
5938 5943 'value': self.file_store_meta_value,
5939 5944 }
5940 5945
5941 5946 return data
5942 5947
5943 5948 def __repr__(self):
5944 5949 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5945 5950 self.file_store_meta_key, self.file_store_meta_value)
5946 5951
5947 5952
5948 5953 class DbMigrateVersion(Base, BaseModel):
5949 5954 __tablename__ = 'db_migrate_version'
5950 5955 __table_args__ = (
5951 5956 base_table_args,
5952 5957 )
5953 5958
5954 5959 repository_id = Column('repository_id', String(250), primary_key=True)
5955 5960 repository_path = Column('repository_path', Text)
5956 5961 version = Column('version', Integer)
5957 5962
5958 5963 @classmethod
5959 5964 def set_version(cls, version):
5960 5965 """
5961 5966 Helper for forcing a different version, usually for debugging purposes via ishell.
5962 5967 """
5963 5968 ver = DbMigrateVersion.query().first()
5964 5969 ver.version = version
5965 5970 Session().commit()
5966 5971
5967 5972
5968 5973 class DbSession(Base, BaseModel):
5969 5974 __tablename__ = 'db_session'
5970 5975 __table_args__ = (
5971 5976 base_table_args,
5972 5977 )
5973 5978
5974 5979 def __repr__(self):
5975 5980 return f'<DB:DbSession({self.id})>'
5976 5981
5977 5982 id = Column('id', Integer())
5978 5983 namespace = Column('namespace', String(255), primary_key=True)
5979 5984 accessed = Column('accessed', DateTime, nullable=False)
5980 5985 created = Column('created', DateTime, nullable=False)
5981 5986 data = Column('data', PickleType, nullable=False)
@@ -1,1050 +1,1055 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 users model for RhodeCode
21 21 """
22 22
23 23 import logging
24 24 import traceback
25 25 import datetime
26 26 import ipaddress
27 27
28 28 from pyramid.threadlocal import get_current_request
29 29 from sqlalchemy.exc import DatabaseError
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.user_log_filter import user_log_filter
33 33 from rhodecode.lib.utils2 import (
34 34 get_current_rhodecode_user, action_logger_generic,
35 35 AttributeDict, str2bool)
36 36 from rhodecode.lib.str_utils import safe_str
37 37 from rhodecode.lib.exceptions import (
38 38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.db import (
44 44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 45 UserEmailMap, UserIpMap, UserLog)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class UserModel(BaseModel):
54 54 cls = User
55 55
56 56 def get(self, user_id, cache=False):
57 57 user = self.sa.query(User)
58 58 if cache:
59 59 user = user.options(
60 60 FromCache("sql_cache_short", f"get_user_{user_id}"))
61 61 return user.get(user_id)
62 62
63 63 def get_user(self, user):
64 64 return self._get_user(user)
65 65
66 66 def _serialize_user(self, user):
67 67 import rhodecode.lib.helpers as h
68 68
69 69 return {
70 70 'id': user.user_id,
71 71 'first_name': user.first_name,
72 72 'last_name': user.last_name,
73 73 'username': user.username,
74 74 'email': user.email,
75 75 'icon_link': h.gravatar_url(user.email, 30),
76 76 'profile_link': h.link_to_user(user),
77 77 'value_display': h.escape(h.person(user)),
78 78 'value': user.username,
79 79 'value_type': 'user',
80 80 'active': user.active,
81 81 }
82 82
83 83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 84
85 85 query = self.sa.query(User)
86 86 if only_active:
87 87 query = query.filter(User.active == true())
88 88
89 89 if name_contains:
90 90 ilike_expression = f'%{safe_str(name_contains)}%'
91 91 query = query.filter(
92 92 or_(
93 93 User.name.ilike(ilike_expression),
94 94 User.lastname.ilike(ilike_expression),
95 95 User.username.ilike(ilike_expression)
96 96 )
97 97 )
98 98 # sort by len to have top most matches first
99 99 query = query.order_by(func.length(User.username))\
100 100 .order_by(User.username)
101 101 query = query.limit(limit)
102 102
103 103 users = query.all()
104 104
105 105 _users = [
106 106 self._serialize_user(user) for user in users
107 107 ]
108 108 return _users
109 109
110 110 def get_by_username(self, username, cache=False, case_insensitive=False):
111 111
112 112 if case_insensitive:
113 113 user = self.sa.query(User).filter(User.username.ilike(username))
114 114 else:
115 115 user = self.sa.query(User)\
116 116 .filter(User.username == username)
117
117 118 if cache:
118 119 name_key = _hash_key(username)
119 120 user = user.options(
120 121 FromCache("sql_cache_short", f"get_user_{name_key}"))
121 122 return user.scalar()
122 123
123 124 def get_by_email(self, email, cache=False, case_insensitive=False):
124 125 return User.get_by_email(email, case_insensitive, cache)
125 126
126 127 def get_by_auth_token(self, auth_token, cache=False):
127 128 return User.get_by_auth_token(auth_token, cache)
128 129
129 130 def get_active_user_count(self, cache=False):
130 131 qry = User.query().filter(
131 132 User.active == true()).filter(
132 133 User.username != User.DEFAULT_USER)
133 134 if cache:
134 135 qry = qry.options(
135 136 FromCache("sql_cache_short", "get_active_users"))
136 137 return qry.count()
137 138
138 139 def create(self, form_data, cur_user=None):
139 140 if not cur_user:
140 141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
141 142
142 143 user_data = {
143 144 'username': form_data['username'],
144 145 'password': form_data['password'],
145 146 'email': form_data['email'],
146 147 'firstname': form_data['firstname'],
147 148 'lastname': form_data['lastname'],
148 149 'active': form_data['active'],
149 150 'extern_type': form_data['extern_type'],
150 151 'extern_name': form_data['extern_name'],
151 152 'admin': False,
152 153 'cur_user': cur_user
153 154 }
154 155
155 156 if 'create_repo_group' in form_data:
156 157 user_data['create_repo_group'] = str2bool(
157 158 form_data.get('create_repo_group'))
158 159
159 160 try:
160 161 if form_data.get('password_change'):
161 162 user_data['force_password_change'] = True
162 163 return UserModel().create_or_update(**user_data)
163 164 except Exception:
164 165 log.error(traceback.format_exc())
165 166 raise
166 167
167 168 def update_user(self, user, skip_attrs=None, **kwargs):
168 169 from rhodecode.lib.auth import get_crypt_password
169 170
170 171 user = self._get_user(user)
171 172 if user.username == User.DEFAULT_USER:
172 173 raise DefaultUserException(
173 174 "You can't edit this user (`%(username)s`) since it's "
174 175 "crucial for entire application" % {
175 176 'username': user.username})
176 177
177 178 # first store only defaults
178 179 user_attrs = {
179 180 'updating_user_id': user.user_id,
180 181 'username': user.username,
181 182 'password': user.password,
182 183 'email': user.email,
183 184 'firstname': user.name,
184 185 'lastname': user.lastname,
185 186 'description': user.description,
186 187 'active': user.active,
187 188 'admin': user.admin,
188 189 'extern_name': user.extern_name,
189 190 'extern_type': user.extern_type,
190 191 'language': user.user_data.get('language')
191 192 }
192 193
193 194 # in case there's new_password, that comes from form, use it to
194 195 # store password
195 196 if kwargs.get('new_password'):
196 197 kwargs['password'] = kwargs['new_password']
197 198
198 199 # cleanups, my_account password change form
199 200 kwargs.pop('current_password', None)
200 201 kwargs.pop('new_password', None)
201 202
202 203 # cleanups, user edit password change form
203 204 kwargs.pop('password_confirmation', None)
204 205 kwargs.pop('password_change', None)
205 206
206 207 # create repo group on user creation
207 208 kwargs.pop('create_repo_group', None)
208 209
209 210 # legacy forms send name, which is the firstname
210 211 firstname = kwargs.pop('name', None)
211 212 if firstname:
212 213 kwargs['firstname'] = firstname
213 214
214 215 for k, v in kwargs.items():
215 216 # skip if we don't want to update this
216 217 if skip_attrs and k in skip_attrs:
217 218 continue
218 219
219 220 user_attrs[k] = v
220 221
221 222 try:
222 223 return self.create_or_update(**user_attrs)
223 224 except Exception:
224 225 log.error(traceback.format_exc())
225 226 raise
226 227
227 228 def create_or_update(
228 229 self, username, password, email, firstname='', lastname='',
229 230 active=True, admin=False, extern_type=None, extern_name=None,
230 231 cur_user=None, plugin=None, force_password_change=False,
231 232 allow_to_create_user=True, create_repo_group=None,
232 233 updating_user_id=None, language=None, description='',
233 234 strict_creation_check=True):
234 235 """
235 236 Creates a new instance if not found, or updates current one
236 237
237 238 :param username:
238 239 :param password:
239 240 :param email:
240 241 :param firstname:
241 242 :param lastname:
242 243 :param active:
243 244 :param admin:
244 245 :param extern_type:
245 246 :param extern_name:
246 247 :param cur_user:
247 248 :param plugin: optional plugin this method was called from
248 249 :param force_password_change: toggles new or existing user flag
249 250 for password change
250 251 :param allow_to_create_user: Defines if the method can actually create
251 252 new users
252 253 :param create_repo_group: Defines if the method should also
253 254 create an repo group with user name, and owner
254 255 :param updating_user_id: if we set it up this is the user we want to
255 256 update this allows to editing username.
256 257 :param language: language of user from interface.
257 258 :param description: user description
258 259 :param strict_creation_check: checks for allowed creation license wise etc.
259 260
260 261 :returns: new User object with injected `is_new_user` attribute.
261 262 """
262 263
263 264 if not cur_user:
264 265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
265 266
266 267 from rhodecode.lib.auth import (
267 268 get_crypt_password, check_password)
268 269 from rhodecode.lib import hooks_base
269 270
270 271 def _password_change(new_user, password):
271 272 old_password = new_user.password or ''
272 273 # empty password
273 274 if not old_password:
274 275 return False
275 276
276 277 # password check is only needed for RhodeCode internal auth calls
277 278 # in case it's a plugin we don't care
278 279 if not plugin:
279 280
280 281 # first check if we gave crypted password back, and if it
281 282 # matches it's not password change
282 283 if new_user.password == password:
283 284 return False
284 285
285 286 password_match = check_password(password, old_password)
286 287 if not password_match:
287 288 return True
288 289
289 290 return False
290 291
291 292 # read settings on default personal repo group creation
292 293 if create_repo_group is None:
293 294 default_create_repo_group = RepoGroupModel()\
294 295 .get_default_create_personal_repo_group()
295 296 create_repo_group = default_create_repo_group
296 297
297 298 user_data = {
298 299 'username': username,
299 300 'password': password,
300 301 'email': email,
301 302 'firstname': firstname,
302 303 'lastname': lastname,
303 304 'active': active,
304 305 'admin': admin
305 306 }
306 307
307 308 if updating_user_id:
308 309 log.debug('Checking for existing account in RhodeCode '
309 310 'database with user_id `%s` ', updating_user_id)
310 311 user = User.get(updating_user_id)
311 312 # now also validate if USERNAME belongs to potentially other user
312 313 maybe_other_user = User.get_by_username(username, case_insensitive=True)
313 314 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
314 315 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
315 316 else:
316 317 log.debug('Checking for existing account in RhodeCode '
317 318 'database with username `%s` ', username)
318 319 user = User.get_by_username(username, case_insensitive=True)
319 320
320 321 if user is None:
321 322 # we check internal flag if this method is actually allowed to
322 323 # create new user
323 324 if not allow_to_create_user:
324 325 msg = ('Method wants to create new user, but it is not '
325 326 'allowed to do so')
326 327 log.warning(msg)
327 328 raise NotAllowedToCreateUserError(msg)
328 329
329 330 log.debug('Creating new user %s', username)
330 331
331 332 # only if we create user that is active
332 333 new_active_user = active
333 334 if new_active_user and strict_creation_check:
334 335 # raises UserCreationError if it's not allowed for any reason to
335 336 # create new active user, this also executes pre-create hooks
336 337 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
337 338 events.trigger(events.UserPreCreate(user_data))
338 339 new_user = User()
339 340 edit = False
340 341 else:
341 342 log.debug('updating user `%s`', username)
342 343 events.trigger(events.UserPreUpdate(user, user_data))
343 344 new_user = user
344 345 edit = True
345 346
346 347 # we're not allowed to edit default user
347 348 if user.username == User.DEFAULT_USER:
348 349 raise DefaultUserException(
349 350 "You can't edit this user (`%(username)s`) since it's "
350 351 "crucial for entire application"
351 352 % {'username': user.username})
352 353
353 354 # inject special attribute that will tell us if User is new or old
354 355 new_user.is_new_user = not edit
355 356 # for users that didn's specify auth type, we use RhodeCode built in
356 357 from rhodecode.authentication.plugins import auth_rhodecode
357 358 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
358 359 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
359 360
360 361 try:
361 362 new_user.username = username
362 363 new_user.admin = admin
363 364 new_user.email = email
364 365 new_user.active = active
365 366 new_user.extern_name = safe_str(extern_name)
366 367 new_user.extern_type = safe_str(extern_type)
367 368 new_user.name = firstname
368 369 new_user.lastname = lastname
369 370 new_user.description = description
370 371
371 372 # set password only if creating an user or password is changed
372 373 if not edit or _password_change(new_user, password):
373 374 reason = 'new password' if edit else 'new user'
374 375 log.debug('Updating password reason=>%s', reason)
375 376 new_user.password = get_crypt_password(password) if password else None
376 377
377 378 if force_password_change:
378 379 new_user.update_userdata(force_password_change=True)
379 380 if language:
380 381 new_user.update_userdata(language=language)
381 382 new_user.update_userdata(notification_status=True)
382 383
383 384 self.sa.add(new_user)
384 385
385 386 if not edit and create_repo_group:
386 387 RepoGroupModel().create_personal_repo_group(
387 388 new_user, commit_early=False)
388 389
389 390 if not edit:
390 391 # add the RSS token
391 392 self.add_auth_token(
392 393 user=username, lifetime_minutes=-1,
393 394 role=self.auth_token_role.ROLE_FEED,
394 395 description='Generated feed token')
395 396
396 397 kwargs = new_user.get_dict()
397 398 # backward compat, require api_keys present
398 399 kwargs['api_keys'] = kwargs['auth_tokens']
399 400 hooks_base.create_user(created_by=cur_user, **kwargs)
400 401 events.trigger(events.UserPostCreate(user_data))
401 402 return new_user
402 403 except (DatabaseError,):
403 404 log.error(traceback.format_exc())
404 405 raise
405 406
406 407 def create_registration(self, form_data,
407 408 extern_name='rhodecode', extern_type='rhodecode'):
408 409 from rhodecode.model.notification import NotificationModel
409 410 from rhodecode.model.notification import EmailNotificationModel
410 411
411 412 try:
412 413 form_data['admin'] = False
413 414 form_data['extern_name'] = extern_name
414 415 form_data['extern_type'] = extern_type
415 416 new_user = self.create(form_data)
416 417
417 418 self.sa.add(new_user)
418 419 self.sa.flush()
419 420
420 421 user_data = new_user.get_dict()
421 422 user_data.update({
422 423 'first_name': user_data.get('firstname'),
423 424 'last_name': user_data.get('lastname'),
424 425 })
425 426 kwargs = {
426 427 # use SQLALCHEMY safe dump of user data
427 428 'user': AttributeDict(user_data),
428 429 'date': datetime.datetime.now()
429 430 }
430 431 notification_type = EmailNotificationModel.TYPE_REGISTRATION
431 432
432 433 # create notification objects, and emails
433 434 NotificationModel().create(
434 435 created_by=new_user,
435 436 notification_subject='', # Filled in based on the notification_type
436 437 notification_body='', # Filled in based on the notification_type
437 438 notification_type=notification_type,
438 439 recipients=None, # all admins
439 440 email_kwargs=kwargs,
440 441 )
441 442
442 443 return new_user
443 444 except Exception:
444 445 log.error(traceback.format_exc())
445 446 raise
446 447
447 448 def _handle_user_repos(self, username, repositories, handle_user,
448 449 handle_mode=None):
449 450
450 451 left_overs = True
451 452
452 453 from rhodecode.model.repo import RepoModel
453 454
454 455 if handle_mode == 'detach':
455 456 for obj in repositories:
456 457 obj.user = handle_user
457 458 # set description we know why we super admin now owns
458 459 # additional repositories that were orphaned !
459 460 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
460 461 self.sa.add(obj)
461 462 left_overs = False
462 463 elif handle_mode == 'delete':
463 464 for obj in repositories:
464 465 RepoModel().delete(obj, forks='detach')
465 466 left_overs = False
466 467
467 468 # if nothing is done we have left overs left
468 469 return left_overs
469 470
470 471 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
471 472 handle_mode=None):
472 473
473 474 left_overs = True
474 475
475 476 from rhodecode.model.repo_group import RepoGroupModel
476 477
477 478 if handle_mode == 'detach':
478 479 for r in repository_groups:
479 480 r.user = handle_user
480 481 # set description we know why we super admin now owns
481 482 # additional repositories that were orphaned !
482 483 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
483 484 r.personal = False
484 485 self.sa.add(r)
485 486 left_overs = False
486 487 elif handle_mode == 'delete':
487 488 for r in repository_groups:
488 489 RepoGroupModel().delete(r)
489 490 left_overs = False
490 491
491 492 # if nothing is done we have left overs left
492 493 return left_overs
493 494
494 495 def _handle_user_user_groups(self, username, user_groups, handle_user,
495 496 handle_mode=None):
496 497
497 498 left_overs = True
498 499
499 500 from rhodecode.model.user_group import UserGroupModel
500 501
501 502 if handle_mode == 'detach':
502 503 for r in user_groups:
503 504 for user_user_group_to_perm in r.user_user_group_to_perm:
504 505 if user_user_group_to_perm.user.username == username:
505 506 user_user_group_to_perm.user = handle_user
506 507 r.user = handle_user
507 508 # set description we know why we super admin now owns
508 509 # additional repositories that were orphaned !
509 510 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
510 511 self.sa.add(r)
511 512 left_overs = False
512 513 elif handle_mode == 'delete':
513 514 for r in user_groups:
514 515 UserGroupModel().delete(r)
515 516 left_overs = False
516 517
517 518 # if nothing is done we have left overs left
518 519 return left_overs
519 520
520 521 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
521 522 handle_mode=None):
522 523 left_overs = True
523 524
524 525 from rhodecode.model.pull_request import PullRequestModel
525 526
526 527 if handle_mode == 'detach':
527 528 for pr in pull_requests:
528 529 pr.user_id = handle_user.user_id
529 530 # set description we know why we super admin now owns
530 531 # additional repositories that were orphaned !
531 532 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
532 533 self.sa.add(pr)
533 534 left_overs = False
534 535 elif handle_mode == 'delete':
535 536 for pr in pull_requests:
536 537 PullRequestModel().delete(pr)
537 538
538 539 left_overs = False
539 540
540 541 # if nothing is done we have leftovers left
541 542 return left_overs
542 543
543 544 def _handle_user_artifacts(self, username, artifacts, handle_user,
544 545 handle_mode=None):
545 546
546 547 left_overs = True
547 548
548 549 if handle_mode == 'detach':
549 550 for a in artifacts:
550 551 a.upload_user = handle_user
551 552 # set description we know why we super admin now owns
552 553 # additional artifacts that were orphaned !
553 554 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
554 555 self.sa.add(a)
555 556 left_overs = False
556 557 elif handle_mode == 'delete':
557 558 from rhodecode.apps.file_store import utils as store_utils
558 559 request = get_current_request()
559 560 storage = store_utils.get_file_storage(request.registry.settings)
560 561 for a in artifacts:
561 562 file_uid = a.file_uid
562 563 storage.delete(file_uid)
563 564 self.sa.delete(a)
564 565
565 566 left_overs = False
566 567
567 568 # if nothing is done we have left overs left
568 569 return left_overs
569 570
570 571 def delete(self, user, cur_user=None, handle_repos=None,
571 572 handle_repo_groups=None, handle_user_groups=None,
572 573 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
573 574 from rhodecode.lib import hooks_base
574 575
575 576 if not cur_user:
576 577 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
577 578
578 579 user = self._get_user(user)
579 580
580 581 try:
581 582 if user.username == User.DEFAULT_USER:
582 583 raise DefaultUserException(
583 584 "You can't remove this user since it's"
584 585 " crucial for entire application")
585 586 handle_user = handle_new_owner or self.cls.get_first_super_admin()
586 587 log.debug('New detached objects owner %s', handle_user)
587 588
588 589 left_overs = self._handle_user_repos(
589 590 user.username, user.repositories, handle_user, handle_repos)
590 591 if left_overs and user.repositories:
591 592 repos = [x.repo_name for x in user.repositories]
592 593 raise UserOwnsReposException(
593 594 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
594 595 'removed. Switch owners or remove those repositories:%(list_repos)s'
595 596 % {'username': user.username, 'len_repos': len(repos),
596 597 'list_repos': ', '.join(repos)})
597 598
598 599 left_overs = self._handle_user_repo_groups(
599 600 user.username, user.repository_groups, handle_user, handle_repo_groups)
600 601 if left_overs and user.repository_groups:
601 602 repo_groups = [x.group_name for x in user.repository_groups]
602 603 raise UserOwnsRepoGroupsException(
603 604 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
604 605 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
605 606 % {'username': user.username, 'len_repo_groups': len(repo_groups),
606 607 'list_repo_groups': ', '.join(repo_groups)})
607 608
608 609 left_overs = self._handle_user_user_groups(
609 610 user.username, user.user_groups, handle_user, handle_user_groups)
610 611 if left_overs and user.user_groups:
611 612 user_groups = [x.users_group_name for x in user.user_groups]
612 613 raise UserOwnsUserGroupsException(
613 614 'user "%s" still owns %s user groups and cannot be '
614 615 'removed. Switch owners or remove those user groups:%s'
615 616 % (user.username, len(user_groups), ', '.join(user_groups)))
616 617
617 618 left_overs = self._handle_user_pull_requests(
618 619 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
619 620 if left_overs and user.user_pull_requests:
620 621 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
621 622 raise UserOwnsPullRequestsException(
622 623 'user "%s" still owns %s pull requests and cannot be '
623 624 'removed. Switch owners or remove those pull requests:%s'
624 625 % (user.username, len(pull_requests), ', '.join(pull_requests)))
625 626
626 627 left_overs = self._handle_user_artifacts(
627 628 user.username, user.artifacts, handle_user, handle_artifacts)
628 629 if left_overs and user.artifacts:
629 630 artifacts = [x.file_uid for x in user.artifacts]
630 631 raise UserOwnsArtifactsException(
631 632 'user "%s" still owns %s artifacts and cannot be '
632 633 'removed. Switch owners or remove those artifacts:%s'
633 634 % (user.username, len(artifacts), ', '.join(artifacts)))
634 635
635 636 user_data = user.get_dict() # fetch user data before expire
636 637
637 638 # we might change the user data with detach/delete, make sure
638 639 # the object is marked as expired before actually deleting !
639 640 self.sa.expire(user)
640 641 self.sa.delete(user)
641 642
642 643 hooks_base.delete_user(deleted_by=cur_user, **user_data)
643 644 except Exception:
644 645 log.error(traceback.format_exc())
645 646 raise
646 647
647 648 def reset_password_link(self, data, pwd_reset_url):
648 649 from rhodecode.lib.celerylib import tasks, run_task
649 650 from rhodecode.model.notification import EmailNotificationModel
650 651 user_email = data['email']
651 652 try:
652 653 user = User.get_by_email(user_email)
653 654 if user:
654 655 log.debug('password reset user found %s', user)
655 656
656 657 email_kwargs = {
657 658 'password_reset_url': pwd_reset_url,
658 659 'user': user,
659 660 'email': user_email,
660 661 'date': datetime.datetime.now(),
661 662 'first_admin_email': User.get_first_super_admin().email
662 663 }
663 664
664 665 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 666 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666 667
667 668 recipients = [user_email]
668 669
669 670 action_logger_generic(
670 671 'sending password reset email to user: {}'.format(
671 672 user), namespace='security.password_reset')
672 673
673 674 run_task(tasks.send_email, recipients, subject,
674 675 email_body_plaintext, email_body)
675 676
676 677 else:
677 678 log.debug("password reset email %s not found", user_email)
678 679 except Exception:
679 680 log.error(traceback.format_exc())
680 681 return False
681 682
682 683 return True
683 684
684 685 def reset_password(self, data):
685 686 from rhodecode.lib.celerylib import tasks, run_task
686 687 from rhodecode.model.notification import EmailNotificationModel
687 688 from rhodecode.lib import auth
688 689 user_email = data['email']
689 690 pre_db = True
690 691 try:
691 692 user = User.get_by_email(user_email)
692 693 new_passwd = auth.PasswordGenerator().gen_password(
693 694 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
694 695 if user:
695 696 user.password = auth.get_crypt_password(new_passwd)
696 697 # also force this user to reset his password !
697 698 user.update_userdata(force_password_change=True)
698 699
699 700 Session().add(user)
700 701
701 702 # now delete the token in question
702 703 UserApiKeys = AuthTokenModel.cls
703 704 UserApiKeys().query().filter(
704 705 UserApiKeys.api_key == data['token']).delete()
705 706
706 707 Session().commit()
707 708 log.info('successfully reset password for `%s`', user_email)
708 709
709 710 if new_passwd is None:
710 711 raise Exception('unable to generate new password')
711 712
712 713 pre_db = False
713 714
714 715 email_kwargs = {
715 716 'new_password': new_passwd,
716 717 'user': user,
717 718 'email': user_email,
718 719 'date': datetime.datetime.now(),
719 720 'first_admin_email': User.get_first_super_admin().email
720 721 }
721 722
722 723 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
723 724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 725 **email_kwargs)
725 726
726 727 recipients = [user_email]
727 728
728 729 action_logger_generic(
729 730 'sent new password to user: {} with email: {}'.format(
730 731 user, user_email), namespace='security.password_reset')
731 732
732 733 run_task(tasks.send_email, recipients, subject,
733 734 email_body_plaintext, email_body)
734 735
735 736 except Exception:
736 737 log.error('Failed to update user password')
737 738 log.error(traceback.format_exc())
738 739 if pre_db:
739 740 # we rollback only if local db stuff fails. If it goes into
740 741 # run_task, we're pass rollback state this wouldn't work then
741 742 Session().rollback()
742 743
743 744 return True
744 745
745 746 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
746 747 """
747 748 Fetches auth_user by user_id,or api_key if present.
748 749 Fills auth_user attributes with those taken from database.
749 750 Additionally set's is_authenitated if lookup fails
750 751 present in database
751 752
752 753 :param auth_user: instance of user to set attributes
753 754 :param user_id: user id to fetch by
754 755 :param api_key: api key to fetch by
755 756 :param username: username to fetch by
756 757 """
757 758 def token_obfuscate(token):
758 759 if token:
759 760 return token[:4] + "****"
760 761
761 762 if user_id is None and api_key is None and username is None:
762 763 raise Exception('You need to pass user_id, api_key or username')
763 764
764 765 log.debug(
765 766 'AuthUser: fill data execution based on: '
766 767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
767 768 try:
769 found_with = ''
768 770 dbuser = None
769 771 if user_id:
770 772 dbuser = self.get(user_id)
773 found_with = 'user_id'
771 774 elif api_key:
772 775 dbuser = self.get_by_auth_token(api_key)
776 found_with = 'auth_token'
773 777 elif username:
774 778 dbuser = self.get_by_username(username)
779 found_with = 'username'
775 780
776 781 if not dbuser:
777 782 log.warning(
778 'Unable to lookup user by id:%s api_key:%s username:%s',
779 user_id, token_obfuscate(api_key), username)
783 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
784 user_id, token_obfuscate(api_key), username, found_with)
780 785 return False
781 786 if not dbuser.active:
782 787 log.debug('User `%s:%s` is inactive, skipping fill data',
783 788 username, user_id)
784 789 return False
785 790
786 log.debug('AuthUser: filling found user:%s data', dbuser)
791 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
787 792
788 793 attrs = {
789 794 'user_id': dbuser.user_id,
790 795 'username': dbuser.username,
791 796 'name': dbuser.name,
792 797 'first_name': dbuser.first_name,
793 798 'firstname': dbuser.firstname,
794 799 'last_name': dbuser.last_name,
795 800 'lastname': dbuser.lastname,
796 801 'admin': dbuser.admin,
797 802 'active': dbuser.active,
798 803
799 804 'email': dbuser.email,
800 805 'emails': dbuser.emails_cached(),
801 806 'short_contact': dbuser.short_contact,
802 807 'full_contact': dbuser.full_contact,
803 808 'full_name': dbuser.full_name,
804 809 'full_name_or_username': dbuser.full_name_or_username,
805 810
806 811 '_api_key': dbuser._api_key,
807 812 '_user_data': dbuser._user_data,
808 813
809 814 'created_on': dbuser.created_on,
810 815 'extern_name': dbuser.extern_name,
811 816 'extern_type': dbuser.extern_type,
812 817
813 818 'inherit_default_permissions': dbuser.inherit_default_permissions,
814 819
815 820 'language': dbuser.language,
816 821 'last_activity': dbuser.last_activity,
817 822 'last_login': dbuser.last_login,
818 823 'password': dbuser.password,
819 824 }
820 825 auth_user.__dict__.update(attrs)
821 826 except Exception:
822 827 log.error(traceback.format_exc())
823 828 auth_user.is_authenticated = False
824 829 return False
825 830
826 831 return True
827 832
828 833 def has_perm(self, user, perm):
829 834 perm = self._get_perm(perm)
830 835 user = self._get_user(user)
831 836
832 837 return UserToPerm.query().filter(UserToPerm.user == user)\
833 838 .filter(UserToPerm.permission == perm).scalar() is not None
834 839
835 840 def grant_perm(self, user, perm):
836 841 """
837 842 Grant user global permissions
838 843
839 844 :param user:
840 845 :param perm:
841 846 """
842 847 user = self._get_user(user)
843 848 perm = self._get_perm(perm)
844 849 # if this permission is already granted skip it
845 850 _perm = UserToPerm.query()\
846 851 .filter(UserToPerm.user == user)\
847 852 .filter(UserToPerm.permission == perm)\
848 853 .scalar()
849 854 if _perm:
850 855 return
851 856 new = UserToPerm()
852 857 new.user = user
853 858 new.permission = perm
854 859 self.sa.add(new)
855 860 return new
856 861
857 862 def revoke_perm(self, user, perm):
858 863 """
859 864 Revoke users global permissions
860 865
861 866 :param user:
862 867 :param perm:
863 868 """
864 869 user = self._get_user(user)
865 870 perm = self._get_perm(perm)
866 871
867 872 obj = UserToPerm.query()\
868 873 .filter(UserToPerm.user == user)\
869 874 .filter(UserToPerm.permission == perm)\
870 875 .scalar()
871 876 if obj:
872 877 self.sa.delete(obj)
873 878
874 879 def add_extra_email(self, user, email):
875 880 """
876 881 Adds email address to UserEmailMap
877 882
878 883 :param user:
879 884 :param email:
880 885 """
881 886
882 887 user = self._get_user(user)
883 888
884 889 obj = UserEmailMap()
885 890 obj.user = user
886 891 obj.email = email
887 892 self.sa.add(obj)
888 893 return obj
889 894
890 895 def delete_extra_email(self, user, email_id):
891 896 """
892 897 Removes email address from UserEmailMap
893 898
894 899 :param user:
895 900 :param email_id:
896 901 """
897 902 user = self._get_user(user)
898 903 obj = UserEmailMap.query().get(email_id)
899 904 if obj and obj.user_id == user.user_id:
900 905 self.sa.delete(obj)
901 906
902 907 def parse_ip_range(self, ip_range):
903 908 ip_list = []
904 909
905 910 def make_unique(value):
906 911 seen = []
907 912 return [c for c in value if not (c in seen or seen.append(c))]
908 913
909 914 # firsts split by commas
910 915 for ip_range in ip_range.split(','):
911 916 if not ip_range:
912 917 continue
913 918 ip_range = ip_range.strip()
914 919 if '-' in ip_range:
915 920 start_ip, end_ip = ip_range.split('-', 1)
916 921 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
917 922 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
918 923 parsed_ip_range = []
919 924
920 925 for index in range(int(start_ip), int(end_ip) + 1):
921 926 new_ip = ipaddress.ip_address(index)
922 927 parsed_ip_range.append(str(new_ip))
923 928 ip_list.extend(parsed_ip_range)
924 929 else:
925 930 ip_list.append(ip_range)
926 931
927 932 return make_unique(ip_list)
928 933
929 934 def add_extra_ip(self, user, ip, description=None):
930 935 """
931 936 Adds ip address to UserIpMap
932 937
933 938 :param user:
934 939 :param ip:
935 940 """
936 941
937 942 user = self._get_user(user)
938 943 obj = UserIpMap()
939 944 obj.user = user
940 945 obj.ip_addr = ip
941 946 obj.description = description
942 947 self.sa.add(obj)
943 948 return obj
944 949
945 950 auth_token_role = AuthTokenModel.cls
946 951
947 952 def add_auth_token(self, user, lifetime_minutes, role, description='',
948 953 scope_callback=None):
949 954 """
950 955 Add AuthToken for user.
951 956
952 957 :param user: username/user_id
953 958 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
954 959 :param role: one of AuthTokenModel.cls.ROLE_*
955 960 :param description: optional string description
956 961 """
957 962
958 963 token = AuthTokenModel().create(
959 964 user, description, lifetime_minutes, role)
960 965 if scope_callback and callable(scope_callback):
961 966 # call the callback if we provide, used to attach scope for EE edition
962 967 scope_callback(token)
963 968 return token
964 969
965 970 def delete_extra_ip(self, user, ip_id):
966 971 """
967 972 Removes ip address from UserIpMap
968 973
969 974 :param user:
970 975 :param ip_id:
971 976 """
972 977 user = self._get_user(user)
973 978 obj = UserIpMap.query().get(ip_id)
974 979 if obj and obj.user_id == user.user_id:
975 980 self.sa.delete(obj)
976 981
977 982 def get_accounts_in_creation_order(self, current_user=None):
978 983 """
979 984 Get accounts in order of creation for deactivation for license limits
980 985
981 986 pick currently logged in user, and append to the list in position 0
982 987 pick all super-admins in order of creation date and add it to the list
983 988 pick all other accounts in order of creation and add it to the list.
984 989
985 990 Based on that list, the last accounts can be disabled as they are
986 991 created at the end and don't include any of the super admins as well
987 992 as the current user.
988 993
989 994 :param current_user: optionally current user running this operation
990 995 """
991 996
992 997 if not current_user:
993 998 current_user = get_current_rhodecode_user()
994 999 active_super_admins = [
995 1000 x.user_id for x in User.query()
996 1001 .filter(User.user_id != current_user.user_id)
997 1002 .filter(User.active == true())
998 1003 .filter(User.admin == true())
999 1004 .order_by(User.created_on.asc())]
1000 1005
1001 1006 active_regular_users = [
1002 1007 x.user_id for x in User.query()
1003 1008 .filter(User.user_id != current_user.user_id)
1004 1009 .filter(User.active == true())
1005 1010 .filter(User.admin == false())
1006 1011 .order_by(User.created_on.asc())]
1007 1012
1008 1013 list_of_accounts = [current_user.user_id]
1009 1014 list_of_accounts += active_super_admins
1010 1015 list_of_accounts += active_regular_users
1011 1016
1012 1017 return list_of_accounts
1013 1018
1014 1019 def deactivate_last_users(self, expected_users, current_user=None):
1015 1020 """
1016 1021 Deactivate accounts that are over the license limits.
1017 1022 Algorithm of which accounts to disabled is based on the formula:
1018 1023
1019 1024 Get current user, then super admins in creation order, then regular
1020 1025 active users in creation order.
1021 1026
1022 1027 Using that list we mark all accounts from the end of it as inactive.
1023 1028 This way we block only latest created accounts.
1024 1029
1025 1030 :param expected_users: list of users in special order, we deactivate
1026 1031 the end N amount of users from that list
1027 1032 """
1028 1033
1029 1034 list_of_accounts = self.get_accounts_in_creation_order(
1030 1035 current_user=current_user)
1031 1036
1032 1037 for acc_id in list_of_accounts[expected_users + 1:]:
1033 1038 user = User.get(acc_id)
1034 1039 log.info('Deactivating account %s for license unlock', user)
1035 1040 user.active = False
1036 1041 Session().add(user)
1037 1042 Session().commit()
1038 1043
1039 1044 return
1040 1045
1041 1046 def get_user_log(self, user, filter_term):
1042 1047 user_log = UserLog.query()\
1043 1048 .filter(or_(UserLog.user_id == user.user_id,
1044 1049 UserLog.username == user.username))\
1045 1050 .options(joinedload(UserLog.user))\
1046 1051 .options(joinedload(UserLog.repository))\
1047 1052 .order_by(UserLog.action_date.desc())
1048 1053
1049 1054 user_log = user_log_filter(user_log, filter_term)
1050 1055 return user_log
@@ -1,217 +1,217 b''
1 1 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
2 2
3 3 <%def name="form_item(position=None, title=None, redirect_url=None, repo=None, repo_group=None)">
4 4 <tr>
5 5 <td class="td-align-top" >
6 6 <div class="label">
7 7 <label for="position">${_('Position')}:</label>
8 8 </div>
9 9 <div class="input">
10 10 <input type="text" name="position" value="${position}" style="width: 40px"/>
11 11 ${h.hidden('cur_position', position)}
12 12 </div>
13 13 </td>
14 14
15 15 <td>
16 16 <div class="label">
17 17 <label for="title">${_('Bookmark title (max 30 characters, optional)')}:</label>
18 18 </div>
19 19 <div class="input">
20 20 <input type="text" name="title" value="${title}" style="width: 300px" maxlength="30"/>
21 21
22 22 <div class="field pull-right">
23 23 <div>
24 24 <label class="btn-link btn-danger">${_('Clear')}:</label>
25 25 ${h.checkbox('remove', value=True)}
26 26 </div>
27 27 </div>
28 28 </div>
29 29
30 30 <div class="label" style="margin-top:10px">
31 31 <label for="redirect_url">${_('Redirect URL')}:</label>
32 32 </div>
33 33 <div class="input">
34 34 <input type="text" name="redirect_url" value="${redirect_url}" style="width: 600px"/>
35 35 </div>
36 36 <p class="help-block help-block-inline">
37 37 ${_('Server URL is available as ${server_url} variable. E.g. Redirect url: ${server_url}/_admin/exception_tracker')}
38 38 </p>
39 39
40 40 <div class="select" style="margin-top:5px">
41 41 <div class="label">
42 42 <label for="redirect_url">${_('Templates')}:</label>
43 43 </div>
44 44
45 45 % if repo:
46 46 ${dt.repo_name(name=repo.repo_name, rtype=repo.repo_type,rstate=None,private=None,archived=False, fork_repo_name=None)}
47 47 ${h.hidden('bookmark_repo', repo.repo_id)}
48 48 % elif repo_group:
49 49 ${dt.repo_group_name(repo_group.group_name)}
50 50 ${h.hidden('bookmark_repo_group', repo_group.group_id)}
51 51 % else:
52 52 <div>
53 53 ${h.hidden('bookmark_repo', class_='bookmark_repo')}
54 54 <p class="help-block help-block-inline">${_('Available as ${repo_url} e.g. Redirect url: ${repo_url}/changelog')}</p>
55 55 </div>
56 56 <div style="margin-top:5px">
57 57 ${h.hidden('bookmark_repo_group', class_='bookmark_repo_group')}
58 58 <p class="help-block help-block-inline">${_('Available as ${repo_group_url} e.g. Redirect url: ${repo_group_url}')}</p>
59 59 </div>
60 60
61 61 % endif
62 62 </div>
63 63
64 64 </td>
65 65
66 66 </tr>
67 67 </%def>
68 68
69 69 <div class="panel panel-default">
70 70 <div class="panel-heading">
71 71 <h3 class="panel-title">${_('Your Bookmarks')}</h3>
72 72 </div>
73 73
74 74 <div class="panel-body">
75 75 <p>
76 76 ${_('Store upto 10 bookmark links to favorite repositories, external issue tracker or CI server. ')}
77 77 <br/>
78 78 ${_('Bookmarks are accessible from your username dropdown or by keyboard shortcut `g 0-9`')}
79 79 </p>
80 80
81 81 ${h.secure_form(h.route_path('my_account_bookmarks_update'), request=request)}
82 82 <div class="form-vertical">
83 83 <table class="rctable">
84 84 ## generate always 10 entries
85 85 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
86 % for item in (c.bookmark_items + [None for i in range(10)])[:10]:
86 % for item in (c.user_bookmark_items + [None for i in range(10)])[:10]:
87 87 <input type="hidden" name="__start__" value="bookmark:mapping"/>
88 88 % if item is None:
89 89 ## empty placehodlder
90 90 ${form_item()}
91 91 % else:
92 92 ## actual entry
93 ${form_item(position=item.position, title=item.title, redirect_url=item.redirect_url, repo=item.repository, repo_group=item.repository_group)}
93 ${form_item(position=item[0].position, title=item[0].title, redirect_url=item[0].redirect_url, repo=item[1], repo_group=item[2])}
94 94 % endif
95 95 <input type="hidden" name="__end__" value="bookmark:mapping"/>
96 96 % endfor
97 97 <input type="hidden" name="__end__" value="bookmarks:sequence"/>
98 98 </table>
99 99 <div class="buttons">
100 100 ${h.submit('save',_('Save'),class_="btn")}
101 101 </div>
102 102 </div>
103 103 ${h.end_form()}
104 104 </div>
105 105 </div>
106 106
107 107 <script>
108 108 $(document).ready(function(){
109 109
110 110
111 111 var repoFilter = function (data) {
112 112 var results = [];
113 113
114 114 if (!data.results[0]) {
115 115 return data
116 116 }
117 117
118 118 $.each(data.results[0].children, function () {
119 119 // replace name to ID for submision
120 120 this.id = this.repo_id;
121 121 results.push(this);
122 122 });
123 123
124 124 data.results[0].children = results;
125 125 return data;
126 126 };
127 127
128 128
129 129 $(".bookmark_repo").select2({
130 130 cachedDataSource: {},
131 131 minimumInputLength: 2,
132 132 placeholder: "${_('repository')}",
133 133 dropdownAutoWidth: true,
134 134 containerCssClass: "drop-menu",
135 135 dropdownCssClass: "drop-menu-dropdown",
136 136 formatResult: formatRepoResult,
137 137 query: $.debounce(250, function (query) {
138 138 self = this;
139 139 var cacheKey = query.term;
140 140 var cachedData = self.cachedDataSource[cacheKey];
141 141
142 142 if (cachedData) {
143 143 query.callback({results: cachedData.results});
144 144 } else {
145 145 $.ajax({
146 146 url: pyroutes.url('repo_list_data'),
147 147 data: {'query': query.term},
148 148 dataType: 'json',
149 149 type: 'GET',
150 150 success: function (data) {
151 151 data = repoFilter(data);
152 152 self.cachedDataSource[cacheKey] = data;
153 153 query.callback({results: data.results});
154 154 },
155 155 error: function (data, textStatus, errorThrown) {
156 156 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
157 157 }
158 158 })
159 159 }
160 160 }),
161 161 });
162 162
163 163 var repoGroupFilter = function (data) {
164 164 var results = [];
165 165
166 166 if (!data.results[0]) {
167 167 return data
168 168 }
169 169
170 170 $.each(data.results[0].children, function () {
171 171 // replace name to ID for submision
172 172 this.id = this.repo_group_id;
173 173 results.push(this);
174 174 });
175 175
176 176 data.results[0].children = results;
177 177 return data;
178 178 };
179 179
180 180 $(".bookmark_repo_group").select2({
181 181 cachedDataSource: {},
182 182 minimumInputLength: 2,
183 183 placeholder: "${_('repository group')}",
184 184 dropdownAutoWidth: true,
185 185 containerCssClass: "drop-menu",
186 186 dropdownCssClass: "drop-menu-dropdown",
187 187 formatResult: formatRepoGroupResult,
188 188 query: $.debounce(250, function (query) {
189 189 self = this;
190 190 var cacheKey = query.term;
191 191 var cachedData = self.cachedDataSource[cacheKey];
192 192
193 193 if (cachedData) {
194 194 query.callback({results: cachedData.results});
195 195 } else {
196 196 $.ajax({
197 197 url: pyroutes.url('repo_group_list_data'),
198 198 data: {'query': query.term},
199 199 dataType: 'json',
200 200 type: 'GET',
201 201 success: function (data) {
202 202 data = repoGroupFilter(data);
203 203 self.cachedDataSource[cacheKey] = data;
204 204 query.callback({results: data.results});
205 205 },
206 206 error: function (data, textStatus, errorThrown) {
207 207 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
208 208 }
209 209 })
210 210 }
211 211 })
212 212 });
213 213
214 214
215 215 });
216 216
217 217 </script>
@@ -1,1263 +1,1263 b''
1 1
2 2 <%!
3 3 from rhodecode.lib import html_filters
4 4 %>
5 5
6 6 <%inherit file="root.mako"/>
7 7
8 8 <%include file="/ejs_templates/templates.html"/>
9 9
10 10 <div class="outerwrapper">
11 11 <!-- HEADER -->
12 12 <div class="header">
13 13 <div id="header-inner" class="wrapper">
14 14 <div id="logo">
15 15 <div class="logo-wrapper">
16 16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 17 </div>
18 18 % if c.rhodecode_name:
19 19 <div class="branding">
20 20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 21 </div>
22 22 % endif
23 23 </div>
24 24 <!-- MENU BAR NAV -->
25 25 ${self.menu_bar_nav()}
26 26 <!-- END MENU BAR NAV -->
27 27 </div>
28 28 </div>
29 29 ${self.menu_bar_subnav()}
30 30 <!-- END HEADER -->
31 31
32 32 <!-- CONTENT -->
33 33 <div id="content" class="wrapper">
34 34
35 35 <rhodecode-toast id="notifications"></rhodecode-toast>
36 36
37 37 <div class="main">
38 38 ${next.main()}
39 39 </div>
40 40
41 41 </div>
42 42 <!-- END CONTENT -->
43 43
44 44 </div>
45 45
46 46 <!-- FOOTER -->
47 47 <div id="footer">
48 48 <div id="footer-inner" class="title wrapper">
49 49 <div>
50 50 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
51 51
52 52 <p class="footer-link-right">
53 53 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
54 54 RhodeCode
55 55 % if c.visual.show_version:
56 56 ${c.rhodecode_version}
57 57 % endif
58 58 ${c.rhodecode_edition}
59 59 </a> |
60 60
61 61 % if c.visual.rhodecode_support_url:
62 62 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
63 63 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
64 64 % endif
65 65
66 66 </p>
67 67
68 68 <p class="server-instance" style="display:${sid}">
69 69 ## display hidden instance ID if specially defined
70 70 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
71 71 % if c.rhodecode_instanceid:
72 72 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
73 73 % endif
74 74 </p>
75 75 </div>
76 76 </div>
77 77 </div>
78 78
79 79 <!-- END FOOTER -->
80 80
81 81 ### MAKO DEFS ###
82 82
83 83 <%def name="menu_bar_subnav()">
84 84 </%def>
85 85
86 86 <%def name="breadcrumbs(class_='breadcrumbs')">
87 87 <div class="${class_}">
88 88 ${self.breadcrumbs_links()}
89 89 </div>
90 90 </%def>
91 91
92 92 <%def name="admin_menu(active=None)">
93 93
94 94 <div id="context-bar">
95 95 <div class="wrapper">
96 96 <div class="title">
97 97 <div class="title-content">
98 98 <div class="title-main">
99 99 % if c.is_super_admin:
100 100 ${_('Super-admin Panel')}
101 101 % else:
102 102 ${_('Delegated Admin Panel')}
103 103 % endif
104 104 </div>
105 105 </div>
106 106 </div>
107 107
108 108 <ul id="context-pages" class="navigation horizontal-list">
109 109
110 110 ## super-admin case (Top Menu)
111 111 % if c.is_super_admin:
112 112 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
113 113 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
114 114 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
115 115 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
116 116 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
117 117 <li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
118 118 <li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
119 119 <li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
120 120 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
121 121 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
122 122 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
123 123 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
124 124 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
125 125
126 126 ## delegated admin
127 127 % elif c.is_delegated_admin:
128 128 <%
129 129 repositories=c.auth_user.repositories_admin or c.can_create_repo
130 130 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
131 131 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
132 132 %>
133 133
134 134 %if repositories:
135 135 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
136 136 %endif
137 137 %if repository_groups:
138 138 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
139 139 %endif
140 140 %if user_groups:
141 141 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
142 142 %endif
143 143 % endif
144 144 </ul>
145 145
146 146 </div>
147 147 <div class="clear"></div>
148 148 </div>
149 149 </%def>
150 150
151 151 <%def name="dt_info_panel(elements)">
152 152 <dl class="dl-horizontal">
153 153 %for dt, dd, title, show_items in elements:
154 154 <dt>${dt}:</dt>
155 155 <dd title="${h.tooltip(title)}">
156 156 %if callable(dd):
157 157 ## allow lazy evaluation of elements
158 158 ${dd()}
159 159 %else:
160 160 ${dd}
161 161 %endif
162 162 %if show_items:
163 163 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
164 164 %endif
165 165 </dd>
166 166
167 167 %if show_items:
168 168 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
169 169 %for item in show_items:
170 170 <dt></dt>
171 171 <dd>${item}</dd>
172 172 %endfor
173 173 </div>
174 174 %endif
175 175
176 176 %endfor
177 177 </dl>
178 178 </%def>
179 179
180 180 <%def name="tr_info_entry(element)">
181 181 <% key, val, title, show_items = element %>
182 182
183 183 <tr>
184 184 <td style="vertical-align: top">${key}</td>
185 185 <td title="${h.tooltip(title)}">
186 186 %if callable(val):
187 187 ## allow lazy evaluation of elements
188 188 ${val()}
189 189 %else:
190 190 ${val}
191 191 %endif
192 192 %if show_items:
193 193 <div class="collapsable-content" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details" style="display: none">
194 194 % for item in show_items:
195 195 <dt></dt>
196 196 <dd>${item}</dd>
197 197 % endfor
198 198 </div>
199 199 %endif
200 200 </td>
201 201 <td style="vertical-align: top">
202 202 %if show_items:
203 203 <span class="btn-collapse" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details">${_('Show More')} </span>
204 204 %endif
205 205 </td>
206 206 </tr>
207 207
208 208 </%def>
209 209
210 210 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
211 211 <%
212 212 if size > 16:
213 213 gravatar_class = ['gravatar','gravatar-large']
214 214 else:
215 215 gravatar_class = ['gravatar']
216 216
217 217 data_hovercard_url = ''
218 218 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
219 219
220 220 if tooltip:
221 221 gravatar_class += ['tooltip-hovercard']
222 222 if extra_class:
223 223 gravatar_class += extra_class
224 224 if tooltip and user:
225 225 if user.username == h.DEFAULT_USER:
226 226 gravatar_class.pop(-1)
227 227 else:
228 228 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
229 229 gravatar_class = ' '.join(gravatar_class)
230 230
231 231 %>
232 232 <%doc>
233 233 TODO: johbo: For now we serve double size images to make it smooth
234 234 for retina. This is how it worked until now. Should be replaced
235 235 with a better solution at some point.
236 236 </%doc>
237 237
238 238 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2, request=request)}" />
239 239 </%def>
240 240
241 241
242 242 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
243 243 <%
244 244 email = h.email_or_none(contact)
245 245 rc_user = h.discover_user(contact)
246 246 %>
247 247
248 248 <div class="${_class}">
249 249 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
250 250 <span class="${('user user-disabled' if show_disabled else 'user')}">
251 251 ${h.link_to_user(rc_user or contact)}
252 252 </span>
253 253 </div>
254 254 </%def>
255 255
256 256
257 257 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
258 258 <%
259 259 if (size > 16):
260 260 gravatar_class = 'icon-user-group-alt'
261 261 else:
262 262 gravatar_class = 'icon-user-group-alt'
263 263
264 264 if tooltip:
265 265 gravatar_class += ' tooltip-hovercard'
266 266
267 267 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
268 268 %>
269 269 <%doc>
270 270 TODO: johbo: For now we serve double size images to make it smooth
271 271 for retina. This is how it worked until now. Should be replaced
272 272 with a better solution at some point.
273 273 </%doc>
274 274
275 275 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
276 276 </%def>
277 277
278 278 <%def name="repo_page_title(repo_instance)">
279 279 <div class="title-content repo-title">
280 280
281 281 <div class="title-main">
282 282 ## SVN/HG/GIT icons
283 283 %if h.is_hg(repo_instance):
284 284 <i class="icon-hg"></i>
285 285 %endif
286 286 %if h.is_git(repo_instance):
287 287 <i class="icon-git"></i>
288 288 %endif
289 289 %if h.is_svn(repo_instance):
290 290 <i class="icon-svn"></i>
291 291 %endif
292 292
293 293 ## public/private
294 294 %if repo_instance.private:
295 295 <i class="icon-repo-private"></i>
296 296 %else:
297 297 <i class="icon-repo-public"></i>
298 298 %endif
299 299
300 300 ## repo name with group name
301 301 ${h.breadcrumb_repo_link(repo_instance)}
302 302
303 303 ## Context Actions
304 304 <div class="pull-right">
305 305 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 307
308 308 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
309 309 % if c.repository_is_user_following:
310 310 <i class="icon-eye-off"></i>${_('Unwatch')}
311 311 % else:
312 312 <i class="icon-eye"></i>${_('Watch')}
313 313 % endif
314 314
315 315 </a>
316 316 %else:
317 317 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
318 318 %endif
319 319 </div>
320 320
321 321 </div>
322 322
323 323 ## FORKED
324 324 %if repo_instance.fork:
325 325 <p class="discreet">
326 326 <i class="icon-code-fork"></i> ${_('Fork of')}
327 327 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
328 328 </p>
329 329 %endif
330 330
331 331 ## IMPORTED FROM REMOTE
332 332 %if repo_instance.clone_uri:
333 333 <p class="discreet">
334 334 <i class="icon-code-fork"></i> ${_('Clone from')}
335 335 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
336 336 </p>
337 337 %endif
338 338
339 339 ## LOCKING STATUS
340 340 %if repo_instance.locked[0]:
341 341 <p class="locking_locked discreet">
342 342 <i class="icon-repo-lock"></i>
343 343 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
344 344 </p>
345 345 %elif repo_instance.enable_locking:
346 346 <p class="locking_unlocked discreet">
347 347 ${_('Repository not locked. Pull repository to lock it.')}
348 348 </p>
349 349 %endif
350 350
351 351 </div>
352 352 </%def>
353 353
354 354 <%def name="repo_menu(active=None)">
355 355 <%
356 356 ## determine if we have "any" option available
357 357 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
358 358 has_actions = can_lock
359 359
360 360 %>
361 361 % if c.rhodecode_db_repo.archived:
362 362 <div class="alert alert-warning text-center">
363 363 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
364 364 </div>
365 365 % endif
366 366
367 367 <!--- REPO CONTEXT BAR -->
368 368 <div id="context-bar">
369 369 <div class="wrapper">
370 370
371 371 <div class="title">
372 372 ${self.repo_page_title(c.rhodecode_db_repo)}
373 373 </div>
374 374
375 375 <ul id="context-pages" class="navigation horizontal-list">
376 376 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
377 377 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
378 378 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
379 379 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
380 380
381 381 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
382 382 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
383 383 <li class="${h.is_active('showpullrequest', active)}">
384 384 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
385 385 <div class="menulabel">
386 386 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
387 387 </div>
388 388 </a>
389 389 </li>
390 390 %endif
391 391
392 392 <li class="${h.is_active('artifacts', active)}">
393 393 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
394 394 <div class="menulabel">
395 395 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
396 396 </div>
397 397 </a>
398 398 </li>
399 399
400 400 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
401 401 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
402 402 %endif
403 403
404 404 <li class="${h.is_active('options', active)}">
405 405 % if has_actions:
406 406 <a class="menulink dropdown">
407 407 <div class="menulabel">${_('Quick Actions')}<div class="show_more"></div></div>
408 408 </a>
409 409 <ul class="submenu">
410 410 %if can_lock:
411 411 %if c.rhodecode_db_repo.locked[0]:
412 412 <li><a class="locking_del" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_unlock': 1})}">${_('Unlock Repository')}</a></li>
413 413 %else:
414 414 <li><a class="locking_add" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_lock': 1})}">${_('Lock Repository')}</a></li>
415 415 %endif
416 416 %endif
417 417 </ul>
418 418 % endif
419 419 </li>
420 420
421 421 </ul>
422 422 </div>
423 423 <div class="clear"></div>
424 424 </div>
425 425
426 426 <!--- REPO END CONTEXT BAR -->
427 427
428 428 </%def>
429 429
430 430 <%def name="repo_group_page_title(repo_group_instance)">
431 431 <div class="title-content">
432 432 <div class="title-main">
433 433 ## Repository Group icon
434 434 <i class="icon-repo-group"></i>
435 435
436 436 ## repo name with group name
437 437 ${h.breadcrumb_repo_group_link(repo_group_instance)}
438 438 </div>
439 439
440 440 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
441 441 <div class="repo-group-desc discreet">
442 442 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
443 443 </div>
444 444
445 445 </div>
446 446 </%def>
447 447
448 448
449 449 <%def name="repo_group_menu(active=None)">
450 450 <%
451 451 gr_name = c.repo_group.group_name if c.repo_group else None
452 452 # create repositories with write permission on group is set to true
453 453 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
454 454
455 455 %>
456 456
457 457
458 458 <!--- REPO GROUP CONTEXT BAR -->
459 459 <div id="context-bar">
460 460 <div class="wrapper">
461 461 <div class="title">
462 462 ${self.repo_group_page_title(c.repo_group)}
463 463 </div>
464 464
465 465 <ul id="context-pages" class="navigation horizontal-list">
466 466 <li class="${h.is_active('home', active)}">
467 467 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
468 468 </li>
469 469 % if c.is_super_admin or group_admin:
470 470 <li class="${h.is_active('settings', active)}">
471 471 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
472 472 </li>
473 473 % endif
474 474
475 475 </ul>
476 476 </div>
477 477 <div class="clear"></div>
478 478 </div>
479 479
480 480 <!--- REPO GROUP CONTEXT BAR -->
481 481
482 482 </%def>
483 483
484 484
485 485 <%def name="usermenu(active=False)">
486 486 <%
487 487 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
488 488
489 489 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
490 490 # create repositories with write permission on group is set to true
491 491
492 492 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
493 493 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
494 494 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
495 495 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
496 496
497 497 can_create_repos = c.is_super_admin or c.can_create_repo
498 498 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
499 499
500 500 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
501 501 can_create_repo_groups_in_group = c.is_super_admin or group_admin
502 502 %>
503 503
504 504 % if not_anonymous:
505 505 <%
506 506 default_target_group = dict()
507 507 if c.rhodecode_user.personal_repo_group:
508 508 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
509 509 %>
510 510
511 511 ## create action
512 512 <li>
513 513 <a href="#create-actions" onclick="return false;" class="menulink childs">
514 514 <i class="icon-plus-circled"></i>
515 515 </a>
516 516
517 517 <div class="action-menu submenu">
518 518
519 519 <ol>
520 520 ## scope of within a repository
521 521 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
522 522 <li class="submenu-title">${_('This Repository')}</li>
523 523 <li>
524 524 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
525 525 </li>
526 526 % if can_fork:
527 527 <li>
528 528 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
529 529 </li>
530 530 % endif
531 531 % endif
532 532
533 533 ## scope of within repository groups
534 534 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
535 535 <li class="submenu-title">${_('This Repository Group')}</li>
536 536
537 537 % if can_create_repos_in_group:
538 538 <li>
539 539 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
540 540 </li>
541 541 % endif
542 542
543 543 % if can_create_repo_groups_in_group:
544 544 <li>
545 545 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository Group')}</a>
546 546 </li>
547 547 % endif
548 548 % endif
549 549
550 550 ## personal group
551 551 % if c.rhodecode_user.personal_repo_group:
552 552 <li class="submenu-title">Personal Group</li>
553 553
554 554 <li>
555 555 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
556 556 </li>
557 557
558 558 <li>
559 559 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
560 560 </li>
561 561 % endif
562 562
563 563 ## Global actions
564 564 <li class="submenu-title">RhodeCode</li>
565 565 % if can_create_repos:
566 566 <li>
567 567 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
568 568 </li>
569 569 % endif
570 570
571 571 % if can_create_repo_groups:
572 572 <li>
573 573 <a href="${h.route_path('repo_group_new')}" >${_('New Repository Group')}</a>
574 574 </li>
575 575 % endif
576 576
577 577 <li>
578 578 <a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
579 579 </li>
580 580
581 581 </ol>
582 582
583 583 </div>
584 584 </li>
585 585
586 586 ## notifications
587 587 <li>
588 588 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
589 589 ${c.unread_notifications}
590 590 </a>
591 591 </li>
592 592 % endif
593 593
594 594 ## USER MENU
595 595 <li id="quick_login_li" class="${'active' if active else ''}">
596 596 % if c.rhodecode_user.username == h.DEFAULT_USER:
597 597 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
598 598 ${gravatar(c.rhodecode_user.email, 20)}
599 599 <span class="user">
600 600 <span>${_('Sign in')}</span>
601 601 </span>
602 602 </a>
603 603 % else:
604 604 ## logged in user
605 605 <a id="quick_login_link" class="menulink childs">
606 606 ${gravatar(c.rhodecode_user.email, 20)}
607 607 <span class="user">
608 608 <span class="menu_link_user">${c.rhodecode_user.username}</span>
609 609 <div class="show_more"></div>
610 610 </span>
611 611 </a>
612 612 ## subnav with menu for logged in user
613 613 <div class="user-menu submenu">
614 614 <div id="quick_login">
615 615 %if c.rhodecode_user.username != h.DEFAULT_USER:
616 616 <div class="">
617 617 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
618 618 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
619 619 <div class="email">${c.rhodecode_user.email}</div>
620 620 </div>
621 621 <div class="">
622 622 <ol class="links">
623 623 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
624 624 % if c.rhodecode_user.personal_repo_group:
625 625 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
626 626 % endif
627 627 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
628 628
629 629 % if c.debug_style:
630 630 <li>
631 631 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
632 632 <div class="menulabel">${_('[Style]')}</div>
633 633 </a>
634 634 </li>
635 635 % endif
636 636
637 637 ## bookmark-items
638 638 <li class="bookmark-items">
639 639 ${_('Bookmarks')}
640 640 <div class="pull-right">
641 641 <a href="${h.route_path('my_account_bookmarks')}">
642 642
643 643 <i class="icon-cog"></i>
644 644 </a>
645 645 </div>
646 646 </li>
647 647 % if not c.bookmark_items:
648 648 <li>
649 649 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
650 650 </li>
651 651 % endif
652 652 % for item in c.bookmark_items:
653 653 <li>
654 % if item.repository:
654 % if item.repo_id:
655 655 <div>
656 656 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
657 657 <code>${item.position}</code>
658 % if item.repository.repo_type == 'hg':
658 % if item.repo_type == 'hg':
659 659 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
660 % elif item.repository.repo_type == 'git':
660 % elif item.repo_type == 'git':
661 661 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
662 % elif item.repository.repo_type == 'svn':
662 % elif item.repo_type == 'svn':
663 663 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
664 664 % endif
665 ${(item.title or h.shorter(item.repository.repo_name, 30))}
665 ${(item.title or h.shorter(item.repo_name, 30))}
666 666 </a>
667 667 </div>
668 % elif item.repository_group:
668 % elif item.group_id:
669 669 <div>
670 670 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
671 671 <code>${item.position}</code>
672 672 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
673 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
673 ${(item.title or h.shorter(item.group_name, 30))}
674 674 </a>
675 675 </div>
676 676 % else:
677 677 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
678 678 <code>${item.position}</code>
679 679 ${item.title}
680 680 </a>
681 681 % endif
682 682 </li>
683 683 % endfor
684 684
685 685 <li class="logout">
686 686 ${h.secure_form(h.route_path('logout'), request=request)}
687 687 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
688 688 ${h.end_form()}
689 689 </li>
690 690 </ol>
691 691 </div>
692 692 %endif
693 693 </div>
694 694 </div>
695 695
696 696 % endif
697 697 </li>
698 698 </%def>
699 699
700 700 <%def name="menu_items(active=None)">
701 701 <%
702 702 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
703 703 notice_display = 'none' if len(notice_messages) == 0 else ''
704 704 %>
705 705
706 706 <ul id="quick" class="main_nav navigation horizontal-list">
707 707 ## notice box for important system messages
708 708 <li style="display: ${notice_display}">
709 709 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
710 710 <div class="menulabel-notice ${notice_level}" >
711 711 ${len(notice_messages)}
712 712 </div>
713 713 </a>
714 714 </li>
715 715 <div class="notice-messages-container" style="display: none">
716 716 <div class="notice-messages">
717 717 <table class="rctable">
718 718 % for notice in notice_messages:
719 719 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
720 720 <td style="vertical-align: text-top; width: 20px">
721 721 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
722 722 </td>
723 723 <td>
724 724 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
725 725 ${notice['subject']}
726 726
727 727 <div id="notice-${notice['msg_id']}" style="display: none">
728 728 ${h.render(notice['body'], renderer='markdown')}
729 729 </div>
730 730 </td>
731 731 <td style="vertical-align: text-top; width: 35px;">
732 732 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
733 733 <i class="icon-remove icon-filled-red"></i>
734 734 </a>
735 735 </td>
736 736 </tr>
737 737
738 738 % endfor
739 739 </table>
740 740 </div>
741 741 </div>
742 742 ## Main filter
743 743 <li>
744 744 <div class="menulabel main_filter_box">
745 745 <div class="main_filter_input_box">
746 746 <ul class="searchItems">
747 747
748 748 <li class="searchTag searchTagIcon">
749 749 <i class="icon-search"></i>
750 750 </li>
751 751
752 752 % if c.template_context['search_context']['repo_id']:
753 753 <li class="searchTag searchTagFilter searchTagHidable" >
754 754 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
755 755 <span class="tag">
756 756 This repo
757 757 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
758 758 </span>
759 759 ##</a>
760 760 </li>
761 761 % elif c.template_context['search_context']['repo_group_id']:
762 762 <li class="searchTag searchTagFilter searchTagHidable">
763 763 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
764 764 <span class="tag">
765 765 This group
766 766 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
767 767 </span>
768 768 ##</a>
769 769 </li>
770 770 % endif
771 771
772 772 <li class="searchTagInput">
773 773 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
774 774 </li>
775 775 <li class="searchTag searchTagHelp">
776 776 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
777 777 </li>
778 778 </ul>
779 779 </div>
780 780 </div>
781 781
782 782 <div id="main_filter_help" style="display: none">
783 783 - Use '/' key to quickly access this field.
784 784
785 785 - Enter a name of repository, or repository group for quick search.
786 786
787 787 - Prefix query to allow special search:
788 788
789 789 <strong>user:</strong>admin, to search for usernames, always global
790 790
791 791 <strong>user_group:</strong>devops, to search for user groups, always global
792 792
793 793 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
794 794
795 795 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
796 796
797 797 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
798 798
799 799 % if c.template_context['search_context']['repo_id']:
800 800 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
801 801 % elif c.template_context['search_context']['repo_group_id']:
802 802 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
803 803 % else:
804 804 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
805 805 % endif
806 806 </div>
807 807 </li>
808 808
809 809 ## ROOT MENU
810 810 <li class="${h.is_active('home', active)}">
811 811 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
812 812 <div class="menulabel">${_('Home')}</div>
813 813 </a>
814 814 </li>
815 815
816 816 %if c.rhodecode_user.username != h.DEFAULT_USER:
817 817 <li class="${h.is_active('journal', active)}">
818 818 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
819 819 <div class="menulabel">${_('Journal')}</div>
820 820 </a>
821 821 </li>
822 822 %else:
823 823 <li class="${h.is_active('journal', active)}">
824 824 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
825 825 <div class="menulabel">${_('Public journal')}</div>
826 826 </a>
827 827 </li>
828 828 %endif
829 829
830 830 <li class="${h.is_active('gists', active)}">
831 831 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
832 832 <div class="menulabel">${_('Gists')}</div>
833 833 </a>
834 834 </li>
835 835
836 836 % if c.is_super_admin or c.is_delegated_admin:
837 837 <li class="${h.is_active('admin', active)}">
838 838 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
839 839 <div class="menulabel">${_('Admin')} </div>
840 840 </a>
841 841 </li>
842 842 % endif
843 843
844 844 ## render extra user menu
845 845 ${usermenu(active=(active=='my_account'))}
846 846
847 847 </ul>
848 848
849 849 <script type="text/javascript">
850 850 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
851 851
852 852 var formatRepoResult = function(result, container, query, escapeMarkup) {
853 853 return function(data, escapeMarkup) {
854 854 if (!data.repo_id){
855 855 return data.text; // optgroup text Repositories
856 856 }
857 857
858 858 var tmpl = '';
859 859 var repoType = data['repo_type'];
860 860 var repoName = data['text'];
861 861
862 862 if(data && data.type == 'repo'){
863 863 if(repoType === 'hg'){
864 864 tmpl += '<i class="icon-hg"></i> ';
865 865 }
866 866 else if(repoType === 'git'){
867 867 tmpl += '<i class="icon-git"></i> ';
868 868 }
869 869 else if(repoType === 'svn'){
870 870 tmpl += '<i class="icon-svn"></i> ';
871 871 }
872 872 if(data['private']){
873 873 tmpl += '<i class="icon-lock" ></i> ';
874 874 }
875 875 else if(visualShowPublicIcon){
876 876 tmpl += '<i class="icon-unlock-alt"></i> ';
877 877 }
878 878 }
879 879 tmpl += escapeMarkup(repoName);
880 880 return tmpl;
881 881
882 882 }(result, escapeMarkup);
883 883 };
884 884
885 885 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
886 886 return function(data, escapeMarkup) {
887 887 if (!data.repo_group_id){
888 888 return data.text; // optgroup text Repositories
889 889 }
890 890
891 891 var tmpl = '';
892 892 var repoGroupName = data['text'];
893 893
894 894 if(data){
895 895
896 896 tmpl += '<i class="icon-repo-group"></i> ';
897 897
898 898 }
899 899 tmpl += escapeMarkup(repoGroupName);
900 900 return tmpl;
901 901
902 902 }(result, escapeMarkup);
903 903 };
904 904
905 905 var escapeRegExChars = function (value) {
906 906 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
907 907 };
908 908
909 909 var getRepoIcon = function(repo_type) {
910 910 if (repo_type === 'hg') {
911 911 return '<i class="icon-hg"></i> ';
912 912 }
913 913 else if (repo_type === 'git') {
914 914 return '<i class="icon-git"></i> ';
915 915 }
916 916 else if (repo_type === 'svn') {
917 917 return '<i class="icon-svn"></i> ';
918 918 }
919 919 return ''
920 920 };
921 921
922 922 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
923 923
924 924 if (value.split(':').length === 2) {
925 925 value = value.split(':')[1]
926 926 }
927 927
928 928 var searchType = data['type'];
929 929 var searchSubType = data['subtype'];
930 930 var valueDisplay = data['value_display'];
931 931 var valueIcon = data['value_icon'];
932 932
933 933 var pattern = '(' + escapeRegExChars(value) + ')';
934 934
935 935 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
936 936
937 937 // highlight match
938 938 if (searchType != 'text') {
939 939 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
940 940 }
941 941
942 942 var icon = '';
943 943
944 944 if (searchType === 'hint') {
945 945 icon += '<i class="icon-repo-group"></i> ';
946 946 }
947 947 // full text search/hints
948 948 else if (searchType === 'search') {
949 949 if (valueIcon === undefined) {
950 950 icon += '<i class="icon-more"></i> ';
951 951 } else {
952 952 icon += valueIcon + ' ';
953 953 }
954 954
955 955 if (searchSubType !== undefined && searchSubType == 'repo') {
956 956 valueDisplay += '<div class="pull-right tag">repository</div>';
957 957 }
958 958 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
959 959 valueDisplay += '<div class="pull-right tag">repo group</div>';
960 960 }
961 961 }
962 962 // repository
963 963 else if (searchType === 'repo') {
964 964
965 965 var repoIcon = getRepoIcon(data['repo_type']);
966 966 icon += repoIcon;
967 967
968 968 if (data['private']) {
969 969 icon += '<i class="icon-lock" ></i> ';
970 970 }
971 971 else if (visualShowPublicIcon) {
972 972 icon += '<i class="icon-unlock-alt"></i> ';
973 973 }
974 974 }
975 975 // repository groups
976 976 else if (searchType === 'repo_group') {
977 977 icon += '<i class="icon-repo-group"></i> ';
978 978 }
979 979 // user group
980 980 else if (searchType === 'user_group') {
981 981 icon += '<i class="icon-group"></i> ';
982 982 }
983 983 // user
984 984 else if (searchType === 'user') {
985 985 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
986 986 }
987 987 // pull request
988 988 else if (searchType === 'pull_request') {
989 989 icon += '<i class="icon-merge"></i> ';
990 990 }
991 991 // commit
992 992 else if (searchType === 'commit') {
993 993 var repo_data = data['repo_data'];
994 994 var repoIcon = getRepoIcon(repo_data['repository_type']);
995 995 if (repoIcon) {
996 996 icon += repoIcon;
997 997 } else {
998 998 icon += '<i class="icon-tag"></i>';
999 999 }
1000 1000 }
1001 1001 // file
1002 1002 else if (searchType === 'file') {
1003 1003 var repo_data = data['repo_data'];
1004 1004 var repoIcon = getRepoIcon(repo_data['repository_type']);
1005 1005 if (repoIcon) {
1006 1006 icon += repoIcon;
1007 1007 } else {
1008 1008 icon += '<i class="icon-tag"></i>';
1009 1009 }
1010 1010 }
1011 1011 // generic text
1012 1012 else if (searchType === 'text') {
1013 1013 icon = '';
1014 1014 }
1015 1015
1016 1016 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1017 1017 return tmpl.format(icon, valueDisplay);
1018 1018 };
1019 1019
1020 1020 var handleSelect = function(element, suggestion) {
1021 1021 if (suggestion.type === "hint") {
1022 1022 // we skip action
1023 1023 $('#main_filter').focus();
1024 1024 }
1025 1025 else if (suggestion.type === "text") {
1026 1026 // we skip action
1027 1027 $('#main_filter').focus();
1028 1028
1029 1029 } else {
1030 1030 window.location = suggestion['url'];
1031 1031 }
1032 1032 };
1033 1033
1034 1034 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1035 1035 if (queryLowerCase.split(':').length === 2) {
1036 1036 queryLowerCase = queryLowerCase.split(':')[1]
1037 1037 }
1038 1038 if (suggestion.type === "text") {
1039 1039 // special case we don't want to "skip" display for
1040 1040 return true
1041 1041 }
1042 1042 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1043 1043 };
1044 1044
1045 1045 var cleanContext = {
1046 1046 repo_view_type: null,
1047 1047
1048 1048 repo_id: null,
1049 1049 repo_name: "",
1050 1050
1051 1051 repo_group_id: null,
1052 1052 repo_group_name: null
1053 1053 };
1054 1054 var removeGoToFilter = function () {
1055 1055 $('.searchTagHidable').hide();
1056 1056 $('#main_filter').autocomplete(
1057 1057 'setOptions', {params:{search_context: cleanContext}});
1058 1058 };
1059 1059
1060 1060 $('#main_filter').autocomplete({
1061 1061 serviceUrl: pyroutes.url('goto_switcher_data'),
1062 1062 params: {
1063 1063 "search_context": templateContext.search_context
1064 1064 },
1065 1065 minChars:2,
1066 1066 maxHeight:400,
1067 1067 deferRequestBy: 300, //miliseconds
1068 1068 tabDisabled: true,
1069 1069 autoSelectFirst: false,
1070 1070 containerClass: 'autocomplete-qfilter-suggestions',
1071 1071 formatResult: autocompleteMainFilterFormatResult,
1072 1072 lookupFilter: autocompleteMainFilterResult,
1073 1073 onSelect: function (element, suggestion) {
1074 1074 handleSelect(element, suggestion);
1075 1075 return false;
1076 1076 },
1077 1077 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1078 1078 if (jqXHR !== 'abort') {
1079 1079 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1080 1080 SwalNoAnimation.fire({
1081 1081 icon: 'error',
1082 1082 title: _gettext('Error during search operation'),
1083 1083 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1084 1084 }).then(function(result) {
1085 1085 window.location.reload();
1086 1086 })
1087 1087 }
1088 1088 },
1089 1089 onSearchStart: function (params) {
1090 1090 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1091 1091 },
1092 1092 onSearchComplete: function (query, suggestions) {
1093 1093 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1094 1094 },
1095 1095 });
1096 1096
1097 1097 showMainFilterBox = function () {
1098 1098 $('#main_filter_help').toggle();
1099 1099 };
1100 1100
1101 1101 $('#main_filter').on('keydown.autocomplete', function (e) {
1102 1102
1103 1103 var BACKSPACE = 8;
1104 1104 var el = $(e.currentTarget);
1105 1105 if(e.which === BACKSPACE){
1106 1106 var inputVal = el.val();
1107 1107 if (inputVal === ""){
1108 1108 removeGoToFilter()
1109 1109 }
1110 1110 }
1111 1111 });
1112 1112
1113 1113 var dismissNotice = function(noticeId) {
1114 1114
1115 1115 var url = pyroutes.url('user_notice_dismiss',
1116 1116 {"user_id": templateContext.rhodecode_user.user_id});
1117 1117
1118 1118 var postData = {
1119 1119 'csrf_token': CSRF_TOKEN,
1120 1120 'notice_id': noticeId,
1121 1121 };
1122 1122
1123 1123 var success = function(response) {
1124 1124 $('#notice-message-' + noticeId).remove();
1125 1125 return false;
1126 1126 };
1127 1127 var failure = function(data, textStatus, xhr) {
1128 1128 alert("error processing request: " + textStatus);
1129 1129 return false;
1130 1130 };
1131 1131 ajaxPOST(url, postData, success, failure);
1132 1132 }
1133 1133
1134 1134 var hideLicenseWarning = function () {
1135 1135 var fingerprint = templateContext.session_attrs.license_fingerprint;
1136 1136 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1137 1137 $('#notifications').hide();
1138 1138 }
1139 1139
1140 1140 var hideLicenseError = function () {
1141 1141 var fingerprint = templateContext.session_attrs.license_fingerprint;
1142 1142 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1143 1143 $('#notifications').hide();
1144 1144 }
1145 1145
1146 1146 </script>
1147 1147 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1148 1148 </%def>
1149 1149
1150 1150 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1151 1151 <div class="modal-dialog">
1152 1152 <div class="modal-content">
1153 1153 <div class="modal-header">
1154 1154 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1155 1155 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1156 1156 </div>
1157 1157 <div class="modal-body">
1158 1158 <div class="block-left">
1159 1159 <table class="keyboard-mappings">
1160 1160 <tbody>
1161 1161 <tr>
1162 1162 <th></th>
1163 1163 <th>${_('Site-wide shortcuts')}</th>
1164 1164 </tr>
1165 1165 <%
1166 1166 elems = [
1167 1167 ('/', 'Use quick search box'),
1168 1168 ('g h', 'Goto home page'),
1169 1169 ('g g', 'Goto my private gists page'),
1170 1170 ('g G', 'Goto my public gists page'),
1171 1171 ('g 0-9', 'Goto bookmarked items from 0-9'),
1172 1172 ('n r', 'New repository page'),
1173 1173 ('n g', 'New gist page'),
1174 1174 ]
1175 1175 %>
1176 1176 %for key, desc in elems:
1177 1177 <tr>
1178 1178 <td class="keys">
1179 1179 <span class="key tag">${key}</span>
1180 1180 </td>
1181 1181 <td>${desc}</td>
1182 1182 </tr>
1183 1183 %endfor
1184 1184 </tbody>
1185 1185 </table>
1186 1186 </div>
1187 1187 <div class="block-left">
1188 1188 <table class="keyboard-mappings">
1189 1189 <tbody>
1190 1190 <tr>
1191 1191 <th></th>
1192 1192 <th>${_('Repositories')}</th>
1193 1193 </tr>
1194 1194 <%
1195 1195 elems = [
1196 1196 ('g s', 'Goto summary page'),
1197 1197 ('g c', 'Goto changelog page'),
1198 1198 ('g f', 'Goto files page'),
1199 1199 ('g F', 'Goto files page with file search activated'),
1200 1200 ('g p', 'Goto pull requests page'),
1201 1201 ('g o', 'Goto repository settings'),
1202 1202 ('g O', 'Goto repository access permissions settings'),
1203 1203 ('t s', 'Toggle sidebar on some pages'),
1204 1204 ]
1205 1205 %>
1206 1206 %for key, desc in elems:
1207 1207 <tr>
1208 1208 <td class="keys">
1209 1209 <span class="key tag">${key}</span>
1210 1210 </td>
1211 1211 <td>${desc}</td>
1212 1212 </tr>
1213 1213 %endfor
1214 1214 </tbody>
1215 1215 </table>
1216 1216 </div>
1217 1217 </div>
1218 1218 <div class="modal-footer">
1219 1219 </div>
1220 1220 </div><!-- /.modal-content -->
1221 1221 </div><!-- /.modal-dialog -->
1222 1222 </div><!-- /.modal -->
1223 1223
1224 1224
1225 1225 <script type="text/javascript">
1226 1226 (function () {
1227 1227 "use sctrict";
1228 1228
1229 1229 // details block auto-hide menu
1230 1230 $(document).mouseup(function(e) {
1231 1231 var container = $('.details-inline-block');
1232 1232 if (!container.is(e.target) && container.has(e.target).length === 0) {
1233 1233 $('.details-inline-block[open]').removeAttr('open')
1234 1234 }
1235 1235 });
1236 1236
1237 1237 var $sideBar = $('.right-sidebar');
1238 1238 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1239 1239 var sidebarState = templateContext.session_attrs.sidebarState;
1240 1240 var sidebarEnabled = $('aside.right-sidebar').get(0);
1241 1241
1242 1242 if (sidebarState === 'expanded') {
1243 1243 expanded = true
1244 1244 } else if (sidebarState === 'collapsed') {
1245 1245 expanded = false
1246 1246 }
1247 1247 if (sidebarEnabled) {
1248 1248 // show sidebar since it's hidden on load
1249 1249 $('.right-sidebar').show();
1250 1250
1251 1251 // init based on set initial class, or if defined user session attrs
1252 1252 if (expanded) {
1253 1253 window.expandSidebar();
1254 1254 window.updateStickyHeader();
1255 1255
1256 1256 } else {
1257 1257 window.collapseSidebar();
1258 1258 window.updateStickyHeader();
1259 1259 }
1260 1260 }
1261 1261 })()
1262 1262
1263 1263 </script>
General Comments 0
You need to be logged in to leave comments. Login now