Changeset - 68011c4e6f33
[Not reviewed]
default
0 7 0
Mads Kiilerich - 10 years ago 2015-08-26 17:28:59
madski@unity3d.com
pull requests: call it 'owner' instead of 'author'

Prepare for making it possible to transfer ownership of PRs.
7 files changed with 22 insertions and 25 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/pullrequests.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.controllers.pullrequests
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
pull requests controller for Kallithea for initializing pull requests
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: May 7, 2012
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
import traceback
 
import formencode
 
import re
 

	
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
 

	
 
from pylons import request, tmpl_context as c, url
 
from pylons.controllers.util import redirect
 
from pylons.i18n.translation import _
 

	
 
from kallithea.lib.vcs.utils.hgcompat import unionrepo
 
from kallithea.lib.compat import json
 
from kallithea.lib.base import BaseRepoController, render
 
from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
 
    NotAnonymous
 
from kallithea.lib.helpers import Page
 
from kallithea.lib import helpers as h
 
from kallithea.lib import diffs
 
from kallithea.lib.exceptions import UserInvalidException
 
from kallithea.lib.utils import action_logger, jsonify
 
from kallithea.lib.vcs.utils import safe_str
 
from kallithea.lib.vcs.exceptions import EmptyRepositoryError
 
from kallithea.lib.diffs import LimitedDiffContainer
 
from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
 
    PullRequestReviewers
 
from kallithea.model.pull_request import PullRequestModel
 
from kallithea.model.meta import Session
 
from kallithea.model.repo import RepoModel
 
from kallithea.model.comment import ChangesetCommentsModel
 
from kallithea.model.changeset_status import ChangesetStatusModel
 
from kallithea.model.forms import PullRequestForm, PullRequestPostForm
 
from kallithea.lib.utils2 import safe_int
 
from kallithea.controllers.changeset import _ignorews_url, _context_url
 
from kallithea.controllers.compare import CompareController
 
