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