new file mode 100644
--- /dev/null
+++ b/hgext/reviewboard/tests/test-review-request-delegation.t
@@ -0,0 +1,333 @@
+#require mozreviewdocker
+
+ $ . $TESTDIR/hgext/reviewboard/tests/helpers.sh
+ $ commonenv
+
+ $ cd client
+ $ echo foo0 > foo
+ $ hg -q commit -A -m 'root commit'
+ $ hg phase --public -r .
+
+Set up users
+
+ $ mozreview create-user l3author@example.com password 'L3 Contributor [:l3author]' --scm-level 3 --uid 2004 --key-file "$MOZREVIEW_HOME/keys/l3author@example.com"
+ Created user 6
+ $ mozreview create-user reviewer1@example.com password 'Reviewer 1 [:reviewer1]' --scm-level 3 --bugzilla-group editbugs --uid 2005 --key-file "$MOZREVIEW_HOME/keys/reviewer1@example.com"
+ Created user 7
+ $ mozreview create-user reviewer2@example.com password 'Reviewer 2 [:reviewer2]' --scm-level 3 --uid 2006 --key-file "$MOZREVIEW_HOME/keys/reviewer2@example.com"
+ Created user 8
+
+Create bug and review
+
+ $ l3key=`mozreview create-api-key l3author@example.com`
+ $ exportbzauth l3author@example.com password
+ $ bugzilla create-bug TestProduct TestComponent 'First Bug'
+ $ echo fruit > foo
+ $ hg commit -m 'Bug 1 - Initial commit to review r?reviewer1'
+ $ echo water >> foo
+ $ hg commit -m 'Bug 1 - Forgot water r?reviewer1'
+ $ hg --config bugzilla.username=l3author@example.com --config bugzilla.apikey=$l3key --config reviewboard.autopublish=true push
+ pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+ (adding commit id to 2 changesets)
+ saved backup bundle to $TESTTMP/client/.hg/strip-backup/*-addcommitid.hg (glob)
+ searching for changes
+ remote: adding changesets
+ remote: adding manifests
+ remote: adding file changes
+ remote: added 3 changesets with 3 changes to 1 files
+ remote: recorded push in pushlog
+ submitting 2 changesets for review
+
+ changeset: 1:80ffd9136b8b
+ summary: Bug 1 - Initial commit to review r?reviewer1
+ review: http://$DOCKER_HOSTNAME:$HGPORT1/r/2 (draft)
+
+ changeset: 2:493559840037
+ summary: Bug 1 - Forgot water r?reviewer1
+ review: http://$DOCKER_HOSTNAME:$HGPORT1/r/3 (draft)
+
+ review id: bz://1/mynick
+ review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
+ (published review request 1)
+
+Change the reviewer while logged in as reviewer1
+
+ $ exportbzauth reviewer1@example.com password
+ $ rbmanage modify-reviewers 1 2 'reviewer2'
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer2
+ reviewers_status:
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+
+Test multiple reviewers
+
+ $ rbmanage modify-reviewers 1 2 'reviewer1,reviewer2'
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+
+Test invalid reviewer
+
+ $ rbmanage modify-reviewers 1 2 'invalid'
+ API Error: 400: 105: The reviewer 'invalid' was not found
+ [1]
+
+Change the reviewer while logged in as the submitter
+
+ $ exportbzauth l3author@example.com password
+ $ rbmanage modify-reviewers 1 2 'reviewer2'
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer2
+ reviewers_status:
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+
+Test user without editbugs
+
+ $ exportbzauth reviewer2@example.com password
+ $ rbmanage modify-reviewers 1 2 'reviewer1'
+ API Error: 500: 225: Error publishing: Bugzilla error: You are not authorized to edit attachment 1.
+ [1]
+
+Test verify-reviewers
+
+ $ rbmanage verify-reviewers 'reviewer1'
+ $ rbmanage verify-reviewers 'reviewer1,reviewer2'
+ $ rbmanage verify-reviewers 'invalid'
+ API Error: 400: 105: The reviewer 'invalid' was not found
+ [1]
+ $ rbmanage verify-reviewers 'reviewer1,invalid'
+ API Error: 400: 105: The reviewer 'invalid' was not found
+ [1]
+
+Test ensure-drafts
+
+ $ exportbzauth l3author@example.com password
+ $ rbmanage add-reviewer 2 --user reviewer1
+ 2 people listed on review request
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: true
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: true
+ reviewers:
+ - reviewer2
+ reviewers_status:
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+ $ rbmanage ensure-drafts 1
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: true
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: true
+ reviewers:
+ - reviewer2
+ reviewers_status:
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: true
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+ $ rbmanage publish 1
+ $ rbmanage dump-summary 1
+ parent:
+ summary: bz://1/mynick
+ id: 1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ children:
+ - summary: Bug 1 - Initial commit to review r?reviewer1
+ id: 2
+ commit: 80ffd9136b8b9d9541de1780e1a3e027665017fb
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ - reviewer2
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
+ reviewer2:
+ review_flag: r?
+ ship_it: false
+ - summary: Bug 1 - Forgot water r?reviewer1
+ id: 3
+ commit: 4935598400374354824ffde84a8b6767823100d1
+ submitter: l3author
+ issue_open_count: 0
+ status: pending
+ has_draft: false
+ reviewers:
+ - reviewer1
+ reviewers_status:
+ reviewer1:
+ review_flag: r?
+ ship_it: false
--- a/pylib/mozreview/mozreview/bugzilla/client.py
+++ b/pylib/mozreview/mozreview/bugzilla/client.py
@@ -3,20 +3,22 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
import posixpath
import xmlrpclib
from urlparse import urlparse, urlunparse
+from django.contrib.auth.models import User
from djblets.siteconfig.models import SiteConfiguration
from djblets.util.decorators import simple_decorator
from mozreview.bugzilla.errors import BugzillaError, BugzillaUrlError
+from mozreview.bugzilla.models import get_or_create_bugzilla_users
from mozreview.bugzilla.transports import bugzilla_transport
from mozautomation.commitparser import replace_reviewers
logger = logging.getLogger(__name__)
@@ -335,16 +337,25 @@ class Bugzilla(object):
"""Convert an integer user ID to string username."""
params = self._auth_params({
'ids': [userid],
'include_fields': self.user_fields
})
return self.proxy.User.get(params)
@xmlrpc_to_bugzilla_errors
+ def get_user_from_irc_nick(self, irc_nick):
+ irc_nick = irc_nick.lstrip(":").lower()
+ users = get_or_create_bugzilla_users(self.query_users(":" + irc_nick))
+ for user in users:
+ if user.username.lower() == irc_nick:
+ return user
+ raise User.DoesNotExist()
+
+ @xmlrpc_to_bugzilla_errors
def post_comment(self, bug_id, comment):
params = self._auth_params({
'id': bug_id,
'comment': comment
})
logger.info('Posting comment on bug %d.' % bug_id)
return self.proxy.Bug.add_comment(params)
--- a/pylib/mozreview/mozreview/errors.py
+++ b/pylib/mozreview/mozreview/errors.py
@@ -77,8 +77,13 @@ AUTOLAND_REQUEST_IN_PROGRESS = WebAPIErr
"An autoland request for this review request is already in progress. "
"Please wait for that request to finish.",
http_status=405) # 405 Method Not Allowed
AUTOLAND_REVIEW_NOT_APPROVED = WebAPIError(
1008,
"Unable to continue as the review has not been approved.",
http_status=405) # 405 Method Not Allowed
+
+REVIEW_REQUEST_UPDATE_NOT_ALLOWED = WebAPIError(
+ 1009,
+ "Updating review request not allowed at this time.",
+ http_status=405) # 405 Method not Allowed
--- a/pylib/mozreview/mozreview/extension.py
+++ b/pylib/mozreview/mozreview/extension.py
@@ -80,19 +80,28 @@ from mozreview.resources.batch_review_re
batch_review_request_resource,
)
from mozreview.resources.commit_data import (
commit_data_resource,
)
from mozreview.resources.commit_rewrite import (
commit_rewrite_resource,
)
+from mozreview.resources.ensure_drafts import (
+ ensure_drafts_resource,
+)
+from mozreview.resources.modify_reviewer import (
+ modify_reviewer_resource,
+)
from mozreview.resources.review_request_summary import (
review_request_summary_resource,
)
+from mozreview.resources.verify_reviewer import (
+ verify_reviewer_resource,
+)
from mozreview.signal_handlers import (
initialize_signal_handlers,
)
SETTINGS_PATH = os.path.join('/', 'mozreview-settings.json')
SETTINGS = None
@@ -172,20 +181,23 @@ class MozReviewExtension(Extension):
autoland_enable_resource,
autoland_request_update_resource,
autoland_trigger_resource,
batch_review_request_resource,
batch_review_resource,
bugzilla_api_key_login_resource,
commit_data_resource,
commit_rewrite_resource,
+ ensure_drafts_resource,
file_diff_reviewer_resource,
ldap_association_resource,
+ modify_reviewer_resource,
review_request_summary_resource,
try_autoland_trigger_resource,
+ verify_reviewer_resource,
]
middleware = [
MozReviewCacheDisableMiddleware,
MozReviewUserProfileMiddleware,
]
def initialize(self):
@@ -275,17 +287,17 @@ class MozReviewExtension(Extension):
TemplateHook(self, 'base-extrahead',
'mozreview/base-extrahead-login-form.html',
apply_to=['login'])
TemplateHook(self, 'before-login-form',
'mozreview/before-login-form.html', apply_to=['login'])
TemplateHook(self, 'after-login-form',
'mozreview/after-login-form.html', apply_to=['login'])
TemplateHook(self, 'base-after-content',
- 'mozreview/scm_level.html')
+ 'mozreview/user-data.html')
TemplateHook(self, 'base-after-content',
'mozreview/repository.html')
TemplateHook(self, 'base-after-content',
'mozreview/user_review_flag.html',
apply_to=review_request_url_names)
ReviewRequestFieldsHook(self, 'main', [CommitsListField])
# This forces the Commits field to be the top item.
--- a/pylib/mozreview/mozreview/extra_data.py
+++ b/pylib/mozreview/mozreview/extra_data.py
@@ -27,16 +27,17 @@ AUTHOR_KEY = MOZREVIEW_KEY + '.author'
BASE_COMMIT_KEY = MOZREVIEW_KEY + '.base_commit'
COMMIT_ID_KEY = MOZREVIEW_KEY + '.commit_id'
COMMITS_KEY = MOZREVIEW_KEY + '.commits'
DISCARD_ON_PUBLISH_KEY = MOZREVIEW_KEY + '.discard_on_publish_rids'
FIRST_PUBLIC_ANCESTOR_KEY = MOZREVIEW_KEY + '.first_public_ancestor'
IDENTIFIER_KEY = MOZREVIEW_KEY + '.identifier'
SQUASHED_KEY = MOZREVIEW_KEY + '.is_squashed'
UNPUBLISHED_KEY = MOZREVIEW_KEY + '.unpublished_rids'
+PUBLISH_AS_KEY = MOZREVIEW_KEY + '.publish_as'
# CommitData fields which should be automatically copied from
# draft_extra_data to extra_data when a review request is published.
DRAFTED_COMMIT_DATA_KEYS = (
AUTHOR_KEY,
FIRST_PUBLIC_ANCESTOR_KEY,
IDENTIFIER_KEY,
COMMIT_ID_KEY,
@@ -202,8 +203,29 @@ def update_parent_rr_reviewers(parent_rr
parent_rr_draft.target_people = total_reviewers
parent_rr_draft.extra_data[REVIEWER_MAP_KEY] = json.dumps(reviewers_map_after)
parent_rr = parent_rr_draft.get_review_request()
if parent_rr.public and parent_rr_draft.changedesc is None:
parent_rr_draft.changedesc = ChangeDescription.objects.create()
parent_rr_draft.save()
+
+
+def set_publish_as(parent_rr, user, commit_data=None):
+ """Set PUBLISH_AS to the specified user.
+
+ When the specified review request is published, updates to Bugzilla
+ will be performed as the specified user.
+ """
+ commit_data = fetch_commit_data(parent_rr, commit_data)
+ commit_data.draft_extra_data.update({
+ PUBLISH_AS_KEY: user.id
+ })
+ commit_data.save(update_fields=['draft_extra_data'])
+
+
+def clear_publish_as(parent_rr, commit_data=None):
+ """Clears a previously set PUBLISH_AS."""
+ commit_data = fetch_commit_data(parent_rr, commit_data)
+ if PUBLISH_AS_KEY in commit_data.draft_extra_data:
+ del commit_data.draft_extra_data[PUBLISH_AS_KEY]
+ commit_data.save(update_fields=['draft_extra_data'])
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/resources/ensure_drafts.py
@@ -0,0 +1,88 @@
+from __future__ import unicode_literals
+
+from django.db import (
+ transaction,
+)
+from djblets.webapi.decorators import (
+ webapi_login_required,
+ webapi_request_fields,
+ webapi_response_errors,
+)
+from djblets.webapi.errors import (
+ DOES_NOT_EXIST, INVALID_FORM_DATA,
+ NOT_LOGGED_IN,
+ PERMISSION_DENIED,
+)
+from reviewboard.reviews.models import (
+ ReviewRequest,
+ ReviewRequestDraft,
+)
+from reviewboard.site.urlresolvers import (
+ local_site_reverse,
+)
+from reviewboard.webapi.errors import (
+ PUBLISH_ERROR,
+)
+from reviewboard.webapi.resources import (
+ WebAPIResource,
+)
+
+from mozreview.errors import (
+ NOT_PARENT,
+)
+from mozreview.extra_data import (
+ is_parent,
+ gen_child_rrs,
+)
+
+
+class EnsureDraftsResource(WebAPIResource):
+ """Ensure drafts exist for each child request.
+
+ This causes Review Board to show the draft banner on the parent and all
+ children when any child is updated.
+ """
+
+ name = 'ensure_draft'
+ allowed_methods = ('GET', 'POST',)
+
+ @webapi_login_required
+ @webapi_response_errors(DOES_NOT_EXIST, INVALID_FORM_DATA,
+ PUBLISH_ERROR, NOT_PARENT,
+ NOT_LOGGED_IN, PERMISSION_DENIED)
+ @webapi_request_fields(
+ required={
+ 'parent_request_id': {
+ 'type': int,
+ 'description': 'The parent review request to update',
+ },
+ },
+ )
+ def create(self, request, parent_request_id, *args, **kwargs):
+ try:
+ parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
+ except ReviewRequest.DoesNotExist:
+ return DOES_NOT_EXIST
+
+ if not (parent_rr.is_accessible_by(request.user)
+ or parent_rr.is_mutable_by(request.user)):
+ return PERMISSION_DENIED
+
+ if not is_parent(parent_rr):
+ return NOT_PARENT
+
+ with transaction.atomic():
+ for child_rr in gen_child_rrs(parent_rr):
+ if child_rr.get_draft() is None:
+ ReviewRequestDraft.create(child_rr)
+ if parent_rr.get_draft() is None:
+ ReviewRequestDraft.create(parent_rr)
+
+ return 200, {}
+
+ def get_uri(self, request):
+ named_url = self._build_named_url(self.name_plural)
+ return request.build_absolute_uri(
+ local_site_reverse(named_url, request=request, kwargs={}))
+
+ensure_drafts_resource = EnsureDraftsResource()
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/resources/modify_reviewer.py
@@ -0,0 +1,190 @@
+from __future__ import unicode_literals
+
+import itertools
+import json
+import logging
+
+from django.contrib.auth.models import (
+ User,
+)
+from django.db import (
+ transaction,
+)
+from django.utils import (
+ six,
+)
+from djblets.webapi.decorators import (
+ webapi_login_required,
+ webapi_request_fields,
+ webapi_response_errors,
+)
+from djblets.webapi.errors import (
+ DOES_NOT_EXIST,
+ INVALID_FORM_DATA,
+ NOT_LOGGED_IN,
+ PERMISSION_DENIED,
+)
+from reviewboard.reviews.errors import (
+ PublishError,
+)
+from reviewboard.reviews.models import (
+ ReviewRequest,
+ ReviewRequestDraft,
+)
+from reviewboard.site.urlresolvers import (
+ local_site_reverse,
+)
+from reviewboard.webapi.errors import (
+ PUBLISH_ERROR,
+)
+from reviewboard.webapi.resources import (
+ WebAPIResource,
+)
+
+from mozreview.bugzilla.client import (
+ Bugzilla,
+)
+from mozreview.errors import (
+ NOT_PARENT,
+ REVIEW_REQUEST_UPDATE_NOT_ALLOWED,
+)
+from mozreview.extra_data import (
+ is_parent,
+ gen_child_rrs,
+ set_publish_as,
+ clear_publish_as,
+ update_parent_rr_reviewers,
+)
+from mozreview.models import (
+ get_bugzilla_api_key,
+)
+
+
+class ModifyReviewerResource(WebAPIResource):
+ """Resource to modify the reviewers for a particular review request.
+
+ We require a separate resource to handle this so we can allow
+ anyone with permissions in bugzilla to modify the request.
+
+ The reviewers JSON is in the form of:
+ {
+ child_rrid: [ 'reviewer1', 'reviewer2', ... ],
+ ...
+ }
+ eg. {"5":["level1"]} updates rrid 5, clearing all existing reviewers then
+ setting the reviewers to the "level1" user.
+ """
+
+ name = 'modify_reviewer'
+ allowed_methods = ('GET', 'POST',)
+
+ @webapi_login_required
+ @webapi_response_errors(DOES_NOT_EXIST, INVALID_FORM_DATA,
+ PUBLISH_ERROR, NOT_PARENT,
+ NOT_LOGGED_IN, PERMISSION_DENIED)
+ @webapi_request_fields(
+ required={
+ 'parent_request_id': {
+ 'type': int,
+ 'description': 'The parent review request to update',
+ },
+ 'reviewers': {
+ 'type': six.text_type,
+ 'description': 'A JSON string contining the new reviewers'
+ },
+ },
+ )
+ def create(self, request, parent_request_id, reviewers, *args, **kwargs):
+ try:
+ parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
+ except ReviewRequest.DoesNotExist:
+ return DOES_NOT_EXIST
+
+ if not (parent_rr.is_accessible_by(request.user)
+ or parent_rr.is_mutable_by(request.user)):
+ return PERMISSION_DENIED
+
+ if not is_parent(parent_rr):
+ return NOT_PARENT
+
+ # Validate and expand the new reviewer list.
+
+ bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
+ child_reviewers = json.loads(reviewers)
+ invalid_reviewers = []
+ for child_rrid in child_reviewers:
+ users = []
+ for username in child_reviewers[child_rrid]:
+ try:
+ users.append(bugzilla.get_user_from_irc_nick(username))
+ except User.DoesNotExist:
+ invalid_reviewers.append(username)
+ child_reviewers[child_rrid] = users
+
+ if invalid_reviewers:
+ # Because this isn't called through Review Board's built-in
+ # backbone system, it's dramatically simpler to return just the
+ # intended error message instead of categorising the errors by
+ # field.
+ if len(invalid_reviewers) == 1:
+ return INVALID_FORM_DATA.with_message(
+ "The reviewer '%s' was not found" % invalid_reviewers[0])
+ else:
+ return INVALID_FORM_DATA.with_message(
+ "The reviewers '%s' were not found"
+ % "', '".join(invalid_reviewers))
+
+ # Review Board only supports the submitter updating a review
+ # request. In order for this to work, we publish these changes
+ # in Review Board under the review submitter's account, and
+ # set an extra_data field which instructs our bugzilla
+ # connector to use this request's user when adjusting flags.
+ #
+ # Updating the review request requires creating a draft and
+ # publishing it, so we have to be careful to not overwrite
+ # existing drafts.
+
+ try:
+ with transaction.atomic():
+ for rr in itertools.chain([parent_rr],
+ gen_child_rrs(parent_rr)):
+ if rr.get_draft() is not None:
+ return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message(
+ "Unable to update reviewers as the review "
+ "request has pending changes (the patch author "
+ "has a draft)")
+
+ try:
+ for child_rr in gen_child_rrs(parent_rr):
+ if str(child_rr.id) in child_reviewers:
+ if not child_rr.is_accessible_by(request.user):
+ return PERMISSION_DENIED.with_message(
+ "You do not have permission to update "
+ "reviewers on review request %s"
+ % child_rr.id)
+
+ draft = ReviewRequestDraft.create(child_rr)
+ draft.target_people.clear()
+ for user in child_reviewers[str(child_rr.id)]:
+ draft.target_people.add(user)
+
+ set_publish_as(parent_rr, request.user)
+ parent_rr_draft = ReviewRequestDraft.create(parent_rr)
+ update_parent_rr_reviewers(parent_rr_draft)
+ parent_rr.publish(user=parent_rr.submitter)
+ finally:
+ clear_publish_as(parent_rr)
+
+ except PublishError as e:
+ logging.error("failed to update reviewers on %s: %s"
+ % (parent_rr.id, str(e)))
+ return PUBLISH_ERROR.with_message(str(e))
+
+ return 200, {}
+
+ def get_uri(self, request):
+ named_url = self._build_named_url(self.name_plural)
+ return request.build_absolute_uri(
+ local_site_reverse(named_url, request=request, kwargs={}))
+
+modify_reviewer_resource = ModifyReviewerResource()
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/resources/verify_reviewer.py
@@ -0,0 +1,78 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth.models import (
+ User,
+)
+from django.utils import (
+ six,
+)
+from djblets.webapi.decorators import (
+ webapi_login_required,
+ webapi_request_fields,
+ webapi_response_errors,
+)
+from djblets.webapi.errors import (
+ INVALID_FORM_DATA,
+ NOT_LOGGED_IN,
+)
+from mozreview.bugzilla.client import (
+ Bugzilla,
+)
+from mozreview.models import (
+ get_bugzilla_api_key,
+)
+from reviewboard.site.urlresolvers import (
+ local_site_reverse,
+)
+from reviewboard.webapi.resources import (
+ WebAPIResource,
+)
+
+
+class VerifyReviewerResource(WebAPIResource):
+ """Resource to check the validity of provided reviewer names."""
+
+ allowed_methods = ('GET', 'POST')
+ name = 'verify_reviewer'
+
+ @webapi_login_required
+ @webapi_response_errors(INVALID_FORM_DATA, NOT_LOGGED_IN)
+ @webapi_request_fields(
+ required={
+ 'reviewers': {
+ 'type': six.text_type,
+ 'description': 'A comma separated list of reviewers'
+ },
+ },
+ )
+ def create(self, request, reviewers, *args, **kwargs):
+ bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
+ new_reviewers = [u.strip() for u in reviewers.split(',') if u.strip()]
+ invalid_reviewers = []
+ for reviewer in new_reviewers:
+ try:
+ bugzilla.get_user_from_irc_nick(reviewer)
+ except User.DoesNotExist:
+ invalid_reviewers.append(reviewer)
+
+ if invalid_reviewers:
+ # Because this isn't called through Review Board's built-in
+ # backbone system, it's dramatically simpler to return just the
+ # intended error message instead of categorising the errors by
+ # field.
+ if len(invalid_reviewers) == 1:
+ return INVALID_FORM_DATA.with_message(
+ "The reviewer '%s' was not found" % invalid_reviewers[0])
+ else:
+ return INVALID_FORM_DATA.with_message(
+ "The reviewers '%s' were not found"
+ % "', '".join(invalid_reviewers))
+
+ return 200, {}
+
+ def get_uri(self, request):
+ named_url = self._build_named_url(self.name_plural)
+ return request.build_absolute_uri(
+ local_site_reverse(named_url, request=request, kwargs={}))
+
+verify_reviewer_resource = VerifyReviewerResource()
--- a/pylib/mozreview/mozreview/signal_handlers.py
+++ b/pylib/mozreview/mozreview/signal_handlers.py
@@ -1,24 +1,28 @@
from __future__ import unicode_literals
import copy
import json
import logging
+from django.contrib.auth.models import (
+ User,
+)
from django.db.models.signals import (
post_save,
pre_delete,
pre_save,
)
from djblets.siteconfig.models import (
SiteConfiguration,
)
from reviewboard.reviews.errors import (
+ NotModifiedError,
PublishError,
)
from reviewboard.extensions.hooks import (
SignalHook,
)
from reviewboard.reviews.models import (
Review,
ReviewRequest,
@@ -55,16 +59,17 @@ from mozreview.extra_data import (
gen_rrs_by_rids,
get_parent_rr,
IDENTIFIER_KEY,
is_parent,
is_pushed,
REVIEWID_RE,
REVIEW_FLAG_KEY,
UNPUBLISHED_KEY,
+ PUBLISH_AS_KEY,
update_parent_rr_reviewers,
)
from mozreview.messages import (
AUTO_CLOSE_DESCRIPTION,
AUTO_SUBMITTED_DESCRIPTION,
NEVER_USED_DESCRIPTION,
OBSOLETE_DESCRIPTION,
)
@@ -261,17 +266,23 @@ def on_review_request_publishing(user, r
except (TypeError, ValueError):
raise InvalidBugIdError(bug_id)
siteconfig = SiteConfiguration.objects.get_current()
using_bugzilla = (
siteconfig.settings.get("auth_backend", "builtin") == "bugzilla")
if using_bugzilla:
- b = Bugzilla(get_bugzilla_api_key(user))
+ commit_data = fetch_commit_data(review_request_draft)
+ publish_as_id = commit_data.draft_extra_data.get(PUBLISH_AS_KEY)
+ if publish_as_id:
+ u = User.objects.get(id=publish_as_id)
+ b = Bugzilla(get_bugzilla_api_key(u))
+ else:
+ b = Bugzilla(get_bugzilla_api_key(user))
try:
if b.is_bug_confidential(bug_id):
raise ConfidentialBugError
except BugzillaError as e:
# Special cases:
# 100: Invalid Bug Alias
# 101: Bug does not exist
@@ -324,16 +335,21 @@ def on_review_request_publishing(user, r
# Setup the parent signal handler to approve the publish
# and then publish the child.
commit_request_publishing.connect(approve_publish,
sender=child,
weak=False)
try:
child.publish(user=user)
+ except NotModifiedError:
+ # As we create empty drafts as part of allowing reviewer
+ # delegation, delete these empty drafts instead of
+ # throwing an error.
+ child.get_draft(user=user).delete()
finally:
commit_request_publishing.disconnect(
receiver=approve_publish,
sender=child,
weak=False)
if child.id in unpublished_rids:
unpublished_rids.remove(child.id)
--- a/pylib/mozreview/mozreview/static/mozreview/css/commits.less
+++ b/pylib/mozreview/mozreview/static/mozreview/css/commits.less
@@ -283,8 +283,16 @@
}
.action-separator{
background-color: #E4E3DD;
height:1px;
margin: 5px;
}
}
+
+#local-draft-banner {
+ margin-top: 10px;
+ margin-bottom: -10px;
+ border-bottom: none;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
--- a/pylib/mozreview/mozreview/static/mozreview/css/review.less
+++ b/pylib/mozreview/mozreview/static/mozreview/css/review.less
@@ -19,18 +19,16 @@
padding: 10px 50px;
color: #333;
text-decoration: none;
}
}
}
#draft-banner {
- cursor: pointer;
-
#field_changedescription,
p {
display: none;
}
}
#discard-banner {
#btn-review-request-reopen,
@@ -119,16 +117,23 @@ label[for="mozreview-autoland-try-syntax
}
}
#try-syntax-error {
display: none;
color: red;
}
+.parent-request {
+ #id_shipit,
+ label[for="id_shipit"] {
+ display: none;
+ }
+}
+
.parent-request #review-form .edit-field {
// Without the Ship It checkbox, the edit field gets squished up
// with the Markdown Reference link unless we clear the latter's float.
clear: right;
}
// Hide the "Reviewers" section; unfortunately these don't have IDs
// TODO: Update this to use fieldset IDs once they are available upstream
@@ -146,8 +151,24 @@ label[for="mozreview-autoland-try-syntax
}
// Hide the "Ship it" checkbox, since we use the r?, r+, r- flags provided
// by MRReviewFlag.
#id_shipit,
label[for="id_shipit"] {
display: none;
}
+
+// Don't show gravatar for meta changes; RB is hardcoded to always use the
+// submitter, but we allow non-submitters to update the target people.
+#reviews {
+ .changedesc {
+ .box-status {
+ display: none;
+ }
+ .box-container {
+ padding-left: 0;
+ }
+ .box::before, .box::after {
+ display: none;
+ }
+ }
+}
--- a/pylib/mozreview/mozreview/static/mozreview/js/commits.js
+++ b/pylib/mozreview/mozreview/static/mozreview/js/commits.js
@@ -19,148 +19,377 @@
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
$(document).on("mozreview_ready", function() {
- if (!MozReview.isParent) {
- // At this time, there's no need to set up the editors for the reviewers if
- // we're not looking at the parent review request.
+ if (!RB.UserSession.instance.get("username")) {
return;
}
- console.assert(MozReview.parentEditor, "We should have a parent commit editor");
- console.assert(MozReview.parentView, "We should have a parent commit editor view");
-
- /**
- * A not-amazing error reporting mechanism that depends on a DOM
- * node with ID "error-container" existing on the page somewhere.
- */
- var $ErrorContainer = $("#error-container");
- function reportError(aMsg) {
- if (typeof(aMsg) == "object") {
- if (aMsg.errorText) {
- aMsg = aMsg.errorText;
- } else {
- aMsg = JSON.stringify(aMsg, null, "\t");
+ // Simple case-insensitive array of strings comparison
+ function arraysEqualsCI(a, b) {
+ var i = a.length;
+ if (i != b.length) {
+ return false;
+ }
+ while (i--) {
+ if (a[i].toLowerCase() != b[i].toLowerCase()) {
+ return false;
}
}
+ return true;
+ }
- $("#error-info").text(aMsg);
- $("#error-stack").text(new Error().stack);
- $ErrorContainer.attr("haserror", "true");
- RB.PageManager.getPage().reviewRequestEditorView._scheduleResizeLayout();
+ function showError(errorMessage, xhr) {
+ if (xhr && xhr.responseJSON && xhr.responseJSON.err) {
+ errorMessage = xhr.responseJSON.err.msg;
+ }
+ $("#review-request-warning")
+ .delay(6000)
+ .fadeOut(400, function() {
+ $(this).hide();
+ })
+ .show()
+ .text(errorMessage);
}
$("#error-close").click(function() {
- $ErrorContainer.attr("haserror", "false");
+ $("#error-container").attr("haserror", "false");
});
$("#error-stack-toggle").click(function() {
$("#error-stack").toggle();
});
- // Hook up the inline editor for each commit's reviewer list. This inline editor
- // code is mostly copied from Review Board itself - please see the copyright
- // notice in the header.
- var editorOptions = {
- editIconClass: "rb-icon rb-icon-edit",
- useEditIconOnly: true,
- enabled: true
- };
+ /*
+ * Review Board only allows the review request submitter to change the
+ * target reviewers. As MozReview wants to allow 'anyone' to change the
+ * reviewers, we expose the editor to all users (Bugzilla will perform the
+ * permissions check for us).
+ *
+ * In order for this to work correctly there are two separate paths depending
+ * on if the user is the submitter or not.
+ *
+ * Submitters use Review Board's normal draft mechanism, with a small tweak
+ * that creates a draft on all children when any reviewers are updated. This
+ * causes Review Board to show the draft banner when viewing any review
+ * request in the series.
+ *
+ * Non-submitters can't use a normal draft, as a review request can only have
+ * one, and it's used by the submitter. Instead we create a client-side
+ * fake draft in local storage, and display a fake draft banner above the
+ * commits table.
+ */
+
+ //
+ // Local Drafts
+ //
+
+ function getLocalDraft() {
+ var localDrafts = window.localStorage.localDrafts ?
+ JSON.parse(window.localStorage.localDrafts) : {};
+ var parent_rrid = $("#mozreview-parent-request").data("id");
+ return localDrafts[parent_rrid] ? localDrafts[parent_rrid] : {};
+ }
+
+ function setLocalDraft(draft) {
+ var localDrafts = window.localStorage.localDrafts ?
+ JSON.parse(window.localStorage.localDrafts) : {};
+ var parent_rrid = $("#mozreview-parent-request").data("id");
+ if (draft) {
+ localDrafts[parent_rrid] = draft;
+ }
+ else {
+ delete localDrafts[parent_rrid];
+ }
+ window.localStorage.localDrafts = JSON.stringify(localDrafts);
+ }
+
+ function hasLocalDraft() {
+ return Object.keys(getLocalDraft()).length !== 0;
+ }
+
+ function publishLocalDraft() {
+ if (!hasLocalDraft()) {
+ discardLocalDraft();
+ return;
+ }
+ var draft = getLocalDraft();
+ RB.setActivityIndicator(true, {});
+ $.ajax({
+ type: "POST",
+ data: {
+ parent_request_id: $("#mozreview-parent-request").data("id"),
+ reviewers: JSON.stringify(draft)
+ },
+ url: "/api/extensions/mozreview.extension.MozReviewExtension/modify-reviewers/",
+ success: function(rsp) {
+ discardLocalDraft(true);
+ window.location.reload(true);
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ RB.setActivityIndicator(false, {});
+ showError(errorThrown, xhr);
+ }
+ });
+ }
+
+ function discardLocalDraft(silent) {
+ setLocalDraft(undefined);
+ if (!silent) {
+ $(".mozreview-child-reviewer-list").each(function() {
+ restoreOriginalReviewerState($(this));
+ });
+ hideLocalDraftBanner();
+ }
+ }
- var reviewerList = $(".mozreview-child-reviewer-list");
+ function restoreLocalDraftState($reviewer_list) {
+ var draft = getLocalDraft();
+ if (!$reviewer_list) {
+ $reviewer_list = $(".mozreview-child-reviewer-list");
+ }
+ $reviewer_list.each(function() {
+ var $this = $(this);
+ var rrid = $this.data("id");
+ if (draft[rrid]) {
+ saveOriginalReviewerState($this);
+ $this.html(draft[rrid].join(", "));
+ }
+ });
+ }
+
+ function showLocalDraftBanner() {
+ if (!hasLocalDraft()) {
+ $("#local-draft-banner").remove();
+ return;
+ }
+ if ($("#local-draft-banner").length) {
+ return;
+ }
+ $("<div/>")
+ .attr("id", "local-draft-banner")
+ .addClass("banner")
+ .addClass("box-inner")
+ .append(
+ $("<p>")
+ .text("You have pending changes to this review.")
+ )
+ .append(
+ $("<span/>")
+ .addClass("banner-actions")
+ .append(
+ $("<input/>")
+ .attr("type", "button")
+ .addClass("publish-button")
+ .val("Publish")
+ .click(function(event) {
+ event.preventDefault();
+ publishLocalDraft();
+ })
+ )
+ .append(" ")
+ .append(
+ $("<input/>")
+ .attr("type", "button")
+ .addClass("discard-button")
+ .val("Discard")
+ .click(function(event) {
+ event.preventDefault();
+ discardLocalDraft();
+ })
+ )
+ )
+ .insertBefore("#mozreview-child-requests");
+ RB.PageManager.getPage().reviewRequestEditorView._scheduleResizeLayout();
+ }
+
+ function hideLocalDraftBanner() {
+ $("#local-draft-banner").remove();
+ RB.PageManager.getPage().reviewRequestEditorView._scheduleResizeLayout();
+ }
+
+ function saveOriginalReviewerState($reviewer_list) {
+ if (!$reviewer_list.data("orig-html")) {
+ $reviewer_list.data("orig-html", $reviewer_list.html().trim());
+ var reviewers = $reviewer_list.find(".reviewer-name")
+ .map(function() { return $(this).text(); })
+ .sort();
+ $reviewer_list.data("orig-reviewers", $.makeArray(reviewers));
+ }
+ }
+
+ function restoreOriginalReviewerState($reviewer_list) {
+ if ($reviewer_list.data("orig-html")) {
+ $reviewer_list.html($reviewer_list.data("orig-html"));
+ }
+ }
+
+ function ensureNativeDrafts() {
+ RB.setActivityIndicator(true, {});
+ $.ajax({
+ type: "POST",
+ data: {
+ parent_request_id: $("#mozreview-parent-request").data("id")
+ },
+ url: "/api/extensions/mozreview.extension.MozReviewExtension/ensure-drafts/",
+ success: function(rsp) {
+ RB.setActivityIndicator(false, {});
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ RB.setActivityIndicator(false, {});
+ showError(errorThrown, xhr);
+ }
+ });
+ }
+
+ function augmentNativeBanner() {
+ if (!MozReview.isParent) {
+ // Unfortunately we cannot publish from children, so provide a link
+ // to the parent instead.
+ var parent_rrid = $("#mozreview-parent-request").data("id");
+ $("#draft-banner").append(
+ $('<a href="../' + parent_rrid + '/" title="You can only Publish or Discard when ' +
+ 'viewing the \'Review Summary / Parent\'.">Publish or Discard my changes.</a>'));
+ }
+ }
+
var editors = {};
- if (MozReview.currentIsMutableByUser) {
- var reviewerListEditors = reviewerList
- .inlineEditor(editorOptions)
+ function updateReviewers($reviewer_list, value) {
+ var rrid = $reviewer_list.data("id");
+
+ // Parse updated reviewer list.
+ var reviewers = value.split(/[ ,]+/)
+ .map(function(name) {
+ name = name.trim();
+ if (name.substr(name, 0, 1) === ":") {
+ name = name.substring(1);
+ }
+ return name;
+ })
+ .filter(function(name) {
+ return name !== "";
+ })
+ .sort();
+
+
+ // No need to do anything if nothing is changed.
+ if (arraysEqualsCI($reviewer_list.data("orig-reviewers"), reviewers)) {
+ $reviewer_list.html($reviewer_list.data("orig-html"));
+ return;
+ }
+
+ // TODO retain reviewer background status colour after an edit
+ $reviewer_list.text(reviewers.join(" , "));
+
+ if (MozReview.isSubmitter) {
+ // When the submitter updates reviewers, use RB's native drafts.
+ var editor;
+ if (!editors[rrid]) {
+ var rr = new RB.ReviewRequest({ id: rrid });
+ editor = new RB.ReviewRequestEditor({ reviewRequest: rr });
+ editors[rrid] = editor;
+ } else {
+ editor = editors[rrid];
+ }
+
+ editor.setDraftField(
+ "targetPeople",
+ reviewers.join(","),
+ {
+ jsonFieldName: "target_people",
+ error: function(error) {
+ showError(error.errorText);
+ restoreOriginalReviewerState($reviewer_list);
+ },
+ success: function() {
+ MozReview.reviewEditor.set("public", false);
+ // Our draft relies on a field that isn't part of RB's front-end
+ // model, so changes aren't picked up by the model automatically.
+ // Manually record a draft exists so the banner will be displayed.
+ var view = RB.PageManager.getPage().reviewRequestEditorView;
+ view.model.set("hasDraft", true);
+ MozReview.reviewEditor.trigger("saved");
+ // Extend the draft to encompass the parent and all children, so
+ // the draft banner is visible on all review requests in the set.
+ ensureNativeDrafts();
+ augmentNativeBanner();
+ }
+ }, this);
+
+ } else {
+ // Otherwise use our local draft.
+
+ RB.setActivityIndicator(true, {});
+ $.ajax({
+ type: "POST",
+ data: { reviewers: reviewers.join(",") },
+ url: "/api/extensions/mozreview.extension.MozReviewExtension/verify-reviewers/",
+ success: function(rsp) {
+ RB.setActivityIndicator(false, {});
+ // All reviewrs ok - create fake draft in localStorage.
+ var localDraft = getLocalDraft();
+ localDraft[rrid] = reviewers;
+ setLocalDraft(localDraft);
+ showLocalDraftBanner();
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ RB.setActivityIndicator(false, {});
+ restoreLocalDraftState($reviewer_list);
+ showError(errorThrown, xhr);
+ }
+ });
+ }
+ }
+
+ $(".mozreview-child-reviewer-list")
+ .inlineEditor({
+ editIconClass: "rb-icon rb-icon-edit",
+ useEditIconOnly: true,
+ enabled: true,
+ setFieldValue: function(editor, value) {
+ editor._field.val(value.trim());
+ }
+ })
.on({
beginEdit: function() {
- // The editCount is used to determine if we should warn the user before
- // unloading the page because they still have an editor open.
- console.log("beginning edit " + $(this).data("id"));
- MozReview.parentEditor.incr("editCount");
+ $reviewer_list = $(this);
+ // Store the original html and reviewer list so we can restore later.
+ saveOriginalReviewerState($reviewer_list);
+ // store the current edit to support cancelling
+ $reviewer_list.data("prior", $reviewer_list.html());
+ // Inc editCount to enable "leave this page" warning.
+ MozReview.reviewEditor.incr("editCount");
},
cancel: function() {
- MozReview.parentEditor.decr("editCount");
+ $reviewer_list = $(this);
+ // restoreOriginalReviewerState($reviewer_list);
+ $reviewer_list.html($reviewer_list.data("prior"));
+ $reviewer_list.data("prior", "");
+ MozReview.reviewEditor.decr("editCount");
},
complete: function(e, value) {
- // The ReviewRequestEditor is the interface that we use to modify
- // a review request easily.
- var editor, reviewRequest;
- var id = $(this).data("id");
- if (!editors[id]) {
- reviewRequest = new RB.ReviewRequest({id: $(this).data("id")});
- editor = new RB.ReviewRequestEditor({reviewRequest: reviewRequest});
- editors[id] = editor;
- } else {
- editor = editors[id];
- }
-
- var originalContents = $(this).text();
-
- var warning = $("#review-request-warning");
- // For Mozilla, we sometimes use colons as a prefix for searching for
- // IRC nicks - that's just a convention that has developed over time.
- // Since IRC nicks are what MozReview recognizes, we need to be careful
- // that the user hasn't actually included those colon prefixes, otherwise
- // MozReview is going to complain that it doesn't recognize the user (since
- // MozReview's notion of a username doesn't include the colon prefix).
- var sanitized = value.split(" ").map(function(aName) {
- var trimmed = aName.trim();
- if (trimmed.indexOf(":") == 0) {
- trimmed = trimmed.substring(1);
- }
- return trimmed;
- });
-
- // This sets the reviewers on the child review request.
- editor.setDraftField(
- "targetPeople",
- sanitized.join(", "),
- {
- jsonFieldName: "target_people",
- error: function(error) {
- MozReview.parentEditor.decr("editCount");
- console.error(error.errorText);
-
- // This error display code is copied pretty much verbatim
- // from Review Board core to match the behaviour of attempting
- // to set a target reviewer to one or more users that does not
- // exist.
- warning
- .delay(6000)
- .fadeOut(400, function() {
- $(this).hide();
- })
- .show()
- .html(error.errorText);
-
- // Revert the list back to what we started with.
- $(this).text(originalContents);
- },
- success: function() {
- MozReview.parentEditor.decr("editCount");
- MozReview.parentEditor.set('public', false);
- // Our draft relies on a field that isn't part of RB's front-end
- // model, so changes aren't picked up by the model automatically.
- // Manually record a draft exists so the banner will be displayed.
- var view = RB.PageManager.getPage().reviewRequestEditorView;
- view.model.set("hasDraft", true);
-
- MozReview.parentEditor.trigger('saved');
-
- }
- }, this);
+ $reviewer_list = $(this);
+ $reviewer_list.data("prior", "");
+ MozReview.reviewEditor.decr("editCount");
+ updateReviewers($reviewer_list, value);
}
});
+
+ // Update UI if there's an existing draft.
+ if (MozReview.isSubmitter) {
+ augmentNativeBanner();
+ } else if (hasLocalDraft()) {
+ showLocalDraftBanner();
+ restoreLocalDraftState();
}
// This next bit sets up the autocomplete popups for reviewers. This
// code is mostly copied from Review Board itself - please see the
// copyright notice in the header.
var acOptions = {
fieldName: "users",
nameKey: "username",
@@ -195,20 +424,18 @@
};
// Again, this is copied almost verbatim from Review Board core to
// mimic traditional behaviour for this kind of field.
$("#mozreview-child-requests input[type=text]").mozreviewautocomplete({
formatItem: function(data) {
var s = data[acOptions.nameKey];
if (acOptions.descKey && data[acOptions.descKey]) {
- s += ' <span>(' + _.escape(data[acOptions.descKey]) +
- ')</span>';
+ s += " <span>(" + _.escape(data[acOptions.descKey]) + ")</span>";
}
-
return s;
},
matchCase: false,
multiple: true,
searchPrefix: ":",
parse: function(data) {
var items = data[acOptions.fieldName],
itemsLen = items.length,
@@ -224,43 +451,36 @@
value: value[acOptions.nameKey],
result: value[acOptions.nameKey]
});
}
return parsed;
},
url: SITE_ROOT +
- "api/" + (acOptions.resourceName || acOptions.fieldName) + '/',
+ "api/" + (acOptions.resourceName || acOptions.fieldName) + "/",
extraParams: acOptions.extraParams,
cmp: acOptions.cmp,
width: 350,
error: function(xhr) {
- var text;
- try {
- text = $.parseJSON(xhr.responseText).err.msg;
- } catch (e) {
- text = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
- }
- reportError(text);
+ showError(xhr.statusText, xhr);
}
}).on("autocompleteshow", function() {
/*
* Add the footer to the bottom of the results pane the
* first time it's created.
*
* Note that we may have multiple .ui-autocomplete-results
* elements, and we don't necessarily know which is tied to
* this. So, we'll look for all instances that don't contain
* a footer.
*/
- var resultsPane = $('.ui-autocomplete-results:not(' +
- ':has(.ui-autocomplete-footer))');
+ var resultsPane = $(".ui-autocomplete-results:not(" +
+ ":has(.ui-autocomplete-footer))");
if (resultsPane.length > 0) {
- $('<div/>')
- .addClass('ui-autocomplete-footer')
- .text(gettext('Press Tab to auto-complete.'))
+ $("<div/>")
+ .addClass("ui-autocomplete-footer")
+ .text(gettext("Press Tab to auto-complete."))
.appendTo(resultsPane);
}
});
-
});
--- a/pylib/mozreview/mozreview/static/mozreview/js/init_rr.js
+++ b/pylib/mozreview/mozreview/static/mozreview/js/init_rr.js
@@ -8,29 +8,29 @@ var MozReview = {};
var parentID = $("#mozreview-parent-request").data("id");
if (!parentID) {
console.error("Could not find a valid id for the parent review " +
"request.");
return;
}
- // The current user's scm level has been injected in an invisible div.
- MozReview.scmLevel = $("#scm-level").data("scm-level");
+ // Load injected user data>
+ var $userData = $("#user_data");
+ MozReview.scmLevel = $userData.data("scm-level");
MozReview.hasScmLevel1 = MozReview.scmLevel >= 1;
MozReview.hasScmLevel3 = MozReview.scmLevel == 3;
+ MozReview.isSubmitter = !!$userData.data("is-submitter");
// Whether or not the repository has associated try and landing repositories
// is in an invisible div.
MozReview.autolandingToTryEnabled = $("#repository").data("autolanding-to-try-enabled");
MozReview.autolandingEnabled = $("#repository").data("autolanding-enabled");
MozReview.landingRepository = $("#repository").data("landing-repository");
- console.log("Found parent review request ID: " + parentID);
-
var page = RB.PageManager.getPage();
// Setup a CSS class so we can differentiate between parent
// and commit review requests.
var currentID = page.reviewRequest.id;
if (currentID == parentID) {
$("body").addClass("parent-request");
@@ -39,18 +39,17 @@ var MozReview = {};
}
var pageReviewRequest = page.reviewRequest;
var pageEditor = page.reviewRequestEditor;
var pageView = page.reviewRequestEditorView;
MozReview.currentIsMutableByUser = pageEditor.get("mutableByUser");
MozReview.isParent = (parentID == pageReviewRequest.id);
- MozReview.parentEditor = MozReview.isParent ? pageEditor
- : null;
+ MozReview.reviewEditor = pageEditor;
MozReview.parentView = MozReview.isParent ? pageView
: null;
// Review Board doesn't currently expose approval status in the
// review request model so we extend it and use our own model
// for the parent so we can access it.
var patchedRR = RB.ReviewRequest.extend({
defaults: function() {
rename from pylib/mozreview/mozreview/templates/mozreview/scm_level.html
rename to pylib/mozreview/mozreview/templates/mozreview/user-data.html
--- a/pylib/mozreview/mozreview/templates/mozreview/scm_level.html
+++ b/pylib/mozreview/mozreview/templates/mozreview/user-data.html
@@ -1,2 +1,9 @@
{% load mozreview %}
-<div id="scm-level" data-scm-level="{{ request.mozreview_profile|scm_level }}"></div>
+<div id="user_data"
+ data-scm-level="{{ request.mozreview_profile|scm_level }}"
+ {% if request.user and review_request %}
+ {% if review_request.submitter.id == request.user.id %}
+ data-is-submitter="true"
+ {% endif %}
+ {% endif %}
+></div>
--- a/pylib/mozreview/mozreview/templatetags/mozreview.py
+++ b/pylib/mozreview/mozreview/templatetags/mozreview.py
@@ -52,18 +52,22 @@ def extra_data(review_request, key):
@register.filter()
def scm_level(mozreview_profile):
if mozreview_profile is None:
return ''
elif mozreview_profile.has_scm_ldap_group('scm_level_3'):
return '3'
+ elif mozreview_profile.has_scm_ldap_group('scm_level_2'):
+ return '2'
elif mozreview_profile.has_scm_ldap_group('scm_level_1'):
return '1'
+ else:
+ return ''
@register.filter()
def required_ldap_group(repository):
try:
return repository.extra_data['required_ldap_group']
except (AttributeError, KeyError):
return ''
--- a/testing/vcttesting/reviewboard/mach_commands.py
+++ b/testing/vcttesting/reviewboard/mach_commands.py
@@ -785,8 +785,58 @@ class ReviewBoardCommands(object):
d['commit'] = commit['commit']
for k in ('id', 'commit'):
if k in commit:
d[k] = commit[k]
d['reviewers'] = list(commit['reviewers'])
result['commits'].append(d)
print(yaml.safe_dump(result, default_flow_style=False).rstrip())
+
+ @Command('modify-reviewers', category='reviewboard',
+ description='Update reviewers on a child review request')
+ @CommandArgument('parent_rrid', help='Parent review request id')
+ @CommandArgument('child_rrid', help='Child review request id')
+ @CommandArgument('reviewers', help='Comma delimited list of reviewers')
+ def modify_reviewers(self, parent_rrid, child_rrid, reviewers):
+ from rbtools.api.errors import APIError
+ import json
+ c = self._get_client()
+ try:
+ r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
+ '/modify-reviewers/')
+ reviewers = {child_rrid: reviewers.split(',')}
+ r.create(parent_request_id=parent_rrid,
+ reviewers=json.dumps(reviewers))
+ except APIError as e:
+ print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
+ e.rsp['err']['msg']))
+ return 1
+
+ @Command('verify-reviewers', category='reviewboard',
+ description='Update reviewers on a child review request')
+ @CommandArgument('reviewers', help='Comma delimited list of reviewers')
+ def verify_reviewers(self, reviewers):
+ from rbtools.api.errors import APIError
+ c = self._get_client()
+ try:
+ r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
+ '/verify-reviewers/')
+ r.create(reviewers=reviewers)
+ except APIError as e:
+ print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
+ e.rsp['err']['msg']))
+ return 1
+
+ @Command('ensure-drafts', category='reviewboard',
+ description='Create drafts on all review request children')
+ @CommandArgument('parent_rrid', help='Parent review request id')
+ def ensure_drafts(self, parent_rrid):
+ from rbtools.api.errors import APIError
+ c = self._get_client()
+ try:
+ r = c.get_path('/extensions/mozreview.extension.MozReviewExtension'
+ '/ensure-drafts/')
+ r.create(parent_request_id=parent_rrid)
+ except APIError as e:
+ print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
+ e.rsp['err']['msg']))
+ return 1