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