@@ -1953,385 +1953,391 @@ class UserRepoGroupToPerm(Base, BaseMode
user = relationship('User')
group = relationship('RepoGroup')
permission = relationship('Permission')
@classmethod
def create(cls, user, repository_group, permission):
n = cls()
n.user = user
n.group = repository_group
n.permission = permission
Session().add(n)
return n
class UserGroupRepoGroupToPerm(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', 'sqlite_autoincrement': True}
)
users_group_repo_group_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
users_group = relationship('UserGroup')
def create(cls, user_group, repository_group, permission):
n.users_group = user_group
class Statistics(Base, BaseModel):
__tablename__ = 'statistics'
UniqueConstraint('repository_id'),
stat_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
stat_on_revision = Column(Integer(), nullable=False)
commit_activity = Column(LargeBinary(1000000), nullable=False)#JSON data
commit_activity_combined = Column(LargeBinary(), nullable=False)#JSON data
languages = Column(LargeBinary(1000000), nullable=False)#JSON data
repository = relationship('Repository', single_parent=True)
class UserFollowing(Base, BaseModel):
__tablename__ = 'user_followings'
UniqueConstraint('user_id', 'follows_repository_id'),
UniqueConstraint('user_id', 'follows_user_id'),
user_following_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
user_id = Column(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(Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
follows_from = Column(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')
def get_repo_followers(cls, repo_id):
return cls.query().filter(cls.follows_repo_id == repo_id)
class CacheInvalidation(Base, BaseModel):
__tablename__ = 'cache_invalidation'
UniqueConstraint('cache_key'),
Index('key_idx', 'cache_key'),
'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
# cache_id, not used
cache_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
# cache_key as created by _get_cache_key
cache_key = Column(String(255, convert_unicode=False))
# cache_args is a repo_name
cache_args = Column(String(255, convert_unicode=False))
# instance sets cache_active True when it is caching, other instances set
# cache_active to False to indicate that this cache is invalid
cache_active = Column(Boolean(), nullable=True, unique=None, default=False)
def __init__(self, cache_key, repo_name=''):
self.cache_key = cache_key
self.cache_args = repo_name
self.cache_active = False
def __unicode__(self):
return u"<%s('%s:%s[%s]')>" % (
self.__class__.__name__,
self.cache_id, self.cache_key, self.cache_active)
def _cache_key_partition(self):
prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
return prefix, repo_name, suffix
def get_prefix(self):
"""
get prefix that might have been used in _get_cache_key to
generate self.cache_key. Only used for informational purposes
in repo_edit.html.
# prefix, repo_name, suffix
return self._cache_key_partition()[0]
def get_suffix(self):
get suffix that might have been used in _get_cache_key to
return self._cache_key_partition()[2]
def clear_cache(cls):
Delete all cache keys from database.
Should only be run when all instances are down and all entries thus stale.
cls.query().delete()
Session().commit()
def _get_cache_key(cls, key):
Wrapper for generating a unique cache key for this instance and "key".
key must / will start with a repo_name which will be stored in .cache_args .
import kallithea
prefix = kallithea.CONFIG.get('instance_id', '')
return "%s%s" % (prefix, key)
def set_invalidate(cls, repo_name, delete=False):
Mark all caches of a repo as invalid in the database.
inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
log.debug('for repo %s got %s invalidation objects',
safe_str(repo_name), inv_objs)
for inv_obj in inv_objs:
log.debug('marking %s key for invalidation based on repo_name=%s',
inv_obj, safe_str(repo_name))
if delete:
Session().delete(inv_obj)
else:
inv_obj.cache_active = False
Session().add(inv_obj)
def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
Mark this cache key as active and currently cached.
Return True if the existing cache registration still was valid.
Return False to indicate that it had been invalidated and caches should be refreshed.
key = (repo_name + '_' + kind) if kind else repo_name
cache_key = cls._get_cache_key(key)
if valid_cache_keys and cache_key in valid_cache_keys:
return True
inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
if not inv_obj:
inv_obj = CacheInvalidation(cache_key, repo_name)
if inv_obj.cache_active:
inv_obj.cache_active = True
try:
except exc.IntegrityError:
raise
# TOCTOU - another thread added the key at the same time; no further action required
return False
def get_valid_cache_keys(cls):
Return opaque object with information of which caches still are valid
and can be used without checking for invalidation.
return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
class ChangesetComment(Base, BaseModel):
__tablename__ = 'changeset_comments'
Index('cc_revision_idx', 'revision'),
Index('cc_pull_request_id_idx', 'pull_request_id'),
comment_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
revision = Column(String(40))
pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'))
line_no = Column(Unicode(10))
f_path = Column(Unicode(1000))
user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
text = Column(UnicodeText(25000), nullable=False)
created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
author = relationship('User')
repo = relationship('Repository')
# status_change is frequently used directly in templates - make it a lazy
# join to avoid fetching each related ChangesetStatus on demand.
# There will only be one ChangesetStatus referencing each comment so the join will not explode.
status_change = relationship('ChangesetStatus',
cascade="all, delete-orphan", lazy='joined')
pull_request = relationship('PullRequest')
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 is not None:
q = q.filter(cls.revision == revision)
elif pull_request_id is not None:
q = q.filter(cls.pull_request_id == pull_request_id)
return q.all()
def url(self):
anchor = "comment-%s" % self.comment_id
import kallithea.lib.helpers as h
if self.revision:
return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
elif self.pull_request_id is not None:
return self.pull_request.url(anchor=anchor)
class ChangesetStatus(Base, BaseModel):
__tablename__ = 'changeset_statuses'
Index('cs_revision_idx', 'revision'),
Index('cs_version_idx', 'version'),
Index('cs_pull_request_id_idx', 'pull_request_id'),
Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
UniqueConstraint('repo_id', 'revision', 'version'),
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(Integer(), nullable=False, unique=True, primary_key=True)
user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
revision = Column(String(40), nullable=False)
status = Column(String(128), nullable=False, default=DEFAULT)
changeset_comment_id = Column(Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
version = Column(Integer(), nullable=False, default=0)
pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
comment = relationship('ChangesetComment')
return u"<%s('%s:%s')>" % (
self.status, self.author
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'
Index('pr_org_repo_id_idx', 'org_repo_id'),
Index('pr_other_repo_id_idx', 'other_repo_id'),
# values for .status
STATUS_NEW = u'new'
STATUS_CLOSED = u'closed'
pull_request_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
title = Column(Unicode(255), nullable=True)
description = Column(UnicodeText(10240))
status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
_revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
org_ref = Column(Unicode(255), nullable=False)
other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
other_ref = Column(Unicode(255), nullable=False)
@hybrid_property
def revisions(self):
return self._revisions.split(':')
@revisions.setter
def revisions(self, val):
self._revisions = safe_unicode(':'.join(val))
def org_ref_parts(self):
return self.org_ref.split(':')
def other_ref_parts(self):
return self.other_ref.split(':')
owner = relationship('User')
reviewers = relationship('PullRequestReviewers',
cascade="all, 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',
def is_closed(self):
return self.status == self.STATUS_CLOSED
def user_review_status(self, user_id):
"""Return the user's latest status votes on PR"""
# note: no filtering on repo - that would be redundant
status = ChangesetStatus.query()\
.filter(ChangesetStatus.pull_request == self)\
.filter(ChangesetStatus.user_id == user_id)\
.order_by(ChangesetStatus.version)\
.first()
return str(status.status) if status else ''
def make_nice_id(cls, pull_request_id):
'''Return pull request id nicely formatted for displaying'''
return '#%s' % pull_request_id
def nice_id(self):
'''Return the id of this pull request, nicely formatted for displaying'''
return self.make_nice_id(self.pull_request_id)
def __json__(self):
Status change: