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