%s >' % (self.users_group, self.repository)
+
+
+class UsersGroupToPerm(Base, BaseModel):
+ __tablename__ = 'users_group_to_perm'
+ __table_args__ = (
+ UniqueConstraint('users_group_id', 'permission_id',),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+ users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+ permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+ users_group = relationship('UsersGroup')
+ permission = relationship('Permission')
+
+
+class UserRepoGroupToPerm(Base, BaseModel):
+ __tablename__ = 'user_repo_group_to_perm'
+ __table_args__ = (
+ UniqueConstraint('user_id', 'group_id', 'permission_id'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+
+ group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+ group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
+ permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+ user = relationship('User')
+ group = relationship('RepoGroup')
+ permission = relationship('Permission')
+
+
+class UsersGroupRepoGroupToPerm(Base, BaseModel):
+ __tablename__ = 'users_group_repo_group_to_perm'
+ __table_args__ = (
+ UniqueConstraint('users_group_id', 'group_id'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+
+ 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)
+ users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+ group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
+ permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+ users_group = relationship('UsersGroup')
+ permission = relationship('Permission')
+ group = relationship('RepoGroup')
+
+
+class Statistics(Base, BaseModel):
+ __tablename__ = 'statistics'
+ __table_args__ = (
+ UniqueConstraint('repository_id'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+ stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
+ stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
+ commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
+ commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
+ languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
+
+ repository = relationship('Repository', single_parent=True)
+
+
+class UserFollowing(Base, BaseModel):
+ __tablename__ = 'user_followings'
+ __table_args__ = (
+ UniqueConstraint('user_id', 'follows_repository_id'),
+ UniqueConstraint('user_id', 'follows_user_id'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+
+ user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+ follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
+ follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
+ follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
+
+ user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
+
+ follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
+ follows_repository = relationship('Repository', order_by='Repository.repo_name')
+
+ @classmethod
+ def get_repo_followers(cls, repo_id):
+ return cls.query().filter(cls.follows_repo_id == repo_id)
+
+
+class CacheInvalidation(Base, BaseModel):
+ __tablename__ = 'cache_invalidation'
+ __table_args__ = (
+ UniqueConstraint('cache_key'),
+ Index('key_idx', 'cache_key'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+ cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+ cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+ cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
+
+ def __init__(self, cache_key, cache_args=''):
+ self.cache_key = cache_key
+ self.cache_args = cache_args
+ self.cache_active = False
+
+ def __unicode__(self):
+ return u"<%s('%s:%s')>" % (self.__class__.__name__,
+ self.cache_id, self.cache_key)
+
+ @property
+ def prefix(self):
+ _split = self.cache_key.split(self.cache_args, 1)
+ if _split and len(_split) == 2:
+ return _split[0]
+ return ''
+
+ @classmethod
+ def clear_cache(cls):
+ cls.query().delete()
+
+ @classmethod
+ def _get_key(cls, key):
+ """
+ Wrapper for generating a key, together with a prefix
+
+ :param key:
+ """
+ import rhodecode
+ prefix = ''
+ org_key = key
+ iid = rhodecode.CONFIG.get('instance_id')
+ if iid:
+ prefix = iid
+
+ return "%s%s" % (prefix, key), prefix, org_key
+
+ @classmethod
+ def get_by_key(cls, key):
+ return cls.query().filter(cls.cache_key == key).scalar()
+
+ @classmethod
+ def get_by_repo_name(cls, repo_name):
+ return cls.query().filter(cls.cache_args == repo_name).all()
+
+ @classmethod
+ def _get_or_create_key(cls, key, repo_name, commit=True):
+ inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
+ if not inv_obj:
+ try:
+ inv_obj = CacheInvalidation(key, repo_name)
+ Session().add(inv_obj)
+ if commit:
+ Session().commit()
+ except Exception:
+ log.error(traceback.format_exc())
+ Session().rollback()
+ return inv_obj
+
+ @classmethod
+ def invalidate(cls, key):
+ """
+ Returns Invalidation object if this given key should be invalidated
+ None otherwise. `cache_active = False` means that this cache
+ state is not valid and needs to be invalidated
+
+ :param key:
+ """
+ repo_name = key
+ repo_name = remove_suffix(repo_name, '_README')
+ repo_name = remove_suffix(repo_name, '_RSS')
+ repo_name = remove_suffix(repo_name, '_ATOM')
+
+ # adds instance prefix
+ key, _prefix, _org_key = cls._get_key(key)
+ inv = cls._get_or_create_key(key, repo_name)
+
+ if inv and inv.cache_active is False:
+ return inv
+
+ @classmethod
+ def set_invalidate(cls, key=None, repo_name=None):
+ """
+ Mark this Cache key for invalidation, either by key or whole
+ cache sets based on repo_name
+
+ :param key:
+ """
+ if key:
+ key, _prefix, _org_key = cls._get_key(key)
+ inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
+ elif repo_name:
+ inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
+
+ log.debug('marking %s key[s] for invalidation based on key=%s,repo_name=%s'
+ % (len(inv_objs), key, repo_name))
+ try:
+ for inv_obj in inv_objs:
+ inv_obj.cache_active = False
+ Session().add(inv_obj)
+ Session().commit()
+ except Exception:
+ log.error(traceback.format_exc())
+ Session().rollback()
+
+ @classmethod
+ def set_valid(cls, key):
+ """
+ Mark this cache key as active and currently cached
+
+ :param key:
+ """
+ inv_obj = cls.get_by_key(key)
+ inv_obj.cache_active = True
+ Session().add(inv_obj)
+ Session().commit()
+
+ @classmethod
+ def get_cache_map(cls):
+
+ class cachemapdict(dict):
+
+ def __init__(self, *args, **kwargs):
+ fixkey = kwargs.get('fixkey')
+ if fixkey:
+ del kwargs['fixkey']
+ self.fixkey = fixkey
+ super(cachemapdict, self).__init__(*args, **kwargs)
+
+ def __getattr__(self, name):
+ key = name
+ if self.fixkey:
+ key, _prefix, _org_key = cls._get_key(key)
+ if key in self.__dict__:
+ return self.__dict__[key]
+ else:
+ return self[key]
+
+ def __getitem__(self, key):
+ if self.fixkey:
+ key, _prefix, _org_key = cls._get_key(key)
+ try:
+ return super(cachemapdict, self).__getitem__(key)
+ except KeyError:
+ return
+
+ cache_map = cachemapdict(fixkey=True)
+ for obj in cls.query().all():
+ cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
+ return cache_map
+
+
+class ChangesetComment(Base, BaseModel):
+ __tablename__ = 'changeset_comments'
+ __table_args__ = (
+ Index('cc_revision_idx', 'revision'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+ comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
+ repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+ revision = Column('revision', String(40), nullable=True)
+ pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
+ line_no = Column('line_no', Unicode(10), nullable=True)
+ hl_lines = Column('hl_lines', Unicode(512), nullable=True)
+ f_path = Column('f_path', Unicode(1000), nullable=True)
+ user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
+ text = Column('text', UnicodeText(25000), nullable=False)
+ created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+ modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+
+ author = relationship('User', lazy='joined')
+ repo = relationship('Repository')
+ status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
+ pull_request = relationship('PullRequest', lazy='joined')
+
+ @classmethod
+ def get_users(cls, revision=None, pull_request_id=None):
+ """
+ Returns user associated with this ChangesetComment. ie those
+ who actually commented
+
+ :param cls:
+ :param revision:
+ """
+ q = Session().query(User)\
+ .join(ChangesetComment.author)
+ if revision:
+ q = q.filter(cls.revision == revision)
+ elif pull_request_id:
+ q = q.filter(cls.pull_request_id == pull_request_id)
+ return q.all()
+
+
+class ChangesetStatus(Base, BaseModel):
+ __tablename__ = 'changeset_statuses'
+ __table_args__ = (
+ Index('cs_revision_idx', 'revision'),
+ Index('cs_version_idx', 'version'),
+ UniqueConstraint('repo_id', 'revision', 'version'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+ STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
+ STATUS_APPROVED = 'approved'
+ STATUS_REJECTED = 'rejected'
+ STATUS_UNDER_REVIEW = 'under_review'
+
+ STATUSES = [
+ (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
+ (STATUS_APPROVED, _("Approved")),
+ (STATUS_REJECTED, _("Rejected")),
+ (STATUS_UNDER_REVIEW, _("Under Review")),
+ ]
+
+ changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
+ repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+ user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
+ revision = Column('revision', String(40), nullable=False)
+ status = Column('status', String(128), nullable=False, default=DEFAULT)
+ changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
+ modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
+ version = Column('version', Integer(), nullable=False, default=0)
+ pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
+
+ author = relationship('User', lazy='joined')
+ repo = relationship('Repository')
+ comment = relationship('ChangesetComment', lazy='joined')
+ pull_request = relationship('PullRequest', lazy='joined')
+
+ def __unicode__(self):
+ return u"<%s('%s:%s')>" % (
+ self.__class__.__name__,
+ self.status, self.author
+ )
+
+ @classmethod
+ def get_status_lbl(cls, value):
+ return dict(cls.STATUSES).get(value)
+
+ @property
+ def status_lbl(self):
+ return ChangesetStatus.get_status_lbl(self.status)
+
+
+class PullRequest(Base, BaseModel):
+ __tablename__ = 'pull_requests'
+ __table_args__ = (
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+
+ STATUS_NEW = u'new'
+ STATUS_OPEN = u'open'
+ STATUS_CLOSED = u'closed'
+
+ pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
+ title = Column('title', Unicode(256), nullable=True)
+ description = Column('description', UnicodeText(10240), nullable=True)
+ status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
+ created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+ updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+ user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
+ _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
+ org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+ org_ref = Column('org_ref', Unicode(256), nullable=False)
+ other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+ other_ref = Column('other_ref', Unicode(256), nullable=False)
+
+ @hybrid_property
+ def revisions(self):
+ return self._revisions.split(':')
+
+ @revisions.setter
+ def revisions(self, val):
+ self._revisions = ':'.join(val)
+
+ @property
+ def org_ref_parts(self):
+ return self.org_ref.split(':')
+
+ @property
+ def other_ref_parts(self):
+ return self.other_ref.split(':')
+
+ author = relationship('User', lazy='joined')
+ reviewers = relationship('PullRequestReviewers',
+ cascade="all, delete, delete-orphan")
+ org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
+ other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
+ statuses = relationship('ChangesetStatus')
+ comments = relationship('ChangesetComment',
+ cascade="all, delete, delete-orphan")
+
+ def is_closed(self):
+ return self.status == self.STATUS_CLOSED
+
+ def __json__(self):
+ return dict(
+ revisions=self.revisions
+ )
+
+
+class PullRequestReviewers(Base, BaseModel):
+ __tablename__ = 'pull_request_reviewers'
+ __table_args__ = (
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+
+ def __init__(self, user=None, pull_request=None):
+ self.user = user
+ self.pull_request = pull_request
+
+ pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
+ pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
+ user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
+
+ user = relationship('User')
+ pull_request = relationship('PullRequest')
+
+
+class Notification(Base, BaseModel):
+ __tablename__ = 'notifications'
+ __table_args__ = (
+ Index('notification_type_idx', 'type'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+
+ TYPE_CHANGESET_COMMENT = u'cs_comment'
+ TYPE_MESSAGE = u'message'
+ TYPE_MENTION = u'mention'
+ TYPE_REGISTRATION = u'registration'
+ TYPE_PULL_REQUEST = u'pull_request'
+ TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
+
+ notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
+ subject = Column('subject', Unicode(512), nullable=True)
+ body = Column('body', UnicodeText(50000), nullable=True)
+ created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
+ created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+ type_ = Column('type', Unicode(256))
+
+ created_by_user = relationship('User')
+ notifications_to_users = relationship('UserNotification', lazy='joined',
+ cascade="all, delete, delete-orphan")
+
+ @property
+ def recipients(self):
+ return [x.user for x in UserNotification.query()\
+ .filter(UserNotification.notification == self)\
+ .order_by(UserNotification.user_id.asc()).all()]
+
+ @classmethod
+ def create(cls, created_by, subject, body, recipients, type_=None):
+ if type_ is None:
+ type_ = Notification.TYPE_MESSAGE
+
+ notification = cls()
+ notification.created_by_user = created_by
+ notification.subject = subject
+ notification.body = body
+ notification.type_ = type_
+ notification.created_on = datetime.datetime.now()
+
+ for u in recipients:
+ assoc = UserNotification()
+ assoc.notification = notification
+ u.notifications.append(assoc)
+ Session().add(notification)
+ return notification
+
+ @property
+ def description(self):
+ from rhodecode.model.notification import NotificationModel
+ return NotificationModel().make_description(self)
+
+
+class UserNotification(Base, BaseModel):
+ __tablename__ = 'user_to_notification'
+ __table_args__ = (
+ UniqueConstraint('user_id', 'notification_id'),
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'}
+ )
+ user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
+ notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
+ read = Column('read', Boolean, default=False)
+ sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
+
+ user = relationship('User', lazy="joined")
+ notification = relationship('Notification', lazy="joined",
+ order_by=lambda: Notification.created_on.desc(),)
+
+ def mark_as_read(self):
+ self.read = True
+ Session().add(self)
+
+
+class DbMigrateVersion(Base, BaseModel):
+ __tablename__ = 'db_migrate_version'
+ __table_args__ = (
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+ repository_id = Column('repository_id', String(250), primary_key=True)
+ repository_path = Column('repository_path', Text)
+ version = Column('version', Integer)
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -668,6 +668,44 @@ class UsersGroupMember(Base, BaseModel):
self.user_id = u_id
+class RepositoryField(Base, BaseModel):
+ __tablename__ = 'repositories_fields'
+ __table_args__ = (
+ UniqueConstraint('repository_id', 'field_key'), # no-multi field
+ {'extend_existing': True, 'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'},
+ )
+ PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
+
+ repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+ repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
+ field_key = Column("field_key", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
+ field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
+ field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
+ field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
+ field_type = Column("field_type", String(256), nullable=False, unique=None)
+ created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+
+ repository = relationship('Repository')
+
+ @property
+ def field_key_prefixed(self):
+ return 'ex_%s' % self.field_key
+
+ @classmethod
+ def un_prefix_key(cls, key):
+ if key.startswith(cls.PREFIX):
+ return key[len(cls.PREFIX):]
+ return key
+
+ @classmethod
+ def get_by_key_name(cls, key, repo):
+ row = cls.query()\
+ .filter(cls.repository == repo)\
+ .filter(cls.field_key == key).scalar()
+ return row
+
+
class Repository(Base, BaseModel):
__tablename__ = 'repositories'
__table_args__ = (
@@ -706,6 +744,8 @@ class Repository(Base, BaseModel):
followers = relationship('UserFollowing',
primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
cascade='all')
+ extra_fields = relationship('RepositoryField',
+ cascade="all, delete, delete-orphan")
logs = relationship('UserLog')
comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
@@ -932,6 +972,11 @@ class Repository(Base, BaseModel):
enable_downloads=repo.enable_downloads,
last_changeset=repo.changeset_cache
)
+ rc_config = RhodeCodeSetting.get_app_settings()
+ repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
+ if repository_fields:
+ for f in self.extra_fields:
+ data[f.field_key_prefixed] = f.field_value
return data
diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py
--- a/rhodecode/model/forms.py
+++ b/rhodecode/model/forms.py
@@ -204,6 +204,22 @@ def RepoForm(edit=False, old_data={}, su
return _RepoForm
+def RepoFieldForm():
+ class _RepoFieldForm(formencode.Schema):
+ filter_extra_fields = True
+ allow_extra_fields = True
+
+ new_field_key = All(v.FieldKey(),
+ v.UnicodeString(strip=True, min=3, not_empty=True))
+ new_field_value = v.UnicodeString(not_empty=False, if_missing='')
+ new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
+ if_missing='str')
+ new_field_label = v.UnicodeString(not_empty=False)
+ new_field_desc = v.UnicodeString(not_empty=False)
+
+ return _RepoFieldForm
+
+
def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
repo_groups=[], landing_revs=[]):
class _RepoForm(formencode.Schema):
@@ -266,6 +282,7 @@ def ApplicationVisualisationForm():
rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
rhodecode_lightweight_dashboard = v.StringBoolean(if_missing=False)
+ rhodecode_repository_fields = v.StringBoolean(if_missing=False)
rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
return _ApplicationVisualisationForm
diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py
--- a/rhodecode/model/repo.py
+++ b/rhodecode/model/repo.py
@@ -39,7 +39,7 @@ from rhodecode.lib.hooks import log_crea
from rhodecode.model import BaseModel
from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup,\
- RhodeCodeSetting
+ RhodeCodeSetting, RepositoryField
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import HasRepoPermissionAny
@@ -314,6 +314,13 @@ class RepoModel(BaseModel):
new_name = cur_repo.get_new_name(kwargs['repo_name'])
cur_repo.repo_name = new_name
+ #handle extra fields
+ for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
+ k = RepositoryField.un_prefix_key(field)
+ ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
+ if ex_field:
+ ex_field.field_value = kwargs[field]
+ self.sa.add(ex_field)
self.sa.add(cur_repo)
if org_repo_name != new_name:
diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py
--- a/rhodecode/model/validators.py
+++ b/rhodecode/model/validators.py
@@ -784,3 +784,16 @@ def ValidIp():
value, state)
return _validator
+
+
+def FieldKey():
+ class _validator(formencode.validators.FancyValidator):
+ messages = dict(
+ badFormat=_('Key name can only consist of letters, '
+ 'underscore, dash or numbers'),)
+
+ def validate_python(self, value, state):
+ if not re.match('[a-zA-Z0-9_-]+$', value):
+ raise formencode.Invalid(self.message('badFormat', state),
+ value, state)
+ return _validator
diff --git a/rhodecode/templates/admin/repos/repo_edit.html b/rhodecode/templates/admin/repos/repo_edit.html
--- a/rhodecode/templates/admin/repos/repo_edit.html
+++ b/rhodecode/templates/admin/repos/repo_edit.html
@@ -128,7 +128,22 @@
-
+ %if c.visual.repository_fields:
+ ## EXTRA FIELDS
+ %for field in c.repo_fields:
+
+
+ ${field.field_label} (${field.field_key}):
+
+
+ ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
+ %if field.field_desc:
+ ${field.field_desc}
+ %endif
+
+
+ %endfor
+ %endif
${_('Permissions')}:
@@ -286,4 +301,68 @@
${h.end_form()}
+##TODO: this should be controlled by the VISUAL setting
+%if c.visual.repository_fields:
+
+
+
+
${_('Extra fields')}
+
+
+
+
+ %for field in c.repo_fields:
+
+ ${field.field_label} (${field.field_key})
+ ${field.field_type}
+
+ ${h.form(url('delete_repo_fields', repo_name=c.repo_info.repo_name, field_id=field.repo_field_id),method='delete')}
+ ${h.submit('remove_%s' % field.repo_field_id, _('delete'), id="remove_field_%s" % field.repo_field_id,
+ class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this field: %s') % field.field_key+"');")}
+ ${h.end_form()}
+
+
+ %endfor
+
+
+
+ ${h.form(url('create_repo_fields', repo_name=c.repo_info.repo_name),method='put')}
+
+ ${h.end_form()}
+
+%endif
%def>
diff --git a/rhodecode/templates/admin/settings/settings.html b/rhodecode/templates/admin/settings/settings.html
--- a/rhodecode/templates/admin/settings/settings.html
+++ b/rhodecode/templates/admin/settings/settings.html
@@ -131,7 +131,13 @@
${h.checkbox('rhodecode_lightweight_dashboard','True')}
${_('Use lightweight dashboard')}
-
+
+
+
+ ${h.checkbox('rhodecode_repository_fields','True')}
+ ${_('Use repository extra fields')}
+
+
diff --git a/rhodecode/templates/settings/repo_settings.html b/rhodecode/templates/settings/repo_settings.html
--- a/rhodecode/templates/settings/repo_settings.html
+++ b/rhodecode/templates/settings/repo_settings.html
@@ -80,6 +80,22 @@
${_('Private repositories are only visible to people explicitly added as collaborators.')}
+ %if c.visual.repository_fields:
+ ## EXTRA FIELDS
+ %for field in c.repo_fields:
+
+
+ ${field.field_label} (${field.field_key}):
+
+
+ ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
+ %if field.field_desc:
+ ${field.field_desc}
+ %endif
+
+
+ %endfor
+ %endif