##// END OF EJS Templates
pull-requests: redo my account pull request page with datagrid. Fixes #4297...
marcink -
r1084:84545e70 default
parent child Browse files
Show More
@@ -39,19 +39,20 b' from rhodecode.lib.auth import ('
39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.utils import jsonify
41 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils2 import safe_int, md5
42 from rhodecode.lib.utils2 import safe_int, md5, str2bool
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44
44
45 from rhodecode.model.validation_schema.schemas import user_schema
45 from rhodecode.model.validation_schema.schemas import user_schema
46 from rhodecode.model.db import (
46 from rhodecode.model.db import (
47 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
47 Repository, PullRequest, UserEmailMap, User, UserFollowing)
48 UserFollowing)
49 from rhodecode.model.forms import UserForm
48 from rhodecode.model.forms import UserForm
50 from rhodecode.model.scm import RepoList
49 from rhodecode.model.scm import RepoList
51 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
52 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
54 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.comment import ChangesetCommentsModel
55
56
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
@@ -289,25 +290,85 b' class MyAccountController(BaseController'
289 category='success')
290 category='success')
290 return redirect(url('my_account_emails'))
291 return redirect(url('my_account_emails'))
291
292
293 def _extract_ordering(self, request):
294 column_index = safe_int(request.GET.get('order[0][column]'))
295 order_dir = request.GET.get('order[0][dir]', 'desc')
296 order_by = request.GET.get(
297 'columns[%s][data][sort]' % column_index, 'name_raw')
298 return order_by, order_dir
299
300 def _get_pull_requests_list(self, statuses):
301 start = safe_int(request.GET.get('start'), 0)
302 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
303 order_by, order_dir = self._extract_ordering(request)
304
305 pull_requests = PullRequestModel().get_im_participating_in(
306 user_id=c.rhodecode_user.user_id,
307 statuses=statuses,
308 offset=start, length=length, order_by=order_by,
309 order_dir=order_dir)
310
311 pull_requests_total_count = PullRequestModel().count_im_participating_in(
312 user_id=c.rhodecode_user.user_id, statuses=statuses)
313
314 from rhodecode.lib.utils import PartialRenderer
315 _render = PartialRenderer('data_table/_dt_elements.html')
316 data = []
317 for pr in pull_requests:
318 repo_id = pr.target_repo_id
319 comments = ChangesetCommentsModel().get_all_comments(
320 repo_id, pull_request=pr)
321 owned = pr.user_id == c.rhodecode_user.user_id
322 status = pr.calculated_review_status()
323
324 data.append({
325 'target_repo': _render('pullrequest_target_repo',
326 pr.target_repo.repo_name),
327 'name': _render('pullrequest_name',
328 pr.pull_request_id, pr.target_repo.repo_name,
329 short=True),
330 'name_raw': pr.pull_request_id,
331 'status': _render('pullrequest_status', status),
332 'title': _render(
333 'pullrequest_title', pr.title, pr.description),
334 'description': h.escape(pr.description),
335 'updated_on': _render('pullrequest_updated_on',
336 h.datetime_to_time(pr.updated_on)),
337 'updated_on_raw': h.datetime_to_time(pr.updated_on),
338 'created_on': _render('pullrequest_updated_on',
339 h.datetime_to_time(pr.created_on)),
340 'created_on_raw': h.datetime_to_time(pr.created_on),
341 'author': _render('pullrequest_author',
342 pr.author.full_contact, ),
343 'author_raw': pr.author.full_name,
344 'comments': _render('pullrequest_comments', len(comments)),
345 'comments_raw': len(comments),
346 'closed': pr.is_closed(),
347 'owned': owned
348 })
349 # json used to render the grid
350 data = ({
351 'data': data,
352 'recordsTotal': pull_requests_total_count,
353 'recordsFiltered': pull_requests_total_count,
354 })
355 return data
356
292 def my_account_pullrequests(self):
357 def my_account_pullrequests(self):
293 c.active = 'pullrequests'
358 c.active = 'pullrequests'
294 self.__load_data()
359 self.__load_data()
295 c.show_closed = request.GET.get('pr_show_closed')
360 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
296
297 def _filter(pr):
298 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
299 if not c.show_closed:
300 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
301 return s
302
361
303 c.my_pull_requests = _filter(
362 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
304 PullRequest.query().filter(
363 if c.show_closed:
305 PullRequest.user_id == c.rhodecode_user.user_id).all())
364 statuses += [PullRequest.STATUS_CLOSED]
306 my_prs = [
365 data = self._get_pull_requests_list(statuses)
307 x.pull_request for x in PullRequestReviewers.query().filter(
366 if not request.is_xhr:
308 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
367 c.data_participate = json.dumps(data['data'])
309 c.participate_in_pull_requests = _filter(my_prs)
368 c.records_total_participate = data['recordsTotal']
310 return render('admin/my_account/my_account.html')
369 return render('admin/my_account/my_account.html')
370 else:
371 return json.dumps(data)
311
372
312 def my_account_auth_tokens(self):
373 def my_account_auth_tokens(self):
313 c.active = 'auth_tokens'
374 c.active = 'auth_tokens'
@@ -3203,14 +3203,10 b' class PullRequest(Base, _PullRequestBase'
3203 }
3203 }
3204
3204
3205 def calculated_review_status(self):
3205 def calculated_review_status(self):
3206 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3207 # because it's tricky on how to use ChangesetStatusModel from there
3208 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3209 from rhodecode.model.changeset_status import ChangesetStatusModel
3206 from rhodecode.model.changeset_status import ChangesetStatusModel
3210 return ChangesetStatusModel().calculated_review_status(self)
3207 return ChangesetStatusModel().calculated_review_status(self)
3211
3208
3212 def reviewers_statuses(self):
3209 def reviewers_statuses(self):
3213 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3214 from rhodecode.model.changeset_status import ChangesetStatusModel
3210 from rhodecode.model.changeset_status import ChangesetStatusModel
3215 return ChangesetStatusModel().reviewers_statuses(self)
3211 return ChangesetStatusModel().reviewers_statuses(self)
3216
3212
@@ -31,6 +31,7 b' import urllib'
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34
35
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
@@ -159,12 +160,16 b' class PullRequestModel(BaseModel):'
159 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 opened_by=None, order_by=None,
161 opened_by=None, order_by=None,
161 order_dir='desc'):
162 order_dir='desc'):
162 repo = self._get_repo(repo_name)
163 repo = None
164 if repo_name:
165 repo = self._get_repo(repo_name)
166
163 q = PullRequest.query()
167 q = PullRequest.query()
168
164 # source or target
169 # source or target
165 if source:
170 if repo and source:
166 q = q.filter(PullRequest.source_repo == repo)
171 q = q.filter(PullRequest.source_repo == repo)
167 else:
172 elif repo:
168 q = q.filter(PullRequest.target_repo == repo)
173 q = q.filter(PullRequest.target_repo == repo)
169
174
170 # closed,opened
175 # closed,opened
@@ -179,7 +184,8 b' class PullRequestModel(BaseModel):'
179 order_map = {
184 order_map = {
180 'name_raw': PullRequest.pull_request_id,
185 'name_raw': PullRequest.pull_request_id,
181 'title': PullRequest.title,
186 'title': PullRequest.title,
182 'updated_on_raw': PullRequest.updated_on
187 'updated_on_raw': PullRequest.updated_on,
188 'target_repo': PullRequest.target_repo_id
183 }
189 }
184 if order_dir == 'asc':
190 if order_dir == 'asc':
185 q = q.order_by(order_map[order_by].asc())
191 q = q.order_by(order_map[order_by].asc())
@@ -337,6 +343,59 b' class PullRequestModel(BaseModel):'
337 PullRequestReviewers.user_id == user_id).all()
343 PullRequestReviewers.user_id == user_id).all()
338 ]
344 ]
339
345
346 def _prepare_participating_query(self, user_id=None, statuses=None,
347 order_by=None, order_dir='desc'):
348 q = PullRequest.query()
349 if user_id:
350 reviewers_subquery = Session().query(
351 PullRequestReviewers.pull_request_id).filter(
352 PullRequestReviewers.user_id == user_id).subquery()
353 user_filter= or_(
354 PullRequest.user_id == user_id,
355 PullRequest.pull_request_id.in_(reviewers_subquery)
356 )
357 q = PullRequest.query().filter(user_filter)
358
359 # closed,opened
360 if statuses:
361 q = q.filter(PullRequest.status.in_(statuses))
362
363 if order_by:
364 order_map = {
365 'name_raw': PullRequest.pull_request_id,
366 'title': PullRequest.title,
367 'updated_on_raw': PullRequest.updated_on,
368 'target_repo': PullRequest.target_repo_id
369 }
370 if order_dir == 'asc':
371 q = q.order_by(order_map[order_by].asc())
372 else:
373 q = q.order_by(order_map[order_by].desc())
374
375 return q
376
377 def count_im_participating_in(self, user_id=None, statuses=None):
378 q = self._prepare_participating_query(user_id, statuses=statuses)
379 return q.count()
380
381 def get_im_participating_in(
382 self, user_id=None, statuses=None, offset=0,
383 length=None, order_by=None, order_dir='desc'):
384 """
385 Get all Pull requests that i'm participating in, or i have opened
386 """
387
388 q = self._prepare_participating_query(
389 user_id, statuses=statuses, order_by=order_by,
390 order_dir=order_dir)
391
392 if length:
393 pull_requests = q.limit(length).offset(offset).all()
394 else:
395 pull_requests = q.all()
396
397 return pull_requests
398
340 def get_versions(self, pull_request):
399 def get_versions(self, pull_request):
341 """
400 """
342 returns version of pull request sorted by ID descending
401 returns version of pull request sorted by ID descending
@@ -11,123 +11,12 b''
11 </div>
11 </div>
12
12
13 <div class="panel panel-default">
13 <div class="panel panel-default">
14 <div class="panel-heading">
14 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Opened')}: ${len(c.my_pull_requests)}</h3>
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}: ${c.records_total_participate}</h3>
16 </div>
17 <div class="panel-body">
18 <div class="pullrequestlist">
19 %if c.my_pull_requests:
20 <table class="rctable">
21 <thead>
22 <th class="td-status"></th>
23 <th>${_('Target Repo')}</th>
24 <th>${_('Author')}</th>
25 <th></th>
26 <th>${_('Title')}</th>
27 <th class="td-time">${_('Last Update')}</th>
28 <th></th>
29 </thead>
30 %for pull_request in c.my_pull_requests:
31 <tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
32 <td class="td-status">
33 <div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
34 </td>
35 <td class="truncate-wrap td-componentname">
36 <div class="truncate">
37 ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
38 </div>
39 </td>
40 <td class="user">
41 ${base.gravatar_with_user(pull_request.author.email, 16)}
42 </td>
43 <td class="td-message expand_commit" data-pr-id="m${pull_request.pull_request_id}" title="${_('Expand commit message')}">
44 <div class="show_more_col">
45 <i class="show_more"></i>&nbsp;
46 </div>
47 </td>
48 <td class="mid td-description">
49 <div class="log-container truncate-wrap">
50 <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>\
51 %if pull_request.is_closed():
52 &nbsp;(${_('Closed')})\
53 %endif
54 <br/>${pull_request.description}</div>
55 </div>
56 </td>
57 <td class="td-time">
58 ${h.age_component(pull_request.updated_on)}
59 </td>
60 <td class="td-action repolist_actions">
61 ${h.secure_form(url('pullrequest_delete', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')}
62 ${h.submit('remove_%s' % pull_request.pull_request_id, _('Delete'),
63 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
64 ${h.end_form()}
65 </td>
66 </tr>
67 %endfor
68 </table>
69 %else:
70 <h2><span class="empty_data">${_('You currently have no open pull requests.')}</span></h2>
71 %endif
72 </div>
16 </div>
73 </div>
17 <div class="panel-body">
74 </div>
18 <table id="pull_request_list_table_participate" class="display"></table>
75
76 <div class="panel panel-default">
77 <div class="panel-heading">
78 <h3 class="panel-title">${_('Pull Requests You Participate In')}: ${len(c.participate_in_pull_requests)}</h3>
79 </div>
80
81 <div class="panel-body">
82 <div class="pullrequestlist">
83 %if c.participate_in_pull_requests:
84 <table class="rctable">
85 <thead>
86 <th class="td-status"></th>
87 <th>${_('Target Repo')}</th>
88 <th>${_('Author')}</th>
89 <th></th>
90 <th>${_('Title')}</th>
91 <th class="td-time">${_('Last Update')}</th>
92 </thead>
93 %for pull_request in c.participate_in_pull_requests:
94 <tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
95 <td class="td-status">
96 <div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
97 </td>
98 <td class="truncate-wrap td-componentname">
99 <div class="truncate">
100 ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
101 </div>
102 </td>
103 <td class="user">
104 ${base.gravatar_with_user(pull_request.author.email, 16)}
105 </td>
106 <td class="td-message expand_commit" data-pr-id="p${pull_request.pull_request_id}" title="${_('Expand commit message')}">
107 <div class="show_more_col">
108 <i class="show_more"></i>&nbsp;
109 </div>
110 </td>
111 <td class="mid td-description">
112 <div class="log-container truncate-wrap">
113 <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>\
114 %if pull_request.is_closed():
115 &nbsp;(${_('Closed')})\
116 %endif
117 <br/>${pull_request.description}</div>
118 </div>
119 </td>
120 <td class="td-time">
121 ${h.age_component(pull_request.updated_on)}
122 </td>
123 </tr>
124 %endfor
125 </table>
126 %else:
127 <h2 class="empty_data">${_('There are currently no open pull requests requiring your participation.')}</h2>
128 %endif
129 </div>
19 </div>
130 </div>
131 </div>
20 </div>
132
21
133 <script>
22 <script>
@@ -139,17 +28,51 b''
139 window.location = "${h.url('my_account_pullrequests')}";
28 window.location = "${h.url('my_account_pullrequests')}";
140 }
29 }
141 });
30 });
142 $('.expand_commit').on('click',function(e){
31 $(document).ready(function() {
143 var target_expand = $(this);
32
144 var cid = target_expand.data('prId');
33 var columnsDefs = [
34 { data: {"_": "status",
35 "sort": "status"}, title: "", className: "td-status", orderable: false},
36 { data: {"_": "target_repo",
37 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
38 { data: {"_": "name",
39 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
40 { data: {"_": "author",
41 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
42 { data: {"_": "title",
43 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
44 { data: {"_": "comments",
45 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
46 { data: {"_": "updated_on",
47 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
48 ];
145
49
146 if (target_expand.hasClass('open')){
50 // participating object list
147 $('#c-'+cid).css({'height': '2.75em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
51 $('#pull_request_list_table_participate').DataTable({
148 target_expand.removeClass('open');
52 data: ${c.data_participate|n},
149 }
53 processing: true,
150 else {
54 serverSide: true,
151 $('#c-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible'});
55 deferLoading: ${c.records_total_participate},
152 target_expand.addClass('open');
56 ajax: "",
153 }
57 dom: 'tp',
58 pageLength: ${c.visual.dashboard_items},
59 order: [[ 2, "desc" ]],
60 columns: columnsDefs,
61 language: {
62 paginate: DEFAULT_GRID_PAGINATION,
63 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
64 },
65 "drawCallback": function( settings, json ) {
66 timeagoActivate();
67 },
68 "createdRow": function ( row, data, index ) {
69 if (data['closed']) {
70 $(row).addClass('closed');
71 }
72 if (data['owned']) {
73 $(row).addClass('owned');
74 }
75 }
76 });
154 });
77 });
155 </script>
78 </script>
@@ -271,6 +271,12 b''
271
271
272
272
273 ## PULL REQUESTS GRID RENDERERS
273 ## PULL REQUESTS GRID RENDERERS
274
275 <%def name="pullrequest_target_repo(repo_name)">
276 <div class="truncate">
277 ${h.link_to(repo_name,h.url('summary_home',repo_name=repo_name))}
278 </div>
279 </%def>
274 <%def name="pullrequest_status(status)">
280 <%def name="pullrequest_status(status)">
275 <div class="${'flag_status %s' % status} pull-left"></div>
281 <div class="${'flag_status %s' % status} pull-left"></div>
276 </%def>
282 </%def>
@@ -284,9 +290,13 b''
284 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
290 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
285 </%def>
291 </%def>
286
292
287 <%def name="pullrequest_name(pull_request_id, target_repo_name)">
293 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
288 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
294 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
289 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
295 % if short:
296 #${pull_request_id}
297 % else:
298 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
299 % endif
290 </a>
300 </a>
291 </%def>
301 </%def>
292
302
@@ -85,14 +85,13 b' class TestMyAccountController(TestContro'
85 def test_my_account_my_pullrequests(self, pr_util):
85 def test_my_account_my_pullrequests(self, pr_util):
86 self.log_user()
86 self.log_user()
87 response = self.app.get(url('my_account_pullrequests'))
87 response = self.app.get(url('my_account_pullrequests'))
88 response.mustcontain('You currently have no open pull requests.')
88 response.mustcontain('There are currently no open pull '
89 'requests requiring your participation.')
89
90
90 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 response = self.app.get(url('my_account_pullrequests'))
92 response = self.app.get(url('my_account_pullrequests'))
92 response.mustcontain('There are currently no open pull requests '
93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
93 'requiring your participation')
94 response.mustcontain('TestMyAccountPR')
94
95 response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
96
95
97 def test_my_account_my_emails(self):
96 def test_my_account_my_emails(self):
98 self.log_user()
97 self.log_user()
General Comments 0
You need to be logged in to leave comments. Login now