Show the approval rights for a project in the project admin screen

This way its clear what rights have been granted out on this project
to any existing groups.

If the right isn't given to the magic wildcard project then the right
can be deleted from the project.  Wildcard rights must be managed by
the site administrators only, and may initially only be available from
within the raw database.

Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-01-03 14:57:28 -08:00
parent a26dc96cf8
commit d0207f556a
15 changed files with 376 additions and 16 deletions

View File

@@ -252,10 +252,15 @@ public class AccountGroupScreen extends AccountScreen {
} }
private void display(final AccountGroupDetail result) { private void display(final AccountGroupDetail result) {
setTitleText(Util.M.group(result.group.getName())); final AccountGroup group = result.group;
groupNameTxt.setText(result.group.getName()); setTitleText(Util.M.group(group.getName()));
ownerTxt.setText(result.ownerGroup.getName()); groupNameTxt.setText(group.getName());
descTxt.setText(result.group.getDescription()); if (result.ownerGroup != null) {
ownerTxt.setText(result.ownerGroup.getName());
} else {
ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
}
descTxt.setText(group.getDescription());
if (result.autoGroup) { if (result.autoGroup) {
memberPanel.setVisible(false); memberPanel.setVisible(false);
} else { } else {

View File

@@ -30,11 +30,16 @@ public interface AdminConstants extends Constants {
String headingDescription(); String headingDescription();
String headingMembers(); String headingMembers();
String headingCreateGroup(); String headingCreateGroup();
String headingAccessRights();
String columnMember(); String columnMember();
String columnEmailAddress(); String columnEmailAddress();
String columnGroupName(); String columnGroupName();
String columnProjectName();
String columnGroupDescription(); String columnGroupDescription();
String columnProjectDescription();
String columnApprovalCategory();
String columnRightRange();
String groupListTitle(); String groupListTitle();
String projectListTitle(); String projectListTitle();

View File

@@ -11,11 +11,16 @@ headingOwner = Owners
headingDescription = Description headingDescription = Description
headingMembers = Members headingMembers = Members
headingCreateGroup = Create New Group headingCreateGroup = Create New Group
headingAccessRights = Access Rights
columnMember = Member columnMember = Member
columnEmailAddress = Email Address columnEmailAddress = Email Address
columnGroupName = Name columnGroupName = Group Name
columnProjectName = Project Name
columnGroupDescription = Description columnGroupDescription = Description
columnProjectDescription = Description
columnApprovalCategory = Category
columnRightRange = Permitted Range
groupListTitle = Groups groupListTitle = Groups
projectListTitle = Projects projectListTitle = Projects

View File

@@ -19,4 +19,5 @@ import com.google.gwt.i18n.client.Messages;
public interface AdminMessages extends Messages { public interface AdminMessages extends Messages {
String group(String name); String group(String name);
String project(String name); String project(String name);
String deletedGroup(int id);
} }

View File

@@ -1,2 +1,4 @@
group = Group {0} group = Group {0}
project = Project {0} project = Project {0}
deletedGroup = Deleted Group {0}

View File

@@ -14,21 +14,37 @@
package com.google.gerrit.client.admin; package com.google.gerrit.client.admin;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.data.GerritConfig;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.client.reviewdb.Project; import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle; import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
import com.google.gerrit.client.ui.AccountScreen; import com.google.gerrit.client.ui.AccountScreen;
import com.google.gerrit.client.ui.DomUtil;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.TextSaveButtonListener; import com.google.gerrit.client.ui.TextSaveButtonListener;
import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ClickListener; import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SourcesTableEvents;
import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.TableListener;
import com.google.gwt.user.client.ui.TextArea; import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget; import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwtjsonrpc.client.VoidResult; import com.google.gwtjsonrpc.client.VoidResult;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
public class ProjectAdminScreen extends AccountScreen { public class ProjectAdminScreen extends AccountScreen {
private Project.Id projectId; private Project.Id projectId;
@@ -39,6 +55,9 @@ public class ProjectAdminScreen extends AccountScreen {
private TextArea descTxt; private TextArea descTxt;
private Button saveDesc; private Button saveDesc;
private RightsTable rights;
private Button delRight;
public ProjectAdminScreen(final Project.Id toShow) { public ProjectAdminScreen(final Project.Id toShow) {
projectId = toShow; projectId = toShow;
} }
@@ -68,14 +87,15 @@ public class ProjectAdminScreen extends AccountScreen {
private void enableForm(final boolean on) { private void enableForm(final boolean on) {
ownerTxtBox.setEnabled(on); ownerTxtBox.setEnabled(on);
descTxt.setEnabled(on); descTxt.setEnabled(on);
delRight.setEnabled(on);
} }
private void initUI() { private void initUI() {
initOwner(); initOwner();
initDescription(); initDescription();
initRights();
} }
private void initOwner() { private void initOwner() {
final VerticalPanel ownerPanel = new VerticalPanel(); final VerticalPanel ownerPanel = new VerticalPanel();
final Label ownerHdr = new Label(Util.C.headingOwner()); final Label ownerHdr = new Label(Util.C.headingOwner());
@@ -136,9 +156,187 @@ public class ProjectAdminScreen extends AccountScreen {
new TextSaveButtonListener(descTxt, saveDesc); new TextSaveButtonListener(descTxt, saveDesc);
} }
private void initRights() {
final Label rightsHdr = new Label(Util.C.headingAccessRights());
rightsHdr.setStyleName("gerrit-SmallHeading");
rights = new RightsTable();
delRight = new Button(Util.C.buttonDeleteGroupMembers());
delRight.addClickListener(new ClickListener() {
public void onClick(final Widget sender) {
rights.deleteChecked();
}
});
add(rightsHdr);
add(rights);
add(delRight);
}
private void display(final ProjectDetail result) { private void display(final ProjectDetail result) {
setTitleText(Util.M.project(result.project.getName())); final Project project = result.project;
ownerTxt.setText(result.ownerGroup.getName()); final AccountGroup owner = result.groups.get(project.getOwnerGroupId());
descTxt.setText(result.project.getDescription()); setTitleText(Util.M.project(project.getName()));
if (owner != null) {
ownerTxt.setText(owner.getName());
} else {
ownerTxt.setText(Util.M.deletedGroup(project.getOwnerGroupId().get()));
}
descTxt.setText(project.getDescription());
rights.display(result.groups, result.rights);
}
private class RightsTable extends FancyFlexTable<ProjectRight> {
RightsTable() {
table.setText(0, 2, Util.C.columnApprovalCategory());
table.setText(0, 3, Util.C.columnGroupName());
table.setText(0, 4, Util.C.columnRightRange());
table.addTableListener(new TableListener() {
public void onCellClicked(SourcesTableEvents sender, int row, int cell) {
if (cell != 1 && getRowItem(row) != null) {
movePointerTo(row);
}
}
});
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(0, 1, S_ICON_HEADER);
fmt.addStyleName(0, 2, S_DATA_HEADER);
fmt.addStyleName(0, 3, S_DATA_HEADER);
fmt.addStyleName(0, 4, S_DATA_HEADER);
}
@Override
protected Object getRowItemKey(final ProjectRight item) {
return item.getKey();
}
@Override
protected boolean onKeyPress(final char keyCode, final int modifiers) {
if (super.onKeyPress(keyCode, modifiers)) {
return true;
}
if (modifiers == 0) {
switch (keyCode) {
case 's':
case 'c':
toggleCurrentRow();
return true;
}
}
return false;
}
@Override
protected void onOpenItem(final ProjectRight item) {
toggleCurrentRow();
}
private void toggleCurrentRow() {
final CheckBox cb = (CheckBox) table.getWidget(getCurrentRow(), 1);
cb.setChecked(!cb.isChecked());
}
void deleteChecked() {
final HashSet<ProjectRight.Key> ids = new HashSet<ProjectRight.Key>();
for (int row = 1; row < table.getRowCount(); row++) {
final ProjectRight k = getRowItem(row);
if (k != null && table.getWidget(row, 1) instanceof CheckBox
&& ((CheckBox) table.getWidget(row, 1)).isChecked()) {
ids.add(k.getKey());
}
}
if (!ids.isEmpty()) {
Util.PROJECT_SVC.deleteRight(ids, new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
for (int row = 1; row < table.getRowCount();) {
final ProjectRight k = getRowItem(row);
if (k != null && ids.contains(k.getKey())) {
table.removeRow(row);
} else {
row++;
}
}
}
});
}
}
void display(final Map<AccountGroup.Id, AccountGroup> groups,
final List<ProjectRight> result) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
for (final ProjectRight k : result) {
final int row = table.getRowCount();
table.insertRow(row);
populate(row, groups, k);
}
}
void populate(final int row,
final Map<AccountGroup.Id, AccountGroup> groups, final ProjectRight k) {
final GerritConfig config = Gerrit.getGerritConfig();
final ApprovalType ar = config.getApprovalType(k.getApprovalCategoryId());
final AccountGroup group = groups.get(k.getAccountGroupId());
if (ProjectRight.WILD_PROJECT.equals(k.getProjectId())) {
table.setText(row, 1, "");
} else {
table.setWidget(row, 1, new CheckBox());
}
if (ar != null) {
table.setText(row, 2, ar.getCategory().getName());
} else {
table.setText(row, 2, k.getApprovalCategoryId().get());
}
if (group != null) {
table.setText(row, 3, group.getName());
} else {
table.setText(row, 3, Util.M.deletedGroup(k.getAccountGroupId().get()));
}
{
final StringBuilder m = new StringBuilder();
final ApprovalCategoryValue min, max;
min = ar != null ? ar.getValue(k.getMinValue()) : null;
max = ar != null ? ar.getValue(k.getMaxValue()) : null;
formatValue(m, k.getMinValue(), min);
if (k.getMinValue() != k.getMaxValue()) {
m.append("<br>");
formatValue(m, k.getMaxValue(), max);
}
table.setHTML(row, 4, m.toString());
}
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(row, 1, S_ICON_CELL);
fmt.addStyleName(row, 2, S_DATA_CELL);
fmt.addStyleName(row, 3, S_DATA_CELL);
fmt.addStyleName(row, 4, S_DATA_CELL);
fmt.addStyleName(row, 4, "gerrit-ProjectAdmin-ApprovalCategoryRangeLine");
setRowItem(row, k);
}
private void formatValue(final StringBuilder m, final short v,
final ApprovalCategoryValue e) {
m.append("<span class=\"gerrit-ProjectAdmin-ApprovalCategoryValue\">");
if (v == 0) {
m.append(' ');
} else if (v > 0) {
m.append('+');
}
m.append(v);
m.append("</span>");
if (e != null) {
m.append(": ");
m.append(DomUtil.escape(e.getName()));
}
}
} }
} }

View File

@@ -15,12 +15,14 @@
package com.google.gerrit.client.admin; package com.google.gerrit.client.admin;
import com.google.gerrit.client.reviewdb.Project; import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.rpc.SignInRequired; import com.google.gerrit.client.rpc.SignInRequired;
import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.RemoteJsonService; import com.google.gwtjsonrpc.client.RemoteJsonService;
import com.google.gwtjsonrpc.client.VoidResult; import com.google.gwtjsonrpc.client.VoidResult;
import java.util.List; import java.util.List;
import java.util.Set;
public interface ProjectAdminService extends RemoteJsonService { public interface ProjectAdminService extends RemoteJsonService {
@SignInRequired @SignInRequired
@@ -36,4 +38,7 @@ public interface ProjectAdminService extends RemoteJsonService {
@SignInRequired @SignInRequired
void changeProjectOwner(Project.Id projectId, String newOwnerName, void changeProjectOwner(Project.Id projectId, String newOwnerName,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@SignInRequired
void deleteRight(Set<ProjectRight.Key> ids, AsyncCallback<VoidResult> callback);
} }

View File

@@ -16,18 +16,53 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.reviewdb.AccountGroup; import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.Project; import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.reviewdb.ReviewDb; import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ProjectDetail { public class ProjectDetail {
protected Project project; protected Project project;
protected AccountGroup ownerGroup; protected Map<AccountGroup.Id, AccountGroup> groups;
protected List<ProjectRight> rights;
public ProjectDetail() { public ProjectDetail() {
} }
public void load(final ReviewDb db, final Project g) throws OrmException { public void load(final ReviewDb db, final Project g) throws OrmException {
project = g; project = g;
ownerGroup = db.accountGroups().get(project.getOwnerGroupId()); groups = new HashMap<AccountGroup.Id, AccountGroup>();
wantGroup(g.getOwnerGroupId());
rights = new ArrayList<ProjectRight>();
loadRights(db, project.getId());
loadRights(db, ProjectRight.WILD_PROJECT);
loadGroups(db);
}
private void loadRights(final ReviewDb db, final Project.Id projectId)
throws OrmException {
for (final ProjectRight p : db.projectRights().byProject(projectId)) {
rights.add(p);
wantGroup(p.getAccountGroupId());
}
}
private void wantGroup(final AccountGroup.Id id) {
groups.put(id, null);
}
private void loadGroups(final ReviewDb db) throws OrmException {
final ResultSet<AccountGroup> r = db.accountGroups().get(groups.keySet());
groups.clear();
for (final AccountGroup g : r) {
groups.put(g.getId(), g);
}
} }
} }

View File

@@ -71,8 +71,8 @@ public class ProjectListScreen extends AccountScreen {
private class ProjectTable extends FancyFlexTable<Project> { private class ProjectTable extends FancyFlexTable<Project> {
ProjectTable() { ProjectTable() {
table.setText(0, 1, Util.C.columnGroupName()); table.setText(0, 1, Util.C.columnProjectName());
table.setText(0, 2, Util.C.columnGroupDescription()); table.setText(0, 2, Util.C.columnProjectDescription());
table.addTableListener(new TableListener() { table.addTableListener(new TableListener() {
public void onCellClicked(SourcesTableEvents sender, int row, int cell) { public void onCellClicked(SourcesTableEvents sender, int row, int cell) {
if (cell != 1 && getRowItem(row) != null) { if (cell != 1 && getRowItem(row) != null) {

View File

@@ -74,13 +74,22 @@ public class ApprovalType {
return maxPositive == ca.getValue(); return maxPositive == ca.getValue();
} }
public ApprovalCategoryValue getValue(final short value) {
initByValue();
return byValue.get(value);
}
public ApprovalCategoryValue getValue(final ChangeApproval ca) { public ApprovalCategoryValue getValue(final ChangeApproval ca) {
initByValue();
return byValue.get(ca.getValue());
}
private void initByValue() {
if (byValue == null) { if (byValue == null) {
byValue = new HashMap<Short, ApprovalCategoryValue>(); byValue = new HashMap<Short, ApprovalCategoryValue>();
for (final ApprovalCategoryValue acv : values) { for (final ApprovalCategoryValue acv : values) {
byValue.put(acv.getValue(), acv); byValue.put(acv.getValue(), acv);
} }
} }
return byValue.get(ca.getValue());
} }
} }

View File

@@ -14,8 +14,12 @@
package com.google.gerrit.client.data; package com.google.gerrit.client.data;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
public class GerritConfig { public class GerritConfig {
protected String canonicalUrl; protected String canonicalUrl;
@@ -23,6 +27,7 @@ public class GerritConfig {
protected List<ApprovalType> approvalTypes; protected List<ApprovalType> approvalTypes;
protected List<ApprovalType> actionTypes; protected List<ApprovalType> actionTypes;
protected int sshdPort; protected int sshdPort;
private transient Map<ApprovalCategory.Id, ApprovalType> byCategoryId;
public GerritConfig() { public GerritConfig() {
} }
@@ -82,4 +87,22 @@ public class GerritConfig {
public void setSshdPort(final int p) { public void setSshdPort(final int p) {
sshdPort = p; sshdPort = p;
} }
public ApprovalType getApprovalType(final ApprovalCategory.Id id) {
if (byCategoryId == null) {
byCategoryId = new HashMap<ApprovalCategory.Id, ApprovalType>();
if (actionTypes != null) {
for (final ApprovalType t : actionTypes) {
byCategoryId.put(t.getCategory().getId(), t);
}
}
if (approvalTypes != null) {
for (final ApprovalType t : approvalTypes) {
byCategoryId.put(t.getCategory().getId(), t);
}
}
}
return byCategoryId.get(id);
}
} }

View File

@@ -50,6 +50,10 @@ public final class ProjectRight {
return projectId; return projectId;
} }
public Project.Id getProjectId() {
return projectId;
}
@Override @Override
public com.google.gwtorm.client.Key<?>[] members() { public com.google.gwtorm.client.Key<?>[] members() {
return new com.google.gwtorm.client.Key<?>[] {categoryId, groupId}; return new com.google.gwtorm.client.Key<?>[] {categoryId, groupId};
@@ -76,6 +80,18 @@ public final class ProjectRight {
return key; return key;
} }
public Project.Id getProjectId() {
return key.projectId;
}
public ApprovalCategory.Id getApprovalCategoryId() {
return key.categoryId;
}
public AccountGroup.Id getAccountGroupId() {
return key.groupId;
}
public short getMinValue() { public short getMinValue() {
return minValue; return minValue;
} }

View File

@@ -203,8 +203,18 @@ public class ImportGerrit1 {
final ProjectRight.Key key = final ProjectRight.Key key =
new ProjectRight.Key(proj.getId(), category.getId(), groupId); new ProjectRight.Key(proj.getId(), category.getId(), groupId);
final ProjectRight pr = new ProjectRight(key); final ProjectRight pr = new ProjectRight(key);
pr.setMinValue(Short.MIN_VALUE); if (category == approveCategory) {
pr.setMaxValue(Short.MAX_VALUE); pr.setMinValue((short) -2);
pr.setMaxValue((short) 2);
} else if (category == verifyCategory) {
pr.setMinValue((short) -1);
pr.setMaxValue((short) 1);
} else if (category == submitCategory) {
pr.setMinValue((short) 1);
pr.setMaxValue((short) 1);
} else {
throw new OrmException("Cannot import category " + category.getId());
}
db.projectRights().insert(Collections.singleton(pr)); db.projectRights().insert(Collections.singleton(pr));
} }

View File

@@ -543,3 +543,10 @@
padding: 5px 5px 5px 5px; padding: 5px 5px 5px 5px;
border: 1px solid #b0bdcc; border: 1px solid #b0bdcc;
} }
.gerrit-ProjectAdmin-ApprovalCategoryRangeLine {
white-space: nowrap;
}
.gerrit-ProjectAdmin-ApprovalCategoryValue {
font-family: Courier New, Courier, monospace;
}

View File

@@ -19,6 +19,7 @@ import com.google.gerrit.client.admin.ProjectDetail;
import com.google.gerrit.client.reviewdb.Account; import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup; import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.Project; import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.reviewdb.ReviewDb; import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.BaseServiceImplementation; import com.google.gerrit.client.rpc.BaseServiceImplementation;
import com.google.gerrit.client.rpc.NoSuchEntityException; import com.google.gerrit.client.rpc.NoSuchEntityException;
@@ -28,9 +29,12 @@ import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.OrmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
public class ProjectAdminServiceImpl extends BaseServiceImplementation public class ProjectAdminServiceImpl extends BaseServiceImplementation
implements ProjectAdminService { implements ProjectAdminService {
@@ -116,6 +120,33 @@ public class ProjectAdminServiceImpl extends BaseServiceImplementation
}); });
} }
public void deleteRight(final Set<ProjectRight.Key> keys,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
final Set<Project.Id> owned = ids(myOwnedProjects(db));
Boolean amAdmin = null;
for (final ProjectRight.Key k : keys) {
if (!owned.contains(k.getProjectId())) {
if (amAdmin == null) {
amAdmin = groupCache.isAdministrator(RpcUtil.getAccountId());
}
if (!amAdmin) {
throw new Failure(new NoSuchEntityException());
}
}
}
for (final ProjectRight.Key k : keys) {
final ProjectRight m = db.projectRights().get(k);
if (m != null) {
db.projectRights().delete(Collections.singleton(m));
}
}
return VoidResult.INSTANCE;
}
});
}
private void assertAmProjectOwner(final ReviewDb db, private void assertAmProjectOwner(final ReviewDb db,
final Project.Id projectId) throws OrmException, Failure { final Project.Id projectId) throws OrmException, Failure {
final Project project = db.projects().get(projectId); final Project project = db.projects().get(projectId);
@@ -139,4 +170,12 @@ public class ProjectAdminServiceImpl extends BaseServiceImplementation
} }
return own; return own;
} }
private static Set<Project.Id> ids(final Collection<Project> projectList) {
final HashSet<Project.Id> r = new HashSet<Project.Id>();
for (final Project project : projectList) {
r.add(project.getId());
}
return r;
}
} }