from kallithea.lib.graphmod import graph_data
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class PullrequestsController(BaseRepoController):
 

	
 
    def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
 
        """return a structure with repo's interesting changesets, suitable for
 
        the selectors in pullrequest.html
 

	
 
        rev: a revision that must be in the list somehow and selected by default
 
        branch: a branch that must be in the list and selected by default - even if closed
 
        branch_rev: a revision of which peers should be preferred and available."""
 
        # list named branches that has been merged to this named branch - it should probably merge back
 
        peers = []
 

	
 
        if rev:
 
            rev = safe_str(rev)
 

	
 
        if branch:
 
            branch = safe_str(branch)
 

	
 
        if branch_rev:
 
            branch_rev = safe_str(branch_rev)
 
            # a revset not restricting to merge() would be better
 
            # (especially because it would get the branch point)
 
            # ... but is currently too expensive
 
            # including branches of children could be nice too
 
            peerbranches = set()
 
            for i in repo._repo.revs(
 
                "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
 
                branch_rev, branch_rev):
 
                abranch = repo.get_changeset(i).branch
 
                if abranch not in peerbranches:
 
                    n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
 
                    peers.append((n, abranch))
 
                    peerbranches.add(abranch)
 

	
 
        selected = None
 
        tiprev = repo.tags.get('tip')
 
        tipbranch = None
 

	
 
        branches = []
 
        for abranch, branchrev in repo.branches.iteritems():
 
            n = 'branch:%s:%s' % (abranch, branchrev)
 
            desc = abranch
 
            if branchrev == tiprev:
 
                tipbranch = abranch
 
                desc = '%s (current tip)' % desc
 
            branches.append((n, desc))
 
            if rev == branchrev:
 
                selected = n
 
            if branch == abranch:
 
                if not rev:
 
                    selected = n
 
                branch = None
 
        if branch:  # branch not in list - it is probably closed
 
            branchrev = repo.closed_branches.get(branch)
 
            if branchrev:
 
                n = 'branch:%s:%s' % (branch, branchrev)
 
                branches.append((n, _('%s (closed)') % branch))
 
                selected = n
 
                branch = None
 
            if branch:
 
                log.debug('branch %r not found in %s', branch, repo)
 

	
 
        bookmarks = []
 
        for bookmark, bookmarkrev in repo.bookmarks.iteritems():
 
            n = 'book:%s:%s' % (bookmark, bookmarkrev)
 
            bookmarks.append((n, bookmark))
 
            if rev == bookmarkrev:
 
                selected = n
 

	
 
        tags = []
 
        for tag, tagrev in repo.tags.iteritems():
 
            if tag == 'tip':
 
                continue
 
            n = 'tag:%s:%s' % (tag, tagrev)
 
            tags.append((n, tag))
 
            if rev == tagrev:
 
                selected = n
 

	
 
        # prio 1: rev was selected as existing entry above
 

	
 
        # prio 2: create special entry for rev; rev _must_ be used
 
        specials = []
 
        if rev and selected is None:
 
            selected = 'rev:%s:%s' % (rev, rev)
 
            specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
 

	
 
        # prio 3: most recent peer branch
 
        if peers and not selected:
 
            selected = peers[0][0]
 

	
 
        # prio 4: tip revision
 
        if not selected:
 
            if h.is_hg(repo):
 
                if tipbranch:
 
                    selected = 'branch:%s:%s' % (tipbranch, tiprev)
 
                else:
 
                    selected = 'tag:null:' + repo.EMPTY_CHANGESET
 
                    tags.append((selected, 'null'))
 
            else:
 
                if 'master' in repo.branches:
 
                    selected = 'branch:master:%s' % repo.branches['master']
 
                else:
 
                    k, v = repo.branches.items()[0]
 
                    selected = 'branch:%s:%s' % (k, v)
 

	
 
        groups = [(specials, _("Special")),
 
                  (peers, _("Peer branches")),
 
                  (bookmarks, _("Bookmarks")),
 
                  (branches, _("Branches")),
 
                  (tags, _("Tags")),
 
                  ]
 
        return [g for g in groups if g[0]], selected
 

	
 
    def _get_is_allowed_change_status(self, pull_request):
 
        if pull_request.is_closed():
 
            return False
 

	
 
        owner = self.authuser.user_id == pull_request.user_id
 
        reviewer = self.authuser.user_id in [x.user_id for x in
 
                                                   pull_request.reviewers]
 
        return self.authuser.admin or owner or reviewer
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def show_all(self, repo_name):
 
        c.from_ = request.GET.get('from_') or ''
 
        c.closed = request.GET.get('closed') or ''
 
        c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
 
        c.repo_name = repo_name
 
        p = safe_int(request.GET.get('page', 1), 1)
 

	
 
        c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
 

	
 
        if request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            return render('/pullrequests/pullrequest_data.html')
 

	
 
        return render('/pullrequests/pullrequest_show_all.html')
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    def show_my(self):
 
        c.closed = request.GET.get('closed') or ''
 

	
 
        def _filter(pr):
 
            s = sorted(pr, key=lambda o: o.created_on, reverse=True)
 
            if not c.closed:
 
                s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
 
            return s
 

	
 
        c.my_pull_requests = _filter(PullRequest.query()\
 
                                .filter(PullRequest.user_id ==
 
                                        self.authuser.user_id)\
 
                                .all())
 

	
 
        c.participate_in_pull_requests = _filter(PullRequest.query()\
 
                                .join(PullRequestReviewers)\
 
                                .filter(PullRequestReviewers.user_id ==
 
                                        self.authuser.user_id)\
 
                                                 )
 

	
 
        return render('/pullrequests/pullrequest_show_my.html')
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def index(self):
 
        org_repo = c.db_repo
 
        org_scm_instance = org_repo.scm_instance
 
        try:
 
            org_scm_instance.get_changeset()
 
        except EmptyRepositoryError as e:
 
            h.flash(h.literal(_('There are no changesets yet')),
 
                    category='warning')
 
            redirect(url('summary_home', repo_name=org_repo.repo_name))
 

	
 
        org_rev = request.GET.get('rev_end')
 
        # rev_start is not directly useful - its parent could however be used
 
        # as default for other and thus give a simple compare view
 
        #other_rev = request.POST.get('rev_start')
 
        branch = request.GET.get('branch')
 

	
 
        c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
 
        c.default_cs_repo = org_repo.repo_name
 
        c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
 

	
 
        default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
 
        if default_cs_ref_type != 'branch':
 
            default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
 

	
 
        # add org repo to other so we can open pull request against peer branches on itself
 
        c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
 

	
 
        if org_repo.parent:
 
            # add parent of this fork also and select it.
 
            # use the same branch on destination as on source, if available.
 
            c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
 
            c.a_repo = org_repo.parent
 
            c.a_refs, c.default_a_ref = self._get_repo_refs(
 
                    org_repo.parent.scm_instance, branch=default_cs_branch)
 

	
 
        else:
 
            c.a_repo = org_repo
 
            c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
 

	
 
        # gather forks and add to this list ... even though it is rare to
 
        # request forks to pull from their parent
 
        for fork in org_repo.forks:
 
            c.a_repos.append((fork.repo_name, fork.repo_name))
 

	
 
        return render('/pullrequests/pullrequest.html')
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    def repo_info(self, repo_name):
 
        repo = RepoModel()._get_repo(repo_name)
 
        refs, selected_ref = self._get_repo_refs(repo.scm_instance)
 
        return {
 
            'description': repo.description.split('\n', 1)[0],
 
            'selected_ref': selected_ref,
 
            'refs': refs,
 
            }
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def create(self, repo_name):
 
        repo = RepoModel()._get_repo(repo_name)
 
        try:
 
            _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
 
        except formencode.Invalid as errors:
 
            log.error(traceback.format_exc())
 
            log.error(str(errors))
 
            msg = _('Error creating pull request: %s') % errors.msg
 
            h.flash(msg, 'error')
 
            raise HTTPBadRequest
 

	
 
        # heads up: org and other might seem backward here ...
 
        org_repo_name = _form['org_repo']
 
        org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
 
        org_repo = RepoModel()._get_repo(org_repo_name)
 
        (org_ref_type,
 
         org_ref_name,
 
         org_rev) = org_ref.split(':')
 
        if org_ref_type == 'rev':
 
            org_ref_type = 'branch'
 
            cs = org_repo.scm_instance.get_changeset(org_rev)
 
            org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
 

	
 
        other_repo_name = _form['other_repo']
 
        other_ref = _form['other_ref'] # will have symbolic name and head revision
 
        other_repo = RepoModel()._get_repo(other_repo_name)
 
        (other_ref_type,
 
         other_ref_name,
 
         other_rev) = other_ref.split(':')
 

	
 
        cs_ranges, _cs_ranges_not, ancestor_rev = \
 
            CompareController._get_changesets(org_repo.scm_instance.alias,
 
                                              other_repo.scm_instance, other_rev, # org and other "swapped"
 
                                              org_repo.scm_instance, org_rev,
 
                                              )
 
        if ancestor_rev is None:
 
            ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
 
        revisions = [cs_.raw_id for cs_ in cs_ranges]
 

	
 
        # hack: ancestor_rev is not an other_rev but we want to show the
 
        # requested destination and have the exact ancestor
 
        other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
 

	
 
        reviewers = _form['review_members']
 

	
 
        title = _form['pullrequest_title']
 
        if not title:
 
            if org_repo_name == other_repo_name:
 
                title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
 
                                      h.short_ref(other_ref_type, other_ref_name))
 
            else:
 
                title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
 
                                            other_repo_name, h.short_ref(other_ref_type, other_ref_name))
 
        description = _form['pullrequest_desc'].strip() or _('No description')
 
        try:
 
            pull_request = PullRequestModel().create(
 
                self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
 
                other_ref, revisions, reviewers, title, description
 
            )
 
            Session().commit()
 
            h.flash(_('Successfully opened new pull request'),
 
                    category='success')
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 
        except Exception:
 
            h.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            return redirect(url('pullrequest_home', repo_name=repo_name))
 

	
 
        return redirect(pull_request.url())
 

	
 
    def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
 
        org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
 
        org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
 
        new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
 

	
 
        other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
 
        other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
 
        #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
 
        new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
 

	
 
        cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
 
            other_repo.scm_instance, new_other_rev, # org and other "swapped"
 
            org_repo.scm_instance, new_org_rev)
 

	
 
        old_revisions = set(old_pull_request.revisions)
 
        revisions = [cs.raw_id for cs in cs_ranges]
 
        new_revisions = [r for r in revisions if r not in old_revisions]
 
        lost = old_revisions.difference(revisions)
 

	
 
        infos = ['This is an update of %s "%s".' %
 
                 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
 
                      pull_request_id=old_pull_request.pull_request_id),
 
                  old_pull_request.title)]
 

	
 
        if lost:
 
            infos.append(_('Missing changesets since the previous pull request:'))
 
            for r in old_pull_request.revisions:
 
                if r in lost:
 
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
 
                    infos.append('  %s "%s"' % (h.short_id(r), rev_desc))
 

	
 
        if new_revisions:
 
            infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
 
            for r in reversed(revisions):
 
                if r in new_revisions:
 
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
 
                    infos.append('  %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
 

	
 
            if ancestor_rev == other_rev:
 
                infos.append(_("Ancestor didn't change - show diff since previous version:"))
 
                infos.append(h.canonical_url('compare_url',
 
                                 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
 
                                 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
 
                                 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
 
                                 )) # note: linear diff, merge or not doesn't matter
 
            else:
 
                infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
 
        else:
 
           infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
 
           # TODO: fail?
 

	
 
        # hack: ancestor_rev is not an other_ref but we want to show the
 
        # requested destination and have the exact ancestor
 
        new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
 
        new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
 

	
 
        try:
 
            title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
 
            v = int(old_v) + 1
 
        except (AttributeError, ValueError):
 
            v = 2
 
        title = '%s (v%s)' % (title.strip(), v)
 

	
 
        # using a mail-like separator, insert new update info at the top of the list
 
        descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
 
        description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
 
        if len(descriptions) > 1:
 
            description += '\n\n' + descriptions[1].strip()
 

	
 
        try:
 
            pull_request = PullRequestModel().create(
 
                self.authuser.user_id,
 
                old_pull_request.org_repo.repo_name, new_org_ref,
 
                old_pull_request.other_repo.repo_name, new_other_ref,
 
                revisions, reviewers_ids, title, description
 
            )
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 
        except Exception:
 
            h.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            return redirect(old_pull_request.url())
 

	
 
        ChangesetCommentsModel().create(
 
            text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
 
            repo=old_pull_request.other_repo.repo_id,
 
            user=c.authuser.user_id,
 
            pull_request=old_pull_request.pull_request_id,
 
            closing_pr=True)
 
        PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
 

	
 
        Session().commit()
 
        h.flash(_('Pull request update created'),
 
                category='success')
 

	
 
        return redirect(pull_request.url())
 

	
 
    # pullrequest_post for PR editing
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def post(self, repo_name, pull_request_id):
 
        pull_request = PullRequest.get_or_404(pull_request_id)
 
        if pull_request.is_closed():
 
            raise HTTPForbidden()
 
        assert pull_request.other_repo.repo_name == repo_name
 
        #only owner or admin can update it
 
        owner = pull_request.author.user_id == c.authuser.user_id
 
        owner = pull_request.owner.user_id == c.authuser.user_id
 
        repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
 
        if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
 
            raise HTTPForbidden()
 

	
 
        _form = PullRequestPostForm()().to_python(request.POST)
 
        reviewers_ids = [int(s) for s in _form['review_members']]
 

	
 
        if _form['updaterev']:
 
            return self.create_update(pull_request,
 
                                      _form['updaterev'],
 
                                      _form['pullrequest_title'],
 
                                      _form['pullrequest_desc'],
 
                                      reviewers_ids)
 

	
 
        old_description = pull_request.description
 
        pull_request.title = _form['pullrequest_title']
 
        pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
 
        try:
 
            PullRequestModel().mention_from_description(pull_request, old_description)
 
            PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 

	
 
        Session().commit()
 
        h.flash(_('Pull request updated'), category='success')
 

	
 
        return redirect(pull_request.url())
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    def delete(self, repo_name, pull_request_id):
 
        pull_request = PullRequest.get_or_404(pull_request_id)
 
        #only owner can delete it !
 
        if pull_request.author.user_id == c.authuser.user_id:
 
        if pull_request.owner.user_id == c.authuser.user_id:
 
            PullRequestModel().delete(pull_request)
 
            Session().commit()
 
            h.flash(_('Successfully deleted pull request'),
 
                    category='success')
 
            return redirect(url('my_pullrequests'))
 
        raise HTTPForbidden()
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def show(self, repo_name, pull_request_id, extra=None):
 
        repo_model = RepoModel()
 
        c.users_array = repo_model.get_users_js()
 
        c.user_groups_array = repo_model.get_user_groups_js()
 
        c.pull_request = PullRequest.get_or_404(pull_request_id)
 
        c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
 
        cc_model = ChangesetCommentsModel()
 
        cs_model = ChangesetStatusModel()
 

	
 
        # pull_requests repo_name we opened it against
 
        # ie. other_repo must match
 
        if repo_name != c.pull_request.other_repo.repo_name:
 
            raise HTTPNotFound
 

	
 
        # load compare data into template context
 
        c.cs_repo = c.pull_request.org_repo
 
        (c.cs_ref_type,
 
         c.cs_ref_name,
 
         c.cs_rev) = c.pull_request.org_ref.split(':')
 

	
 
        c.a_repo = c.pull_request.other_repo
 
        (c.a_ref_type,
 
         c.a_ref_name,
 
         c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
 

	
 
        org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
 
        c.cs_repo = c.cs_repo
 
        c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
 
        c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
 
        revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
 
        c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
 

	
 
        avail_revs = set()
 
        avail_show = []
 
        c.cs_branch_name = c.cs_ref_name
 
        other_scm_instance = c.a_repo.scm_instance
 
        c.update_msg = ""
 
        c.update_msg_other = ""
 
        if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
 
            if c.cs_ref_type != 'branch':
 
                c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
 
            c.a_branch_name = c.a_ref_name
 
            if c.a_ref_type != 'branch':
 
                try:
 
                    c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
 
                except EmptyRepositoryError:
 
                    c.a_branch_name = 'null' # not a branch name ... but close enough
 
            # candidates: descendants of old head that are on the right branch
 
            #             and not are the old head itself ...
 
            #             and nothing at all if old head is a descendant of target ref name
 
            if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
 
                c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
 
            elif c.pull_request.is_closed():
 
                c.update_msg = _('This pull request has been closed and can not be updated.')
 
            else: # look for descendants of PR head on source branch in org repo
 
                avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
 
                                                         revs[0], c.cs_branch_name)
 
                if len(avail_revs) > 1: # more than just revs[0]
 
                    # also show changesets that not are descendants but would be merged in
 
                    targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
 
                    if org_scm_instance.path != other_scm_instance.path:
 
                        # Note: org_scm_instance.path must come first so all
 
                        # valid revision numbers are 100% org_scm compatible
 
                        # - both for avail_revs and for revset results
 
                        hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
 
                                                           org_scm_instance.path,
 
                                                           other_scm_instance.path)
 
                    else:
 
                        hgrepo = org_scm_instance._repo
 
                    show = set(hgrepo.revs('::%ld & !::%s & !::%s',
 
                                           avail_revs, revs[0], targethead))
 
                    c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
 
                else:
 
                    show = set()
 
                    c.update_msg = _('No changesets found for updating this pull request.')
 

	
 
                # TODO: handle branch heads that not are tip-most
 
                brevs = org_scm_instance._repo.revs('%s - %ld', c.cs_branch_name, avail_revs)
 
                if brevs:
 
                    # also show changesets that are on branch but neither ancestors nor descendants
 
                    show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
 
                    show.add(revs[0]) # make sure graph shows this so we can see how they relate
 
                    c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
 
                        h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
 

	
 
                avail_show = sorted(show, reverse=True)
 

	
 
        elif org_scm_instance.alias == 'git':
 
            c.update_msg = _("Git pull requests don't support updates yet.")
 

	
 
        c.avail_revs = avail_revs
 
        c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
 
        c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
 

	
 
        raw_ids = [x.raw_id for x in c.cs_ranges]
 
        c.cs_comments = c.cs_repo.get_comments(raw_ids)
 
        c.statuses = c.cs_repo.statuses(raw_ids)
 

	
 
        ignore_whitespace = request.GET.get('ignorews') == '1'
 
        line_context = request.GET.get('context', 3)
 
        c.ignorews_url = _ignorews_url
 
        c.context_url = _context_url
 
        c.fulldiff = request.GET.get('fulldiff')
 
        diff_limit = self.cut_off_limit if not c.fulldiff else None
 

	
 
        # we swap org/other ref since we run a simple diff on one repo
 
        log.debug('running diff between %s and %s in %s',
 
                  c.a_rev, c.cs_rev, org_scm_instance.path)
 
        txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
 
                                      ignore_whitespace=ignore_whitespace,
 
                                      context=line_context)
 

	
 
        diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
 
                                             diff_limit=diff_limit)
 
        _parsed = diff_processor.prepare()
 

	
 
        c.limited_diff = False
 
        if isinstance(_parsed, LimitedDiffContainer):
 
            c.limited_diff = True
 

	
 
        c.files = []
 
        c.changes = {}
 
        c.lines_added = 0
 
        c.lines_deleted = 0
 

	
 
        for f in _parsed:
 
            st = f['stats']
 
            c.lines_added += st['added']
 
            c.lines_deleted += st['deleted']
 
            fid = h.FID('', f['filename'])
 
            c.files.append([fid, f['operation'], f['filename'], f['stats']])
 
            htmldiff = diff_processor.as_html(enable_comments=True,
 
                                              parsed_lines=[f])
 
            c.changes[fid] = [f['operation'], f['filename'], htmldiff]
 

	
 
        # inline comments
 
        c.inline_cnt = 0
 
        c.inline_comments = cc_model.get_inline_comments(
 
                                c.db_repo.repo_id,
 
                                pull_request=pull_request_id)
 
        # count inline comments
 
        for __, lines in c.inline_comments:
 
            for comments in lines.values():
 
                c.inline_cnt += len(comments)
 
        # comments
 
        c.comments = cc_model.get_comments(c.db_repo.repo_id,
 
                                           pull_request=pull_request_id)
 

	
 
        # (badly named) pull-request status calculation based on reviewer votes
 
        (c.pull_request_reviewers,
 
         c.pull_request_pending_reviewers,
 
         c.current_voting_result,
 
         ) = cs_model.calculate_pull_request_result(c.pull_request)
 
        c.changeset_statuses = ChangesetStatus.STATUSES
 

	
 
        c.as_form = False
 
        c.ancestor = None # there is one - but right here we don't know which
 
        return render('/pullrequests/pullrequest_show.html')
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    def comment(self, repo_name, pull_request_id):
 
        pull_request = PullRequest.get_or_404(pull_request_id)
 

	
 
        status = request.POST.get('changeset_status')
 
        close_pr = request.POST.get('save_close')
 
        f_path = request.POST.get('f_path')
 
        line_no = request.POST.get('line')
 

	
 
        if (status or close_pr) and (f_path or line_no):
 
            # status votes and closing is only possible in general comments
 
            raise HTTPBadRequest()
 

	
 
        allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
 
        if not allowed_to_change_status:
 
            if status or close_pr:
 
                h.flash(_('No permission to change pull request status'), 'error')
 
                raise HTTPForbidden()
 

	
 
        text = request.POST.get('text', '').strip()
 
        if close_pr:
 
            text = _('Closing.') + '\n' + text
 

	
 
        comment = ChangesetCommentsModel().create(
 
            text=text,
 
            repo=c.db_repo.repo_id,
 
            user=c.authuser.user_id,
 
            pull_request=pull_request_id,
 
            f_path=f_path,
 
            line_no=line_no,
 
            status_change=(ChangesetStatus.get_status_lbl(status)
 
                           if status and allowed_to_change_status else None),
 
            closing_pr=close_pr
 
        )
 

	
 
        action_logger(self.authuser,
 
                      'user_commented_pull_request:%s' % pull_request_id,
 
                      c.db_repo, self.ip_addr, self.sa)
 

	
 
        if status:
 
            ChangesetStatusModel().set_status(
 
                c.db_repo.repo_id,
 
                status,
 
                c.authuser.user_id,
 
                comment,
 
                pull_request=pull_request_id
 
            )
 

	
 
        if close_pr:
 
            PullRequestModel().close_pull_request(pull_request_id)
 
            action_logger(self.authuser,
 
                          'user_closed_pull_request:%s' % pull_request_id,
 
                          c.db_repo, self.ip_addr, self.sa)
 

	
 
        Session().commit()
 

	
 
        if not request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            return redirect(pull_request.url())
 

	
 
        data = {
 
           'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
 
        }
 
        if comment is not None:
 
            c.comment = comment
 
            data.update(comment.get_dict())
 
            data.update({'rendered_text':
 
                         render('changeset/changeset_comment_block.html')})
 

	
 
        return data
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    def delete_comment(self, repo_name, comment_id):
 
        co = ChangesetComment.get(comment_id)
 
        if co.pull_request.is_closed():
 
            #don't allow deleting comments on closed pull request
 
            raise HTTPForbidden()
 

	
 
        owner = co.author.user_id == c.authuser.user_id
 
        repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
 
        if h.HasPermissionAny('hg.admin') or repo_admin or owner:
 
            ChangesetCommentsModel().delete(comment=co)
 
            Session().commit()
 
            return True
 
        else:
 
            raise HTTPForbidden()
