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