Auto status change to "Under Review"
Show More
@@ -76,7 +76,11 b' class TestMyAccountEdit(TestController):' | |||
|
76 | 76 | 'requests requiring your participation.') |
|
77 | 77 | |
|
78 | 78 | @pytest.mark.backends("git", "hg") |
|
79 | def test_my_account_my_pullrequests_data(self, pr_util, xhr_header): | |
|
79 | @pytest.mark.parametrize('params, expected_title', [ | |
|
80 | ({'closed': 1}, 'Closed'), | |
|
81 | ({'awaiting_my_review': 1}, 'Awaiting my review'), | |
|
82 | ]) | |
|
83 | def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title): | |
|
80 | 84 | self.log_user() |
|
81 | 85 | response = self.app.get(route_path('my_account_pullrequests_data'), |
|
82 | 86 | extra_environ=xhr_header) |
@@ -43,7 +43,7 b' from rhodecode.model.comment import Comm' | |||
|
43 | 43 | from rhodecode.model.db import ( |
|
44 | 44 | IntegrityError, or_, in_filter_generator, |
|
45 | 45 | Repository, UserEmailMap, UserApiKeys, UserFollowing, |
|
46 | PullRequest, UserBookmark, RepoGroup) | |
|
46 | PullRequest, UserBookmark, RepoGroup, ChangesetStatus) | |
|
47 | 47 | from rhodecode.model.meta import Session |
|
48 | 48 | from rhodecode.model.pull_request import PullRequestModel |
|
49 | 49 | from rhodecode.model.user import UserModel |
@@ -654,13 +654,23 b' class MyAccountView(BaseAppView, DataGri' | |||
|
654 | 654 | Session().commit() |
|
655 | 655 | return user.user_data['notification_status'] |
|
656 | 656 | |
|
657 | def _get_pull_requests_list(self, statuses): | |
|
657 | def _get_pull_requests_list(self, statuses, filter_type=None): | |
|
658 | 658 | draw, start, limit = self._extract_chunk(self.request) |
|
659 | 659 | search_q, order_by, order_dir = self._extract_ordering(self.request) |
|
660 | 660 | |
|
661 | 661 | _render = self.request.get_partial_renderer( |
|
662 | 662 | 'rhodecode:templates/data_table/_dt_elements.mako') |
|
663 | 663 | |
|
664 | if filter_type == 'awaiting_my_review': | |
|
665 | pull_requests = PullRequestModel().get_im_participating_in_for_review( | |
|
666 | user_id=self._rhodecode_user.user_id, | |
|
667 | statuses=statuses, query=search_q, | |
|
668 | offset=start, length=limit, order_by=order_by, | |
|
669 | order_dir=order_dir) | |
|
670 | ||
|
671 | pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review( | |
|
672 | user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q) | |
|
673 | else: | |
|
664 | 674 | pull_requests = PullRequestModel().get_im_participating_in( |
|
665 | 675 | user_id=self._rhodecode_user.user_id, |
|
666 | 676 | statuses=statuses, query=search_q, |
@@ -678,6 +688,12 b' class MyAccountView(BaseAppView, DataGri' | |||
|
678 | 688 | repo_id, pull_request=pr, include_drafts=False, count_only=True) |
|
679 | 689 | owned = pr.user_id == self._rhodecode_user.user_id |
|
680 | 690 | |
|
691 | review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user) | |
|
692 | my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED | |
|
693 | if review_statuses and review_statuses[4]: | |
|
694 | _review_obj, _user, _reasons, _mandatory, statuses = review_statuses | |
|
695 | my_review_status = statuses[0][1].status | |
|
696 | ||
|
681 | 697 | data.append({ |
|
682 | 698 | 'target_repo': _render('pullrequest_target_repo', |
|
683 | 699 | pr.target_repo.repo_name), |
@@ -688,6 +704,8 b' class MyAccountView(BaseAppView, DataGri' | |||
|
688 | 704 | 'name_raw': pr.pull_request_id, |
|
689 | 705 | 'status': _render('pullrequest_status', |
|
690 | 706 | pr.calculated_review_status()), |
|
707 | 'my_status': _render('pullrequest_status', | |
|
708 | my_review_status), | |
|
691 | 709 | 'title': _render('pullrequest_title', pr.title, pr.description), |
|
692 | 710 | 'description': h.escape(pr.description), |
|
693 | 711 | 'updated_on': _render('pullrequest_updated_on', |
@@ -723,7 +741,14 b' class MyAccountView(BaseAppView, DataGri' | |||
|
723 | 741 | c.active = 'pullrequests' |
|
724 | 742 | req_get = self.request.GET |
|
725 | 743 | |
|
726 |
c.closed = str2bool(req_get.get(' |
|
|
744 | c.closed = str2bool(req_get.get('closed')) | |
|
745 | c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) | |
|
746 | ||
|
747 | c.selected_filter = 'all' | |
|
748 | if c.closed: | |
|
749 | c.selected_filter = 'all_closed' | |
|
750 | if c.awaiting_my_review: | |
|
751 | c.selected_filter = 'awaiting_my_review' | |
|
727 | 752 | |
|
728 | 753 | return self._get_template_context(c) |
|
729 | 754 | |
@@ -732,13 +757,19 b' class MyAccountView(BaseAppView, DataGri' | |||
|
732 | 757 | def my_account_pullrequests_data(self): |
|
733 | 758 | self.load_default_context() |
|
734 | 759 | req_get = self.request.GET |
|
760 | ||
|
761 | awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) | |
|
735 | 762 | closed = str2bool(req_get.get('closed')) |
|
736 | 763 | |
|
737 | 764 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
738 | 765 | if closed: |
|
739 | 766 | statuses += [PullRequest.STATUS_CLOSED] |
|
740 | 767 | |
|
741 | data = self._get_pull_requests_list(statuses=statuses) | |
|
768 | filter_type = \ | |
|
769 | 'awaiting_my_review' if awaiting_my_review \ | |
|
770 | else None | |
|
771 | ||
|
772 | data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type) | |
|
742 | 773 | return data |
|
743 | 774 | |
|
744 | 775 | @LoginRequired() |
@@ -41,7 +41,7 b' class TestPullRequestList(object):' | |||
|
41 | 41 | |
|
42 | 42 | @pytest.mark.parametrize('params, expected_title', [ |
|
43 | 43 | ({'source': 0, 'closed': 1}, 'Closed'), |
|
44 |
({'source': 0, 'my': 1}, ' |
|
|
44 | ({'source': 0, 'my': 1}, 'Created by me'), | |
|
45 | 45 | ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'), |
|
46 | 46 | ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'), |
|
47 | 47 | ({'source': 1}, 'From this repo'), |
@@ -79,21 +79,20 b' class RepoPullRequestsView(RepoAppView, ' | |||
|
79 | 79 | |
|
80 | 80 | if filter_type == 'awaiting_review': |
|
81 | 81 | pull_requests = PullRequestModel().get_awaiting_review( |
|
82 | repo_name, search_q=search_q, source=source, opened_by=opened_by, | |
|
83 | statuses=statuses, offset=start, length=limit, | |
|
84 | order_by=order_by, order_dir=order_dir) | |
|
82 | repo_name, | |
|
83 | search_q=search_q, statuses=statuses, | |
|
84 | offset=start, length=limit, order_by=order_by, order_dir=order_dir) | |
|
85 | 85 | pull_requests_total_count = PullRequestModel().count_awaiting_review( |
|
86 | repo_name, search_q=search_q, source=source, statuses=statuses, | |
|
87 | opened_by=opened_by) | |
|
86 | repo_name, | |
|
87 | search_q=search_q, statuses=statuses) | |
|
88 | 88 | elif filter_type == 'awaiting_my_review': |
|
89 | 89 | pull_requests = PullRequestModel().get_awaiting_my_review( |
|
90 | repo_name, search_q=search_q, source=source, opened_by=opened_by, | |
|
91 |
|
|
|
92 | offset=start, length=limit, order_by=order_by, | |
|
93 | order_dir=order_dir) | |
|
90 | repo_name, self._rhodecode_user.user_id, | |
|
91 | search_q=search_q, statuses=statuses, | |
|
92 | offset=start, length=limit, order_by=order_by, order_dir=order_dir) | |
|
94 | 93 | pull_requests_total_count = PullRequestModel().count_awaiting_my_review( |
|
95 |
repo_name, |
|
|
96 |
statuses=statuses |
|
|
94 | repo_name, self._rhodecode_user.user_id, | |
|
95 | search_q=search_q, statuses=statuses) | |
|
97 | 96 | else: |
|
98 | 97 | pull_requests = PullRequestModel().get_all( |
|
99 | 98 | repo_name, search_q=search_q, source=source, opened_by=opened_by, |
@@ -110,6 +109,12 b' class RepoPullRequestsView(RepoAppView, ' | |||
|
110 | 109 | self.db_repo.repo_id, pull_request=pr, |
|
111 | 110 | include_drafts=False, count_only=True) |
|
112 | 111 | |
|
112 | review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user) | |
|
113 | my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED | |
|
114 | if review_statuses and review_statuses[4]: | |
|
115 | _review_obj, _user, _reasons, _mandatory, statuses = review_statuses | |
|
116 | my_review_status = statuses[0][1].status | |
|
117 | ||
|
113 | 118 | data.append({ |
|
114 | 119 | 'name': _render('pullrequest_name', |
|
115 | 120 | pr.pull_request_id, pr.pull_request_state, |
@@ -118,6 +123,8 b' class RepoPullRequestsView(RepoAppView, ' | |||
|
118 | 123 | 'name_raw': pr.pull_request_id, |
|
119 | 124 | 'status': _render('pullrequest_status', |
|
120 | 125 | pr.calculated_review_status()), |
|
126 | 'my_status': _render('pullrequest_status', | |
|
127 | my_review_status), | |
|
121 | 128 | 'title': _render('pullrequest_title', pr.title, pr.description), |
|
122 | 129 | 'description': h.escape(pr.description), |
|
123 | 130 | 'updated_on': _render('pullrequest_updated_on', |
@@ -354,7 +354,7 b' class ChangesetStatusModel(BaseModel):' | |||
|
354 | 354 | Session().add(new_status) |
|
355 | 355 | return new_statuses |
|
356 | 356 | |
|
357 | def aggregate_votes_by_user(self, commit_statuses, reviewers_data): | |
|
357 | def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None): | |
|
358 | 358 | |
|
359 | 359 | commit_statuses_map = collections.defaultdict(list) |
|
360 | 360 | for st in commit_statuses: |
@@ -368,6 +368,10 b' class ChangesetStatusModel(BaseModel):' | |||
|
368 | 368 | for obj in reviewers_data: |
|
369 | 369 | if not obj.user: |
|
370 | 370 | continue |
|
371 | if user and obj.user.username != user.username: | |
|
372 | # single user filter | |
|
373 | continue | |
|
374 | ||
|
371 | 375 | statuses = commit_statuses_map.get(obj.user.username, None) |
|
372 | 376 | if statuses: |
|
373 | 377 | status_groups = itertools.groupby( |
@@ -376,16 +380,19 b' class ChangesetStatusModel(BaseModel):' | |||
|
376 | 380 | |
|
377 | 381 | reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses)) |
|
378 | 382 | |
|
383 | if user: | |
|
384 | return reviewers[0] if reviewers else reviewers | |
|
385 | else: | |
|
379 | 386 | return reviewers |
|
380 | 387 | |
|
381 | def reviewers_statuses(self, pull_request): | |
|
388 | def reviewers_statuses(self, pull_request, user=None): | |
|
382 | 389 | _commit_statuses = self.get_statuses( |
|
383 | 390 | pull_request.source_repo, |
|
384 | 391 | pull_request=pull_request, |
|
385 | 392 | with_revisions=True) |
|
386 | 393 | reviewers = pull_request.get_pull_request_reviewers( |
|
387 | 394 | role=PullRequestReviewers.ROLE_REVIEWER) |
|
388 | return self.aggregate_votes_by_user(_commit_statuses, reviewers) | |
|
395 | return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user) | |
|
389 | 396 | |
|
390 | 397 | def calculated_review_status(self, pull_request): |
|
391 | 398 | """ |
@@ -41,10 +41,10 b' from sqlalchemy import (' | |||
|
41 | 41 | Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, |
|
42 | 42 | Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, |
|
43 | 43 | Text, Float, PickleType, BigInteger) |
|
44 | from sqlalchemy.sql.expression import true, false, case | |
|
44 | from sqlalchemy.sql.expression import true, false, case, null | |
|
45 | 45 | from sqlalchemy.sql.functions import coalesce, count # pragma: no cover |
|
46 | 46 | from sqlalchemy.orm import ( |
|
47 | relationship, joinedload, class_mapper, validates, aliased) | |
|
47 | relationship, lazyload, joinedload, class_mapper, validates, aliased) | |
|
48 | 48 | from sqlalchemy.ext.declarative import declared_attr |
|
49 | 49 | from sqlalchemy.ext.hybrid import hybrid_property |
|
50 | 50 | from sqlalchemy.exc import IntegrityError # pragma: no cover |
@@ -4479,9 +4479,9 b' class PullRequest(Base, _PullRequestBase' | |||
|
4479 | 4479 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
4480 | 4480 | return ChangesetStatusModel().calculated_review_status(self) |
|
4481 | 4481 | |
|
4482 | def reviewers_statuses(self): | |
|
4482 | def reviewers_statuses(self, user=None): | |
|
4483 | 4483 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
4484 | return ChangesetStatusModel().reviewers_statuses(self) | |
|
4484 | return ChangesetStatusModel().reviewers_statuses(self, user=user) | |
|
4485 | 4485 | |
|
4486 | 4486 | def get_pull_request_reviewers(self, role=None): |
|
4487 | 4487 | qry = PullRequestReviewers.query()\ |
@@ -56,7 +56,7 b' from rhodecode.model import BaseModel' | |||
|
56 | 56 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
57 | 57 | from rhodecode.model.comment import CommentsModel |
|
58 | 58 | from rhodecode.model.db import ( |
|
59 | or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus, | |
|
59 | aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus, | |
|
60 | 60 | PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User) |
|
61 | 61 | from rhodecode.model.meta import Session |
|
62 | 62 | from rhodecode.model.notification import NotificationModel, \ |
@@ -319,7 +319,7 b' class PullRequestModel(BaseModel):' | |||
|
319 | 319 | |
|
320 | 320 | if search_q: |
|
321 | 321 | like_expression = u'%{}%'.format(safe_unicode(search_q)) |
|
322 | q = q.join(User) | |
|
322 | q = q.join(User, User.user_id == PullRequest.user_id) | |
|
323 | 323 | q = q.filter(or_( |
|
324 | 324 | cast(PullRequest.pull_request_id, String).ilike(like_expression), |
|
325 | 325 | User.username.ilike(like_expression), |
@@ -405,36 +405,30 b' class PullRequestModel(BaseModel):' | |||
|
405 | 405 | |
|
406 | 406 | return pull_requests |
|
407 | 407 | |
|
408 |
def count_awaiting_review(self, repo_name, search_q=None, |
|
|
409 | opened_by=None): | |
|
408 | def count_awaiting_review(self, repo_name, search_q=None, statuses=None): | |
|
410 | 409 | """ |
|
411 | 410 | Count the number of pull requests for a specific repository that are |
|
412 | 411 | awaiting review. |
|
413 | 412 | |
|
414 | 413 | :param repo_name: target or source repo |
|
415 | 414 | :param search_q: filter by text |
|
416 | :param source: boolean flag to specify if repo_name refers to source | |
|
417 | 415 | :param statuses: list of pull request statuses |
|
418 | :param opened_by: author user of the pull request | |
|
419 | 416 | :returns: int number of pull requests |
|
420 | 417 | """ |
|
421 | 418 | pull_requests = self.get_awaiting_review( |
|
422 |
repo_name, search_q=search_q, |
|
|
419 | repo_name, search_q=search_q, statuses=statuses) | |
|
423 | 420 | |
|
424 | 421 | return len(pull_requests) |
|
425 | 422 | |
|
426 |
def get_awaiting_review(self, repo_name, search_q=None, |
|
|
427 |
|
|
|
428 | order_by=None, order_dir='desc'): | |
|
423 | def get_awaiting_review(self, repo_name, search_q=None, statuses=None, | |
|
424 | offset=0, length=None, order_by=None, order_dir='desc'): | |
|
429 | 425 | """ |
|
430 | 426 | Get all pull requests for a specific repository that are awaiting |
|
431 | 427 | review. |
|
432 | 428 | |
|
433 | 429 | :param repo_name: target or source repo |
|
434 | 430 | :param search_q: filter by text |
|
435 | :param source: boolean flag to specify if repo_name refers to source | |
|
436 | 431 | :param statuses: list of pull request statuses |
|
437 | :param opened_by: author user of the pull request | |
|
438 | 432 | :param offset: pagination offset |
|
439 | 433 | :param length: length of returned list |
|
440 | 434 | :param order_by: order of the returned list |
@@ -442,8 +436,8 b' class PullRequestModel(BaseModel):' | |||
|
442 | 436 | :returns: list of pull requests |
|
443 | 437 | """ |
|
444 | 438 | pull_requests = self.get_all( |
|
445 |
repo_name, search_q=search_q, |
|
|
446 |
|
|
|
439 | repo_name, search_q=search_q, statuses=statuses, | |
|
440 | order_by=order_by, order_dir=order_dir) | |
|
447 | 441 | |
|
448 | 442 | _filtered_pull_requests = [] |
|
449 | 443 | for pr in pull_requests: |
@@ -456,68 +450,117 b' class PullRequestModel(BaseModel):' | |||
|
456 | 450 | else: |
|
457 | 451 | return _filtered_pull_requests |
|
458 | 452 | |
|
459 | def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None, | |
|
460 | opened_by=None, user_id=None): | |
|
453 | def _prepare_awaiting_my_review_review_query( | |
|
454 | self, repo_name, user_id, search_q=None, statuses=None, | |
|
455 | order_by=None, order_dir='desc'): | |
|
456 | ||
|
457 | for_review_statuses = [ | |
|
458 | ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED | |
|
459 | ] | |
|
460 | ||
|
461 | pull_request_alias = aliased(PullRequest) | |
|
462 | status_alias = aliased(ChangesetStatus) | |
|
463 | reviewers_alias = aliased(PullRequestReviewers) | |
|
464 | repo_alias = aliased(Repository) | |
|
465 | ||
|
466 | last_ver_subq = Session()\ | |
|
467 | .query(func.min(ChangesetStatus.version)) \ | |
|
468 | .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\ | |
|
469 | .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \ | |
|
470 | .subquery() | |
|
471 | ||
|
472 | q = Session().query(pull_request_alias) \ | |
|
473 | .options(lazyload(pull_request_alias.author)) \ | |
|
474 | .join(reviewers_alias, | |
|
475 | reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \ | |
|
476 | .join(repo_alias, | |
|
477 | repo_alias.repo_id == pull_request_alias.target_repo_id) \ | |
|
478 | .outerjoin(status_alias, | |
|
479 | and_(status_alias.user_id == reviewers_alias.user_id, | |
|
480 | status_alias.pull_request_id == reviewers_alias.pull_request_id)) \ | |
|
481 | .filter(or_(status_alias.version == null(), | |
|
482 | status_alias.version == last_ver_subq)) \ | |
|
483 | .filter(reviewers_alias.user_id == user_id) \ | |
|
484 | .filter(repo_alias.repo_name == repo_name) \ | |
|
485 | .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \ | |
|
486 | .group_by(pull_request_alias) | |
|
487 | ||
|
488 | # closed,opened | |
|
489 | if statuses: | |
|
490 | q = q.filter(pull_request_alias.status.in_(statuses)) | |
|
491 | ||
|
492 | if search_q: | |
|
493 | like_expression = u'%{}%'.format(safe_unicode(search_q)) | |
|
494 | q = q.join(User, User.user_id == pull_request_alias.user_id) | |
|
495 | q = q.filter(or_( | |
|
496 | cast(pull_request_alias.pull_request_id, String).ilike(like_expression), | |
|
497 | User.username.ilike(like_expression), | |
|
498 | pull_request_alias.title.ilike(like_expression), | |
|
499 | pull_request_alias.description.ilike(like_expression), | |
|
500 | )) | |
|
501 | ||
|
502 | if order_by: | |
|
503 | order_map = { | |
|
504 | 'name_raw': pull_request_alias.pull_request_id, | |
|
505 | 'title': pull_request_alias.title, | |
|
506 | 'updated_on_raw': pull_request_alias.updated_on, | |
|
507 | 'target_repo': pull_request_alias.target_repo_id | |
|
508 | } | |
|
509 | if order_dir == 'asc': | |
|
510 | q = q.order_by(order_map[order_by].asc()) | |
|
511 | else: | |
|
512 | q = q.order_by(order_map[order_by].desc()) | |
|
513 | ||
|
514 | return q | |
|
515 | ||
|
516 | def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None): | |
|
461 | 517 | """ |
|
462 | 518 | Count the number of pull requests for a specific repository that are |
|
463 | 519 | awaiting review from a specific user. |
|
464 | 520 | |
|
465 | 521 | :param repo_name: target or source repo |
|
522 | :param user_id: reviewer user of the pull request | |
|
466 | 523 | :param search_q: filter by text |
|
467 | :param source: boolean flag to specify if repo_name refers to source | |
|
468 | 524 | :param statuses: list of pull request statuses |
|
469 | :param opened_by: author user of the pull request | |
|
470 | :param user_id: reviewer user of the pull request | |
|
471 | 525 | :returns: int number of pull requests |
|
472 | 526 | """ |
|
473 |
|
|
|
474 |
repo_name, search_q=search_q, |
|
|
475 | opened_by=opened_by, user_id=user_id) | |
|
527 | q = self._prepare_awaiting_my_review_review_query( | |
|
528 | repo_name, user_id, search_q=search_q, statuses=statuses) | |
|
529 | return q.count() | |
|
476 | 530 | |
|
477 | return len(pull_requests) | |
|
478 | ||
|
479 | def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None, | |
|
480 | opened_by=None, user_id=None, offset=0, | |
|
481 | length=None, order_by=None, order_dir='desc'): | |
|
531 | def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None, | |
|
532 | offset=0, length=None, order_by=None, order_dir='desc'): | |
|
482 | 533 | """ |
|
483 | 534 | Get all pull requests for a specific repository that are awaiting |
|
484 | 535 | review from a specific user. |
|
485 | 536 | |
|
486 | 537 | :param repo_name: target or source repo |
|
538 | :param user_id: reviewer user of the pull request | |
|
487 | 539 | :param search_q: filter by text |
|
488 | :param source: boolean flag to specify if repo_name refers to source | |
|
489 | 540 | :param statuses: list of pull request statuses |
|
490 | :param opened_by: author user of the pull request | |
|
491 | :param user_id: reviewer user of the pull request | |
|
492 | 541 | :param offset: pagination offset |
|
493 | 542 | :param length: length of returned list |
|
494 | 543 | :param order_by: order of the returned list |
|
495 | 544 | :param order_dir: 'asc' or 'desc' ordering direction |
|
496 | 545 | :returns: list of pull requests |
|
497 | 546 | """ |
|
498 | pull_requests = self.get_all( | |
|
499 | repo_name, search_q=search_q, source=source, statuses=statuses, | |
|
500 | opened_by=opened_by, order_by=order_by, order_dir=order_dir) | |
|
501 | 547 | |
|
502 | _my = PullRequestModel().get_not_reviewed(user_id) | |
|
503 | my_participation = [] | |
|
504 | for pr in pull_requests: | |
|
505 | if pr in _my: | |
|
506 | my_participation.append(pr) | |
|
507 | _filtered_pull_requests = my_participation | |
|
548 | q = self._prepare_awaiting_my_review_review_query( | |
|
549 | repo_name, user_id, search_q=search_q, statuses=statuses, | |
|
550 | order_by=order_by, order_dir=order_dir) | |
|
551 | ||
|
508 | 552 | if length: |
|
509 | return _filtered_pull_requests[offset:offset+length] | |
|
553 | pull_requests = q.limit(length).offset(offset).all() | |
|
510 | 554 | else: |
|
511 |
|
|
|
555 | pull_requests = q.all() | |
|
556 | ||
|
557 | return pull_requests | |
|
512 | 558 | |
|
513 | def get_not_reviewed(self, user_id): | |
|
514 | return [ | |
|
515 | x.pull_request for x in PullRequestReviewers.query().filter( | |
|
516 | PullRequestReviewers.user_id == user_id).all() | |
|
517 | ] | |
|
518 | ||
|
519 | def _prepare_participating_query(self, user_id=None, statuses=None, query='', | |
|
559 | def _prepare_im_participating_query(self, user_id=None, statuses=None, query='', | |
|
520 | 560 | order_by=None, order_dir='desc'): |
|
561 | """ | |
|
562 | return a query of pull-requests user is an creator, or he's added as a reviewer | |
|
563 | """ | |
|
521 | 564 | q = PullRequest.query() |
|
522 | 565 | if user_id: |
|
523 | 566 | reviewers_subquery = Session().query( |
@@ -535,7 +578,7 b' class PullRequestModel(BaseModel):' | |||
|
535 | 578 | |
|
536 | 579 | if query: |
|
537 | 580 | like_expression = u'%{}%'.format(safe_unicode(query)) |
|
538 | q = q.join(User) | |
|
581 | q = q.join(User, User.user_id == PullRequest.user_id) | |
|
539 | 582 | q = q.filter(or_( |
|
540 | 583 | cast(PullRequest.pull_request_id, String).ilike(like_expression), |
|
541 | 584 | User.username.ilike(like_expression), |
@@ -557,17 +600,97 b' class PullRequestModel(BaseModel):' | |||
|
557 | 600 | return q |
|
558 | 601 | |
|
559 | 602 | def count_im_participating_in(self, user_id=None, statuses=None, query=''): |
|
560 | q = self._prepare_participating_query(user_id, statuses=statuses, query=query) | |
|
603 | q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query) | |
|
561 | 604 | return q.count() |
|
562 | 605 | |
|
563 | 606 | def get_im_participating_in( |
|
564 | 607 | self, user_id=None, statuses=None, query='', offset=0, |
|
565 | 608 | length=None, order_by=None, order_dir='desc'): |
|
566 | 609 | """ |
|
567 | Get all Pull requests that i'm participating in, or i have opened | |
|
610 | Get all Pull requests that i'm participating in as a reviewer, or i have opened | |
|
568 | 611 | """ |
|
569 | 612 | |
|
570 | q = self._prepare_participating_query( | |
|
613 | q = self._prepare_im_participating_query( | |
|
614 | user_id, statuses=statuses, query=query, order_by=order_by, | |
|
615 | order_dir=order_dir) | |
|
616 | ||
|
617 | if length: | |
|
618 | pull_requests = q.limit(length).offset(offset).all() | |
|
619 | else: | |
|
620 | pull_requests = q.all() | |
|
621 | ||
|
622 | return pull_requests | |
|
623 | ||
|
624 | def _prepare_participating_in_for_review_query( | |
|
625 | self, user_id, statuses=None, query='', order_by=None, order_dir='desc'): | |
|
626 | ||
|
627 | for_review_statuses = [ | |
|
628 | ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED | |
|
629 | ] | |
|
630 | ||
|
631 | pull_request_alias = aliased(PullRequest) | |
|
632 | status_alias = aliased(ChangesetStatus) | |
|
633 | reviewers_alias = aliased(PullRequestReviewers) | |
|
634 | ||
|
635 | last_ver_subq = Session()\ | |
|
636 | .query(func.min(ChangesetStatus.version)) \ | |
|
637 | .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\ | |
|
638 | .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \ | |
|
639 | .subquery() | |
|
640 | ||
|
641 | q = Session().query(pull_request_alias) \ | |
|
642 | .options(lazyload(pull_request_alias.author)) \ | |
|
643 | .join(reviewers_alias, | |
|
644 | reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \ | |
|
645 | .outerjoin(status_alias, | |
|
646 | and_(status_alias.user_id == reviewers_alias.user_id, | |
|
647 | status_alias.pull_request_id == reviewers_alias.pull_request_id)) \ | |
|
648 | .filter(or_(status_alias.version == null(), | |
|
649 | status_alias.version == last_ver_subq)) \ | |
|
650 | .filter(reviewers_alias.user_id == user_id) \ | |
|
651 | .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \ | |
|
652 | .group_by(pull_request_alias) | |
|
653 | ||
|
654 | # closed,opened | |
|
655 | if statuses: | |
|
656 | q = q.filter(pull_request_alias.status.in_(statuses)) | |
|
657 | ||
|
658 | if query: | |
|
659 | like_expression = u'%{}%'.format(safe_unicode(query)) | |
|
660 | q = q.join(User, User.user_id == pull_request_alias.user_id) | |
|
661 | q = q.filter(or_( | |
|
662 | cast(pull_request_alias.pull_request_id, String).ilike(like_expression), | |
|
663 | User.username.ilike(like_expression), | |
|
664 | pull_request_alias.title.ilike(like_expression), | |
|
665 | pull_request_alias.description.ilike(like_expression), | |
|
666 | )) | |
|
667 | ||
|
668 | if order_by: | |
|
669 | order_map = { | |
|
670 | 'name_raw': pull_request_alias.pull_request_id, | |
|
671 | 'title': pull_request_alias.title, | |
|
672 | 'updated_on_raw': pull_request_alias.updated_on, | |
|
673 | 'target_repo': pull_request_alias.target_repo_id | |
|
674 | } | |
|
675 | if order_dir == 'asc': | |
|
676 | q = q.order_by(order_map[order_by].asc()) | |
|
677 | else: | |
|
678 | q = q.order_by(order_map[order_by].desc()) | |
|
679 | ||
|
680 | return q | |
|
681 | ||
|
682 | def count_im_participating_in_for_review(self, user_id, statuses=None, query=''): | |
|
683 | q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query) | |
|
684 | return q.count() | |
|
685 | ||
|
686 | def get_im_participating_in_for_review( | |
|
687 | self, user_id, statuses=None, query='', offset=0, | |
|
688 | length=None, order_by=None, order_dir='desc'): | |
|
689 | """ | |
|
690 | Get all Pull requests that needs user approval or rejection | |
|
691 | """ | |
|
692 | ||
|
693 | q = self._prepare_participating_in_for_review_query( | |
|
571 | 694 | user_id, statuses=statuses, query=query, order_by=order_by, |
|
572 | 695 | order_dir=order_dir) |
|
573 | 696 |
@@ -618,13 +618,13 b' input[type="reset"] {' | |||
|
618 | 618 | text-align: right; |
|
619 | 619 | |
|
620 | 620 | li { |
|
621 | ||
|
622 | ||
|
621 | list-style: none; | |
|
622 | text-align: right; | |
|
623 | display: inline-block; | |
|
623 | 624 | } |
|
624 | 625 | |
|
625 |
|
|
|
626 | background-color: @grey6; | |
|
627 | .border ( @border-thickness, @grey4 ); | |
|
626 | a.active { | |
|
627 | border: 2px solid @rcblue; | |
|
628 | 628 | } |
|
629 | 629 | |
|
630 | 630 | } |
@@ -44,7 +44,7 b'' | |||
|
44 | 44 | padding: @panel-padding; |
|
45 | 45 | |
|
46 | 46 | &.panel-body-min-height { |
|
47 |
min-height: |
|
|
47 | min-height: 200px | |
|
48 | 48 | } |
|
49 | 49 | } |
|
50 | 50 |
@@ -1,17 +1,29 b'' | |||
|
1 | 1 | <%namespace name="base" file="/base/base.mako"/> |
|
2 | 2 | |
|
3 | 3 | <div class="panel panel-default"> |
|
4 |
<div class="panel- |
|
|
5 | <div style="height: 35px"> | |
|
6 | <% | |
|
7 | selected_filter = 'all' | |
|
8 | if c.closed: | |
|
9 | selected_filter = 'all_closed' | |
|
10 | %> | |
|
4 | <div class="panel-heading"> | |
|
5 | <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3> | |
|
6 | </div> | |
|
11 | 7 | |
|
8 | <div class="panel-body panel-body-min-height"> | |
|
9 | <div class="title"> | |
|
12 | 10 | <ul class="button-links"> |
|
13 |
<li class="btn ${h.is_active('all', selected_filter)}" |
|
|
14 | <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li> | |
|
11 | <li><a class="btn ${h.is_active('all', c.selected_filter)}" | |
|
12 | href="${h.route_path('my_account_pullrequests', _query={})}"> | |
|
13 | ${_('Open')} | |
|
14 | </a> | |
|
15 | </li> | |
|
16 | <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}" | |
|
17 | href="${h.route_path('my_account_pullrequests', _query={'closed':1})}"> | |
|
18 | ${_('All + Closed')} | |
|
19 | </a> | |
|
20 | </li> | |
|
21 | <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}" | |
|
22 | href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}"> | |
|
23 | ||
|
24 | ${_('Awaiting my review')} | |
|
25 | </a> | |
|
26 | </li> | |
|
15 | 27 | </ul> |
|
16 | 28 | |
|
17 | 29 | <div class="grid-quick-filter"> |
@@ -20,19 +32,13 b'' | |||
|
20 | 32 | <i class="icon-search"></i> |
|
21 | 33 | </li> |
|
22 | 34 | <li class="grid-filter-box-input"> |
|
23 |
<input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" |
|
|
35 | <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" | |
|
36 | placeholder="${_('quick filter...')}" value=""/> | |
|
24 | 37 | </li> |
|
25 | 38 | </ul> |
|
26 | 39 | </div> |
|
27 | 40 | </div> |
|
28 | </div> | |
|
29 | </div> | |
|
30 | 41 | |
|
31 | <div class="panel panel-default"> | |
|
32 | <div class="panel-heading"> | |
|
33 | <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3> | |
|
34 | </div> | |
|
35 | <div class="panel-body panel-body-min-height"> | |
|
36 | 42 | <table id="pull_request_list_table" class="rctable table-bordered"></table> |
|
37 | 43 | </div> |
|
38 | 44 | </div> |
@@ -52,6 +58,7 b'' | |||
|
52 | 58 | "url": "${h.route_path('my_account_pullrequests_data')}", |
|
53 | 59 | "data": function (d) { |
|
54 | 60 | d.closed = "${c.closed}"; |
|
61 | d.awaiting_my_review = "${c.awaiting_my_review}"; | |
|
55 | 62 | }, |
|
56 | 63 | "dataSrc": function (json) { |
|
57 | 64 | return json.data; |
@@ -60,13 +67,19 b'' | |||
|
60 | 67 | |
|
61 | 68 | dom: 'rtp', |
|
62 | 69 | pageLength: ${c.visual.dashboard_items}, |
|
63 |
order: [[ |
|
|
70 | order: [[2, "desc"]], | |
|
64 | 71 | columns: [ |
|
65 | 72 | { |
|
66 | 73 | data: { |
|
67 | 74 | "_": "status", |
|
68 | 75 | "sort": "status" |
|
69 | }, title: "", className: "td-status", orderable: false | |
|
76 | }, title: "PR", className: "td-status", orderable: false | |
|
77 | }, | |
|
78 | { | |
|
79 | data: { | |
|
80 | "_": "my_status", | |
|
81 | "sort": "status" | |
|
82 | }, title: "You", className: "td-status", orderable: false | |
|
70 | 83 | }, |
|
71 | 84 | { |
|
72 | 85 | data: { |
@@ -23,13 +23,14 b'' | |||
|
23 | 23 | |
|
24 | 24 | <div class="box"> |
|
25 | 25 | <div class="title"> |
|
26 | ||
|
26 | 27 | <ul class="button-links"> |
|
27 |
<li class="btn ${h.is_active('open', c.active)}" |
|
|
28 |
<li class="btn ${h.is_active('my', c.active)}" |
|
|
29 |
<li class="btn ${h.is_active('awaiting', c.active)}" |
|
|
30 |
<li class="btn ${h.is_active('awaiting_my', c.active)}" |
|
|
31 |
<li class="btn ${h.is_active('closed', c.active)}" |
|
|
32 |
<li class="btn ${h.is_active('source', c.active)}" |
|
|
28 | <li><a class="btn ${h.is_active('open', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'open':1})}">${_('Open')}</a></li> | |
|
29 | <li><a class="btn ${h.is_active('my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Created by me')}</a></li> | |
|
30 | <li><a class="btn ${h.is_active('awaiting', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li> | |
|
31 | <li><a class="btn ${h.is_active('awaiting_my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li> | |
|
32 | <li><a class="btn ${h.is_active('closed', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li> | |
|
33 | <li><a class="btn ${h.is_active('source', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li> | |
|
33 | 34 | </ul> |
|
34 | 35 | |
|
35 | 36 | <ul class="links"> |
@@ -88,20 +89,50 b'' | |||
|
88 | 89 | }, |
|
89 | 90 | dom: 'rtp', |
|
90 | 91 | pageLength: ${c.visual.dashboard_items}, |
|
91 |
order: [[ |
|
|
92 | order: [[ 2, "desc" ]], | |
|
92 | 93 | columns: [ |
|
93 | { data: {"_": "status", | |
|
94 | "sort": "status"}, title: "", className: "td-status", orderable: false}, | |
|
95 |
|
|
|
96 | "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" }, | |
|
97 | { data: {"_": "title", | |
|
98 | "sort": "title"}, title: "${_('Title')}", className: "td-description" }, | |
|
99 | { data: {"_": "author", | |
|
100 | "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false }, | |
|
101 |
|
|
|
102 | "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false}, | |
|
103 | { data: {"_": "updated_on", | |
|
104 | "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" } | |
|
94 | { | |
|
95 | data: { | |
|
96 | "_": "status", | |
|
97 | "sort": "status" | |
|
98 | }, title: "PR", className: "td-status", orderable: false | |
|
99 | }, | |
|
100 | { | |
|
101 | data: { | |
|
102 | "_": "my_status", | |
|
103 | "sort": "status" | |
|
104 | }, title: "You", className: "td-status", orderable: false | |
|
105 | }, | |
|
106 | { | |
|
107 | data: { | |
|
108 | "_": "name", | |
|
109 | "sort": "name_raw" | |
|
110 | }, title: "${_('Id')}", className: "td-componentname", "type": "num" | |
|
111 | }, | |
|
112 | { | |
|
113 | data: { | |
|
114 | "_": "title", | |
|
115 | "sort": "title" | |
|
116 | }, title: "${_('Title')}", className: "td-description" | |
|
117 | }, | |
|
118 | { | |
|
119 | data: { | |
|
120 | "_": "author", | |
|
121 | "sort": "author_raw" | |
|
122 | }, title: "${_('Author')}", className: "td-user", orderable: false | |
|
123 | }, | |
|
124 | { | |
|
125 | data: { | |
|
126 | "_": "comments", | |
|
127 | "sort": "comments_raw" | |
|
128 | }, title: "", className: "td-comments", orderable: false | |
|
129 | }, | |
|
130 | { | |
|
131 | data: { | |
|
132 | "_": "updated_on", | |
|
133 | "sort": "updated_on_raw" | |
|
134 | }, title: "${_('Last Update')}", className: "td-time" | |
|
135 | } | |
|
105 | 136 | ], |
|
106 | 137 | language: { |
|
107 | 138 | paginate: DEFAULT_GRID_PAGINATION, |
General Comments 1
You need to be logged in to leave comments.
Login now