Remove reviewers from a change

- The new UI adds a "Remove" column and an "X" button on each
  reviewer row to ApprovalTable.  Clicking this button will cause
  a remote call to removeReviewer

- Added a new removeReviewer remote method to PatchDetailService

- Both the addReviewer and removeReviewer remote methods use the
  same result class, which I renamed from AddReviewerResult to
  ReviewerResult

- ReviewerResult received a new error type "COULD_NOT_REMOVE"

Note: the error handling of this CL is not optimal for 2 reasons:

- Since PatchDetailService always catches exceptions and adds them
  to an errors field in ReviewerResult, the call always succeeds
  even if it actually failed.

- We can display a dialog box if the removal failed, but it would
  be better simply not to display the Delete button if the user
  doesn't have enough privileges.

A cleaner way to implement this (future change list) would be to
return this information in ChangeDetail so that ChangeScreen will
only display Delete buttons that will actually work.

Bug: issue 218
Bug: issue 289
Change-Id: I38ce63d76a3588c85472f1fd723cd097aeafccb3
This commit is contained in:
Cedric Beust
2010-04-08 13:01:12 -07:00
committed by Shawn O. Pearce
parent 9d823b2e3e
commit 258f15ea85
13 changed files with 202 additions and 23 deletions

View File

@@ -50,7 +50,11 @@ public interface PatchDetailService extends RemoteJsonService {
@SignInRequired @SignInRequired
void addReviewers(Change.Id id, List<String> reviewers, void addReviewers(Change.Id id, List<String> reviewers,
AsyncCallback<AddReviewerResult> callback); AsyncCallback<ReviewerResult> callback);
@SignInRequired
void removeReviewer(Change.Id id, Account.Id reviewerId,
AsyncCallback<ReviewerResult> callback);
void userApprovals(Set<Change.Id> cids, Account.Id aid, void userApprovals(Set<Change.Id> cids, Account.Id aid,
AsyncCallback<ApprovalSummarySet> callback); AsyncCallback<ApprovalSummarySet> callback);

View File

@@ -18,12 +18,14 @@ package com.google.gerrit.common.data;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** Result from adding a reviewer to a change. */ /**
public class AddReviewerResult { * Result from adding or removing a reviewer from a change.
*/
public class ReviewerResult {
protected List<Error> errors; protected List<Error> errors;
protected ChangeDetail change; protected ChangeDetail change;
public AddReviewerResult() { public ReviewerResult() {
errors = new ArrayList<Error>(); errors = new ArrayList<Error>();
} }
@@ -49,7 +51,10 @@ public class AddReviewerResult {
ACCOUNT_NOT_FOUND, ACCOUNT_NOT_FOUND,
/** The account is not permitted to see the change. */ /** The account is not permitted to see the change. */
CHANGE_NOT_VISIBLE CHANGE_NOT_VISIBLE,
/** Could not remove this reviewer from the change. */
COULD_NOT_REMOVE
} }
protected Type type; protected Type type;

View File

@@ -27,6 +27,7 @@ public interface GerritCss extends CssResource {
String accountName(); String accountName();
String activeRow(); String activeRow();
String addReviewer(); String addReviewer();
String removeReviewer();
String addSshKeyPanel(); String addSshKeyPanel();
String approvalCategoryList(); String approvalCategoryList();
String approvalTable(); String approvalTable();

View File

@@ -21,10 +21,10 @@ import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AccountDashboardLink; import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.AddMemberBox; import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.common.data.AccountInfoCache; import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.common.data.AddReviewerResult;
import com.google.gerrit.common.data.ApprovalDetail; import com.google.gerrit.common.data.ApprovalDetail;
import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ChangeDetail; import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ReviewerResult;
import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.ApprovalCategoryValue; import com.google.gerrit.reviewdb.ApprovalCategoryValue;
@@ -34,6 +34,7 @@ import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.Grid;
@@ -61,7 +62,7 @@ public class ApprovalTable extends Composite {
public ApprovalTable() { public ApprovalTable() {
types = Gerrit.getConfig().getApprovalTypes().getApprovalTypes(); types = Gerrit.getConfig().getApprovalTypes().getApprovalTypes();
table = new Grid(1, 3 + types.size()); table = new Grid(1, 4 + types.size());
table.addStyleName(Gerrit.RESOURCES.css().infoTable()); table.addStyleName(Gerrit.RESOURCES.css().infoTable());
displayHeader(); displayHeader();
@@ -102,6 +103,7 @@ public class ApprovalTable extends Composite {
for (final ApprovalType t : types) { for (final ApprovalType t : types) {
header(col++, t.getCategory().getName()); header(col++, t.getCategory().getName());
} }
header(col++, Util.C.removeReviewer());
applyEdgeStyles(0); applyEdgeStyles(0);
} }
@@ -114,7 +116,8 @@ public class ApprovalTable extends Composite {
final CellFormatter fmt = table.getCellFormatter(); final CellFormatter fmt = table.getCellFormatter();
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().approvalrole()); fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().approvalrole());
fmt.addStyleName(row, 1 + types.size(), Gerrit.RESOURCES.css().rightmost()); fmt.addStyleName(row, 1 + types.size(), Gerrit.RESOURCES.css().rightmost());
fmt.addStyleName(row, 2 + types.size(), Gerrit.RESOURCES.css().approvalhint()); fmt.addStyleName(row, 2 + types.size(), Gerrit.RESOURCES.css().rightmost());
fmt.addStyleName(row, 3 + types.size(), Gerrit.RESOURCES.css().approvalhint());
} }
private void applyScoreStyles(final int row) { private void applyScoreStyles(final int row) {
@@ -189,14 +192,14 @@ public class ApprovalTable extends Composite {
reviewers.add(nameEmail); reviewers.add(nameEmail);
PatchUtil.DETAIL_SVC.addReviewers(changeId, reviewers, PatchUtil.DETAIL_SVC.addReviewers(changeId, reviewers,
new GerritCallback<AddReviewerResult>() { new GerritCallback<ReviewerResult>() {
public void onSuccess(final AddReviewerResult result) { public void onSuccess(final ReviewerResult result) {
addMemberBox.setEnabled(true); addMemberBox.setEnabled(true);
addMemberBox.setText(""); addMemberBox.setText("");
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
final SafeHtmlBuilder r = new SafeHtmlBuilder(); final SafeHtmlBuilder r = new SafeHtmlBuilder();
for (final AddReviewerResult.Error e : result.getErrors()) { for (final ReviewerResult.Error e : result.getErrors()) {
switch (e.getType()) { switch (e.getType()) {
case ACCOUNT_NOT_FOUND: case ACCOUNT_NOT_FOUND:
r.append(Util.M.accountNotFound(e.getName())); r.append(Util.M.accountNotFound(e.getName()));
@@ -278,6 +281,31 @@ public class ApprovalTable extends Composite {
col++; col++;
} }
//
// Remove button
//
if (Gerrit.isSignedIn()) {
Button removeButton = new Button("X");
removeButton.setStyleName(Gerrit.RESOURCES.css().removeReviewer());
removeButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
PatchUtil.DETAIL_SVC.removeReviewer(changeId, ad.getAccount(),
new GerritCallback<ReviewerResult>() {
@Override
public void onSuccess(ReviewerResult result) {
if (result.getErrors().isEmpty()) {
final ChangeDetail r = result.getChange();
display(r.getChange(), r.getMissingApprovals(), r.getApprovals());
} else {
new ErrorDialog(result.getErrors().get(0).toString()).center();
}
}
});
}
});
table.setWidget(row, col++, removeButton);
}
table.setText(row, col++, hint.toString()); table.setText(row, col++, hint.toString());
} }
} }

View File

@@ -69,6 +69,7 @@ public interface ChangeConstants extends Constants {
String changeScreenComments(); String changeScreenComments();
String approvalTableReviewer(); String approvalTableReviewer();
String removeReviewer();
String approvalTableAddReviewer(); String approvalTableAddReviewer();
String changeInfoBlockOwner(); String changeInfoBlockOwner();

View File

@@ -46,6 +46,7 @@ changeScreenNeededBy = Needed By
changeScreenComments = Comments changeScreenComments = Comments
approvalTableReviewer = Reviewer approvalTableReviewer = Reviewer
removeReviewer = Remove
approvalTableAddReviewer = Add Reviewer approvalTableAddReviewer = Add Reviewer
changeInfoBlockOwner = Owner changeInfoBlockOwner = Owner

View File

@@ -881,7 +881,9 @@
margin-top: 5px; margin-top: 5px;
white-space: nowrap; white-space: nowrap;
} }
.removeReviewer {
width: 30px;
}
td.downloadLinkListCell { td.downloadLinkListCell {
padding: 0px; padding: 0px;
} }

View File

@@ -79,7 +79,7 @@ class RequireSslFilter implements Filter {
url = b.toString(); url = b.toString();
} else { } else {
url = urlProvider.get(); url = urlProvider.get() + req.getServletPath();
} }
rsp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); rsp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
rsp.setHeader("Location", url); rsp.setHeader("Location", url);

View File

@@ -14,7 +14,7 @@
package com.google.gerrit.httpd.rpc.patch; package com.google.gerrit.httpd.rpc.patch;
import com.google.gerrit.common.data.AddReviewerResult; import com.google.gerrit.common.data.ReviewerResult;
import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.httpd.rpc.Handler;
@@ -39,7 +39,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
class AddReviewer extends Handler<AddReviewerResult> { class AddReviewer extends Handler<ReviewerResult> {
interface Factory { interface Factory {
AddReviewer create(Change.Id changeId, Collection<String> nameOrEmails); AddReviewer create(Change.Id changeId, Collection<String> nameOrEmails);
} }
@@ -82,23 +82,23 @@ class AddReviewer extends Handler<AddReviewerResult> {
} }
@Override @Override
public AddReviewerResult call() throws Exception { public ReviewerResult call() throws Exception {
final Set<Account.Id> reviewerIds = new HashSet<Account.Id>(); final Set<Account.Id> reviewerIds = new HashSet<Account.Id>();
final ChangeControl control = changeControlFactory.validateFor(changeId); final ChangeControl control = changeControlFactory.validateFor(changeId);
final AddReviewerResult result = new AddReviewerResult(); final ReviewerResult result = new ReviewerResult();
for (final String nameOrEmail : reviewers) { for (final String nameOrEmail : reviewers) {
final Account account = accountResolver.find(nameOrEmail); final Account account = accountResolver.find(nameOrEmail);
if (account == null) { if (account == null) {
result.addError(new AddReviewerResult.Error( result.addError(new ReviewerResult.Error(
AddReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail)); ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail));
continue; continue;
} }
final IdentifiedUser user = identifiedUserFactory.create(account.getId()); final IdentifiedUser user = identifiedUserFactory.create(account.getId());
if (!control.forUser(user).isVisible()) { if (!control.forUser(user).isVisible()) {
result.addError(new AddReviewerResult.Error( result.addError(new ReviewerResult.Error(
AddReviewerResult.Error.Type.CHANGE_NOT_VISIBLE, nameOrEmail)); ReviewerResult.Error.Type.CHANGE_NOT_VISIBLE, nameOrEmail));
continue; continue;
} }

View File

@@ -14,7 +14,7 @@
package com.google.gerrit.httpd.rpc.patch; package com.google.gerrit.httpd.rpc.patch;
import com.google.gerrit.common.data.AddReviewerResult; import com.google.gerrit.common.data.ReviewerResult;
import com.google.gerrit.common.data.ApprovalSummary; import com.google.gerrit.common.data.ApprovalSummary;
import com.google.gerrit.common.data.ApprovalSummarySet; import com.google.gerrit.common.data.ApprovalSummarySet;
import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.data.ApprovalTypes;
@@ -60,6 +60,7 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
private final AccountInfoCacheFactory.Factory accountInfoCacheFactory; private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
private final AddReviewer.Factory addReviewerFactory; private final AddReviewer.Factory addReviewerFactory;
private final ChangeControl.Factory changeControlFactory; private final ChangeControl.Factory changeControlFactory;
private final RemoveReviewer.Factory removeReviewerFactory;
private final FunctionState.Factory functionStateFactory; private final FunctionState.Factory functionStateFactory;
private final PublishComments.Factory publishCommentsFactory; private final PublishComments.Factory publishCommentsFactory;
private final PatchScriptFactory.Factory patchScriptFactoryFactory; private final PatchScriptFactory.Factory patchScriptFactoryFactory;
@@ -71,6 +72,7 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
final ApprovalTypes approvalTypes, final ApprovalTypes approvalTypes,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory, final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final AddReviewer.Factory addReviewerFactory, final AddReviewer.Factory addReviewerFactory,
final RemoveReviewer.Factory removeReviewerFactory,
final ChangeControl.Factory changeControlFactory, final ChangeControl.Factory changeControlFactory,
final FunctionState.Factory functionStateFactory, final FunctionState.Factory functionStateFactory,
final PatchScriptFactory.Factory patchScriptFactoryFactory, final PatchScriptFactory.Factory patchScriptFactoryFactory,
@@ -81,6 +83,7 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
this.accountInfoCacheFactory = accountInfoCacheFactory; this.accountInfoCacheFactory = accountInfoCacheFactory;
this.addReviewerFactory = addReviewerFactory; this.addReviewerFactory = addReviewerFactory;
this.removeReviewerFactory = removeReviewerFactory;
this.changeControlFactory = changeControlFactory; this.changeControlFactory = changeControlFactory;
this.functionStateFactory = functionStateFactory; this.functionStateFactory = functionStateFactory;
this.patchScriptFactoryFactory = patchScriptFactoryFactory; this.patchScriptFactoryFactory = patchScriptFactoryFactory;
@@ -152,10 +155,15 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
} }
public void addReviewers(final Change.Id id, final List<String> reviewers, public void addReviewers(final Change.Id id, final List<String> reviewers,
final AsyncCallback<AddReviewerResult> callback) { final AsyncCallback<ReviewerResult> callback) {
addReviewerFactory.create(id, reviewers).to(callback); addReviewerFactory.create(id, reviewers).to(callback);
} }
public void removeReviewer(final Change.Id id, final Account.Id reviewerId,
final AsyncCallback<ReviewerResult> callback) {
removeReviewerFactory.create(id, reviewerId).to(callback);
}
public void userApprovals(final Set<Change.Id> cids, final Account.Id aid, public void userApprovals(final Set<Change.Id> cids, final Account.Id aid,
final AsyncCallback<ApprovalSummarySet> callback) { final AsyncCallback<ApprovalSummarySet> callback) {
run(callback, new Action<ApprovalSummarySet>() { run(callback, new Action<ApprovalSummarySet>() {

View File

@@ -29,6 +29,7 @@ public class PatchModule extends RpcServletModule {
@Override @Override
protected void configure() { protected void configure() {
factory(AddReviewer.Factory.class); factory(AddReviewer.Factory.class);
factory(RemoveReviewer.Factory.class);
factory(PatchScriptFactory.Factory.class); factory(PatchScriptFactory.Factory.class);
factory(SaveDraft.Factory.class); factory(SaveDraft.Factory.class);
} }

View File

@@ -0,0 +1,96 @@
// Copyright (C) 2010 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd.rpc.patch;
import com.google.gerrit.common.data.ReviewerResult;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.ArrayList;
import java.util.List;
/**
* Implement the remote logic that removes a reviewer from a change.
*/
class RemoveReviewer extends Handler<ReviewerResult> {
interface Factory {
RemoveReviewer create(Change.Id changeId, Account.Id reviewerId);
}
private final Account.Id reviewerId;
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final Change.Id changeId;
private final ChangeDetailFactory.Factory changeDetailFactory;
@Inject
RemoveReviewer(final ReviewDb db, final ChangeControl.Factory changeControlFactory,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted Change.Id changeId, @Assisted Account.Id reviewerId) {
this.db = db;
this.changeControlFactory = changeControlFactory;
this.changeId = changeId;
this.reviewerId = reviewerId;
this.changeDetailFactory = changeDetailFactory;
}
@Override
public ReviewerResult call() throws Exception {
ReviewerResult result = new ReviewerResult();
List<Account.Id> accounts = new ArrayList<Account.Id>();
ChangeControl ctl = changeControlFactory.validateFor(changeId);
boolean permitted = true;
List<PatchSetApproval> toDelete = new ArrayList<PatchSetApproval>();
for (PatchSetApproval psa : db.patchSetApprovals().byChange(changeId)) {
if (psa.getAccountId().equals(reviewerId)) {
if (ctl.canRemoveReviewer(psa)) {
toDelete.add(psa);
} else {
permitted = false;
break;
}
}
}
if (permitted) {
try {
db.patchSetApprovals().delete(toDelete);
} catch (OrmException ex) {
result.addError(new ReviewerResult.Error(
ReviewerResult.Error.Type.COULD_NOT_REMOVE,
"Could not remove reviewer " + reviewerId));
}
} else {
result.addError(new ReviewerResult.Error(
ReviewerResult.Error.Type.COULD_NOT_REMOVE,
"Not allowed to remove reviewer " + reviewerId));
}
// Note: call setChange() after the deletion has been made or it will still
// contain the reviewer we want to delete.
result.setChange(changeDetailFactory.create(changeId).call());
return result;
}
}

View File

@@ -14,7 +14,9 @@
package com.google.gerrit.server.project; package com.google.gerrit.server.project;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
@@ -141,4 +143,34 @@ public class ChangeControl {
} }
return false; return false;
} }
/** @return true if the user is allowed to remove this reviewer. */
public boolean canRemoveReviewer(PatchSetApproval approval) {
if (getChange().getStatus().isOpen()) {
// A user can always remove themselves.
//
if (getCurrentUser() instanceof IdentifiedUser) {
final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
if (i.getAccountId().equals(approval.getAccountId())) {
return true; // can remove self
}
}
// The change owner may remove any zero or positive score.
//
if (isOwner() && 0 <= approval.getValue()) {
return true;
}
// The branch owner, project owner, site admin can remove anyone.
//
if (getRefControl().isOwner() // branch owner
|| getProjectControl().isOwner() // project owner
|| getCurrentUser().isAdministrator()) {
return true;
}
}
return false;
}
} }