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