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