pull-requests: redo my account pull request page with datagrid. Fixes #4297

- consistent ui with per-repo pull-request page
- faster loading due to pagination
- enabled sorting of columns
This commit is contained in:
Marcin Kuzminski 2016-11-01 08:58:57 +01:00
parent e0af9069d6
commit 753d0ff478
6 changed files with 207 additions and 159 deletions

View file

@ -39,19 +39,20 @@ from rhodecode.lib.auth import (
LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.utils import jsonify
from rhodecode.lib.utils2 import safe_int, md5
from rhodecode.lib.utils2 import safe_int, md5, str2bool
from rhodecode.lib.ext_json import json
from rhodecode.model.validation_schema.schemas import user_schema
from rhodecode.model.db import (
Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
UserFollowing)
Repository, PullRequest, UserEmailMap, User, UserFollowing)
from rhodecode.model.forms import UserForm
from rhodecode.model.scm import RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.meta import Session
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.comment import ChangesetCommentsModel
log = logging.getLogger(__name__)
@ -289,25 +290,85 @@ class MyAccountController(BaseController):
category='success')
return redirect(url('my_account_emails'))
def _extract_ordering(self, request):
column_index = safe_int(request.GET.get('order[0][column]'))
order_dir = request.GET.get('order[0][dir]', 'desc')
order_by = request.GET.get(
'columns[%s][data][sort]' % column_index, 'name_raw')
return order_by, order_dir
def _get_pull_requests_list(self, statuses):
start = safe_int(request.GET.get('start'), 0)
length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
order_by, order_dir = self._extract_ordering(request)
pull_requests = PullRequestModel().get_im_participating_in(
user_id=c.rhodecode_user.user_id,
statuses=statuses,
offset=start, length=length, order_by=order_by,
order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_im_participating_in(
user_id=c.rhodecode_user.user_id, statuses=statuses)
from rhodecode.lib.utils import PartialRenderer
_render = PartialRenderer('data_table/_dt_elements.html')
data = []
for pr in pull_requests:
repo_id = pr.target_repo_id
comments = ChangesetCommentsModel().get_all_comments(
repo_id, pull_request=pr)
owned = pr.user_id == c.rhodecode_user.user_id
status = pr.calculated_review_status()
data.append({
'target_repo': _render('pullrequest_target_repo',
pr.target_repo.repo_name),
'name': _render('pullrequest_name',
pr.pull_request_id, pr.target_repo.repo_name,
short=True),
'name_raw': pr.pull_request_id,
'status': _render('pullrequest_status', status),
'title': _render(
'pullrequest_title', pr.title, pr.description),
'description': h.escape(pr.description),
'updated_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.updated_on)),
'updated_on_raw': h.datetime_to_time(pr.updated_on),
'created_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.created_on)),
'created_on_raw': h.datetime_to_time(pr.created_on),
'author': _render('pullrequest_author',
pr.author.full_contact, ),
'author_raw': pr.author.full_name,
'comments': _render('pullrequest_comments', len(comments)),
'comments_raw': len(comments),
'closed': pr.is_closed(),
'owned': owned
})
# json used to render the grid
data = ({
'data': data,
'recordsTotal': pull_requests_total_count,
'recordsFiltered': pull_requests_total_count,
})
return data
def my_account_pullrequests(self):
c.active = 'pullrequests'
self.__load_data()
c.show_closed = request.GET.get('pr_show_closed')
c.show_closed = str2bool(request.GET.get('pr_show_closed'))
def _filter(pr):
s = sorted(pr, key=lambda o: o.created_on, reverse=True)
if not c.show_closed:
s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
return s
c.my_pull_requests = _filter(
PullRequest.query().filter(
PullRequest.user_id == c.rhodecode_user.user_id).all())
my_prs = [
x.pull_request for x in PullRequestReviewers.query().filter(
PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
c.participate_in_pull_requests = _filter(my_prs)
return render('admin/my_account/my_account.html')
statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
if c.show_closed:
statuses += [PullRequest.STATUS_CLOSED]
data = self._get_pull_requests_list(statuses)
if not request.is_xhr:
c.data_participate = json.dumps(data['data'])
c.records_total_participate = data['recordsTotal']
return render('admin/my_account/my_account.html')
else:
return json.dumps(data)
def my_account_auth_tokens(self):
c.active = 'auth_tokens'

View file

@ -3203,14 +3203,10 @@ class PullRequest(Base, _PullRequestBase):
}
def calculated_review_status(self):
# TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
# because it's tricky on how to use ChangesetStatusModel from there
warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
from rhodecode.model.changeset_status import ChangesetStatusModel
return ChangesetStatusModel().calculated_review_status(self)
def reviewers_statuses(self):
warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
from rhodecode.model.changeset_status import ChangesetStatusModel
return ChangesetStatusModel().reviewers_statuses(self)

View file

@ -31,6 +31,7 @@ import urllib
from pylons.i18n.translation import _
from pylons.i18n.translation import lazy_ugettext
from sqlalchemy import or_
from rhodecode.lib import helpers as h, hooks_utils, diffs
from rhodecode.lib.compat import OrderedDict
@ -159,12 +160,16 @@ class PullRequestModel(BaseModel):
def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
opened_by=None, order_by=None,
order_dir='desc'):
repo = self._get_repo(repo_name)
repo = None
if repo_name:
repo = self._get_repo(repo_name)
q = PullRequest.query()
# source or target
if source:
if repo and source:
q = q.filter(PullRequest.source_repo == repo)
else:
elif repo:
q = q.filter(PullRequest.target_repo == repo)
# closed,opened
@ -179,7 +184,8 @@ class PullRequestModel(BaseModel):
order_map = {
'name_raw': PullRequest.pull_request_id,
'title': PullRequest.title,
'updated_on_raw': PullRequest.updated_on
'updated_on_raw': PullRequest.updated_on,
'target_repo': PullRequest.target_repo_id
}
if order_dir == 'asc':
q = q.order_by(order_map[order_by].asc())
@ -337,6 +343,59 @@ class PullRequestModel(BaseModel):
PullRequestReviewers.user_id == user_id).all()
]
def _prepare_participating_query(self, user_id=None, statuses=None,
order_by=None, order_dir='desc'):
q = PullRequest.query()
if user_id:
reviewers_subquery = Session().query(
PullRequestReviewers.pull_request_id).filter(
PullRequestReviewers.user_id == user_id).subquery()
user_filter= or_(
PullRequest.user_id == user_id,
PullRequest.pull_request_id.in_(reviewers_subquery)
)
q = PullRequest.query().filter(user_filter)
# closed,opened
if statuses:
q = q.filter(PullRequest.status.in_(statuses))
if order_by:
order_map = {
'name_raw': PullRequest.pull_request_id,
'title': PullRequest.title,
'updated_on_raw': PullRequest.updated_on,
'target_repo': PullRequest.target_repo_id
}
if order_dir == 'asc':
q = q.order_by(order_map[order_by].asc())
else:
q = q.order_by(order_map[order_by].desc())
return q
def count_im_participating_in(self, user_id=None, statuses=None):
q = self._prepare_participating_query(user_id, statuses=statuses)
return q.count()
def get_im_participating_in(
self, user_id=None, statuses=None, offset=0,
length=None, order_by=None, order_dir='desc'):
"""
Get all Pull requests that i'm participating in, or i have opened
"""
q = self._prepare_participating_query(
user_id, statuses=statuses, order_by=order_by,
order_dir=order_dir)
if length:
pull_requests = q.limit(length).offset(offset).all()
else:
pull_requests = q.all()
return pull_requests
def get_versions(self, pull_request):
"""
returns version of pull request sorted by ID descending

View file

@ -11,123 +11,12 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">${_('Pull Requests You Opened')}: ${len(c.my_pull_requests)}</h3>
</div>
<div class="panel-body">
<div class="pullrequestlist">
%if c.my_pull_requests:
<table class="rctable">
<thead>
<th class="td-status"></th>
<th>${_('Target Repo')}</th>
<th>${_('Author')}</th>
<th></th>
<th>${_('Title')}</th>
<th class="td-time">${_('Last Update')}</th>
<th></th>
</thead>
%for pull_request in c.my_pull_requests:
<tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
<td class="td-status">
<div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
</td>
<td class="truncate-wrap td-componentname">
<div class="truncate">
${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
</div>
</td>
<td class="user">
${base.gravatar_with_user(pull_request.author.email, 16)}
</td>
<td class="td-message expand_commit" data-pr-id="m${pull_request.pull_request_id}" title="${_('Expand commit message')}">
<div class="show_more_col">
<i class="show_more"></i>&nbsp;
</div>
</td>
<td class="mid td-description">
<div class="log-container truncate-wrap">
<div class="message truncate" id="c-m${pull_request.pull_request_id}"><a href="${h.url('pullrequest_show',repo_name=pull_request.target_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">#${pull_request.pull_request_id}: ${pull_request.title}</a>\
%if pull_request.is_closed():
&nbsp;(${_('Closed')})\
%endif
<br/>${pull_request.description}</div>
</div>
</td>
<td class="td-time">
${h.age_component(pull_request.updated_on)}
</td>
<td class="td-action repolist_actions">
${h.secure_form(url('pullrequest_delete', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')}
${h.submit('remove_%s' % pull_request.pull_request_id, _('Delete'),
class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
${h.end_form()}
</td>
</tr>
%endfor
</table>
%else:
<h2><span class="empty_data">${_('You currently have no open pull requests.')}</span></h2>
%endif
<div class="panel-heading">
<h3 class="panel-title">${_('Pull Requests You Participate In')}: ${c.records_total_participate}</h3>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">${_('Pull Requests You Participate In')}: ${len(c.participate_in_pull_requests)}</h3>
</div>
<div class="panel-body">
<div class="pullrequestlist">
%if c.participate_in_pull_requests:
<table class="rctable">
<thead>
<th class="td-status"></th>
<th>${_('Target Repo')}</th>
<th>${_('Author')}</th>
<th></th>
<th>${_('Title')}</th>
<th class="td-time">${_('Last Update')}</th>
</thead>
%for pull_request in c.participate_in_pull_requests:
<tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
<td class="td-status">
<div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
</td>
<td class="truncate-wrap td-componentname">
<div class="truncate">
${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
</div>
</td>
<td class="user">
${base.gravatar_with_user(pull_request.author.email, 16)}
</td>
<td class="td-message expand_commit" data-pr-id="p${pull_request.pull_request_id}" title="${_('Expand commit message')}">
<div class="show_more_col">
<i class="show_more"></i>&nbsp;
</div>
</td>
<td class="mid td-description">
<div class="log-container truncate-wrap">
<div class="message truncate" id="c-p${pull_request.pull_request_id}"><a href="${h.url('pullrequest_show',repo_name=pull_request.target_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">#${pull_request.pull_request_id}: ${pull_request.title}</a>\
%if pull_request.is_closed():
&nbsp;(${_('Closed')})\
%endif
<br/>${pull_request.description}</div>
</div>
</td>
<td class="td-time">
${h.age_component(pull_request.updated_on)}
</td>
</tr>
%endfor
</table>
%else:
<h2 class="empty_data">${_('There are currently no open pull requests requiring your participation.')}</h2>
%endif
<div class="panel-body">
<table id="pull_request_list_table_participate" class="display"></table>
</div>
</div>
</div>
<script>
@ -139,17 +28,51 @@
window.location = "${h.url('my_account_pullrequests')}";
}
});
$('.expand_commit').on('click',function(e){
var target_expand = $(this);
var cid = target_expand.data('prId');
$(document).ready(function() {
if (target_expand.hasClass('open')){
$('#c-'+cid).css({'height': '2.75em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
target_expand.removeClass('open');
}
else {
$('#c-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible'});
target_expand.addClass('open');
}
var columnsDefs = [
{ data: {"_": "status",
"sort": "status"}, title: "", className: "td-status", orderable: false},
{ data: {"_": "target_repo",
"sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
{ data: {"_": "name",
"sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
{ data: {"_": "author",
"sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
{ data: {"_": "title",
"sort": "title"}, title: "${_('Title')}", className: "td-description" },
{ data: {"_": "comments",
"sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
{ data: {"_": "updated_on",
"sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
];
// participating object list
$('#pull_request_list_table_participate').DataTable({
data: ${c.data_participate|n},
processing: true,
serverSide: true,
deferLoading: ${c.records_total_participate},
ajax: "",
dom: 'tp',
pageLength: ${c.visual.dashboard_items},
order: [[ 2, "desc" ]],
columns: columnsDefs,
language: {
paginate: DEFAULT_GRID_PAGINATION,
emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
},
"drawCallback": function( settings, json ) {
timeagoActivate();
},
"createdRow": function ( row, data, index ) {
if (data['closed']) {
$(row).addClass('closed');
}
if (data['owned']) {
$(row).addClass('owned');
}
}
});
});
</script>

View file

@ -271,6 +271,12 @@
## PULL REQUESTS GRID RENDERERS
<%def name="pullrequest_target_repo(repo_name)">
<div class="truncate">
${h.link_to(repo_name,h.url('summary_home',repo_name=repo_name))}
</div>
</%def>
<%def name="pullrequest_status(status)">
<div class="${'flag_status %s' % status} pull-left"></div>
</%def>
@ -284,9 +290,13 @@
<i class="icon-comment icon-comment-colored"></i> ${comments_nr}
</%def>
<%def name="pullrequest_name(pull_request_id, target_repo_name)">
<%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
<a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
% if short:
#${pull_request_id}
% else:
${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
% endif
</a>
</%def>

View file

@ -85,14 +85,13 @@ class TestMyAccountController(TestController):
def test_my_account_my_pullrequests(self, pr_util):
self.log_user()
response = self.app.get(url('my_account_pullrequests'))
response.mustcontain('You currently have no open pull requests.')
response.mustcontain('There are currently no open pull '
'requests requiring your participation.')
pr = pr_util.create_pull_request(title='TestMyAccountPR')
response = self.app.get(url('my_account_pullrequests'))
response.mustcontain('There are currently no open pull requests '
'requiring your participation')
response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
response.mustcontain('"name_raw": %s' % pr.pull_request_id)
response.mustcontain('TestMyAccountPR')
def test_my_account_my_emails(self):
self.log_user()