From 97a71565f6ff5d9722788559ce638863a9e618ab Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 19 Oct 2012 22:47:33 -0400
Subject: [PATCH] New permissions UI for EditRepository (issue 36)

---
 src/com/gitblit/models/UserAccessPermission.java        |   51 +++++
 src/com/gitblit/GitBlit.java                            |   75 +++++++
 src/com/gitblit/wicket/pages/EditRepositoryPage.java    |   48 +---
 src/com/gitblit/wicket/panels/UserPermissionsPanel.java |  157 +++++++++++++++
 src/com/gitblit/wicket/panels/TeamPermissionsPanel.html |   24 ++
 src/com/gitblit/models/TeamAccessPermission.java        |   51 +++++
 src/com/gitblit/wicket/pages/EditRepositoryPage.html    |    1 
 src/com/gitblit/wicket/panels/UserPermissionsPanel.html |   24 ++
 src/com/gitblit/wicket/panels/TeamPermissionsPanel.java |  157 +++++++++++++++
 9 files changed, 551 insertions(+), 37 deletions(-)

diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index a121703..af13e02 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -89,7 +89,9 @@
 import com.gitblit.models.ServerSettings;
 import com.gitblit.models.ServerStatus;
 import com.gitblit.models.SettingModel;
+import com.gitblit.models.TeamAccessPermission;
 import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserAccessPermission;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.ByteFormat;
@@ -630,12 +632,44 @@
 	}
 
 	/**
-	 * Returns the list of all users who are allowed to bypass the access
-	 * restriction placed on the specified repository.
+	 * Returns the list of users and their access permissions for the specified repository.
+	 * 
+	 * @param repository
+	 * @return a list of User-AccessPermission tuples
+	 */
+	public List<UserAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
+		List<UserAccessPermission> permissions = new ArrayList<UserAccessPermission>();
+		for (String user : userService.getUsernamesForRepositoryRole(repository.name)) {
+			AccessPermission ap = userService.getUserModel(user).getRepositoryPermission(repository);
+			permissions.add(new UserAccessPermission(user, ap));
+		}
+		return permissions;
+	}
+	
+	/**
+	 * Sets the access permissions to the specified repository for the specified users.
+	 * 
+	 * @param repository
+	 * @param permissions
+	 * @return true if the user models have been updated
+	 */
+	public boolean setUserAccessPermissions(RepositoryModel repository, List<UserAccessPermission> permissions) {
+		List<UserModel> users = new ArrayList<UserModel>();
+		for (UserAccessPermission up : permissions) {
+			UserModel user = userService.getUserModel(up.user);
+			user.setRepositoryPermission(repository.name, up.permission);
+			users.add(user);
+		}
+		return userService.updateUserModels(users);
+	}
+	
+	/**
+	 * Returns the list of all users who have an explicit access permission
+	 * for the specified repository.
 	 * 
 	 * @see IUserService.getUsernamesForRepositoryRole(String)
 	 * @param repository
-	 * @return list of all usernames that can bypass the access restriction
+	 * @return list of all usernames that have an access permission for the repository
 	 */
 	public List<String> getRepositoryUsers(RepositoryModel repository) {
 		return userService.getUsernamesForRepositoryRole(repository.name);
@@ -726,7 +760,39 @@
 	public TeamModel getTeamModel(String teamname) {
 		return userService.getTeamModel(teamname);
 	}
-
+	
+	/**
+	 * Returns the list of teams and their access permissions for the specified repository.
+	 * 
+	 * @param repository
+	 * @return a list of Team-AccessPermission tuples
+	 */
+	public List<TeamAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
+		List<TeamAccessPermission> permissions = new ArrayList<TeamAccessPermission>();
+		for (String team : userService.getTeamnamesForRepositoryRole(repository.name)) {
+			AccessPermission ap = userService.getTeamModel(team).getRepositoryPermission(repository);
+			permissions.add(new TeamAccessPermission(team, ap));
+		}
+		return permissions;
+	}
+	
+	/**
+	 * Sets the access permissions to the specified repository for the specified teams.
+	 * 
+	 * @param repository
+	 * @param permissions
+	 * @return true if the team models have been updated
+	 */
+	public boolean setTeamAccessPermissions(RepositoryModel repository, List<TeamAccessPermission> permissions) {
+		List<TeamModel> teams = new ArrayList<TeamModel>();
+		for (TeamAccessPermission tp : permissions) {
+			TeamModel team = userService.getTeamModel(tp.team);
+			team.setRepositoryPermission(repository.name, tp.permission);
+			teams.add(team);
+		}
+		return userService.updateTeamModels(teams);
+	}
+	
 	/**
 	 * Returns the list of all teams who are allowed to bypass the access
 	 * restriction placed on the specified repository.
@@ -735,6 +801,7 @@
 	 * @param repository
 	 * @return list of all teamnames that can bypass the access restriction
 	 */