kallithea/model/comment.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.model.comment
 
~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
comments model for Kallithea
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Nov 11, 2011
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 

	
 
from pylons.i18n.translation import _
 
from collections import defaultdict
 

	
 
from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
 
from kallithea.lib import helpers as h
 
from kallithea.model import BaseModel
 
from kallithea.model.db import ChangesetComment, User, \
 
    Notification, PullRequest
 
from kallithea.model.notification import NotificationModel
 
from kallithea.model.meta import Session
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class ChangesetCommentsModel(BaseModel):
 

	
 
    cls = ChangesetComment
 

	
 
    def __get_changeset_comment(self, changeset_comment):
 
        return self._get_instance(ChangesetComment, changeset_comment)
 

	
 
    def __get_pull_request(self, pull_request):
 
        return self._get_instance(PullRequest, pull_request)
 

	
 
    def _extract_mentions(self, s):
 
        user_objects = []
 
        for username in extract_mentioned_users(s):
 
            user_obj = User.get_by_username(username, case_insensitive=True)
 
            if user_obj:
 
                user_objects.append(user_obj)
 
        return user_objects
 

	
 
    def _get_notification_data(self, repo, comment, user, comment_text,
 
                               line_no=None, revision=None, pull_request=None,
 
                               status_change=None, closing_pr=False):
 
        """
 
        :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
 
        """
 
        # make notification
 
        body = comment_text  # text of the comment
 
        line = ''
 
        if line_no:
 
            line = _('on line %s') % line_no
 

	
 
        #changeset
 
        if revision:
 
            notification_type = Notification.TYPE_CHANGESET_COMMENT
 
            cs = repo.scm_instance.get_changeset(revision)
 
            desc = cs.short_id
 

	
 
            threading = ['%s-rev-%s@%s' % (repo.repo_name, revision, h.canonical_hostname())]
 
            if line_no: # TODO: url to file _and_ line number
 
                threading.append('%s-rev-%s-line-%s@%s' % (repo.repo_name, revision, line_no,
 
                                                           h.canonical_hostname()))
 
            comment_url = h.canonical_url('changeset_home',
 
                repo_name=repo.repo_name,
 
                revision=revision,
 
                anchor='comment-%s' % comment.comment_id)
 
            subj = safe_unicode(
 
                h.link_to('Re changeset: %(desc)s %(line)s' % \
 
                          {'desc': desc, 'line': line},
 
                          comment_url)
 
            )
 
            # get the current participants of this changeset
 
            recipients = ChangesetComment.get_users(revision=revision)
 
            # add changeset author if it's in kallithea system
 
            # add changeset author if it's known locally
 
            cs_author = User.get_from_cs_author(cs.author)
 
            if not cs_author:
 
                #use repo owner if we cannot extract the author correctly
 
                cs_author = repo.user
 
            recipients += [cs_author]
 
            email_kwargs = {
 
                'status_change': status_change,
 
                'cs_comment_user': h.person(user, 'full_name_and_username'),
 
                'cs_target_repo': h.canonical_url('summary_home', repo_name=repo.repo_name),
 
                'cs_comment_url': comment_url,
 
                'raw_id': revision,
 
                'message': cs.message,
 
                'repo_name': repo.repo_name,
 
                'short_id': h.short_id(revision),
 
                'branch': cs.branch,
 
                'comment_username': user.username,
 
                'threading': threading,
 
            }
 
        #pull request
 
        elif pull_request:
 
            notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
 
            desc = comment.pull_request.title
 
            _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
 
            threading = ['%s-pr-%s@%s' % (pull_request.other_repo.repo_name,
 
                                          pull_request.pull_request_id,
 
                                          h.canonical_hostname())]
 
            if line_no: # TODO: url to file _and_ line number
 
                threading.append('%s-pr-%s-line-%s@%s' % (pull_request.other_repo.repo_name,
 
                                                          pull_request.pull_request_id, line_no,
 
                                                          h.canonical_hostname()))
 
            comment_url = pull_request.url(canonical=True,
 
                anchor='comment-%s' % comment.comment_id)
 
            subj = safe_unicode(
 
                h.link_to('Re pull request %(pr_nice_id)s: %(desc)s %(line)s' % \
 
                          {'desc': desc,
 
                           'pr_nice_id': comment.pull_request.nice_id(),
 
                           'line': line},
 
                          comment_url)
 
            )
 
            # get the current participants of this pull request
 
            recipients = ChangesetComment.get_users(pull_request_id=
 
                                                pull_request.pull_request_id)
 
            # add pull request author
 
            recipients += [pull_request.author]
 
            recipients += [pull_request.owner]
 

	
 
            # add the reviewers to notification
 
            recipients += [x.user for x in pull_request.reviewers]
 

	
 
            #set some variables for email notification
 
            email_kwargs = {
 
                'pr_title': pull_request.title,
 
                'pr_nice_id': pull_request.nice_id(),
 
                'status_change': status_change,
 
                'closing_pr': closing_pr,
 
                'pr_comment_url': comment_url,
 
                'pr_comment_user': h.person(user, 'full_name_and_username'),
 
                'pr_target_repo': h.canonical_url('summary_home',
 
                                   repo_name=pull_request.other_repo.repo_name),
 
                'repo_name': pull_request.other_repo.repo_name,
 
                'ref': org_ref_name,
 
                'comment_username': user.username,
 
                'threading': threading,
 
            }
 

	
 
        return subj, body, recipients, notification_type, email_kwargs
 

	
 
    def create(self, text, repo, user, revision=None, pull_request=None,
 
               f_path=None, line_no=None, status_change=None, closing_pr=False,
 
               send_email=True):
 
        """
 
        Creates a new comment for either a changeset or a pull request.
 
        status_change and closing_pr is only for the optional email.
 

	
 
        Returns the created comment.
 
        """
 
        if not status_change and not text:
 
            log.warning('Missing text for comment, skipping...')
 
            return None
 

	
 
        repo = self._get_repo(repo)
 
        user = self._get_user(user)
 
        comment = ChangesetComment()
 
        comment.repo = repo
 
        comment.author = user
 
        comment.text = text
 
        comment.f_path = f_path
 
        comment.line_no = line_no
 

	
 
        if revision is not None:
 
            comment.revision = revision
 
        elif pull_request is not None:
 
            pull_request = self.__get_pull_request(pull_request)
 
            comment.pull_request = pull_request
 
        else:
 
            raise Exception('Please specify revision or pull_request_id')
 

	
 
        Session().add(comment)
 
        Session().flush()
 

	
 
        if send_email:
 
            (subj, body, recipients, notification_type,
 
             email_kwargs) = self._get_notification_data(
 
                                repo, comment, user,
 
                                comment_text=text,
 
                                line_no=line_no,
 
                                revision=revision,
 
                                pull_request=pull_request,
 
                                status_change=status_change,
 
                                closing_pr=closing_pr)
 
            email_kwargs['is_mention'] = False
 
            # create notification objects, and emails
 
            NotificationModel().create(
 
                created_by=user, subject=subj, body=body,
 
                recipients=recipients, type_=notification_type,
 
                email_kwargs=email_kwargs,
 
            )
 

	
 
            mention_recipients = set(self._extract_mentions(body))\
 
                                    .difference(recipients)
 
            if mention_recipients:
 
                email_kwargs['is_mention'] = True
 
                subj = _('[Mention]') + ' ' + subj
 
                NotificationModel().create(
 
                    created_by=user, subject=subj, body=body,
 
                    recipients=mention_recipients,
 
                    type_=notification_type,
 
                    email_kwargs=email_kwargs
 
                )
 

	
 
        return comment
 

	
 
    def delete(self, comment):
 
        comment = self.__get_changeset_comment(comment)
 
        Session().delete(comment)
 

	
 
        return comment
 

	
 
    def get_comments(self, repo_id, revision=None, pull_request=None):
 
        """
 
        Gets general comments for either revision or pull_request.
 

	
 
        Returns a list, ordered by creation date.
 
        """
 
        return self._get_comments(repo_id, revision=revision, pull_request=pull_request,
 
                                  inline=False)
 

	
 
    def get_inline_comments(self, repo_id, revision=None, pull_request=None):
 
        """
 
        Gets inline comments for either revision or pull_request.
 

	
 
        Returns a list of tuples with file path and list of comments per line number.
 
        """
 
        comments = self._get_comments(repo_id, revision=revision, pull_request=pull_request,
 
                                      inline=True)
 

	
 
        paths = defaultdict(lambda: defaultdict(list))
 
        for co in comments:
 
            paths[co.f_path][co.line_no].append(co)
 
        return paths.items()
 

	
 
    def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False):
 
        """
 
        Gets comments for either revision or pull_request_id, either inline or general.
 
        """
 
        q = Session().query(ChangesetComment)\
 
            .filter(ChangesetComment.repo_id == repo_id)
 

	
 
        if inline:
 
            q = q.filter(ChangesetComment.line_no != None)\
 
                .filter(ChangesetComment.f_path != None)
 
        else:
 
            q = q.filter(ChangesetComment.line_no == None)\
 
                .filter(ChangesetComment.f_path == None)
 

	
 
        if revision:
 
            q = q.filter(ChangesetComment.revision == revision)
 
        elif pull_request:
 
            pull_request = self.__get_pull_request(pull_request)
 
            q = q.filter(ChangesetComment.pull_request == pull_request)
 
        else:
 
            raise Exception('Please specify either revision or pull_request')
 

	
 
        return q.order_by(ChangesetComment.created_on).all()
