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