##// END OF EJS Templates
repository extra fields implementation...
marcink -
r3308:72a91632 beta
parent child Browse files
Show More
@@ -26,7 +26,7 b''
26 import sys
26 import sys
27 import platform
27 import platform
28
28
29 VERSION = (1, 5, 3, 'b')
29 VERSION = (1, 6, 0, 'b')
30
30
31 try:
31 try:
32 from rhodecode.lib import get_current_revision
32 from rhodecode.lib import get_current_revision
@@ -38,7 +38,7 b' except ImportError:'
38
38
39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
40 '.'.join(VERSION[3:]))
40 '.'.join(VERSION[3:]))
41 __dbversion__ = 10 # defines current db version for migrations
41 __dbversion__ = 11 # defines current db version for migrations
42 __platform__ = platform.system()
42 __platform__ = platform.system()
43 __license__ = 'GPLv3'
43 __license__ = 'GPLv3'
44 __py_version__ = sys.version_info
44 __py_version__ = sys.version_info
@@ -145,6 +145,14 b' def make_map(config):'
145 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
145 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
146 action="repo_locking", conditions=dict(method=["PUT"],
146 action="repo_locking", conditions=dict(method=["PUT"],
147 function=check_repo))
147 function=check_repo))
148 #repo fields
149 m.connect('create_repo_fields', "/repo_fields/{repo_name:.*?}/new",
150 action="create_repo_field", conditions=dict(method=["PUT"],
151 function=check_repo))
152
153 m.connect('delete_repo_fields', "/repo_fields/{repo_name:.*?}/{field_id}",
154 action="delete_repo_field", conditions=dict(method=["DELETE"],
155 function=check_repo))
148
156
149 with rmap.submapper(path_prefix=ADMIN_PREFIX,
157 with rmap.submapper(path_prefix=ADMIN_PREFIX,
150 controller='admin/repos_groups') as m:
158 controller='admin/repos_groups') as m:
@@ -43,8 +43,8 b' from rhodecode.lib.utils import invalida'
43 from rhodecode.lib.helpers import get_token
43 from rhodecode.lib.helpers import get_token
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup,\
45 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup,\
46 RhodeCodeSetting
46 RhodeCodeSetting, RepositoryField
47 from rhodecode.model.forms import RepoForm
47 from rhodecode.model.forms import RepoForm, RepoFieldForm
48 from rhodecode.model.scm import ScmModel, GroupList
48 from rhodecode.model.scm import ScmModel, GroupList
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.lib.compat import json
50 from rhodecode.lib.compat import json
@@ -118,6 +118,9 b' class ReposController(BaseRepoController'
118 c.stats_percentage = '%.2f' % ((float((last_rev)) /
118 c.stats_percentage = '%.2f' % ((float((last_rev)) /
119 c.repo_last_rev) * 100)
119 c.repo_last_rev) * 100)
120
120
121 c.repo_fields = RepositoryField.query()\
122 .filter(RepositoryField.repository == db_repo).all()
123
121 defaults = RepoModel()._get_defaults(repo_name)
124 defaults = RepoModel()._get_defaults(repo_name)
122
125
123 c.repos_list = [('', _('--REMOVE FORK--'))]
126 c.repos_list = [('', _('--REMOVE FORK--'))]
@@ -491,3 +494,38 b' class ReposController(BaseRepoController'
491 encoding="UTF-8",
494 encoding="UTF-8",
492 force_defaults=False
495 force_defaults=False
493 )
496 )
497
498 @HasPermissionAllDecorator('hg.admin')
499 def create_repo_field(self, repo_name):
500 try:
501 form_result = RepoFieldForm()().to_python(dict(request.POST))
502 new_field = RepositoryField()
503 new_field.repository = Repository.get_by_repo_name(repo_name)
504 new_field.field_key = form_result['new_field_key']
505 new_field.field_type = form_result['new_field_type'] # python type
506 new_field.field_value = form_result['new_field_value'] # set initial blank value
507 new_field.field_desc = form_result['new_field_desc']
508 new_field.field_label = form_result['new_field_label']
509 Session().add(new_field)
510 Session().commit()
511
512 except Exception, e:
513 log.error(traceback.format_exc())
514 msg = _('An error occurred during creation of field')
515 if isinstance(e, formencode.Invalid):
516 msg += ". " + e.msg
517 h.flash(msg, category='error')
518 return redirect(url('edit_repo', repo_name=repo_name))
519
520 @HasPermissionAllDecorator('hg.admin')
521 def delete_repo_field(self, repo_name, field_id):
522 field = RepositoryField.get_or_404(field_id)
523 try:
524 Session().delete(field)
525 Session().commit()
526 except Exception, e:
527 log.error(traceback.format_exc())
528 msg = _('An error occurred during removal of field')
529 h.flash(msg, category='error')
530 return redirect(url('edit_repo', repo_name=repo_name))
531
@@ -204,6 +204,11 b' class SettingsController(BaseController)'
204 form_result['rhodecode_lightweight_dashboard']
204 form_result['rhodecode_lightweight_dashboard']
205 Session().add(sett4)
205 Session().add(sett4)
206
206
207 sett4 = RhodeCodeSetting.get_by_name_or_create('repository_fields')
208 sett4.app_settings_value = \
209 form_result['rhodecode_repository_fields']
210 Session().add(sett4)
211
207 Session().commit()
212 Session().commit()
208 set_rhodecode_config(config)
213 set_rhodecode_config(config)
209 h.flash(_('Updated visualisation settings'),
214 h.flash(_('Updated visualisation settings'),
@@ -42,7 +42,7 b' from rhodecode.lib.utils import invalida'
42
42
43 from rhodecode.model.forms import RepoSettingsForm
43 from rhodecode.model.forms import RepoSettingsForm
44 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.db import RepoGroup, Repository
45 from rhodecode.model.db import RepoGroup, Repository, RepositoryField
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.scm import ScmModel, GroupList
47 from rhodecode.model.scm import ScmModel, GroupList
48
48
@@ -75,7 +75,7 b' class SettingsController(BaseRepoControl'
75 """
75 """
76 self.__load_defaults()
76 self.__load_defaults()
77
77
78 c.repo_info = Repository.get_by_repo_name(repo_name)
78 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
79
79
80 if c.repo_info is None:
80 if c.repo_info is None:
81 h.not_mapped_error(repo_name)
81 h.not_mapped_error(repo_name)
@@ -84,7 +84,8 b' class SettingsController(BaseRepoControl'
84 ##override defaults for exact repo info here git/hg etc
84 ##override defaults for exact repo info here git/hg etc
85 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
85 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
86 c.landing_revs_choices = choices
86 c.landing_revs_choices = choices
87
87 c.repo_fields = RepositoryField.query()\
88 .filter(RepositoryField.repository == db_repo).all()
88 defaults = RepoModel()._get_defaults(repo_name)
89 defaults = RepoModel()._get_defaults(repo_name)
89
90
90 return defaults
91 return defaults
@@ -263,6 +263,7 b' class BaseController(WSGIController):'
263 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
263 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
264 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
264 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
265 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
265 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
266 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
266
267
267 c.repo_name = get_repo_slug(request)
268 c.repo_name = get_repo_slug(request)
268 c.backends = BACKENDS.keys()
269 c.backends = BACKENDS.keys()
This diff has been collapsed as it changes many lines, (1926 lines changed) Show them Hide them
@@ -1,6 +1,6 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.db_1_4_0
3 rhodecode.model.db_1_5_2
4 ~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Database Models for RhodeCode <=1.5.X
6 Database Models for RhodeCode <=1.5.X
@@ -23,6 +23,1926 b''
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 #TODO: replace that will db.py content after 1.6 Release
26 import os
27 import logging
28 import datetime
29 import traceback
30 import hashlib
31 import time
32 from collections import defaultdict
33
34 from sqlalchemy import *
35 from sqlalchemy.ext.hybrid import hybrid_property
36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 from sqlalchemy.exc import DatabaseError
38 from beaker.cache import cache_region, region_invalidate
39 from webob.exc import HTTPNotFound
40
41 from pylons.i18n.translation import lazy_ugettext as _
42
43 from rhodecode.lib.vcs import get_backend
44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 from rhodecode.lib.vcs.exceptions import VCSError
46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47
48 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 safe_unicode, remove_suffix, remove_prefix
50 from rhodecode.lib.compat import json
51 from rhodecode.lib.caching_query import FromCache
52
53 from rhodecode.model.meta import Base, Session
54
55 URL_SEP = '/'
56 log = logging.getLogger(__name__)
57
58 #==============================================================================
59 # BASE CLASSES
60 #==============================================================================
61
62 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63
64
65 class BaseModel(object):
66 """
67 Base Model for all classess
68 """
69
70 @classmethod
71 def _get_keys(cls):
72 """return column names for this model """
73 return class_mapper(cls).c.keys()
74
75 def get_dict(self):
76 """
77 return dict with keys and values corresponding
78 to this model data """
79
80 d = {}
81 for k in self._get_keys():
82 d[k] = getattr(self, k)
83
84 # also use __json__() if present to get additional fields
85 _json_attr = getattr(self, '__json__', None)
86 if _json_attr:
87 # update with attributes from __json__
88 if callable(_json_attr):
89 _json_attr = _json_attr()
90 for k, val in _json_attr.iteritems():
91 d[k] = val
92 return d
93
94 def get_appstruct(self):
95 """return list with keys and values tupples corresponding
96 to this model data """
97
98 l = []
99 for k in self._get_keys():
100 l.append((k, getattr(self, k),))
101 return l
102
103 def populate_obj(self, populate_dict):
104 """populate model with data from given populate_dict"""
105
106 for k in self._get_keys():
107 if k in populate_dict:
108 setattr(self, k, populate_dict[k])
109
110 @classmethod
111 def query(cls):
112 return Session().query(cls)
113
114 @classmethod
115 def get(cls, id_):
116 if id_:
117 return cls.query().get(id_)
118
119 @classmethod
120 def get_or_404(cls, id_):
121 try:
122 id_ = int(id_)
123 except (TypeError, ValueError):
124 raise HTTPNotFound
125
126 res = cls.query().get(id_)
127 if not res:
128 raise HTTPNotFound
129 return res
130
131 @classmethod
132 def getAll(cls):
133 return cls.query().all()
134
135 @classmethod
136 def delete(cls, id_):
137 obj = cls.query().get(id_)
138 Session().delete(obj)
139
140 def __repr__(self):
141 if hasattr(self, '__unicode__'):
142 # python repr needs to return str
143 return safe_str(self.__unicode__())
144 return '<DB:%s>' % (self.__class__.__name__)
145
146
147 class RhodeCodeSetting(Base, BaseModel):
148 __tablename__ = 'rhodecode_settings'
149 __table_args__ = (
150 UniqueConstraint('app_settings_name'),
151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 'mysql_charset': 'utf8'}
153 )
154 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157
158 def __init__(self, k='', v=''):
159 self.app_settings_name = k
160 self.app_settings_value = v
161
162 @validates('_app_settings_value')
163 def validate_settings_value(self, key, val):
164 assert type(val) == unicode
165 return val
166
167 @hybrid_property
168 def app_settings_value(self):
169 v = self._app_settings_value
170 if self.app_settings_name in ["ldap_active",
171 "default_repo_enable_statistics",
172 "default_repo_enable_locking",
173 "default_repo_private",
174 "default_repo_enable_downloads"]:
175 v = str2bool(v)
176 return v
177
178 @app_settings_value.setter
179 def app_settings_value(self, val):
180 """
181 Setter that will always make sure we use unicode in app_settings_value
182
183 :param val:
184 """
185 self._app_settings_value = safe_unicode(val)
186
187 def __unicode__(self):
188 return u"<%s('%s:%s')>" % (
189 self.__class__.__name__,
190 self.app_settings_name, self.app_settings_value
191 )
192
193 @classmethod
194 def get_by_name(cls, key):
195 return cls.query()\
196 .filter(cls.app_settings_name == key).scalar()
197
198 @classmethod
199 def get_by_name_or_create(cls, key):
200 res = cls.get_by_name(key)
201 if not res:
202 res = cls(key)
203 return res
204
205 @classmethod
206 def get_app_settings(cls, cache=False):
207
208 ret = cls.query()
209
210 if cache:
211 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
212
213 if not ret:
214 raise Exception('Could not get application settings !')
215 settings = {}
216 for each in ret:
217 settings['rhodecode_' + each.app_settings_name] = \
218 each.app_settings_value
219
220 return settings
221
222 @classmethod
223 def get_ldap_settings(cls, cache=False):
224 ret = cls.query()\
225 .filter(cls.app_settings_name.startswith('ldap_')).all()
226 fd = {}
227 for row in ret:
228 fd.update({row.app_settings_name: row.app_settings_value})
229
230 return fd
231
232 @classmethod
233 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
234 ret = cls.query()\
235 .filter(cls.app_settings_name.startswith('default_')).all()
236 fd = {}
237 for row in ret:
238 key = row.app_settings_name
239 if strip_prefix:
240 key = remove_prefix(key, prefix='default_')
241 fd.update({key: row.app_settings_value})
242
243 return fd
244
245
246 class RhodeCodeUi(Base, BaseModel):
247 __tablename__ = 'rhodecode_ui'
248 __table_args__ = (
249 UniqueConstraint('ui_key'),
250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
251 'mysql_charset': 'utf8'}
252 )
253
254 HOOK_UPDATE = 'changegroup.update'
255 HOOK_REPO_SIZE = 'changegroup.repo_size'
256 HOOK_PUSH = 'changegroup.push_logger'
257 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
258 HOOK_PULL = 'outgoing.pull_logger'
259 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
260
261 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
262 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
266
267 @classmethod
268 def get_by_key(cls, key):
269 return cls.query().filter(cls.ui_key == key).scalar()
270
271 @classmethod
272 def get_builtin_hooks(cls):
273 q = cls.query()
274 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
275 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
276 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
277 return q.all()
278
279 @classmethod
280 def get_custom_hooks(cls):
281 q = cls.query()
282 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
283 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
284 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
285 q = q.filter(cls.ui_section == 'hooks')
286 return q.all()
287
288 @classmethod
289 def get_repos_location(cls):
290 return cls.get_by_key('/').ui_value
291
292 @classmethod
293 def create_or_update_hook(cls, key, val):
294 new_ui = cls.get_by_key(key) or cls()
295 new_ui.ui_section = 'hooks'
296 new_ui.ui_active = True
297 new_ui.ui_key = key
298 new_ui.ui_value = val
299
300 Session().add(new_ui)
301
302 def __repr__(self):
303 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
304 self.ui_value)
305
306
307 class User(Base, BaseModel):
308 __tablename__ = 'users'
309 __table_args__ = (
310 UniqueConstraint('username'), UniqueConstraint('email'),
311 Index('u_username_idx', 'username'),
312 Index('u_email_idx', 'email'),
313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
314 'mysql_charset': 'utf8'}
315 )
316 DEFAULT_USER = 'default'
317 DEFAULT_PERMISSIONS = [
318 'hg.register.manual_activate', 'hg.create.repository',
319 'hg.fork.repository', 'repository.read', 'group.read'
320 ]
321 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
322 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
323 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
325 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
326 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
330 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
333
334 user_log = relationship('UserLog')
335 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
336
337 repositories = relationship('Repository')
338 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
339 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
340
341 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
342 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
343
344 group_member = relationship('UsersGroupMember', cascade='all')
345
346 notifications = relationship('UserNotification', cascade='all')
347 # notifications assigned to this user
348 user_created_notifications = relationship('Notification', cascade='all')
349 # comments created by this user
350 user_comments = relationship('ChangesetComment', cascade='all')
351 #extra emails for this user
352 user_emails = relationship('UserEmailMap', cascade='all')
353
354 @hybrid_property
355 def email(self):
356 return self._email
357
358 @email.setter
359 def email(self, val):
360 self._email = val.lower() if val else None
361
362 @property
363 def firstname(self):
364 # alias for future
365 return self.name
366
367 @property
368 def emails(self):
369 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
370 return [self.email] + [x.email for x in other]
371
372 @property
373 def ip_addresses(self):
374 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
375 return [x.ip_addr for x in ret]
376
377 @property
378 def username_and_name(self):
379 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
380
381 @property
382 def full_name(self):
383 return '%s %s' % (self.firstname, self.lastname)
384
385 @property
386 def full_name_or_username(self):
387 return ('%s %s' % (self.firstname, self.lastname)
388 if (self.firstname and self.lastname) else self.username)
389
390 @property
391 def full_contact(self):
392 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
393
394 @property
395 def short_contact(self):
396 return '%s %s' % (self.firstname, self.lastname)
397
398 @property
399 def is_admin(self):
400 return self.admin
401
402 def __unicode__(self):
403 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
404 self.user_id, self.username)
405
406 @classmethod
407 def get_by_username(cls, username, case_insensitive=False, cache=False):
408 if case_insensitive:
409 q = cls.query().filter(cls.username.ilike(username))
410 else:
411 q = cls.query().filter(cls.username == username)
412
413 if cache:
414 q = q.options(FromCache(
415 "sql_cache_short",
416 "get_user_%s" % _hash_key(username)
417 )
418 )
419 return q.scalar()
420
421 @classmethod
422 def get_by_api_key(cls, api_key, cache=False):
423 q = cls.query().filter(cls.api_key == api_key)
424
425 if cache:
426 q = q.options(FromCache("sql_cache_short",
427 "get_api_key_%s" % api_key))
428 return q.scalar()
429
430 @classmethod
431 def get_by_email(cls, email, case_insensitive=False, cache=False):
432 if case_insensitive:
433 q = cls.query().filter(cls.email.ilike(email))
434 else:
435 q = cls.query().filter(cls.email == email)
436
437 if cache:
438 q = q.options(FromCache("sql_cache_short",
439 "get_email_key_%s" % email))
440
441 ret = q.scalar()
442 if ret is None:
443 q = UserEmailMap.query()
444 # try fetching in alternate email map
445 if case_insensitive:
446 q = q.filter(UserEmailMap.email.ilike(email))
447 else:
448 q = q.filter(UserEmailMap.email == email)
449 q = q.options(joinedload(UserEmailMap.user))
450 if cache:
451 q = q.options(FromCache("sql_cache_short",
452 "get_email_map_key_%s" % email))
453 ret = getattr(q.scalar(), 'user', None)
454
455 return ret
456
457 def update_lastlogin(self):
458 """Update user lastlogin"""
459 self.last_login = datetime.datetime.now()
460 Session().add(self)
461 log.debug('updated user %s lastlogin' % self.username)
462
463 def get_api_data(self):
464 """
465 Common function for generating user related data for API
466 """
467 user = self
468 data = dict(
469 user_id=user.user_id,
470 username=user.username,
471 firstname=user.name,
472 lastname=user.lastname,
473 email=user.email,
474 emails=user.emails,
475 api_key=user.api_key,
476 active=user.active,
477 admin=user.admin,
478 ldap_dn=user.ldap_dn,
479 last_login=user.last_login,
480 ip_addresses=user.ip_addresses
481 )
482 return data
483
484 def __json__(self):
485 data = dict(
486 full_name=self.full_name,
487 full_name_or_username=self.full_name_or_username,
488 short_contact=self.short_contact,
489 full_contact=self.full_contact
490 )
491 data.update(self.get_api_data())
492 return data
493
494
495 class UserEmailMap(Base, BaseModel):
496 __tablename__ = 'user_email_map'
497 __table_args__ = (
498 Index('uem_email_idx', 'email'),
499 UniqueConstraint('email'),
500 {'extend_existing': True, 'mysql_engine': 'InnoDB',
501 'mysql_charset': 'utf8'}
502 )
503 __mapper_args__ = {}
504
505 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
507 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
508 user = relationship('User', lazy='joined')
509
510 @validates('_email')
511 def validate_email(self, key, email):
512 # check if this email is not main one
513 main_email = Session().query(User).filter(User.email == email).scalar()
514 if main_email is not None:
515 raise AttributeError('email %s is present is user table' % email)
516 return email
517
518 @hybrid_property
519 def email(self):
520 return self._email
521
522 @email.setter
523 def email(self, val):
524 self._email = val.lower() if val else None
525
526
527 class UserIpMap(Base, BaseModel):
528 __tablename__ = 'user_ip_map'
529 __table_args__ = (
530 UniqueConstraint('user_id', 'ip_addr'),
531 {'extend_existing': True, 'mysql_engine': 'InnoDB',
532 'mysql_charset': 'utf8'}
533 )
534 __mapper_args__ = {}
535
536 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
537 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
538 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
539 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
540 user = relationship('User', lazy='joined')
541
542 @classmethod
543 def _get_ip_range(cls, ip_addr):
544 from rhodecode.lib import ipaddr
545 net = ipaddr.IPv4Network(ip_addr)
546 return [str(net.network), str(net.broadcast)]
547
548 def __json__(self):
549 return dict(
550 ip_addr=self.ip_addr,
551 ip_range=self._get_ip_range(self.ip_addr)
552 )
553
554
555 class UserLog(Base, BaseModel):
556 __tablename__ = 'user_logs'
557 __table_args__ = (
558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
559 'mysql_charset': 'utf8'},
560 )
561 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
562 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
563 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
564 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
565 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
566 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
567 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
568 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
569
570 @property
571 def action_as_day(self):
572 return datetime.date(*self.action_date.timetuple()[:3])
573
574 user = relationship('User')
575 repository = relationship('Repository', cascade='')
576
577
578 class UsersGroup(Base, BaseModel):
579 __tablename__ = 'users_groups'
580 __table_args__ = (
581 {'extend_existing': True, 'mysql_engine': 'InnoDB',
582 'mysql_charset': 'utf8'},
583 )
584
585 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
586 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
587 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
588 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
589
590 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
591 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
592 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
593
594 def __unicode__(self):
595 return u'<userGroup(%s)>' % (self.users_group_name)
596
597 @classmethod
598 def get_by_group_name(cls, group_name, cache=False,
599 case_insensitive=False):
600 if case_insensitive:
601 q = cls.query().filter(cls.users_group_name.ilike(group_name))
602 else:
603 q = cls.query().filter(cls.users_group_name == group_name)
604 if cache:
605 q = q.options(FromCache(
606 "sql_cache_short",
607 "get_user_%s" % _hash_key(group_name)
608 )
609 )
610 return q.scalar()
611
612 @classmethod
613 def get(cls, users_group_id, cache=False):
614 users_group = cls.query()
615 if cache:
616 users_group = users_group.options(FromCache("sql_cache_short",
617 "get_users_group_%s" % users_group_id))
618 return users_group.get(users_group_id)
619
620 def get_api_data(self):
621 users_group = self
622
623 data = dict(
624 users_group_id=users_group.users_group_id,
625 group_name=users_group.users_group_name,
626 active=users_group.users_group_active,
627 )
628
629 return data
630
631
632 class UsersGroupMember(Base, BaseModel):
633 __tablename__ = 'users_groups_members'
634 __table_args__ = (
635 {'extend_existing': True, 'mysql_engine': 'InnoDB',
636 'mysql_charset': 'utf8'},
637 )
638
639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
642
643 user = relationship('User', lazy='joined')
644 users_group = relationship('UsersGroup')
645
646 def __init__(self, gr_id='', u_id=''):
647 self.users_group_id = gr_id
648 self.user_id = u_id
649
650
651 class Repository(Base, BaseModel):
652 __tablename__ = 'repositories'
653 __table_args__ = (
654 UniqueConstraint('repo_name'),
655 Index('r_repo_name_idx', 'repo_name'),
656 {'extend_existing': True, 'mysql_engine': 'InnoDB',
657 'mysql_charset': 'utf8'},
658 )
659
660 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
661 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
662 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
663 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
664 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
665 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
666 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
667 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
668 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
669 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
670 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
671 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
672 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
673 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
674 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
675
676 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
677 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
678
679 user = relationship('User')
680 fork = relationship('Repository', remote_side=repo_id)
681 group = relationship('RepoGroup')
682 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
683 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
684 stats = relationship('Statistics', cascade='all', uselist=False)
685
686 followers = relationship('UserFollowing',
687 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
688 cascade='all')
689
690 logs = relationship('UserLog')
691 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
692
693 pull_requests_org = relationship('PullRequest',
694 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
695 cascade="all, delete, delete-orphan")
696
697 pull_requests_other = relationship('PullRequest',
698 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
699 cascade="all, delete, delete-orphan")
700
701 def __unicode__(self):
702 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
703 self.repo_name)
704
705 @hybrid_property
706 def locked(self):
707 # always should return [user_id, timelocked]
708 if self._locked:
709 _lock_info = self._locked.split(':')
710 return int(_lock_info[0]), _lock_info[1]
711 return [None, None]
712
713 @locked.setter
714 def locked(self, val):
715 if val and isinstance(val, (list, tuple)):
716 self._locked = ':'.join(map(str, val))
717 else:
718 self._locked = None
719
720 @hybrid_property
721 def changeset_cache(self):
722 from rhodecode.lib.vcs.backends.base import EmptyChangeset
723 dummy = EmptyChangeset().__json__()
724 if not self._changeset_cache:
725 return dummy
726 try:
727 return json.loads(self._changeset_cache)
728 except TypeError:
729 return dummy
730
731 @changeset_cache.setter
732 def changeset_cache(self, val):
733 try:
734 self._changeset_cache = json.dumps(val)
735 except:
736 log.error(traceback.format_exc())
737
738 @classmethod
739 def url_sep(cls):
740 return URL_SEP
741
742 @classmethod
743 def normalize_repo_name(cls, repo_name):
744 """
745 Normalizes os specific repo_name to the format internally stored inside
746 dabatabase using URL_SEP
747
748 :param cls:
749 :param repo_name:
750 """
751 return cls.url_sep().join(repo_name.split(os.sep))
752
753 @classmethod
754 def get_by_repo_name(cls, repo_name):
755 q = Session().query(cls).filter(cls.repo_name == repo_name)
756 q = q.options(joinedload(Repository.fork))\
757 .options(joinedload(Repository.user))\
758 .options(joinedload(Repository.group))
759 return q.scalar()
760
761 @classmethod
762 def get_by_full_path(cls, repo_full_path):
763 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
764 repo_name = cls.normalize_repo_name(repo_name)
765 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
766
767 @classmethod
768 def get_repo_forks(cls, repo_id):
769 return cls.query().filter(Repository.fork_id == repo_id)
770
771 @classmethod
772 def base_path(cls):
773 """
774 Returns base path when all repos are stored
775
776 :param cls:
777 """
778 q = Session().query(RhodeCodeUi)\
779 .filter(RhodeCodeUi.ui_key == cls.url_sep())
780 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
781 return q.one().ui_value
782
783 @property
784 def forks(self):
785 """
786 Return forks of this repo
787 """
788 return Repository.get_repo_forks(self.repo_id)
789
790 @property
791 def parent(self):
792 """
793 Returns fork parent
794 """
795 return self.fork
796
797 @property
798 def just_name(self):
799 return self.repo_name.split(Repository.url_sep())[-1]
800
801 @property
802 def groups_with_parents(self):
803 groups = []
804 if self.group is None:
805 return groups
806
807 cur_gr = self.group
808 groups.insert(0, cur_gr)
809 while 1:
810 gr = getattr(cur_gr, 'parent_group', None)
811 cur_gr = cur_gr.parent_group
812 if gr is None:
813 break
814 groups.insert(0, gr)
815
816 return groups
817
818 @property
819 def groups_and_repo(self):
820 return self.groups_with_parents, self.just_name
821
822 @LazyProperty
823 def repo_path(self):
824 """
825 Returns base full path for that repository means where it actually
826 exists on a filesystem
827 """
828 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
829 Repository.url_sep())
830 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
831 return q.one().ui_value
832
833 @property
834 def repo_full_path(self):
835 p = [self.repo_path]
836 # we need to split the name by / since this is how we store the
837 # names in the database, but that eventually needs to be converted
838 # into a valid system path
839 p += self.repo_name.split(Repository.url_sep())
840 return os.path.join(*p)
841
842 @property
843 def cache_keys(self):
844 """
845 Returns associated cache keys for that repo
846 """
847 return CacheInvalidation.query()\
848 .filter(CacheInvalidation.cache_args == self.repo_name)\
849 .order_by(CacheInvalidation.cache_key)\
850 .all()
851
852 def get_new_name(self, repo_name):
853 """
854 returns new full repository name based on assigned group and new new
855
856 :param group_name:
857 """
858 path_prefix = self.group.full_path_splitted if self.group else []
859 return Repository.url_sep().join(path_prefix + [repo_name])
860
861 @property
862 def _ui(self):
863 """
864 Creates an db based ui object for this repository
865 """
866 from rhodecode.lib.utils import make_ui
867 return make_ui('db', clear_session=False)
868
869 @classmethod
870 def inject_ui(cls, repo, extras={}):
871 from rhodecode.lib.vcs.backends.hg import MercurialRepository
872 from rhodecode.lib.vcs.backends.git import GitRepository
873 required = (MercurialRepository, GitRepository)
874 if not isinstance(repo, required):
875 raise Exception('repo must be instance of %s' % required)
876
877 # inject ui extra param to log this action via push logger
878 for k, v in extras.items():
879 repo._repo.ui.setconfig('rhodecode_extras', k, v)
880
881 @classmethod
882 def is_valid(cls, repo_name):
883 """
884 returns True if given repo name is a valid filesystem repository
885
886 :param cls:
887 :param repo_name:
888 """
889 from rhodecode.lib.utils import is_valid_repo
890
891 return is_valid_repo(repo_name, cls.base_path())
892
893 def get_api_data(self):
894 """
895 Common function for generating repo api data
896
897 """
898 repo = self
899 data = dict(
900 repo_id=repo.repo_id,
901 repo_name=repo.repo_name,
902 repo_type=repo.repo_type,
903 clone_uri=repo.clone_uri,
904 private=repo.private,
905 created_on=repo.created_on,
906 description=repo.description,
907 landing_rev=repo.landing_rev,
908 owner=repo.user.username,
909 fork_of=repo.fork.repo_name if repo.fork else None,
910 enable_statistics=repo.enable_statistics,
911 enable_locking=repo.enable_locking,
912 enable_downloads=repo.enable_downloads,
913 last_changeset=repo.changeset_cache
914 )
915
916 return data
917
918 @classmethod
919 def lock(cls, repo, user_id):
920 repo.locked = [user_id, time.time()]
921 Session().add(repo)
922 Session().commit()
923
924 @classmethod
925 def unlock(cls, repo):
926 repo.locked = None
927 Session().add(repo)
928 Session().commit()
929
930 @property
931 def last_db_change(self):
932 return self.updated_on
933
934 def clone_url(self, **override):
935 from pylons import url
936 from urlparse import urlparse
937 import urllib
938 parsed_url = urlparse(url('home', qualified=True))
939 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
940 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
941 args = {
942 'user': '',
943 'pass': '',
944 'scheme': parsed_url.scheme,
945 'netloc': parsed_url.netloc,
946 'prefix': decoded_path,
947 'path': self.repo_name
948 }
949
950 args.update(override)
951 return default_clone_uri % args
952
953 #==========================================================================
954 # SCM PROPERTIES
955 #==========================================================================
956
957 def get_changeset(self, rev=None):
958 return get_changeset_safe(self.scm_instance, rev)
959
960 def get_landing_changeset(self):
961 """
962 Returns landing changeset, or if that doesn't exist returns the tip
963 """
964 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
965 return cs
966
967 def update_changeset_cache(self, cs_cache=None):
968 """
969 Update cache of last changeset for repository, keys should be::
970
971 short_id
972 raw_id
973 revision
974 message
975 date
976 author
977
978 :param cs_cache:
979 """
980 from rhodecode.lib.vcs.backends.base import BaseChangeset
981 if cs_cache is None:
982 cs_cache = self.get_changeset()
983 if isinstance(cs_cache, BaseChangeset):
984 cs_cache = cs_cache.__json__()
27
985
28 from rhodecode.model.db import *
986 if cs_cache != self.changeset_cache:
987 last_change = cs_cache.get('date') or self.last_change
988 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
989 self.updated_on = last_change
990 self.changeset_cache = cs_cache
991 Session().add(self)
992 Session().commit()
993
994 @property
995 def tip(self):
996 return self.get_changeset('tip')
997
998 @property
999 def author(self):
1000 return self.tip.author
1001
1002 @property
1003 def last_change(self):
1004 return self.scm_instance.last_change
1005
1006 def get_comments(self, revisions=None):
1007 """
1008 Returns comments for this repository grouped by revisions
1009
1010 :param revisions: filter query by revisions only
1011 """
1012 cmts = ChangesetComment.query()\
1013 .filter(ChangesetComment.repo == self)
1014 if revisions:
1015 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1016 grouped = defaultdict(list)
1017 for cmt in cmts.all():
1018 grouped[cmt.revision].append(cmt)
1019 return grouped
1020
1021 def statuses(self, revisions=None):
1022 """
1023 Returns statuses for this repository
1024
1025 :param revisions: list of revisions to get statuses for
1026 :type revisions: list
1027 """
1028
1029 statuses = ChangesetStatus.query()\
1030 .filter(ChangesetStatus.repo == self)\
1031 .filter(ChangesetStatus.version == 0)
1032 if revisions:
1033 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1034 grouped = {}
1035
1036 #maybe we have open new pullrequest without a status ?
1037 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1038 status_lbl = ChangesetStatus.get_status_lbl(stat)
1039 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1040 for rev in pr.revisions:
1041 pr_id = pr.pull_request_id
1042 pr_repo = pr.other_repo.repo_name
1043 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1044
1045 for stat in statuses.all():
1046 pr_id = pr_repo = None
1047 if stat.pull_request:
1048 pr_id = stat.pull_request.pull_request_id
1049 pr_repo = stat.pull_request.other_repo.repo_name
1050 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1051 pr_id, pr_repo]
1052 return grouped
1053
1054 #==========================================================================
1055 # SCM CACHE INSTANCE
1056 #==========================================================================
1057
1058 @property
1059 def invalidate(self):
1060 return CacheInvalidation.invalidate(self.repo_name)
1061
1062 def set_invalidate(self):
1063 """
1064 set a cache for invalidation for this instance
1065 """
1066 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1067
1068 @LazyProperty
1069 def scm_instance(self):
1070 import rhodecode
1071 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1072 if full_cache:
1073 return self.scm_instance_cached()
1074 return self.__get_instance()
1075
1076 def scm_instance_cached(self, cache_map=None):
1077 @cache_region('long_term')
1078 def _c(repo_name):
1079 return self.__get_instance()
1080 rn = self.repo_name
1081 log.debug('Getting cached instance of repo')
1082
1083 if cache_map:
1084 # get using prefilled cache_map
1085 invalidate_repo = cache_map[self.repo_name]
1086 if invalidate_repo:
1087 invalidate_repo = (None if invalidate_repo.cache_active
1088 else invalidate_repo)
1089 else:
1090 # get from invalidate
1091 invalidate_repo = self.invalidate
1092
1093 if invalidate_repo is not None:
1094 region_invalidate(_c, None, rn)
1095 # update our cache
1096 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1097 return _c(rn)
1098
1099 def __get_instance(self):
1100 repo_full_path = self.repo_full_path
1101 try:
1102 alias = get_scm(repo_full_path)[0]
1103 log.debug('Creating instance of %s repository' % alias)
1104 backend = get_backend(alias)
1105 except VCSError:
1106 log.error(traceback.format_exc())
1107 log.error('Perhaps this repository is in db and not in '
1108 'filesystem run rescan repositories with '
1109 '"destroy old data " option from admin panel')
1110 return
1111
1112 if alias == 'hg':
1113
1114 repo = backend(safe_str(repo_full_path), create=False,
1115 baseui=self._ui)
1116 # skip hidden web repository
1117 if repo._get_hidden():
1118 return
1119 else:
1120 repo = backend(repo_full_path, create=False)
1121
1122 return repo
1123
1124
1125 class RepoGroup(Base, BaseModel):
1126 __tablename__ = 'groups'
1127 __table_args__ = (
1128 UniqueConstraint('group_name', 'group_parent_id'),
1129 CheckConstraint('group_id != group_parent_id'),
1130 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1131 'mysql_charset': 'utf8'},
1132 )
1133 __mapper_args__ = {'order_by': 'group_name'}
1134
1135 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1136 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1137 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1138 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1139 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1140
1141 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1142 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
1143
1144 parent_group = relationship('RepoGroup', remote_side=group_id)
1145
1146 def __init__(self, group_name='', parent_group=None):
1147 self.group_name = group_name
1148 self.parent_group = parent_group
1149
1150 def __unicode__(self):
1151 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1152 self.group_name)
1153
1154 @classmethod
1155 def groups_choices(cls, check_perms=False):
1156 from webhelpers.html import literal as _literal
1157 from rhodecode.model.scm import ScmModel
1158 groups = cls.query().all()
1159 if check_perms:
1160 #filter group user have access to, it's done
1161 #magically inside ScmModel based on current user
1162 groups = ScmModel().get_repos_groups(groups)
1163 repo_groups = [('', '')]
1164 sep = ' &raquo; '
1165 _name = lambda k: _literal(sep.join(k))
1166
1167 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1168 for x in groups])
1169
1170 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1171 return repo_groups
1172
1173 @classmethod
1174 def url_sep(cls):
1175 return URL_SEP
1176
1177 @classmethod
1178 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1179 if case_insensitive:
1180 gr = cls.query()\
1181 .filter(cls.group_name.ilike(group_name))
1182 else:
1183 gr = cls.query()\
1184 .filter(cls.group_name == group_name)
1185 if cache:
1186 gr = gr.options(FromCache(
1187 "sql_cache_short",
1188 "get_group_%s" % _hash_key(group_name)
1189 )
1190 )
1191 return gr.scalar()
1192
1193 @property
1194 def parents(self):
1195 parents_recursion_limit = 5
1196 groups = []
1197 if self.parent_group is None:
1198 return groups
1199 cur_gr = self.parent_group
1200 groups.insert(0, cur_gr)
1201 cnt = 0
1202 while 1:
1203 cnt += 1
1204 gr = getattr(cur_gr, 'parent_group', None)
1205 cur_gr = cur_gr.parent_group
1206 if gr is None:
1207 break
1208 if cnt == parents_recursion_limit:
1209 # this will prevent accidental infinit loops
1210 log.error('group nested more than %s' %
1211 parents_recursion_limit)
1212 break
1213
1214 groups.insert(0, gr)
1215 return groups
1216
1217 @property
1218 def children(self):
1219 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1220
1221 @property
1222 def name(self):
1223 return self.group_name.split(RepoGroup.url_sep())[-1]
1224
1225 @property
1226 def full_path(self):
1227 return self.group_name
1228
1229 @property
1230 def full_path_splitted(self):
1231 return self.group_name.split(RepoGroup.url_sep())
1232
1233 @property
1234 def repositories(self):
1235 return Repository.query()\
1236 .filter(Repository.group == self)\
1237 .order_by(Repository.repo_name)
1238
1239 @property
1240 def repositories_recursive_count(self):
1241 cnt = self.repositories.count()
1242
1243 def children_count(group):
1244 cnt = 0
1245 for child in group.children:
1246 cnt += child.repositories.count()
1247 cnt += children_count(child)
1248 return cnt
1249
1250 return cnt + children_count(self)
1251
1252 def recursive_groups_and_repos(self):
1253 """
1254 Recursive return all groups, with repositories in those groups
1255 """
1256 all_ = []
1257
1258 def _get_members(root_gr):
1259 for r in root_gr.repositories:
1260 all_.append(r)
1261 childs = root_gr.children.all()
1262 if childs:
1263 for gr in childs:
1264 all_.append(gr)
1265 _get_members(gr)
1266
1267 _get_members(self)
1268 return [self] + all_
1269
1270 def get_new_name(self, group_name):
1271 """
1272 returns new full group name based on parent and new name
1273
1274 :param group_name:
1275 """
1276 path_prefix = (self.parent_group.full_path_splitted if
1277 self.parent_group else [])
1278 return RepoGroup.url_sep().join(path_prefix + [group_name])
1279
1280
1281 class Permission(Base, BaseModel):
1282 __tablename__ = 'permissions'
1283 __table_args__ = (
1284 Index('p_perm_name_idx', 'permission_name'),
1285 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1286 'mysql_charset': 'utf8'},
1287 )
1288 PERMS = [
1289 ('repository.none', _('Repository no access')),
1290 ('repository.read', _('Repository read access')),
1291 ('repository.write', _('Repository write access')),
1292 ('repository.admin', _('Repository admin access')),
1293
1294 ('group.none', _('Repositories Group no access')),
1295 ('group.read', _('Repositories Group read access')),
1296 ('group.write', _('Repositories Group write access')),
1297 ('group.admin', _('Repositories Group admin access')),
1298
1299 ('hg.admin', _('RhodeCode Administrator')),
1300 ('hg.create.none', _('Repository creation disabled')),
1301 ('hg.create.repository', _('Repository creation enabled')),
1302 ('hg.fork.none', _('Repository forking disabled')),
1303 ('hg.fork.repository', _('Repository forking enabled')),
1304 ('hg.register.none', _('Register disabled')),
1305 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1306 'with manual activation')),
1307
1308 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1309 'with auto activation')),
1310 ]
1311
1312 # defines which permissions are more important higher the more important
1313 PERM_WEIGHTS = {
1314 'repository.none': 0,
1315 'repository.read': 1,
1316 'repository.write': 3,
1317 'repository.admin': 4,
1318
1319 'group.none': 0,
1320 'group.read': 1,
1321 'group.write': 3,
1322 'group.admin': 4,
1323
1324 'hg.fork.none': 0,
1325 'hg.fork.repository': 1,
1326 'hg.create.none': 0,
1327 'hg.create.repository':1
1328 }
1329
1330 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1331 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1332 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1333
1334 def __unicode__(self):
1335 return u"<%s('%s:%s')>" % (
1336 self.__class__.__name__, self.permission_id, self.permission_name
1337 )
1338
1339 @classmethod
1340 def get_by_key(cls, key):
1341 return cls.query().filter(cls.permission_name == key).scalar()
1342
1343 @classmethod
1344 def get_default_perms(cls, default_user_id):
1345 q = Session().query(UserRepoToPerm, Repository, cls)\
1346 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1347 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1348 .filter(UserRepoToPerm.user_id == default_user_id)
1349
1350 return q.all()
1351
1352 @classmethod
1353 def get_default_group_perms(cls, default_user_id):
1354 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1355 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1356 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1357 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1358
1359 return q.all()
1360
1361
1362 class UserRepoToPerm(Base, BaseModel):
1363 __tablename__ = 'repo_to_perm'
1364 __table_args__ = (
1365 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1366 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1367 'mysql_charset': 'utf8'}
1368 )
1369 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1370 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1371 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1372 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1373
1374 user = relationship('User')
1375 repository = relationship('Repository')
1376 permission = relationship('Permission')
1377
1378 @classmethod
1379 def create(cls, user, repository, permission):
1380 n = cls()
1381 n.user = user
1382 n.repository = repository
1383 n.permission = permission
1384 Session().add(n)
1385 return n
1386
1387 def __unicode__(self):
1388 return u'<user:%s => %s >' % (self.user, self.repository)
1389
1390
1391 class UserToPerm(Base, BaseModel):
1392 __tablename__ = 'user_to_perm'
1393 __table_args__ = (
1394 UniqueConstraint('user_id', 'permission_id'),
1395 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1396 'mysql_charset': 'utf8'}
1397 )
1398 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1399 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1400 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1401
1402 user = relationship('User')
1403 permission = relationship('Permission', lazy='joined')
1404
1405
1406 class UsersGroupRepoToPerm(Base, BaseModel):
1407 __tablename__ = 'users_group_repo_to_perm'
1408 __table_args__ = (
1409 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1410 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1411 'mysql_charset': 'utf8'}
1412 )
1413 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1414 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1415 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1416 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1417
1418 users_group = relationship('UsersGroup')
1419 permission = relationship('Permission')
1420 repository = relationship('Repository')
1421
1422 @classmethod
1423 def create(cls, users_group, repository, permission):
1424 n = cls()
1425 n.users_group = users_group
1426 n.repository = repository
1427 n.permission = permission
1428 Session().add(n)
1429 return n
1430
1431 def __unicode__(self):
1432 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1433
1434
1435 class UsersGroupToPerm(Base, BaseModel):
1436 __tablename__ = 'users_group_to_perm'
1437 __table_args__ = (
1438 UniqueConstraint('users_group_id', 'permission_id',),
1439 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1440 'mysql_charset': 'utf8'}
1441 )
1442 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1443 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1445
1446 users_group = relationship('UsersGroup')
1447 permission = relationship('Permission')
1448
1449
1450 class UserRepoGroupToPerm(Base, BaseModel):
1451 __tablename__ = 'user_repo_group_to_perm'
1452 __table_args__ = (
1453 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1454 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1455 'mysql_charset': 'utf8'}
1456 )
1457
1458 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1459 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1460 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1461 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1462
1463 user = relationship('User')
1464 group = relationship('RepoGroup')
1465 permission = relationship('Permission')
1466
1467
1468 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1469 __tablename__ = 'users_group_repo_group_to_perm'
1470 __table_args__ = (
1471 UniqueConstraint('users_group_id', 'group_id'),
1472 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1473 'mysql_charset': 'utf8'}
1474 )
1475
1476 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)
1477 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1478 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1479 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1480
1481 users_group = relationship('UsersGroup')
1482 permission = relationship('Permission')
1483 group = relationship('RepoGroup')
1484
1485
1486 class Statistics(Base, BaseModel):
1487 __tablename__ = 'statistics'
1488 __table_args__ = (
1489 UniqueConstraint('repository_id'),
1490 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1491 'mysql_charset': 'utf8'}
1492 )
1493 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1494 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1495 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1496 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1497 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1498 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1499
1500 repository = relationship('Repository', single_parent=True)
1501
1502
1503 class UserFollowing(Base, BaseModel):
1504 __tablename__ = 'user_followings'
1505 __table_args__ = (
1506 UniqueConstraint('user_id', 'follows_repository_id'),
1507 UniqueConstraint('user_id', 'follows_user_id'),
1508 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1509 'mysql_charset': 'utf8'}
1510 )
1511
1512 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1513 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1514 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1515 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1516 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1517
1518 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1519
1520 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1521 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1522
1523 @classmethod
1524 def get_repo_followers(cls, repo_id):
1525 return cls.query().filter(cls.follows_repo_id == repo_id)
1526
1527
1528 class CacheInvalidation(Base, BaseModel):
1529 __tablename__ = 'cache_invalidation'
1530 __table_args__ = (
1531 UniqueConstraint('cache_key'),
1532 Index('key_idx', 'cache_key'),
1533 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1534 'mysql_charset': 'utf8'},
1535 )
1536 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1537 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1538 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1539 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1540
1541 def __init__(self, cache_key, cache_args=''):
1542 self.cache_key = cache_key
1543 self.cache_args = cache_args
1544 self.cache_active = False
1545
1546 def __unicode__(self):
1547 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1548 self.cache_id, self.cache_key)
1549
1550 @property
1551 def prefix(self):
1552 _split = self.cache_key.split(self.cache_args, 1)
1553 if _split and len(_split) == 2:
1554 return _split[0]
1555 return ''
1556
1557 @classmethod
1558 def clear_cache(cls):
1559 cls.query().delete()
1560
1561 @classmethod
1562 def _get_key(cls, key):
1563 """
1564 Wrapper for generating a key, together with a prefix
1565
1566 :param key:
1567 """
1568 import rhodecode
1569 prefix = ''
1570 org_key = key
1571 iid = rhodecode.CONFIG.get('instance_id')
1572 if iid:
1573 prefix = iid
1574
1575 return "%s%s" % (prefix, key), prefix, org_key
1576
1577 @classmethod
1578 def get_by_key(cls, key):
1579 return cls.query().filter(cls.cache_key == key).scalar()
1580
1581 @classmethod
1582 def get_by_repo_name(cls, repo_name):
1583 return cls.query().filter(cls.cache_args == repo_name).all()
1584
1585 @classmethod
1586 def _get_or_create_key(cls, key, repo_name, commit=True):
1587 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1588 if not inv_obj:
1589 try:
1590 inv_obj = CacheInvalidation(key, repo_name)
1591 Session().add(inv_obj)
1592 if commit:
1593 Session().commit()
1594 except Exception:
1595 log.error(traceback.format_exc())
1596 Session().rollback()
1597 return inv_obj
1598
1599 @classmethod
1600 def invalidate(cls, key):
1601 """
1602 Returns Invalidation object if this given key should be invalidated
1603 None otherwise. `cache_active = False` means that this cache
1604 state is not valid and needs to be invalidated
1605
1606 :param key:
1607 """
1608 repo_name = key
1609 repo_name = remove_suffix(repo_name, '_README')
1610 repo_name = remove_suffix(repo_name, '_RSS')
1611 repo_name = remove_suffix(repo_name, '_ATOM')
1612
1613 # adds instance prefix
1614 key, _prefix, _org_key = cls._get_key(key)
1615 inv = cls._get_or_create_key(key, repo_name)
1616
1617 if inv and inv.cache_active is False:
1618 return inv
1619
1620 @classmethod
1621 def set_invalidate(cls, key=None, repo_name=None):
1622 """
1623 Mark this Cache key for invalidation, either by key or whole
1624 cache sets based on repo_name
1625
1626 :param key:
1627 """
1628 if key:
1629 key, _prefix, _org_key = cls._get_key(key)
1630 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1631 elif repo_name:
1632 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1633
1634 log.debug('marking %s key[s] for invalidation based on key=%s,repo_name=%s'
1635 % (len(inv_objs), key, repo_name))
1636 try:
1637 for inv_obj in inv_objs:
1638 inv_obj.cache_active = False
1639 Session().add(inv_obj)
1640 Session().commit()
1641 except Exception:
1642 log.error(traceback.format_exc())
1643 Session().rollback()
1644
1645 @classmethod
1646 def set_valid(cls, key):
1647 """
1648 Mark this cache key as active and currently cached
1649
1650 :param key:
1651 """
1652 inv_obj = cls.get_by_key(key)
1653 inv_obj.cache_active = True
1654 Session().add(inv_obj)
1655 Session().commit()
1656
1657 @classmethod
1658 def get_cache_map(cls):
1659
1660 class cachemapdict(dict):
1661
1662 def __init__(self, *args, **kwargs):
1663 fixkey = kwargs.get('fixkey')
1664 if fixkey:
1665 del kwargs['fixkey']
1666 self.fixkey = fixkey
1667 super(cachemapdict, self).__init__(*args, **kwargs)
1668
1669 def __getattr__(self, name):
1670 key = name
1671 if self.fixkey:
1672 key, _prefix, _org_key = cls._get_key(key)
1673 if key in self.__dict__:
1674 return self.__dict__[key]
1675 else:
1676 return self[key]
1677
1678 def __getitem__(self, key):
1679 if self.fixkey:
1680 key, _prefix, _org_key = cls._get_key(key)
1681 try:
1682 return super(cachemapdict, self).__getitem__(key)
1683 except KeyError:
1684 return
1685
1686 cache_map = cachemapdict(fixkey=True)
1687 for obj in cls.query().all():
1688 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1689 return cache_map
1690
1691
1692 class ChangesetComment(Base, BaseModel):
1693 __tablename__ = 'changeset_comments'
1694 __table_args__ = (
1695 Index('cc_revision_idx', 'revision'),
1696 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1697 'mysql_charset': 'utf8'},
1698 )
1699 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1700 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1701 revision = Column('revision', String(40), nullable=True)
1702 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1703 line_no = Column('line_no', Unicode(10), nullable=True)
1704 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1705 f_path = Column('f_path', Unicode(1000), nullable=True)
1706 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1707 text = Column('text', UnicodeText(25000), nullable=False)
1708 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1709 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1710
1711 author = relationship('User', lazy='joined')
1712 repo = relationship('Repository')
1713 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1714 pull_request = relationship('PullRequest', lazy='joined')
1715
1716 @classmethod
1717 def get_users(cls, revision=None, pull_request_id=None):
1718 """
1719 Returns user associated with this ChangesetComment. ie those
1720 who actually commented
1721
1722 :param cls:
1723 :param revision:
1724 """
1725 q = Session().query(User)\
1726 .join(ChangesetComment.author)
1727 if revision:
1728 q = q.filter(cls.revision == revision)
1729 elif pull_request_id:
1730 q = q.filter(cls.pull_request_id == pull_request_id)
1731 return q.all()
1732
1733
1734 class ChangesetStatus(Base, BaseModel):
1735 __tablename__ = 'changeset_statuses'
1736 __table_args__ = (
1737 Index('cs_revision_idx', 'revision'),
1738 Index('cs_version_idx', 'version'),
1739 UniqueConstraint('repo_id', 'revision', 'version'),
1740 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1741 'mysql_charset': 'utf8'}
1742 )
1743 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1744 STATUS_APPROVED = 'approved'
1745 STATUS_REJECTED = 'rejected'
1746 STATUS_UNDER_REVIEW = 'under_review'
1747
1748 STATUSES = [
1749 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1750 (STATUS_APPROVED, _("Approved")),
1751 (STATUS_REJECTED, _("Rejected")),
1752 (STATUS_UNDER_REVIEW, _("Under Review")),
1753 ]
1754
1755 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1756 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1757 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1758 revision = Column('revision', String(40), nullable=False)
1759 status = Column('status', String(128), nullable=False, default=DEFAULT)
1760 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1761 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1762 version = Column('version', Integer(), nullable=False, default=0)
1763 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1764
1765 author = relationship('User', lazy='joined')
1766 repo = relationship('Repository')
1767 comment = relationship('ChangesetComment', lazy='joined')
1768 pull_request = relationship('PullRequest', lazy='joined')
1769
1770 def __unicode__(self):
1771 return u"<%s('%s:%s')>" % (
1772 self.__class__.__name__,
1773 self.status, self.author
1774 )
1775
1776 @classmethod
1777 def get_status_lbl(cls, value):
1778 return dict(cls.STATUSES).get(value)
1779
1780 @property
1781 def status_lbl(self):
1782 return ChangesetStatus.get_status_lbl(self.status)
1783
1784
1785 class PullRequest(Base, BaseModel):
1786 __tablename__ = 'pull_requests'
1787 __table_args__ = (
1788 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1789 'mysql_charset': 'utf8'},
1790 )
1791
1792 STATUS_NEW = u'new'
1793 STATUS_OPEN = u'open'
1794 STATUS_CLOSED = u'closed'
1795
1796 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1797 title = Column('title', Unicode(256), nullable=True)
1798 description = Column('description', UnicodeText(10240), nullable=True)
1799 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1800 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1801 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1803 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1804 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1805 org_ref = Column('org_ref', Unicode(256), nullable=False)
1806 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1807 other_ref = Column('other_ref', Unicode(256), nullable=False)
1808
1809 @hybrid_property
1810 def revisions(self):
1811 return self._revisions.split(':')
1812
1813 @revisions.setter
1814 def revisions(self, val):
1815 self._revisions = ':'.join(val)
1816
1817 @property
1818 def org_ref_parts(self):
1819 return self.org_ref.split(':')
1820
1821 @property
1822 def other_ref_parts(self):
1823 return self.other_ref.split(':')
1824
1825 author = relationship('User', lazy='joined')
1826 reviewers = relationship('PullRequestReviewers',
1827 cascade="all, delete, delete-orphan")
1828 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1829 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1830 statuses = relationship('ChangesetStatus')
1831 comments = relationship('ChangesetComment',
1832 cascade="all, delete, delete-orphan")
1833
1834 def is_closed(self):
1835 return self.status == self.STATUS_CLOSED
1836
1837 def __json__(self):
1838 return dict(
1839 revisions=self.revisions
1840 )
1841
1842
1843 class PullRequestReviewers(Base, BaseModel):
1844 __tablename__ = 'pull_request_reviewers'
1845 __table_args__ = (
1846 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1847 'mysql_charset': 'utf8'},
1848 )
1849
1850 def __init__(self, user=None, pull_request=None):
1851 self.user = user
1852 self.pull_request = pull_request
1853
1854 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1855 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1857
1858 user = relationship('User')
1859 pull_request = relationship('PullRequest')
1860
1861
1862 class Notification(Base, BaseModel):
1863 __tablename__ = 'notifications'
1864 __table_args__ = (
1865 Index('notification_type_idx', 'type'),
1866 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1867 'mysql_charset': 'utf8'},
1868 )
1869
1870 TYPE_CHANGESET_COMMENT = u'cs_comment'
1871 TYPE_MESSAGE = u'message'
1872 TYPE_MENTION = u'mention'
1873 TYPE_REGISTRATION = u'registration'
1874 TYPE_PULL_REQUEST = u'pull_request'
1875 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1876
1877 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1878 subject = Column('subject', Unicode(512), nullable=True)
1879 body = Column('body', UnicodeText(50000), nullable=True)
1880 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1881 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1882 type_ = Column('type', Unicode(256))
1883
1884 created_by_user = relationship('User')
1885 notifications_to_users = relationship('UserNotification', lazy='joined',
1886 cascade="all, delete, delete-orphan")
1887
1888 @property
1889 def recipients(self):
1890 return [x.user for x in UserNotification.query()\
1891 .filter(UserNotification.notification == self)\
1892 .order_by(UserNotification.user_id.asc()).all()]
1893
1894 @classmethod
1895 def create(cls, created_by, subject, body, recipients, type_=None):
1896 if type_ is None:
1897 type_ = Notification.TYPE_MESSAGE
1898
1899 notification = cls()
1900 notification.created_by_user = created_by
1901 notification.subject = subject
1902 notification.body = body
1903 notification.type_ = type_
1904 notification.created_on = datetime.datetime.now()
1905
1906 for u in recipients:
1907 assoc = UserNotification()
1908 assoc.notification = notification
1909 u.notifications.append(assoc)
1910 Session().add(notification)
1911 return notification
1912
1913 @property
1914 def description(self):
1915 from rhodecode.model.notification import NotificationModel
1916 return NotificationModel().make_description(self)
1917
1918
1919 class UserNotification(Base, BaseModel):
1920 __tablename__ = 'user_to_notification'
1921 __table_args__ = (
1922 UniqueConstraint('user_id', 'notification_id'),
1923 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1924 'mysql_charset': 'utf8'}
1925 )
1926 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1927 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1928 read = Column('read', Boolean, default=False)
1929 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1930
1931 user = relationship('User', lazy="joined")
1932 notification = relationship('Notification', lazy="joined",
1933 order_by=lambda: Notification.created_on.desc(),)
1934
1935 def mark_as_read(self):
1936 self.read = True
1937 Session().add(self)
1938
1939
1940 class DbMigrateVersion(Base, BaseModel):
1941 __tablename__ = 'db_migrate_version'
1942 __table_args__ = (
1943 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1944 'mysql_charset': 'utf8'},
1945 )
1946 repository_id = Column('repository_id', String(250), primary_key=True)
1947 repository_path = Column('repository_path', Text)
1948 version = Column('version', Integer)
@@ -668,6 +668,44 b' class UsersGroupMember(Base, BaseModel):'
668 self.user_id = u_id
668 self.user_id = u_id
669
669
670
670
671 class RepositoryField(Base, BaseModel):
672 __tablename__ = 'repositories_fields'
673 __table_args__ = (
674 UniqueConstraint('repository_id', 'field_key'), # no-multi field
675 {'extend_existing': True, 'mysql_engine': 'InnoDB',
676 'mysql_charset': 'utf8'},
677 )
678 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
679
680 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
681 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
682 field_key = Column("field_key", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
683 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
684 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
685 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
686 field_type = Column("field_type", String(256), nullable=False, unique=None)
687 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
688
689 repository = relationship('Repository')
690
691 @property
692 def field_key_prefixed(self):
693 return 'ex_%s' % self.field_key
694
695 @classmethod
696 def un_prefix_key(cls, key):
697 if key.startswith(cls.PREFIX):
698 return key[len(cls.PREFIX):]
699 return key
700
701 @classmethod
702 def get_by_key_name(cls, key, repo):
703 row = cls.query()\
704 .filter(cls.repository == repo)\
705 .filter(cls.field_key == key).scalar()
706 return row
707
708
671 class Repository(Base, BaseModel):
709 class Repository(Base, BaseModel):
672 __tablename__ = 'repositories'
710 __tablename__ = 'repositories'
673 __table_args__ = (
711 __table_args__ = (
@@ -706,6 +744,8 b' class Repository(Base, BaseModel):'
706 followers = relationship('UserFollowing',
744 followers = relationship('UserFollowing',
707 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
745 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
708 cascade='all')
746 cascade='all')
747 extra_fields = relationship('RepositoryField',
748 cascade="all, delete, delete-orphan")
709
749
710 logs = relationship('UserLog')
750 logs = relationship('UserLog')
711 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
751 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
@@ -932,6 +972,11 b' class Repository(Base, BaseModel):'
932 enable_downloads=repo.enable_downloads,
972 enable_downloads=repo.enable_downloads,
933 last_changeset=repo.changeset_cache
973 last_changeset=repo.changeset_cache
934 )
974 )
975 rc_config = RhodeCodeSetting.get_app_settings()
976 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
977 if repository_fields:
978 for f in self.extra_fields:
979 data[f.field_key_prefixed] = f.field_value
935
980
936 return data
981 return data
937
982
@@ -204,6 +204,22 b' def RepoForm(edit=False, old_data={}, su'
204 return _RepoForm
204 return _RepoForm
205
205
206
206
207 def RepoFieldForm():
208 class _RepoFieldForm(formencode.Schema):
209 filter_extra_fields = True
210 allow_extra_fields = True
211
212 new_field_key = All(v.FieldKey(),
213 v.UnicodeString(strip=True, min=3, not_empty=True))
214 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
215 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
216 if_missing='str')
217 new_field_label = v.UnicodeString(not_empty=False)
218 new_field_desc = v.UnicodeString(not_empty=False)
219
220 return _RepoFieldForm
221
222
207 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
223 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
208 repo_groups=[], landing_revs=[]):
224 repo_groups=[], landing_revs=[]):
209 class _RepoForm(formencode.Schema):
225 class _RepoForm(formencode.Schema):
@@ -266,6 +282,7 b' def ApplicationVisualisationForm():'
266 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
282 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
267
283
268 rhodecode_lightweight_dashboard = v.StringBoolean(if_missing=False)
284 rhodecode_lightweight_dashboard = v.StringBoolean(if_missing=False)
285 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
269 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
286 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
270
287
271 return _ApplicationVisualisationForm
288 return _ApplicationVisualisationForm
@@ -39,7 +39,7 b' from rhodecode.lib.hooks import log_crea'
39 from rhodecode.model import BaseModel
39 from rhodecode.model import BaseModel
40 from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
40 from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
41 Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup,\
41 Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup,\
42 RhodeCodeSetting
42 RhodeCodeSetting, RepositoryField
43 from rhodecode.lib import helpers as h
43 from rhodecode.lib import helpers as h
44 from rhodecode.lib.auth import HasRepoPermissionAny
44 from rhodecode.lib.auth import HasRepoPermissionAny
45
45
@@ -314,6 +314,13 b' class RepoModel(BaseModel):'
314 new_name = cur_repo.get_new_name(kwargs['repo_name'])
314 new_name = cur_repo.get_new_name(kwargs['repo_name'])
315 cur_repo.repo_name = new_name
315 cur_repo.repo_name = new_name
316
316
317 #handle extra fields
318 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
319 k = RepositoryField.un_prefix_key(field)
320 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
321 if ex_field:
322 ex_field.field_value = kwargs[field]
323 self.sa.add(ex_field)
317 self.sa.add(cur_repo)
324 self.sa.add(cur_repo)
318
325
319 if org_repo_name != new_name:
326 if org_repo_name != new_name:
@@ -784,3 +784,16 b' def ValidIp():'
784 value, state)
784 value, state)
785
785
786 return _validator
786 return _validator
787
788
789 def FieldKey():
790 class _validator(formencode.validators.FancyValidator):
791 messages = dict(
792 badFormat=_('Key name can only consist of letters, '
793 'underscore, dash or numbers'),)
794
795 def validate_python(self, value, state):
796 if not re.match('[a-zA-Z0-9_-]+$', value):
797 raise formencode.Invalid(self.message('badFormat', state),
798 value, state)
799 return _validator
@@ -128,7 +128,22 b''
128 </div>
128 </div>
129 </div>
129 </div>
130 </div>
130 </div>
131
131 %if c.visual.repository_fields:
132 ## EXTRA FIELDS
133 %for field in c.repo_fields:
134 <div class="field">
135 <div class="label">
136 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
137 </div>
138 <div class="input input-medium">
139 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
140 %if field.field_desc:
141 <span class="help-block">${field.field_desc}</span>
142 %endif
143 </div>
144 </div>
145 %endfor
146 %endif
132 <div class="field">
147 <div class="field">
133 <div class="label">
148 <div class="label">
134 <label for="input">${_('Permissions')}:</label>
149 <label for="input">${_('Permissions')}:</label>
@@ -286,4 +301,68 b''
286 ${h.end_form()}
301 ${h.end_form()}
287 </div>
302 </div>
288
303
304 ##TODO: this should be controlled by the VISUAL setting
305 %if c.visual.repository_fields:
306 <div class="box box-left" style="clear:left">
307 <!-- box / title -->
308 <div class="title">
309 <h5>${_('Extra fields')}</h5>
310 </div>
311
312 <div class="emails_wrap">
313 <table class="noborder">
314 %for field in c.repo_fields:
315 <tr>
316 <td>${field.field_label} (${field.field_key})</td>
317 <td>${field.field_type}</td>
318 <td>
319 ${h.form(url('delete_repo_fields', repo_name=c.repo_info.repo_name, field_id=field.repo_field_id),method='delete')}
320 ${h.submit('remove_%s' % field.repo_field_id, _('delete'), id="remove_field_%s" % field.repo_field_id,
321 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this field: %s') % field.field_key+"');")}
322 ${h.end_form()}
323 </td>
324 </tr>
325 %endfor
326 </table>
327 </div>
328
329 ${h.form(url('create_repo_fields', repo_name=c.repo_info.repo_name),method='put')}
330 <div class="form">
331 <!-- fields -->
332 <div class="fields">
333 <div class="field">
334 <div class="label">
335 <label for="new_field_key">${_('New field key')}:</label>
336 </div>
337 <div class="input">
338 ${h.text('new_field_key', class_='small')}
339 </div>
340 </div>
341 <div class="field">
342 <div class="label">
343 <label for="new_field_label">${_('New field label')}:</label>
344 </div>
345 <div class="input">
346 ${h.text('new_field_label', class_='small', placeholder=_('Enter short label'))}
347 </div>
348 </div>
349
350 <div class="field">
351 <div class="label">
352 <label for="new_field_desc">${_('New field description')}:</label>
353 </div>
354 <div class="input">
355 ${h.text('new_field_desc', class_='small', placeholder=_('Enter description of a field'))}
356 </div>
357 </div>
358
359 <div class="buttons">
360 ${h.submit('save',_('Add'),class_="ui-btn large")}
361 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
362 </div>
363 </div>
364 </div>
365 ${h.end_form()}
366 </div>
367 %endif
289 </%def>
368 </%def>
@@ -131,7 +131,13 b''
131 ${h.checkbox('rhodecode_lightweight_dashboard','True')}
131 ${h.checkbox('rhodecode_lightweight_dashboard','True')}
132 <label for="rhodecode_lightweight_dashboard">${_('Use lightweight dashboard')}</label>
132 <label for="rhodecode_lightweight_dashboard">${_('Use lightweight dashboard')}</label>
133 </div>
133 </div>
134 </div>
134 </div>
135 <div class="checkboxes">
136 <div class="checkbox">
137 ${h.checkbox('rhodecode_repository_fields','True')}
138 <label for="rhodecode_repository_fields">${_('Use repository extra fields')}</label>
139 </div>
140 </div>
135 </div>
141 </div>
136
142
137 <div class="field">
143 <div class="field">
@@ -80,6 +80,22 b''
80 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
80 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
81 </div>
81 </div>
82 </div>
82 </div>
83 %if c.visual.repository_fields:
84 ## EXTRA FIELDS
85 %for field in c.repo_fields:
86 <div class="field">
87 <div class="label">
88 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
89 </div>
90 <div class="input input-medium">
91 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
92 %if field.field_desc:
93 <span class="help-block">${field.field_desc}</span>
94 %endif
95 </div>
96 </div>
97 %endfor
98 %endif
83
99
84 <div class="field">
100 <div class="field">
85 <div class="label">
101 <div class="label">
General Comments 0
You need to be logged in to leave comments. Login now