kallithea/model/db.py
Show inline comments
 
@@ -1538,1015 +1538,1015 @@ class RepoGroup(Base, BaseModel):
 
                            "get_group_%s" % _hash_key(group_name)
 
                            )
 
            )
 
        return gr.scalar()
 

	
 
    @property
 
    def parents(self):
 
        parents_recursion_limit = 10
 
        groups = []
 
        if self.parent_group is None:
 
            return groups
 
        cur_gr = self.parent_group
 
        groups.insert(0, cur_gr)
 
        cnt = 0
 
        while 1:
 
            cnt += 1
 
            gr = getattr(cur_gr, 'parent_group', None)
 
            cur_gr = cur_gr.parent_group
 
            if gr is None:
 
                break
 
            if cnt == parents_recursion_limit:
 
                # this will prevent accidental infinite loops
 
                log.error(('more than %s parents found for group %s, stopping '
 
                           'recursive parent fetching' % (parents_recursion_limit, self)))
 
                break
 

	
 
            groups.insert(0, gr)
 
        return groups
 

	
 
    @property
 
    def children(self):
 
        return RepoGroup.query().filter(RepoGroup.parent_group == self)
 

	
 
    @property
 
    def name(self):
 
        return self.group_name.split(RepoGroup.url_sep())[-1]
 

	
 
    @property
 
    def full_path(self):
 
        return self.group_name
 

	
 
    @property
 
    def full_path_splitted(self):
 
        return self.group_name.split(RepoGroup.url_sep())
 

	
 
    @property
 
    def repositories(self):
 
        return Repository.query()\
 
                .filter(Repository.group == self)\
 
                .order_by(Repository.repo_name)
 

	
 
    @property
 
    def repositories_recursive_count(self):
 
        cnt = self.repositories.count()
 

	
 
        def children_count(group):
 
            cnt = 0
 
            for child in group.children:
 
                cnt += child.repositories.count()
 
                cnt += children_count(child)
 
            return cnt
 

	
 
        return cnt + children_count(self)
 

	
 
    def _recursive_objects(self, include_repos=True):
 
        all_ = []
 

	
 
        def _get_members(root_gr):
 
            if include_repos:
 
                for r in root_gr.repositories:
 
                    all_.append(r)
 
            childs = root_gr.children.all()
 
            if childs:
 
                for gr in childs:
 
                    all_.append(gr)
 
                    _get_members(gr)
 

	
 
        _get_members(self)
 
        return [self] + all_
 

	
 
    def recursive_groups_and_repos(self):
 
        """
 
        Recursive return all groups, with repositories in those groups
 
        """
 
        return self._recursive_objects()
 

	
 
    def recursive_groups(self):
 
        """
 
        Returns all children groups for this group including children of children
 
        """
 
        return self._recursive_objects(include_repos=False)
 

	
 
    def get_new_name(self, group_name):
 
        """
 
        returns new full group name based on parent and new name
 

	
 
        :param group_name:
 
        """
 
        path_prefix = (self.parent_group.full_path_splitted if
 
                       self.parent_group else [])
 
        return RepoGroup.url_sep().join(path_prefix + [group_name])
 

	
 
    def get_api_data(self):
 
        """
 
        Common function for generating api data
 

	
 
        """
 
        group = self
 
        data = dict(
 
            group_id=group.group_id,
 
            group_name=group.group_name,
 
            group_description=group.group_description,
 
            parent_group=group.parent_group.group_name if group.parent_group else None,
 
            repositories=[x.repo_name for x in group.repositories],
 
            owner=group.user.username
 
        )
 
        return data
 

	
 

	
 
class Permission(Base, BaseModel):
 
    __tablename__ = 'permissions'
 
    __table_args__ = (
 
        Index('p_perm_name_idx', 'permission_name'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
 
    )
 
    PERMS = [
 
        ('hg.admin', _('Kallithea Administrator')),
 

	
 
        ('repository.none', _('Default user has no access to new repositories')),
 
        ('repository.read', _('Default user has read access to new repositories')),
 
        ('repository.write', _('Default user has write access to new repositories')),
 
        ('repository.admin', _('Default user has admin access to new repositories')),
 

	
 
        ('group.none', _('Default user has no access to new repository groups')),
 
        ('group.read', _('Default user has read access to new repository groups')),
 
        ('group.write', _('Default user has write access to new repository groups')),
 
        ('group.admin', _('Default user has admin access to new repository groups')),
 

	
 
        ('usergroup.none', _('Default user has no access to new user groups')),
 
        ('usergroup.read', _('Default user has read access to new user groups')),
 
        ('usergroup.write', _('Default user has write access to new user groups')),
 
        ('usergroup.admin', _('Default user has admin access to new user groups')),
 

	
 
        ('hg.repogroup.create.false', _('Only admins can create repository groups')),
 
        ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
 

	
 
        ('hg.usergroup.create.false', _('Only admins can create user groups')),
 
        ('hg.usergroup.create.true', _('Non-admins can create user groups')),
 

	
 
        ('hg.create.none', _('Only admins can create top level repositories')),
 
        ('hg.create.repository', _('Non-admins can create top level repositories')),
 

	
 
        ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
 
        ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
 

	
 
        ('hg.fork.none', _('Only admins can fork repositories')),
 
        ('hg.fork.repository', _('Non-admins can can fork repositories')),
 

	
 
        ('hg.register.none', _('Registration disabled')),
 
        ('hg.register.manual_activate', _('User registration with manual account activation')),
 
        ('hg.register.auto_activate', _('User registration with automatic account activation')),
 

	
 
        ('hg.extern_activate.manual', _('Manual activation of external account')),
 
        ('hg.extern_activate.auto', _('Automatic activation of external account')),
 
    ]
 

	
 
    #definition of system default permissions for DEFAULT user
 
    DEFAULT_USER_PERMISSIONS = [
 
        'repository.read',
 
        'group.read',
 
        'usergroup.read',
 
        'hg.create.repository',
 
        'hg.create.write_on_repogroup.true',
 
        'hg.fork.repository',
 
        'hg.register.manual_activate',
 
        'hg.extern_activate.auto',
 
    ]
 

	
 
    # defines which permissions are more important higher the more important
 
    # Weight defines which permissions are more important.
 
    # The higher number the more important.
 
    PERM_WEIGHTS = {
 
        'repository.none': 0,
 
        'repository.read': 1,
 
        'repository.write': 3,
 
        'repository.admin': 4,
 

	
 
        'group.none': 0,
 
        'group.read': 1,
 
        'group.write': 3,
 
        'group.admin': 4,
 

	
 
        'usergroup.none': 0,
 
        'usergroup.read': 1,
 
        'usergroup.write': 3,
 
        'usergroup.admin': 4,
 

	
 
        'hg.repogroup.create.false': 0,
 
        'hg.repogroup.create.true': 1,
 

	
 
        'hg.usergroup.create.false': 0,
 
        'hg.usergroup.create.true': 1,
 

	
 
        'hg.fork.none': 0,
 
        'hg.fork.repository': 1,
 

	
 
        'hg.create.none': 0,
 
        'hg.create.repository': 1
 
    }
 

	
 
    permission_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    permission_name = Column(String(255, convert_unicode=False), nullable=True, unique=None, default=None)
 
    permission_longname = Column(String(255, convert_unicode=False), nullable=True, unique=None, default=None)
 

	
 
    def __unicode__(self):
 
        return u"<%s('%s:%s')>" % (
 
            self.__class__.__name__, self.permission_id, self.permission_name
 
        )
 

	
 
    @classmethod
 
    def get_by_key(cls, key):
 
        return cls.query().filter(cls.permission_name == key).scalar()
 

	
 
    @classmethod
 
    def get_default_perms(cls, default_user_id):
 
        q = Session().query(UserRepoToPerm, Repository, cls)\
 
         .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
 
         .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
 
         .filter(UserRepoToPerm.user_id == default_user_id)
 

	
 
        return q.all()
 

	
 
    @classmethod
 
    def get_default_group_perms(cls, default_user_id):
 
        q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
 
         .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
 
         .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
 
         .filter(UserRepoGroupToPerm.user_id == default_user_id)
 

	
 
        return q.all()
 

	
 
    @classmethod
 
    def get_default_user_group_perms(cls, default_user_id):
 
        q = Session().query(UserUserGroupToPerm, UserGroup, cls)\
 
         .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
 
         .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id))\
 
         .filter(UserUserGroupToPerm.user_id == default_user_id)
 

	
 
        return q.all()
 

	
 

	
 
class UserRepoToPerm(Base, BaseModel):
 
    __tablename__ = 'repo_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('user_id', 'repository_id', 'permission_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    repo_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 
    repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
 

	
 
    user = relationship('User')
 
    repository = relationship('Repository')
 
    permission = relationship('Permission')
 

	
 
    @classmethod
 
    def create(cls, user, repository, permission):
 
        n = cls()
 
        n.user = user
 
        n.repository = repository
 
        n.permission = permission
 
        Session().add(n)
 
        return n
 

	
 
    def __unicode__(self):
 
        return u'<%s => %s >' % (self.user, self.repository)
 

	
 

	
 
class UserUserGroupToPerm(Base, BaseModel):
 
    __tablename__ = 'user_user_group_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    user_user_group_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 
    user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
 

	
 
    user = relationship('User')
 
    user_group = relationship('UserGroup')
 
    permission = relationship('Permission')
 

	
 
    @classmethod
 
    def create(cls, user, user_group, permission):
 
        n = cls()
 
        n.user = user
 
        n.user_group = user_group
 
        n.permission = permission
 
        Session().add(n)
 
        return n
 

	
 
    def __unicode__(self):
 
        return u'<%s => %s >' % (self.user, self.user_group)
 

	
 

	
 