+	@Deprecated
 	public List<String> getRepositoryTeams(RepositoryModel repository) {
 		return userService.getTeamnamesForRepositoryRole(repository.name);
 	}
diff --git a/src/com/gitblit/models/TeamAccessPermission.java b/src/com/gitblit/models/TeamAccessPermission.java
new file mode 100644
index 0000000..23468c6
--- /dev/null
+++ b/src/com/gitblit/models/TeamAccessPermission.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * 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.gitblit.models;
+
+import java.io.Serializable;
+
+import com.gitblit.Constants.AccessPermission;
+
+/**
+ * Represents a Team-AccessPermission tuple.
+ * 
+ * @author James Moger
+ */
+public class TeamAccessPermission implements Serializable, Comparable<TeamAccessPermission> {
+
+	private static final long serialVersionUID = 1L;
+
+	public String team;
+	public AccessPermission permission;
+
+	public TeamAccessPermission() {
+	}
+	
+	public TeamAccessPermission(String team, AccessPermission permission) {
+		this.team = team;
+		this.permission = permission;
+	}
+	
+	@Override
+	public int compareTo(TeamAccessPermission p) {
+		return team.compareTo(p.team);
+	}
+	
+	@Override
+	public String toString() {
+		return permission.asRole("@" + team);
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/models/UserAccessPermission.java b/src/com/gitblit/models/UserAccessPermission.java
new file mode 100644
index 0000000..a77fff2
--- /dev/null
+++ b/src/com/gitblit/models/UserAccessPermission.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * 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.gitblit.models;
+
+import java.io.Serializable;
+
+import com.gitblit.Constants.AccessPermission;
+
+/**
+ * Represents a User-AccessPermission tuple.
+ * 
+ * @author James Moger
+ */
+public class UserAccessPermission implements Serializable, Comparable<UserAccessPermission> {
+
+	private static final long serialVersionUID = 1L;
+
+	public String user;
+	public AccessPermission permission;
+
+	public UserAccessPermission() {
+	}
+	
+	public UserAccessPermission(String user, AccessPermission permission) {
+		this.user = user;
+		this.permission = permission;
+	}
+	
+	@Override
+	public int compareTo(UserAccessPermission p) {
+		return user.compareTo(p.user);
+	}
+	
+	@Override
+	public String toString() {
+		return permission.asRole(user);
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
index 20b77e5..9a98e16 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -38,6 +38,7 @@
 				<tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="18" /> &nbsp;<span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span></label></td></tr>
 				<tr><th colspan="2"><hr/></th></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
+				<tr><th colspan="2"><hr/></th></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
 				<tr><td colspan="2"><h3><wicket:message key="gb.federation"></wicket:message> &nbsp;<small><wicket:message key="gb.federationRepositoryDescription"></wicket:message></small></h3></td></tr>	
 				<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="19" /></td></tr>
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
index a2dad10..4e34d89 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -55,6 +55,8 @@
 import com.gitblit.GitBlitException;
 import com.gitblit.Keys;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamAccessPermission;
+import com.gitblit.models.UserAccessPermission;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
@@ -62,6 +64,8 @@
 import com.gitblit.wicket.StringChoiceRenderer;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.BulletListPanel;
+import com.gitblit.wicket.panels.TeamPermissionsPanel;
+import com.gitblit.wicket.panels.UserPermissionsPanel;
 
 public class EditRepositoryPage extends RootSubPage {
 
@@ -94,6 +98,7 @@
 		}
 		
 		setupPage(model);
+		setStatelessHint(false);
 	}
 
 	public EditRepositoryPage(PageParameters params) {
@@ -103,6 +108,7 @@
 		String name = WicketUtils.getRepositoryName(params);
 		RepositoryModel model = GitBlit.self().getRepositoryModel(name);
 		setupPage(model);
+		setStatelessHint(false);
 	}
 
 	protected void setupPage(final RepositoryModel repositoryModel) {
@@ -111,8 +117,8 @@
 
 		List<String> indexedBranches = new ArrayList<String>();
 		List<String> federationSets = new ArrayList<String>();
-		List<String> repositoryUsers = new ArrayList<String>();
-		List<String> repositoryTeams = new ArrayList<String>();
+		final List<UserAccessPermission> repositoryUsers = new ArrayList<UserAccessPermission>();
+		final List<TeamAccessPermission> repositoryTeams = new ArrayList<TeamAccessPermission>();
 		List<String> preReceiveScripts = new ArrayList<String>();
 		List<String> postReceiveScripts = new ArrayList<String>();
 
@@ -128,8 +134,8 @@
 		} else {
 			super.setupPage(getString("gb.edit"), repositoryModel.name);
 			if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
-				repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel));
-				repositoryTeams.addAll(GitBlit.self().getRepositoryTeams(repositoryModel));
+				repositoryUsers.addAll(GitBlit.self().getUserAccessPermissions(repositoryModel));
+				repositoryTeams.addAll(GitBlit.self().getTeamAccessPermissions(repositoryModel));
 				Collections.sort(repositoryUsers);
 			}
 			federationSets.addAll(repositoryModel.federationSets);
@@ -139,15 +145,9 @@
 		}
 
 		final String oldName = repositoryModel.name;
