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