class UserToPerm(Base, BaseModel):
 
    __tablename__ = 'user_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('user_id', 'permission_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    user_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 

	
 
    user = relationship('User')
 
    permission = relationship('Permission')
 

	
 
    def __unicode__(self):
 
        return u'<%s => %s >' % (self.user, self.permission)
 

	
 

	
 
class UserGroupRepoToPerm(Base, BaseModel):
 
    __tablename__ = 'users_group_repo_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    users_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)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 
    repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
 

	
 
    users_group = relationship('UserGroup')
 
    permission = relationship('Permission')
 
    repository = relationship('Repository')
 

	
 
    @classmethod
 
    def create(cls, users_group, repository, permission):
 
        n = cls()
 
        n.users_group = users_group
 
        n.repository = repository
 
        n.permission = permission
 
        Session().add(n)
 
        return n
 

	
 
    def __unicode__(self):
 
        return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
 

	
 

	
 
class UserGroupUserGroupToPerm(Base, BaseModel):
 
    __tablename__ = 'user_group_user_group_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
 
        CheckConstraint('target_user_group_id != user_group_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    user_group_user_group_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 
    user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
 

	
 
    target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
 
    user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
 
    permission = relationship('Permission')
 

	
 
    @classmethod
 
    def create(cls, target_user_group, user_group, permission):
 
        n = cls()
 
        n.target_user_group = target_user_group
 
        n.user_group = user_group
 
        n.permission = permission
 
        Session().add(n)
 
        return n
 

	
 
    def __unicode__(self):
 
        return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
 

	
 

	
 
class UserGroupToPerm(Base, BaseModel):
 
    __tablename__ = 'users_group_to_perm'
 
    __table_args__ = (
 
        UniqueConstraint('users_group_id', 'permission_id',),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    users_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)
 
    permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
 

	
 
    users_group = relationship('UserGroup')
 
    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', 'sqlite_autoincrement': True}
 
    )
 

	
 
    group_to_perm_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    user_id = Column(Integer(), ForeignKey('users.user_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)
 

	
 
    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')
 
    permission = relationship('Permission')
 
    group = relationship('RepoGroup')
 

	
 
    @classmethod
 
    def create(cls, user_group, repository_group, permission):
 
        n = cls()
 
        n.users_group = user_group
 
        n.group = repository_group
 
        n.permission = permission
 
        Session().add(n)
 
        return n
 

	
 

	
 
class Statistics(Base, BaseModel):
 
    __tablename__ = 'statistics'
 
    __table_args__ = (
 
         UniqueConstraint('repository_id'),
 
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
          'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    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'
 
    __table_args__ = (
 
        UniqueConstraint('user_id', 'follows_repository_id'),
 
        UniqueConstraint('user_id', 'follows_user_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 

	
 
    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')
 

	
 
    @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', '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
 
        generate self.cache_key. Only used for informational purposes
 
        in repo_edit.html.
 
        """
 
        # prefix, repo_name, suffix
 
        return self._cache_key_partition()[2]
 

	
 
    @classmethod
 
    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()
 

	
 
    @classmethod
 
    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)
 

	
 
    @classmethod
 
    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)
 
        Session().commit()
 

	
 
    @classmethod
 
    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:
 
            return True
 
        inv_obj.cache_active = True
 
        Session().add(inv_obj)
 
        Session().commit()
 
        return False
 

	
 
    @classmethod
 
    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'
 
    __table_args__ = (
 
        Index('cc_revision_idx', 'revision'),
 
        Index('cc_pull_request_id_idx', 'pull_request_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
 
    )
 
    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')
 

	
 
    @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 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'
 
    __table_args__ = (
 
        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'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    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)
 
    repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
 
    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)
 

	
 
    author = relationship('User')
 
    repo = relationship('Repository')
 
    comment = relationship('ChangesetComment')
 
    pull_request = relationship('PullRequest')
 

	
 
    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__ = (
 
        Index('pr_org_repo_id_idx', 'org_repo_id'),
 
        Index('pr_other_repo_id_idx', 'other_repo_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
 
    )
 

	
 
    # 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
 
    created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
 
    updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
 
    user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
 
    _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))
 

	
 
    @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')
 
    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',
 
                             cascade="all, delete-orphan")
 

	
 
    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 ''
 

	
 
    @classmethod
 
    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):
 
        return dict(
 
            revisions=self.revisions
 
        )
 

	
 
    def url(self, **kwargs):
 
        canonical = kwargs.pop('canonical', None)
 
        import kallithea.lib.helpers as h
 
        b = self.org_ref_parts[1]
 
        if b != self.other_ref_parts[1]:
 
            s = '/_/' + b
 
        else:
 
            s = '/_/' + self.title
 
        kwargs['extra'] = urlreadable(s)
 
        if canonical:
 
            return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
 
                                   pull_request_id=self.pull_request_id, **kwargs)
 
        return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
 
                     pull_request_id=self.pull_request_id, **kwargs)
 

	
 
class PullRequestReviewers(Base, BaseModel):
 
    __tablename__ = 'pull_request_reviewers'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
 
    )
 

	
 
    def __init__(self, user=None, pull_request=None):
 
        self.user = user
 
        self.pull_request = pull_request
 

	
 
    pull_requests_reviewers_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
 
    user_id = Column(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', 'sqlite_autoincrement': True},
 
    )
 

	
 
    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(Integer(), nullable=False, unique=True, primary_key=True)
 
    subject = Column(Unicode(512), nullable=True)
 
    body = Column(UnicodeText(50000), nullable=True)
 
    created_by = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
 
    created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
 
    type_ = Column('type', Unicode(255))
 

	
 
    created_by_user = relationship('User')
 
    notifications_to_users = relationship('UserNotification', cascade="all, 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
 
            assoc.user_id = u.user_id
 
            Session().add(assoc)
 
        Session().add(notification)
 
        Session().flush() # assign notificaiton.notification_id
 
        return notification
 

	
 
    @property
 
    def description(self):
 
        from kallithea.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', 'sqlite_autoincrement': True}
 
    )
 
    user_id = Column(Integer(), ForeignKey('users.user_id'), primary_key=True)
 
    notification_id = Column(Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
 
    read = Column(Boolean, default=False)
 
    sent_on = Column(DateTime(timezone=False), nullable=True, unique=None)
 

	
 
    user = relationship('User')
 
    notification = relationship('Notification')
 

	
 
    def mark_as_read(self):
 
        self.read = True
 
        Session().add(self)
 

	
 

	
 
class Gist(Base, BaseModel):
 
    __tablename__ = 'gists'
 
    __table_args__ = (
 
        Index('g_gist_access_id_idx', 'gist_access_id'),
 
        Index('g_created_on_idx', 'created_on'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
 
    )
 
    GIST_PUBLIC = u'public'
 
    GIST_PRIVATE = u'private'
 
    DEFAULT_FILENAME = u'gistfile1.txt'
 

	
 
    gist_id = Column(Integer(), nullable=False, unique=True, primary_key=True)
 
    gist_access_id = Column(Unicode(250))
 
    gist_description = Column(UnicodeText(1024))
 
    gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
 
    gist_expires = Column(Float(53), nullable=False)
 
    gist_type = Column(Unicode(128), 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)
 

	
 
    owner = relationship('User')
 

	
 
    def __repr__(self):
 
        return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
 

	
 
    @classmethod
 
    def get_or_404(cls, id_):
 
        res = cls.query().filter(cls.gist_access_id == id_).scalar()
 
        if res is None:
 
            raise HTTPNotFound
 
        return res
 

	
 
    @classmethod
 
    def get_by_access_id(cls, gist_access_id):
 
        return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
 

	
 
    def gist_url(self):
 
        import kallithea
 
        alias_url = kallithea.CONFIG.get('gist_alias_url')
 
        if alias_url:
 
            return alias_url.replace('{gistid}', self.gist_access_id)
 

	
 
        import kallithea.lib.helpers as h
 
        return h.canonical_url('gist', gist_id=self.gist_access_id)
 

	
 
    @classmethod
 
    def base_path(cls):
 
        """
 
        Returns base path where all gists are stored
 

	
 
        :param cls:
 
        """
 
        from kallithea.model.gist import GIST_STORE_LOC
 
        q = Session().query(Ui)\
 
            .filter(Ui.ui_key == URL_SEP)
 
        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
 
        return os.path.join(q.one().ui_value, GIST_STORE_LOC)
 

	
 
    def get_api_data(self):
 
        """
 
        Common function for generating gist related data for API
 
        """
 
        gist = self
 
        data = dict(
 
            gist_id=gist.gist_id,
 
            type=gist.gist_type,
 
            access_id=gist.gist_access_id,
 
            description=gist.gist_description,
 
            url=gist.gist_url(),
 
            expires=gist.gist_expires,
 
            created_on=gist.created_on,
 
        )
 
        return data
 

	
 
    def __json__(self):
 
        data = dict(
 
        )
 
        data.update(self.get_api_data())
 
        return data
 
    ## SCM functions
 

	
 
    @property
 
    def scm_instance(self):
 
        from kallithea.lib.vcs import get_repo
 
        base_path = self.base_path()
 
        return get_repo(os.path.join(*map(safe_str,
 
                                          [base_path, self.gist_access_id])))
 

	
 

	
 
class DbMigrateVersion(Base, BaseModel):
 
    __tablename__ = 'db_migrate_version'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
 
    )
 
    repository_id = Column(String(250), nullable=False, unique=True, primary_key=True)
 
    repository_path = Column(Text)
 
    version = Column(Integer)
kallithea/model/pull_request.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.model.pull_request
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
pull request model for Kallithea
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Jun 6, 2012
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
import datetime
 

	
 
from pylons.i18n.translation import _
 

	
 
from kallithea.model.meta import Session
 
from kallithea.lib import helpers as h
 
from kallithea.lib.exceptions import UserInvalidException
 
from kallithea.model import BaseModel
 
from kallithea.model.db import PullRequest, PullRequestReviewers, Notification,\
 
    ChangesetStatus, User
 
from kallithea.model.notification import NotificationModel
 
from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class PullRequestModel(BaseModel):
 

	
 
    cls = PullRequest
 

	
 
    def __get_pull_request(self, pull_request):
 
        return self._get_instance(PullRequest, pull_request)
 

	
 
    def get_pullrequest_cnt_for_user(self, user):
 
        return PullRequest.query()\
 
                                .join(PullRequestReviewers)\
 
                                .filter(PullRequestReviewers.user_id == user)\
 
                                .filter(PullRequest.status != PullRequest.STATUS_CLOSED)\
 
                                .count()
 

	
 
    def get_all(self, repo_name, from_=False, closed=False):
 
        """Get all PRs for repo.
 
        Default is all PRs to the repo, PRs from the repo if from_.
 
        Closed PRs are only included if closed is true."""
 
        repo = self._get_repo(repo_name)
 
        q = PullRequest.query()
 
        if from_:
 
            q = q.filter(PullRequest.org_repo == repo)
 
        else:
 
            q = q.filter(PullRequest.other_repo == repo)
 
        if not closed:
 
            q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
 
        return q.order_by(PullRequest.created_on.desc()).all()
 

	
 
    def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
 
               revisions, reviewers, title, description=None):
 
        from kallithea.model.changeset_status import ChangesetStatusModel
 

	
 
        created_by_user = self._get_user(created_by)
 
        org_repo = self._get_repo(org_repo)
 
        other_repo = self._get_repo(other_repo)
 

	
 
        new = PullRequest()
 
        new.org_repo = org_repo
 
        new.org_ref = org_ref
 
        new.other_repo = other_repo
 
        new.other_ref = other_ref
 
        new.revisions = revisions
 
        new.title = title
 
        new.description = description
 
        new.author = created_by_user
 
        new.owner = created_by_user
 
        Session().add(new)
 
        Session().flush()
 

	
 
        #reset state to under-review
 
        from kallithea.model.comment import ChangesetCommentsModel
 
        comment = ChangesetCommentsModel().create(
 
            text=u'',
 
            repo=org_repo,
 
            user=new.author,
 
            user=new.owner,
 
            pull_request=new,
 
            send_email=False,
 
            status_change=ChangesetStatus.STATUS_UNDER_REVIEW,
 
        )
 
        ChangesetStatusModel().set_status(
 
            org_repo,
 
            ChangesetStatus.STATUS_UNDER_REVIEW,
 
            new.author,
 
            new.owner,
 
            comment,
 
            pull_request=new
 
        )
 

	
 
        mention_recipients = set(User.get_by_username(username, case_insensitive=True)
 
                                 for username in extract_mentioned_users(new.description))
 
        self.__add_reviewers(new, reviewers, mention_recipients)
 

	
 
        return new
 

	
 
    def __add_reviewers(self, pr, reviewers, mention_recipients=None):
 
        #members
 
        for member in set(reviewers):
 
            _usr = self._get_user(member)
 
            if _usr is None:
 
                raise UserInvalidException(member)
 
            reviewer = PullRequestReviewers(_usr, pr)
 
            Session().add(reviewer)
 

	
 
        revision_data = [(x.raw_id, x.message)
 
                         for x in map(pr.org_repo.get_changeset, pr.revisions)]
 

	
 
        #notification to reviewers
 
        pr_url = pr.url(canonical=True)
 
        threading = ['%s-pr-%s@%s' % (pr.other_repo.repo_name,
 
                                      pr.pull_request_id,
 
                                      h.canonical_hostname())]
 
        subject = safe_unicode(
 
            h.link_to(
 
              _('%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s') % \
 
                {'user': pr.author.username,
 
                {'user': pr.owner.username,
 
                 'pr_title': pr.title,
 
                 'pr_nice_id': pr.nice_id()},
 
                pr_url)
 
            )
 
        body = pr.description
 
        _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
 
        email_kwargs = {
 
            'pr_title': pr.title,
 
            'pr_user_created': h.person(pr.author),
 
            'pr_user_created': h.person(pr.owner),
 
            'pr_repo_url': h.canonical_url('summary_home', repo_name=pr.other_repo.repo_name),
 
            'pr_url': pr_url,
 
            'pr_revisions': revision_data,
 
            'repo_name': pr.other_repo.repo_name,
 
            'pr_nice_id': pr.nice_id(),
 
            'ref': org_ref_name,
 
            'pr_username': pr.author.username,
 
            'pr_username': pr.owner.username,
 
            'threading': threading,
 
            'is_mention': False,
 
            }
 
        if reviewers:
 
            NotificationModel().create(created_by=pr.author, subject=subject, body=body,
 
            NotificationModel().create(created_by=pr.owner, subject=subject, body=body,
 
                                       recipients=reviewers,
 
                                       type_=Notification.TYPE_PULL_REQUEST,
 
                                       email_kwargs=email_kwargs)
 

	
 
        if mention_recipients:
 
            mention_recipients.discard(None)
 
            mention_recipients.difference_update(reviewers)
 
        if mention_recipients:
 
            email_kwargs['is_mention'] = True
 
            subject = _('[Mention]') + ' ' + subject
 

	
 
            NotificationModel().create(created_by=pr.author, subject=subject, body=body,
 
            NotificationModel().create(created_by=pr.owner, subject=subject, body=body,
 
                                       recipients=mention_recipients,
 
                                       type_=Notification.TYPE_PULL_REQUEST,
 
                                       email_kwargs=email_kwargs)
 

	
 
    def mention_from_description(self, pr, old_description=''):
 
        mention_recipients = set(User.get_by_username(username, case_insensitive=True)
 
                                 for username in extract_mentioned_users(pr.description))
 
        mention_recipients.difference_update(User.get_by_username(username, case_insensitive=True)
 
                                             for username in extract_mentioned_users(old_description))
 

	
 
        log.debug("Mentioning %s", mention_recipients)
 
        self.__add_reviewers(pr, [], mention_recipients)
 

	
 
    def update_reviewers(self, pull_request, reviewers_ids):
 
        reviewers_ids = set(reviewers_ids)
 
        pull_request = self.__get_pull_request(pull_request)
 
        current_reviewers = PullRequestReviewers.query()\
 
                            .filter(PullRequestReviewers.pull_request==
 
                                   pull_request)\
 
                            .all()
 
        current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
 

	
 
        to_add = reviewers_ids.difference(current_reviewers_ids)
 
        to_remove = current_reviewers_ids.difference(reviewers_ids)
 

	
 
        log.debug("Adding %s reviewers", to_add)
 
        self.__add_reviewers(pull_request, to_add)
 

	
 
        log.debug("Removing %s reviewers", to_remove)
 
        for uid in to_remove:
 
            reviewer = PullRequestReviewers.query()\
 
                    .filter(PullRequestReviewers.user_id==uid,
 
                            PullRequestReviewers.pull_request==pull_request)\
 
                    .scalar()
 
            if reviewer:
 
                Session().delete(reviewer)
 

	
 
    def delete(self, pull_request):
 
        pull_request = self.__get_pull_request(pull_request)
 
        Session().delete(pull_request)
 

	
 
    def close_pull_request(self, pull_request):
 
        pull_request = self.__get_pull_request(pull_request)
 
        pull_request.status = PullRequest.STATUS_CLOSED
 
        pull_request.updated_on = datetime.datetime.now()
 
        Session().add(pull_request)
kallithea/templates/changeset/changeset_file_comment.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
## usage:
 
## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 
## ${comment.comment_block(co)}
 
##
 
<%def name="comment_block(co)">
 
  <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
 
    <div class="comment-prev-next-links"></div>
 
    <div class="comment-wrapp">
 
      <div class="meta">
 
          <div style="float:left">
 
               ${h.gravatar(co.author.email, size=20)}
 
          </div>
 
          <div class="user">
 
              ${co.author.full_name_and_username}
 
          </div>
 

	
 
          <span>
 
              ${h.age(co.modified_at)}
 
              %if co.pull_request:
 
                ${_('on pull request')}
 
                <a href="${co.pull_request.url()}">"${co.pull_request.title or _("No title")}"</a>
 
              %else:
 
                ${_('on this changeset')}
 
              %endif
 
              <a class="permalink" href="${co.url()}">&para;</a>
 
          </span>
 

	
 
          %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or co.author.user_id == c.authuser.user_id:
 
            <div onClick="confirm('${_("Delete comment?")}') && deleteComment(${co.comment_id})" class="buttons delete-comment btn btn-mini">${_('Delete')}</div>
 
          %endif
 
      </div>
 
      <div class="text">
 
        %if co.status_change:
 
           <div class="rst-block automatic-comment">
 
             <p>
 
               <span title="${_('Changeset status')}" class="changeset-status-lbl">${_("Status change")}: ${co.status_change[0].status_lbl}</span>
 
               <span class="changeset-status-ico"><i class="icon-circle changeset-status-${co.status_change[0].status}"></i></span>
 
             </p>
 
           </div>
 
        %endif
 
        %if co.text:
 
          ${h.rst_w_mentions(co.text)|n}
 
        %endif
 
      </div>
 
    </div>
 
  </div>
 
</%def>
 

	
 

	
 
## expanded with .format(f_path, line_no)
 
## TODO: don't assume line_no is globally unique ...
 
<%def name="comment_inline_form()">
 
<div id='comment-inline-form-template' style="display:none">
 
  <div class="ac">
 
  %if c.authuser.username != 'default':
 
    ${h.form('#', class_='inline-form')}
 
      <div id="edit-container_{1}" class="clearfix">
 
        <div class="comment-help">${_('Commenting on line {1}.')}
 
          ${(_('Comments parsed using %s syntax with %s support.') % (
 
                 ('<a href="%s">RST</a>' % h.url('rst_help')),
 
                   ('<span style="color:#577632" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to notify another user'))
 
               )
 
            )|n
 
           }
 
        </div>
 
        <div class="mentions-container" id="mentions_container_{1}"></div>
 
        <textarea id="text_{1}" name="text" class="comment-block-ta yui-ac-input"></textarea>
 
      </div>
 
      <div id="preview-container_{1}" class="clearfix" style="display:none">
 
        <div class="comment-help">
 
            ${_('Comment preview')}
 
        </div>
 
        <div id="preview-box_{1}" class="preview-box"></div>
 
      </div>
 
      <div class="comment-button">
 
        <div class="submitting-overlay">${_('Submitting ...')}</div>
 
        <input type="hidden" name="f_path" value="{0}">
 
        <input type="hidden" name="line" value="{1}">
 
        ${h.submit('save', _('Comment'), class_='btn btn-small save-inline-form')}
 
        ${h.reset('hide-inline-form', _('Cancel'), class_='btn btn-small hide-inline-form')}
 
        <div id="preview-btn_{1}" class="preview-btn btn btn-small">${_('Preview')}</div>
 
        <div id="edit-btn_{1}" class="edit-btn btn btn-small" style="display:none">${_('Edit')}</div>
 
      </div>
 
    ${h.end_form()}
 
  %else:
 
      ${h.form('')}
 
      <div class="clearfix">
 
          <div class="comment-help">
 
            ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
 
          </div>
 
      </div>
 
      <div class="comment-button">
 
      ${h.reset('hide-inline-form', _('Hide'), class_='btn btn-small hide-inline-form')}
 
      </div>
 
      ${h.end_form()}
 
  %endif
 
  </div>
 
</div>
 
</%def>
 

	
 

	
 
## show comment count as "x comments (y inline, z general)"
 
<%def name="comment_count(inline_cnt, general_cnt)">
 
    ${'%s (%s, %s)' % (
 
        ungettext("%d comment", "%d comments", inline_cnt + general_cnt) % (inline_cnt + general_cnt),
 
        ungettext("%d inline", "%d inline", inline_cnt) % inline_cnt,
 
        ungettext("%d general", "%d general", general_cnt) % general_cnt
 
    )}
 
    <span class="firstlink"></span>
 
</%def>
 

	
 

	
 
## generate inline comments and the main ones
 
<%def name="generate_comments()">
 
<div class="comments">
 
  %for f_path, lines in c.inline_comments:
 
    %for line_no, comments in lines.iteritems():
 
      <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
 
        %for co in comments:
 
            ${comment_block(co)}
 
        %endfor
 
      </div>
 
    %endfor
 
  %endfor
 

	
 
  <div class="comments-number">
 
    ${comment_count(c.inline_cnt, len(c.comments))}
 
  </div>
 

	
 
      <div class="comments-list-general">
 
        %for co in c.comments:
 
            ${comment_block(co)}
 
        %endfor
 
      </div>
 
</div>
 
</%def>
 

	
 
## MAIN COMMENT FORM
 
<%def name="comments(post_url, cur_status, is_pr=False, change_status=True)">
 

	
 
<div class="comments">
 
    %if c.authuser.username != 'default':
 
    <div class="comment-form ac">
 
      ${h.form(post_url, id="main_form")}
 
        <div id="edit-container" class="clearfix">
 
            <div class="comment-help">
 
                ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
 
                  '<span style="color:#577632" class="tooltip" title="%s">@mention</span>' %
 
                  _('Use @username inside this text to notify another user.')))|n}
 
            </div>
 
            <div class="mentions-container" id="mentions_container"></div>
 
            ${h.textarea('text', class_="comment-block-ta")}
 
            %if change_status:
 
              <div id="status_block_container" class="status-block">
 
                %if is_pr:
 
                  ${_('Vote for pull request status')}:
 
                %else:
 
                  ${_('Set changeset status')}:
 
                %endif
 
                <input type="radio" class="status_change_radio" name="changeset_status" id="changeset_status_unchanged" value="" checked="checked" />
 
                <label for="changeset_status_unchanged">
 
                  ${_('No change')}
 
                </label>
 
                %for status,lbl in c.changeset_statuses:
 
                    <span>
 
                        <input type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
 
                        <label for="${status}"><i class="icon-circle changeset-status-${status}" /></i>${lbl}</label>
 
                    </span>
 
                %endfor
 

	
 
                %if is_pr and ( \
 
                    h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) \
 
                    or c.pull_request.author.user_id == c.authuser.user_id):
 
                    or c.pull_request.owner.user_id == c.authuser.user_id):
 
                  <input id="save_close" type="checkbox" name="save_close">
 
                  <label id="save_close_label" for="save_close">${_("Close")}</label>
 
                %endif
 
              </div>
 
            %endif
 
        </div>
 

	
 
        <div id="preview-container" class="clearfix" style="display:none">
 
            <div class="comment-help">
 
                ${_('Comment preview')}
 
            </div>
 
            <div id="preview-box" class="preview-box"></div>
 
        </div>
 

	
 
        <div class="comment-button">
 
            ${h.submit('save', _('Comment'), class_="btn")}
 
            <div id="preview-btn" class="preview-btn btn">${_('Preview')}</div>
 
            <div id="edit-btn" class="edit-btn btn" style="display:none">${_('Edit')}</div>
 
        </div>
 
      ${h.end_form()}
 
    </div>
 
    %endif
 
</div>
 

	
 
<script>
 

	
 
$(document).ready(function () {
 
   MentionsAutoComplete($('#text'), $('#mentions_container'), _USERS_AC_DATA, _GROUPS_AC_DATA);
 

	
 
   $(window).on('beforeunload', function(){
 
      if($('.comment-inline-form textarea[name=text]').size() ||
 
         $('textarea#text').val()) {
 
         // this message will not be displayed on all browsers
 
         // (e.g. some versions of Firefox), but the user will still be warned
 
         return 'There are uncommitted comments.';
 
      }
 
   });
 

	
 
   $('form#main_form').submit(function(){
 
      // if no open inline forms, disable the beforeunload check - it would
 
      // fail in the check for the textarea we are about to submit
 
      if(!$('.form-open').size()){
 
          $(window).off('beforeunload');
 
      }
 
   });
 

	
 
   $('#preview-btn').click(function(){
 
       var _text = $('#text').val();
 
       if(!_text){
 
           return;
 
       }
 
       var post_data = {'text': _text};
 
       $('#preview-box').addClass('unloaded');
 
       $('#preview-box').html(_TM['Loading ...']);
 
       $('#edit-container').hide();
 
       $('#edit-btn').show();
 
       $('#preview-container').show();
 
       $('#preview-btn').hide();
 

	
 
       var url = pyroutes.url('changeset_comment_preview', {'repo_name': '${c.repo_name}'});
 
       ajaxPOST(url,post_data,function(html){
 
           $('#preview-box').html(html);
 
           $('#preview-box').removeClass('unloaded');
 
       });
 
   });
 
   $('#edit-btn').click(function(){
 
       $('#edit-container').show();
 
       $('#edit-btn').hide();
 
       $('#preview-container').hide();
 
       $('#preview-btn').show();
 
   });
 

	
 
});
 
</script>
 
</%def>
kallithea/templates/pullrequests/pullrequest_data.html
Show inline comments
 
## -*- coding: utf-8 -*-
 

	
 
<%def name="pullrequest_overview(pullrequests)">
 

	
 
%if not len(pullrequests):
 
    <div class="normal-indent empty_data">${_('No entries')}</div>
 
    <% return %>
 
%endif
 

	
 
<div class="table">
 
  <table>
 
    <thead>
 
      <tr>
 
        <th class="left">${_('Vote')}</th>
 
        <th class="left">${_('Title')}</th>
 
        <th class="left">${_('Author')}</th>
 
        <th class="left">${_('Owner')}</th>
 
        <th class="left">${_('Age')}</th>
 
        <th class="left">${_('From')}</th>
 
        <th class="left">${_('To')}</th>
 
        <th class="right" style="padding-right:5px">${_('Delete')}</th>
 
      </tr>
 
    </thead>
 
% for pr in pullrequests:
 
    <tr class="${'pr-closed' if pr.is_closed() else ''}">
 
      <td width="80px">
 
        <% status = pr.user_review_status(c.authuser.user_id) %>
 
        %if status:
 
          <i class="icon-circle changeset-status-${status}" title="${_("You voted: %s") % status}"></i>
 
        %else:
 
          <i class="icon-circle changeset-status-not_reviewed" title="${_("You didn't vote")}"></i>
 
        %endif
 
      </td>
 
      <td>
 
        <a href="${pr.url()}">
 
        ${pr.title or _("(no title)")}
 
        %if pr.is_closed():
 
          <span class="pr-closed-tag">${_('Closed')}</span>
 
        %endif
 
        </a>
 
      </td>
 
      <td>
 
        ${pr.author.full_name_and_username}
 
        ${pr.owner.full_name_and_username}
 
      </td>
 
      <td>
 
        <span class="tooltip" title="${h.fmt_date(pr.created_on)}">
 
          ${h.age(pr.created_on)}
 
        </span>
 
      </td>
 
      <td>
 
        <% org_ref_name=pr.org_ref.rsplit(':', 2)[-2] %>
 
        <a href="${h.url('summary_home', repo_name=pr.org_repo.repo_name, anchor=org_ref_name)}">
 
          ${pr.org_repo.repo_name}#${org_ref_name}
 
        </a>
 
      </td>
 
      <td>
 
        <% other_ref_name=pr.other_ref.rsplit(':', 2)[-2] %>
 
        <a href="${h.url('summary_home', repo_name=pr.other_repo.repo_name, anchor=other_ref_name)}">
 
          ${pr.other_repo.repo_name}#${other_ref_name}
 
        </a>
 
      </td>
 
      <td style="text-align:right">
 
        %if pr.author.user_id == c.authuser.user_id:
 
        %if pr.owner.user_id == c.authuser.user_id:
 
          ${h.form(url('pullrequest_delete', repo_name=pr.other_repo.repo_name, pull_request_id=pr.pull_request_id),method='delete', style="display:inline-block")}
 
          <button class="action_button"
 
                  id="remove_${pr.pull_request_id}"
 
                  name="remove_${pr.pull_request_id}"
 
                  title="${_('Delete Pull Request')}"
 
                  onclick="return confirm('${_('Confirm to delete this pull request')}')
 
                      && ((${len(pr.comments)} == 0) ||
 
                          confirm('${_('Confirm again to delete this pull request with %s comments') % len(pr.comments)}'))
 
                      ">
 
            <i class="icon-minus-circled"></i>
 
          </button>
 
          ${h.end_form()}
 
        %endif
 
      </td>
 
    </tr>
 
% endfor
 
  </table>
 
</div>
 

	
 
%if hasattr(pullrequests, 'pager'):
 
<div class="notification-paginator">
 
  <div class="pagination-wh pagination-left">
 
  ${pullrequests.pager('$link_previous ~2~ $link_next', **request.GET.mixed())}
 
  </div>
 
</div>
 
%endif
 

	
 
</%def>
 

	
 
${pullrequest_overview(c.pullrequests_pager)}
kallithea/templates/pullrequests/pullrequest_show.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 

	
 
<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Pull Request %s') % (c.repo_name, c.pull_request.nice_id())}
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('Pull request %s from %s#%s') % (c.pull_request.nice_id(), c.pull_request.org_repo.repo_name, c.cs_branch_name)}
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
<% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.author.user_id == c.authuser.user_id) %>
 
<% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.owner.user_id == c.authuser.user_id) %>
 
${self.repo_context_bar('showpullrequest')}
 
<div class="box">
 
  <!-- box / title -->
 
  <div class="title">
 
    ${self.breadcrumbs()}
 
  </div>
 

	
 
  ${h.form(url('pullrequest_post', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), method='post', id='pull_request_form')}
 
    <div class="form pr-box" style="float: left">
 
      <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
 
          ${_('Title')}: ${c.pull_request.title}
 
          %if c.pull_request.is_closed():
 
              (${_('Closed')})
 
          %endif
 
      </div>
 
      <div id="pr-summary" class="fields">
 

	
 
        <div class="field pr-not-edit" style="min-height:37px">
 
          <div class="label-summary">
 
            <label>${_('Description')}:</label>
 
            %if editable:
 
            <div style="margin: 5px">
 
              <a class="btn btn-small" onclick="$('#pr-edit-form').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
 
            </div>
 
            %endif
 
          </div>
 
          <div class="input">
 
            <div style="white-space:pre-wrap; line-height: 14px">${h.urlify_commit(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
 
          </div>
 
        </div>
 

	
 
        %if editable:
 
        <div id="pr-edit-form" style="display:none">
 
          <div class="field">
 
              <div class="label-summary">
 
                  <label for="pullrequest_title">${_('Title')}:</label>
 
              </div>
 
              <div class="input">
 
                  ${h.text('pullrequest_title',class_="large",value=c.pull_request.title,placeholder=_('Summarize the changes'))}
 
              </div>
 
          </div>
 

	
 
          <div class="field">
 
              <div class="label-summary label-textarea">
 
                  <label for="pullrequest_desc">${_('Description')}:</label>
 
              </div>
 
              <div class="textarea text-area editor">
 
                  ${h.textarea('pullrequest_desc',size=30,content=c.pull_request.description,placeholder=_('Write a short description on this pull request'))}
 
              </div>
 
          </div>
 
        </div>
 
        %endif
 

	
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Reviewer voting result')}:</label>
 
          </div>
 
          <div class="input">
 
            <div class="changeset-status-container" style="float:none;clear:both">
 
            %if c.current_voting_result:
 
              <span class="changeset-status-ico" style="padding:0px 4px 0px 0px">
 
                  <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i></span>
 
              <span class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
 
                %if c.pull_request.is_closed():
 
                    ${_('Closed')},
 
                %endif
 
                ${h.changeset_status_lbl(c.current_voting_result)}
 
              </span>
 
            %endif
 
            </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Still not reviewed by')}:</label>
 
          </div>
 
          <div class="input">
 
            % if len(c.pull_request_pending_reviewers) > 0:
 
                <div class="tooltip" title="${', '.join([x.username for x in c.pull_request_pending_reviewers])}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
 
            % elif len(c.pull_request_reviewers) > 0:
 
                <div>${_('Pull request was reviewed by all reviewers')}</div>
 
            %else:
 
                <div>${_('There are no reviewers')}</div>
 
            %endif
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Origin')}:</label>
 
          </div>
 
          <div class="input">
 
            <div>
 
              ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
 
              %if c.cs_ref_type != 'branch':
 
                ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
 
              %endif
 
            </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Target')}:</label>
 
          </div>
 
          <div class="input">
 
              <div>
 
              ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
 
              ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
 
              </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Pull changes')}:</label>
 
          </div>
 
          <div class="input">
 
              <div>
 
               ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
 
               %if h.is_hg(c.pull_request.org_repo):
 
                 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
 
               %elif h.is_git(c.pull_request.org_repo):
 
                 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
 
               %endif
 
              </div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Created on')}:</label>
 
          </div>
 
          <div class="input">
 
              <div>${h.fmt_date(c.pull_request.created_on)}</div>
 
          </div>
 
        </div>
 
        <div class="field">
 
          <div class="label-summary">
 
              <label>${_('Created by')}:</label>
 
              <label>${_('Owner')}:</label>
 
          </div>
 
          <div class="input">
 
              <div class="author">
 
                  <div class="gravatar">
 
                    ${h.gravatar(c.pull_request.author.email, size=20)}
 
                    ${h.gravatar(c.pull_request.owner.email, size=20)}
 
                  </div>
 
                  <span>${c.pull_request.author.full_name_and_username}</span><br/>
 
                  <span><a href="mailto:${c.pull_request.author.email}">${c.pull_request.author.email}</a></span><br/>
 
              </div>
 
                  <span>${c.pull_request.owner.full_name_and_username}</span><br/>
 
                  <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
 
          </div>
 
        </div>
 

	
 
        <div class="field">
 
            <div class="label-summary">
 
              <label>${_('Update')}:</label>
 
            </div>
 
            <div class="input">
 
              <div class="msg-div">${c.update_msg}</div>
 
              %if c.avail_revs:
 
              <div id="updaterevs" style="max-height:200px; overflow-y:auto; overflow-x:hidden; margin-bottom: 10px; padding: 1px 0">
 
                <div style="height:0">
 
                  <canvas id="avail_graph_canvas" style="width:0"></canvas>
 
                </div>
 
                <table id="updaterevs-table" class="noborder" style="padding-left:50px">
 
                  %for cnt, cs in enumerate(c.avail_cs):
 
                    <tr id="chg_available_${cnt+1}">
 
                      %if cs.revision == c.cs_ranges[-1].revision:
 
                        <td>
 
                          %if editable:
 
                            ${h.radio(name='updaterev', value='')}
 
                          %endif
 
                        </td>
 
                        <td colspan="2">${_("Current revision - no change")}</td>
 
                      %else:
 
                        <td>
 
                          %if editable and cs.revision in c.avail_revs:
 
                            ${h.radio(name='updaterev', value=cs.raw_id)}
 
                          %endif
 
                        </td>
 
                        <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</td>
 
                        <td><div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_commit(cs.message, c.repo_name)}</div></td>
 
                      %endif
 
                    </tr>
 
                  %endfor
 
                </table>
 
              </div>
 
              %endif
 
              <div class="msg-div">${c.update_msg_other}</div>
 
            </div>
 
        </div>
 
      </div>
 
    </div>
 
    ## REVIEWERS
 
    <div style="float:left; border-left:1px dashed #eee">
 
        <div class="pr-details-title">${_('Pull Request Reviewers')}</div>
 
        <div id="reviewers" style="padding:0px 0px 5px 10px">
 
          ## members goes here !
 
          <div>
 
            <ul id="review_members" class="group_members">
 
            %for member,status in c.pull_request_reviewers:
 
              ## WARNING: the HTML below is duplicate with
 
              ## kallithea/public/js/base.js
 
              ## If you change something here it should be reflected in the template too.
 
              <li id="reviewer_${member.user_id}">
 
                <div class="reviewers_member">
 
                    <div class="reviewer_status tooltip" title="${h.changeset_status_lbl(status.status if status else 'not_reviewed')}">
 
                      <i class="icon-circle changeset-status-${status.status if status else 'not_reviewed'}"></i>
 
                    </div>
 
                  <div class="reviewer_gravatar gravatar">
 
                    ${h.gravatar(member.email, size=14)}
 
                  </div>
 
                  <div style="float:left;">
 
                    ${member.full_name_and_username}
 
                    %if c.pull_request.user_id == member.user_id:
 
                      (${_('Owner')})
 
                    %endif
 
                  </div>
 
                  <input type="hidden" value="${member.user_id}" name="review_members" />
 
                  %if editable:
 
                  <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
 
                      <i class="icon-minus-circled"></i>
 
                  </div>
 
                  %endif
 
                </div>
 
              </li>
 
            %endfor
 
            </ul>
 
          </div>
 
          %if editable:
 
          <div class='ac'>
 
            <div class="reviewer_ac">
 
               ${h.text('user', class_='yui-ac-input',placeholder=_('Type name of reviewer to add'))}
 
               <div id="reviewers_container"></div>
 
            </div>
 
          </div>
 
          %endif
 
        </div>
 

	
 
        %if not c.pull_request_reviewers:
 
        <div class="pr-details-title">${_('Potential Reviewers')}</div>
 
        <div style="margin: 10px 0 10px 10px; max-width: 250px">
 
          <div>
 
            ${_('Click to add the repository owner as reviewer:')}
 
          </div>
 
          <ul style="margin-top: 10px">
 
            %for u in [c.pull_request.other_repo.user]:
 
              <li>
 
                <a class="missing_reviewer missing_reviewer_${u.user_id}"
 
                  user_id="${u.user_id}"
 
                  fname="${u.name}"
 
                  lname="${u.lastname}"
 
                  nname="${u.username}"
 
                  gravatar_lnk="${h.gravatar_url(u.email, size=28)}"
 
                  gravatar_size="14"
 
                  title="Click to add reviewer to the list, then Save Changes.">${u.full_name}</a>
 
              </li>
 
            %endfor
 
          </ul>
 
        </div>
 
        %endif
 
    </div>
 
    <div class="form" style="clear:both">
 
      <div class="fields">
 
        %if editable:
 
          <div class="buttons">
 
            ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-small")}
 
            ${h.submit('pr-form-clone',_('Save as New Pull Request'),class_="btn btn-small",disabled='disabled')}
 
            ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-small")}
 
          </div>
 
        %endif
 
      </div>
 
    </div>
 
  ${h.end_form()}
 
</div>
 

	
 
<div class="box">
 
    <div class="title">
 
      <div class="breadcrumbs">${_('Pull Request Content')}</div>
 
    </div>
 
    <div class="table">
 
          <div id="changeset_compare_view_content">
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
                  ${comment.comment_count(c.inline_cnt, len(c.comments))}
 
              </div>
 
              ##CS
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
                ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
 
              </div>
 
              <%include file="/compare/compare_cs.html" />
 

	
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 
              ${_('Common ancestor')}:
 
              ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev))}
 
              </div>
 

	
 
              ## FILES
 
              <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
 

	
 
              % if c.limited_diff:
 
                  ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
 
              % else:
 
                  ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
 
              %endif
 

	
 
              </div>
 
              <div class="cs_files">
 
                %if not c.files:
 
                   <span class="empty_data">${_('No files')}</span>
 
                %endif
 
                %for fid, change, f, stat in c.files:
 
                    <div class="cs_${change}">
 
                      <div class="node">
 
                          <i class="icon-diff-${change}"></i>
 
                          ${h.link_to(h.safe_unicode(f),'#' + fid)}
 
                      </div>
 
                      <div class="changes">${h.fancy_file_stats(stat)}</div>
 
                    </div>
 
                %endfor
 
              </div>
 
              % if c.limited_diff:
 
                <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
 
              % endif
 
          </div>
 
    </div>
 
    <script>
 
    var _USERS_AC_DATA = ${c.users_array|n};
 
    var _GROUPS_AC_DATA = ${c.user_groups_array|n};
 
    // TODO: switch this to pyroutes
 
    AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
 
    AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
 

	
 
    pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
 
    pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
 

	
 
    </script>
 

	
 
    ## diff block
 
    <div class="commentable-diff">
 
    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
 
    ${diff_block.diff_block_js()}
 
    %for fid, change, f, stat in c.files:
 
      ${diff_block.diff_block_simple([c.changes[fid]])}
 
    %endfor
 
    % if c.limited_diff:
 
      <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
 
    % endif
 
    </div>
 

	
 
    ## template for inline comment form
 
    ${comment.comment_inline_form()}
 

	
 
    ## render comments and inlines
 
    ${comment.generate_comments()}
 

	
 
    ## main comment form and it status
 
    ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
 
                              pull_request_id=c.pull_request.pull_request_id),
 
                       c.current_voting_result,
 
                       is_pr=True, change_status=c.allowed_to_change_status)}
 

	
 
    <script type="text/javascript">
 
      $(document).ready(function(){
 
          PullRequestAutoComplete($('#user'), $('#reviewers_container'), _USERS_AC_DATA);
 

	
 
          $('.code-difftable').on('click', '.add-bubble', function(e){
 
              show_comment_form($(this));
 
          });
 

	
 
          var avail_jsdata = ${c.avail_jsdata|n};
 
          var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
 
          avail_r.render(avail_jsdata,40);
 

	
 
          move_comments($(".comments .comments-list-chunk"));
 

	
 
          $('#updaterevs input').change(function(e){
 
              var update = !!e.target.value;
 
              $('#pr-form-save').prop('disabled',update);
 
              $('#pr-form-clone').prop('disabled',!update);
 
          });
 
          var $org_review_members = $('#review_members').clone();
 
          $('#pr-form-reset').click(function(e){
 
              $('#pr-edit-form').hide();
 
              $('.pr-not-edit').show();
 
              $('#pr-form-save').prop('disabled',false);
 
              $('#pr-form-clone').prop('disabled',true);
 
              $('#review_members').html($org_review_members);
 
          });
 

	
 
          // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
 
          if (window.location.hash != "") {
 
              window.location.href = window.location.href;
 
          }
 

	
 
          $('.missing_reviewer').click(function(){
 
            var $this = $(this);
 
            addReviewMember($this.attr('user_id'), $this.attr('fname'), $this.attr('lname'), $this.attr('nname'), $this.attr('gravatar_lnk'), $this.attr('gravatar_size'));
 
          });
 
      });
 
    </script>
 

	
 
</div>
 

	
 
</%def>
0 comments (0 inline, 0 general)