-		// users palette
-		final Palette<String> usersPalette = new Palette<String>("users", new ListModel<String>(
-				repositoryUsers), new CollectionModel<String>(GitBlit.self().getAllUsernames()),
-				new StringChoiceRenderer(), 10, false);
 
-		// teams palette
-		final Palette<String> teamsPalette = new Palette<String>("teams", new ListModel<String>(
-				repositoryTeams), new CollectionModel<String>(GitBlit.self().getAllTeamnames()),
-				new StringChoiceRenderer(), 8, false);
+		UserPermissionsPanel usersPalette = new UserPermissionsPanel("users", repositoryUsers, getAccessPermissions());
+		TeamPermissionsPanel teamsPalette = new TeamPermissionsPanel("teams", repositoryTeams, getAccessPermissions());
 
 		// indexed local branches palette
 		List<String> allLocalBranches = new ArrayList<String>();
@@ -342,28 +342,10 @@
 					// save the repository
 					GitBlit.self().updateRepositoryModel(oldName, repositoryModel, isCreate);
 
-					// repository access
+					// repository access permissions
 					if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
-						// save the user access list
-						Iterator<String> users = usersPalette.getSelectedChoices();
-						List<String> repositoryUsers = new ArrayList<String>();
-						while (users.hasNext()) {
-							repositoryUsers.add(users.next());
-						}
-						// ensure the owner is added to the user list
-						if (repositoryModel.owner != null
-								&& !repositoryUsers.contains(repositoryModel.owner)) {
-							repositoryUsers.add(repositoryModel.owner);
-						}
-						GitBlit.self().setRepositoryUsers(repositoryModel, repositoryUsers);
-
-						// save the team access list
-						Iterator<String> teams = teamsPalette.getSelectedChoices();
-						List<String> repositoryTeams = new ArrayList<String>();
-						while (teams.hasNext()) {
-							repositoryTeams.add(teams.next());
-						}
-						GitBlit.self().setRepositoryTeams(repositoryModel, repositoryTeams);
+						GitBlit.self().setUserAccessPermissions(repositoryModel, repositoryUsers);
+						GitBlit.self().setTeamAccessPermissions(repositoryModel, repositoryTeams);
 					}
 				} catch (GitBlitException e) {
 					error(e.getMessage());
diff --git a/src/com/gitblit/wicket/panels/TeamPermissionsPanel.html b/src/com/gitblit/wicket/panels/TeamPermissionsPanel.html
new file mode 100644
index 0000000..d728f65
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/TeamPermissionsPanel.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+
+	<div wicket:id="permissionRow">
+		<div style="padding-top:10px" class="row-fluid">
+			<span class="span8" wicket:id="team"></span> <select class="input-medium" wicket:id="permission"></select>
+		</div>
+	</div>
+
+	<div style="padding-top:15px;" class="row-fluid">
+		<form class="well form-inline" wicket:id="addPermissionForm">
+			<select class="input-large" wicket:id="team"></select> <select class="input-medium" wicket:id="permission"></select> <input class="btn btn-success" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addPermissionButton"/>
+		</form>
+	</div>	
+	
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/TeamPermissionsPanel.java b/src/com/gitblit/wicket/panels/TeamPermissionsPanel.java
new file mode 100644
index 0000000..e51aab4
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/TeamPermissionsPanel.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * 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.gitblit.wicket.panels;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.OddEvenItem;
+import org.apache.wicket.markup.repeater.RefreshingView;
+import org.apache.wicket.markup.repeater.util.ModelIteratorAdapter;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.GitBlit;
+import com.gitblit.models.TeamAccessPermission;
+import com.gitblit.utils.DeepCopier;
+
+/**
+ * Allows user to manipulate user access permissions.
+ * 
+ * @author James Moger
+ *
+ */
+public class TeamPermissionsPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public TeamPermissionsPanel(String wicketId, final List<TeamAccessPermission> permissions, final Map<AccessPermission, String> translations) {
+		super(wicketId);
+		
+		// update existing permissions repeater
+		RefreshingView<TeamAccessPermission> dataView = new RefreshingView<TeamAccessPermission>("permissionRow") {
+			private static final long serialVersionUID = 1L;
+		
+			@Override
+            protected Iterator<IModel<TeamAccessPermission>> getItemModels() {
+                // the iterator returns RepositoryPermission objects, but we need it to
+                // return models
+                return new ModelIteratorAdapter<TeamAccessPermission>(permissions.iterator()) {
+                    @Override
+                    protected IModel<TeamAccessPermission> model(TeamAccessPermission permission) {
+                        return new CompoundPropertyModel<TeamAccessPermission>(permission);
+                    }
+                };
+            }
+
+            @Override
+            protected Item<TeamAccessPermission> newItem(String id, int index, IModel<TeamAccessPermission> model) {
+                // this item sets markup class attribute to either 'odd' or
+                // 'even' for decoration
+                return new OddEvenItem<TeamAccessPermission>(id, index, model);
+            }
+            
+			public void populateItem(final Item<TeamAccessPermission> item) {
+				final TeamAccessPermission entry = item.getModelObject();
+				item.add(new Label("team", entry.team));
+
+				// use ajax to get immediate update of permission level change
+				// otherwise we can lose it if they change levels and then add
+				// a new repository permission
+				final DropDownChoice<AccessPermission> permissionChoice = new DropDownChoice<AccessPermission>(
+						"permission", Arrays.asList(AccessPermission.values()), new AccessPermissionRenderer(translations));
+				permissionChoice.add(new AjaxFormComponentUpdatingBehavior("onchange") {
+		           
+					private static final long serialVersionUID = 1L;
+
+					protected void onUpdate(AjaxRequestTarget target) {
+		                target.addComponent(permissionChoice);
+		            }
+		        });
+
+				item.add(permissionChoice);
+			}
+		};
+		add(dataView);
+		setOutputMarkupId(true);
+
+		// filter out teams we already have permissions for
+		final List<String> teams = GitBlit.self().getAllTeamnames();
+		for (TeamAccessPermission tp : permissions) {
+			teams.remove(tp.team);
+		}
+
+		// add new permission form
+		IModel<TeamAccessPermission> addPermissionModel = new CompoundPropertyModel<TeamAccessPermission>(new TeamAccessPermission());
+		Form<TeamAccessPermission> addPermissionForm = new Form<TeamAccessPermission>("addPermissionForm", addPermissionModel);
+		addPermissionForm.add(new DropDownChoice<String>("team", teams));
+		addPermissionForm.add(new DropDownChoice<AccessPermission>("permission", Arrays
+				.asList(AccessPermission.NEWPERMISSIONS), new AccessPermissionRenderer(translations)));
+		AjaxButton button = new AjaxButton("addPermissionButton", addPermissionForm) {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+				// add permission to our list
+				TeamAccessPermission tp = (TeamAccessPermission) form.getModel().getObject();
+				permissions.add(DeepCopier.copy(tp));
+				
+				// remove team from available choices
+				teams.remove(tp.team);
+				
+				// force the panel to refresh
+				target.addComponent(TeamPermissionsPanel.this);
+			}
+		};
+		addPermissionForm.add(button);
+		
+		// only show add permission form if we have a team choice
+		add(addPermissionForm.setVisible(teams.size() > 0));
+	}
+	
+	private class AccessPermissionRenderer implements IChoiceRenderer<AccessPermission> {
+
+		private static final long serialVersionUID = 1L;
+
+		private final Map<AccessPermission, String> map;
+
+		public AccessPermissionRenderer(Map<AccessPermission, String> map) {
+			this.map = map;
+		}
+
+		@Override
+		public String getDisplayValue(AccessPermission type) {
+			return map.get(type);
+		}
+
+		@Override
+		public String getIdValue(AccessPermission type, int index) {
+			return Integer.toString(index);
+		}
+	}
+}
diff --git a/src/com/gitblit/wicket/panels/UserPermissionsPanel.html b/src/com/gitblit/wicket/panels/UserPermissionsPanel.html
new file mode 100644
index 0000000..14d4305
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/UserPermissionsPanel.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+
+	<div wicket:id="permissionRow">
+		<div style="padding-top:10px" class="row-fluid">
+			<span class="span8" wicket:id="user"></span> <select class="input-medium" wicket:id="permission"></select>
+		</div>
+	</div>
+
+	<div style="padding-top:15px;" class="row-fluid">
+		<form class="well form-inline" wicket:id="addPermissionForm">
+			<select class="input-large" wicket:id="user"></select> <select class="input-medium" wicket:id="permission"></select> <input class="btn btn-success" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addPermissionButton"/>
+		</form>
+	</div>	
+	
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/UserPermissionsPanel.java b/src/com/gitblit/wicket/panels/UserPermissionsPanel.java
new file mode 100644
index 0000000..6d0ae58
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/UserPermissionsPanel.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * 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.gitblit.wicket.panels;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.OddEvenItem;
+import org.apache.wicket.markup.repeater.RefreshingView;
+import org.apache.wicket.markup.repeater.util.ModelIteratorAdapter;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.GitBlit;
+import com.gitblit.models.UserAccessPermission;
+import com.gitblit.utils.DeepCopier;
+
+/**
+ * Allows user to manipulate user access permissions.
+ * 
+ * @author James Moger
+ *
+ */
+public class UserPermissionsPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public UserPermissionsPanel(String wicketId, final List<UserAccessPermission> permissions, final Map<AccessPermission, String> translations) {
+		super(wicketId);
+		
+		// update existing permissions repeater
+		RefreshingView<UserAccessPermission> dataView = new RefreshingView<UserAccessPermission>("permissionRow") {
+			private static final long serialVersionUID = 1L;
+		
+			@Override
+            protected Iterator<IModel<UserAccessPermission>> getItemModels() {
+                // the iterator returns RepositoryPermission objects, but we need it to
+                // return models
+                return new ModelIteratorAdapter<UserAccessPermission>(permissions.iterator()) {
+                    @Override
+                    protected IModel<UserAccessPermission> model(UserAccessPermission permission) {
+                        return new CompoundPropertyModel<UserAccessPermission>(permission);
+                    }
+                };
+            }
+
+            @Override
+            protected Item<UserAccessPermission> newItem(String id, int index, IModel<UserAccessPermission> model) {
+                // this item sets markup class attribute to either 'odd' or
+                // 'even' for decoration
+                return new OddEvenItem<UserAccessPermission>(id, index, model);
+            }
+            
+			public void populateItem(final Item<UserAccessPermission> item) {
+				final UserAccessPermission entry = item.getModelObject();
+				item.add(new Label("user", entry.user));
+
+				// use ajax to get immediate update of permission level change
+				// otherwise we can lose it if they change levels and then add
+				// a new repository permission
+				final DropDownChoice<AccessPermission> permissionChoice = new DropDownChoice<AccessPermission>(
+						"permission", Arrays.asList(AccessPermission.values()), new AccessPermissionRenderer(translations));
+				permissionChoice.add(new AjaxFormComponentUpdatingBehavior("onchange") {
+		           
+					private static final long serialVersionUID = 1L;
+
+					protected void onUpdate(AjaxRequestTarget target) {
+		                target.addComponent(permissionChoice);
+		            }
+		        });
+
+				item.add(permissionChoice);
+			}
+		};
+		add(dataView);
+		setOutputMarkupId(true);
+
+		// filter out users we already have permissions for
+		final List<String> users = GitBlit.self().getAllUsernames();
+		for (UserAccessPermission up : permissions) {
+			users.remove(up.user);
+		}
+
+		// add new permission form
+		IModel<UserAccessPermission> addPermissionModel = new CompoundPropertyModel<UserAccessPermission>(new UserAccessPermission());
+		Form<UserAccessPermission> addPermissionForm = new Form<UserAccessPermission>("addPermissionForm", addPermissionModel);
+		addPermissionForm.add(new DropDownChoice<String>("user", users));
+		addPermissionForm.add(new DropDownChoice<AccessPermission>("permission", Arrays
+				.asList(AccessPermission.NEWPERMISSIONS), new AccessPermissionRenderer(translations)));
+		AjaxButton button = new AjaxButton("addPermissionButton", addPermissionForm) {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+				// add permission to our list
+				UserAccessPermission up = (UserAccessPermission) form.getModel().getObject();
+				permissions.add(DeepCopier.copy(up));
+				
+				// remove user from available choices
+				users.remove(up.user);
+				
+				// force the panel to refresh
+				target.addComponent(UserPermissionsPanel.this);
+			}
+		};
+		addPermissionForm.add(button);
+		
+		// only show add permission form if we have a user choice
+		add(addPermissionForm.setVisible(users.size() > 0));
+	}
+	
+	private class AccessPermissionRenderer implements IChoiceRenderer<AccessPermission> {
+
+		private static final long serialVersionUID = 1L;
+
+		private final Map<AccessPermission, String> map;
+
+		public AccessPermissionRenderer(Map<AccessPermission, String> map) {
+			this.map = map;
+		}
+
+		@Override
+		public String getDisplayValue(AccessPermission type) {
+			return map.get(type);
+		}
+
+		@Override
+		public String getIdValue(AccessPermission type, int index) {
+			return Integer.toString(index);
+		}
+	}
+}

--
Gitblit v1.9.1