#18: Add/update/delete groups

This commit is contained in:
jendib 2016-03-20 15:09:34 +01:00
parent 21b3ba2bf6
commit 689a4e6aae
15 changed files with 450 additions and 10 deletions

View File

@ -48,6 +48,23 @@ public class GroupDao {
}
}
/**
* Returns a group by ID.
*
* @param id Group ID
* @return Group
*/
public Group getActiveById(String id) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select g from Group g where g.id = :id and g.deleteDate is null");
q.setParameter("id", id);
try {
return (Group) q.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
/**
* Creates a new group.
*
@ -99,6 +116,10 @@ public class GroupDao {
q.setParameter("dateNow", dateNow);
q.executeUpdate();
q = em.createQuery("update Group g set g.parentId = null where g.parentId = :groupId and g.deleteDate is null");
q.setParameter("groupId", groupDb.getId());
q.executeUpdate();
// Create audit log
AuditLogUtil.create(groupDb, AuditLogType.DELETE, userId);
}
@ -211,7 +232,7 @@ public class GroupDao {
return groupDtoList;
}
/**
* Recursively search group's parents.
*
@ -232,5 +253,30 @@ public class GroupDao {
}
}
}
/**
* Update a group.
*
* @param group Group to update
* @param userId User ID
* @return Updated group
*/
public Group update(Group group, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the group
Query q = em.createQuery("select g from Group g where g.id = :id and g.deleteDate is null");
q.setParameter("id", group.getId());
Group groupFromDb = (Group) q.getSingleResult();
// Update the group
groupFromDb.setName(group.getName());
groupFromDb.setParentId(group.getParentId());
// Create audit log
AuditLogUtil.create(groupFromDb, AuditLogType.UPDATE, userId);
return groupFromDb;
}
}

View File

@ -9,6 +9,7 @@ import javax.json.JsonObjectBuilder;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -83,6 +84,87 @@ public class GroupResource extends BaseResource {
return Response.ok().entity(response.build()).build();
}
/**
* Update a group.
*
* @return Response
*/
@POST
@Path("{groupName: [a-zA-Z0-9_]+}")
public Response update(@PathParam("groupName") String groupName,
@FormParam("parent") String parentName,
@FormParam("name") String name) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Validate input
name = ValidationUtil.validateLength(name, "name", 1, 50, false);
ValidationUtil.validateAlphanumeric(name, "name");
// Get the group (by its old name)
GroupDao groupDao = new GroupDao();
Group group = groupDao.getActiveByName(groupName);
if (group == null) {
return Response.status(Status.NOT_FOUND).build();
}
// Avoid duplicates
Group existingGroup = groupDao.getActiveByName(name);
if (existingGroup != null && existingGroup.getId() != group.getId()) {
throw new ClientException("GroupAlreadyExists", MessageFormat.format("This group already exists: {0}", name));
}
// Validate parent
String parentId = null;
if (!Strings.isNullOrEmpty(parentName)) {
Group parentGroup = groupDao.getActiveByName(parentName);
if (parentGroup == null) {
throw new ClientException("ParentGroupNotFound", MessageFormat.format("This group does not exists: {0}", parentName));
}
parentId = parentGroup.getId();
}
// Update the group
groupDao.update(group.setName(name)
.setParentId(parentId), principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/**
* Delete a group.
*
* @return Response
*/
@DELETE
@Path("{groupName: [a-zA-Z0-9_]+}")
public Response delete(@PathParam("groupName") String groupName) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Get the group
GroupDao groupDao = new GroupDao();
Group group = groupDao.getActiveByName(groupName);
if (group == null) {
return Response.status(Status.NOT_FOUND).build();
}
// Delete the group
groupDao.delete(group.getId(), principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/**
* Add a user to a group.
*
@ -213,4 +295,40 @@ public class GroupResource extends BaseResource {
.add("groups", groups);
return Response.ok().entity(response.build()).build();
}
/**
* Get a group.
*
* @param groupName Group name
* @return Response
*/
@GET
@Path("{groupName: [a-zA-Z0-9_]+}")
public Response get(@PathParam("groupName") String groupName) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Get the group
GroupDao groupDao = new GroupDao();
Group group = groupDao.getActiveByName(groupName);
if (group == null) {
return Response.status(Status.NOT_FOUND).build();
}
// Build the response
JsonObjectBuilder response = Json.createObjectBuilder()
.add("name", group.getName());
// Get the parent
if (group.getParentId() != null) {
Group parentGroup = groupDao.getActiveById(group.getParentId());
response.add("parent", parentGroup.getName());
}
// TODO Add members
return Response.ok().entity(response.build()).build();
}
}

View File

@ -106,6 +106,33 @@ angular.module('docs',
}
}
})
.state('settings.group', {
url: '/group',
views: {
'settings': {
templateUrl: 'partial/docs/settings.group.html',
controller: 'SettingsGroup'
}
}
})
.state('settings.group.edit', {
url: '/edit/:name',
views: {
'group': {
templateUrl: 'partial/docs/settings.group.edit.html',
controller: 'SettingsGroupEdit'
}
}
})
.state('settings.group.add', {
url: '/add',
views: {
'group': {
templateUrl: 'partial/docs/settings.group.edit.html',
controller: 'SettingsGroupEdit'
}
}
})
.state('settings.vocabulary', {
url: '/vocabulary',
views: {

View File

@ -0,0 +1,24 @@
'use strict';
/**
* Settings group page controller.
*/
angular.module('docs').controller('SettingsGroup', function($scope, $state, Restangular) {
/**
* Load groups from server.
*/
$scope.loadGroups = function() {
Restangular.one('group').get().then(function(data) {
$scope.groups = data.groups;
});
};
$scope.loadGroups();
/**
* Edit a group.
*/
$scope.editGroup = function(group) {
$state.go('settings.group.edit', { name: group.name });
};
});

View File

@ -0,0 +1,87 @@
'use strict';
/**
* Settings group edition page controller.
*/
angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog, $state, $stateParams, Restangular, $q) {
/**
* Returns true if in edit mode (false in add mode).
*/
$scope.isEdit = function() {
return $stateParams.name;
};
/**
* In edit mode, load the current group.
*/
if ($scope.isEdit()) {
Restangular.one('group', $stateParams.name).get().then(function(data) {
$scope.group = data;
});
}
/**
* Update the current group.
*/
$scope.edit = function() {
var promise = null;
var group = angular.copy($scope.group);
if ($scope.isEdit()) {
promise = Restangular
.one('group', $stateParams.name)
.post('', group);
} else {
promise = Restangular
.one('group')
.put(group);
}
promise.then(function() {
$scope.loadGroups();
if ($scope.isEdit()) {
$state.go('settings.group');
} else {
// Go to edit this group to add members
$state.go('settings.group.edit', { name: group.name });
}
});
};
/**
* Delete the current group.
*/
$scope.remove = function () {
var title = 'Delete group';
var msg = 'Do you really want to delete this group?';
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns, function(result) {
if (result == 'ok') {
Restangular.one('group', $stateParams.name).remove().then(function() {
$scope.loadGroups();
$state.go('settings.group');
}, function () {
$state.go('settings.group');
});
}
});
};
/**
* Returns a promise for typeahead group.
*/
$scope.getGroupTypeahead = function($viewValue) {
var deferred = $q.defer();
Restangular.one('group')
.getList('', {
sort_column: 1,
asc: true
}).then(function(data) {
deferred.resolve(_.pluck(_.filter(data.groups, function(group) {
return group.name.indexOf($viewValue) !== -1;
}), 'name'));
});
return deferred.promise;
};
});

View File

@ -31,12 +31,12 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
if ($scope.isEdit()) {
promise = Restangular
.one('user', $stateParams.username)
.post('', user);
.one('user', $stateParams.username)
.post('', user);
} else {
promise = Restangular
.one('user')
.put(user);
.one('user')
.put(user);
}
promise.then(function() {

View File

@ -60,6 +60,8 @@
<script src="app/docs/controller/SettingsLog.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsUser.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsUserEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsGroup.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsGroupEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsVocabulary.js" type="text/javascript"></script>
<script src="app/docs/controller/User.js" type="text/javascript"></script>
<script src="app/docs/controller/UserProfile.js" type="text/javascript"></script>
@ -109,7 +111,7 @@
<a href="#/tag"><span class="glyphicon glyphicon-tags"></span> Tags</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/user.*">
<a href="#/user"><span class="glyphicon glyphicon-user"></span> Users</a>
<a href="#/user"><span class="glyphicon glyphicon-user"></span> Users &amp; Groups</a>
</li>
</ul>

View File

@ -0,0 +1,49 @@
<img src="img/loader.gif" ng-show="!group && isEdit()" />
<div ng-show="group || !isEdit()">
<h2 ng-show="isEdit()">Edit
<small>"{{ group.name }}"</small>
</h2>
<h2 ng-show="!isEdit()">Add
<small>group</small>
</h2>
<form class="form-horizontal" name="editGroupForm" novalidate>
<div class="form-group" ng-class="{ 'has-error': !editGroupForm.name.$valid, success: editGroupForm.name.$valid }">
<label class="col-sm-2 control-label" for="inputName">Name</label>
<div class="col-sm-7">
<input name="name" type="text" id="inputName" required class="form-control"
ng-minlength="3" ng-maxlength="50" placeholder="Name" ng-model="group.name"/>
</div>
<div class="col-sm-3">
<span class="help-block" ng-show="editGroupForm.name.$error.required">Required</span>
<span class="help-block" ng-show="editGroupForm.name.$error.minlength">Too short</span>
<span class="help-block" ng-show="editGroupForm.name.$error.maxlength">Too long</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputName">Parent group</label>
<div class="col-sm-7">
<input name="name" type="text" id="inputParent" class="form-control" autocomplete="off"
placeholder="Type a group name" ng-model="group.parent"
typeahead="group for group in getGroupTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" typeahead-editable="false" />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editGroupForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'Edit' : 'Add' }}
</button>
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
<span class="glyphicon glyphicon-trash"></span> Delete
</button>
</div>
</div>
</form>
<h3>Members</h3>
</div>

View File

@ -0,0 +1,23 @@
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add</a></h1>
<div class="row">
<div class="col-md-4 well">
<table class="table table-striped table-hover table-users">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in groups | orderBy: 'name'" ng-click="editGroup(group)"
ng-class="{ active: $stateParams.name == group.name }">
<td>{{ group.name }}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-8">
<div ui-view="group"></div>
</div>
</div>

View File

@ -12,6 +12,7 @@
<div class="panel-heading" ng-show="isAdmin"><strong>General settings</strong></div>
<ul class="list-group">
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
</ul>

View File

@ -37,6 +37,15 @@
<span class="help-block" ng-show="editUserForm.email.$error.maxlength">Too long</span>
</div>
</div>
<div class="form-group" ng-if="user.groups.length > 0">
<label class="col-sm-2 control-label">Groups</label>
<div class="col-sm-7">
<a class="btn btn-default"
ng-repeat="group in user.groups"
href="#/settings/group/edit/{{ group }}">{{ group }}</a>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !editUserForm.storage_quota.$valid, success: editUserForm.storage_quota.$valid }">
<label class="col-sm-2 control-label" for="inputQuota">Storage quota</label>
@ -90,5 +99,4 @@
</div>
</div>
</form>
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
</div>

View File

@ -10,7 +10,8 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)">
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)"
ng-class="{ active: $stateParams.username == user.username }">
<td>{{ user.username }}</td>
<td>{{ user.create_date | date: 'yyyy-MM-dd' }}</td>
</tr>

View File

@ -2,6 +2,13 @@
<h1>{{ user.username }} <small>{{ user.email }}</small></h1>
</div>
<h4 ng-if="user.groups.length > 0">Groups</h4>
<ul ng-if="user.groups.length > 0">
<li ng-repeat="group in user.groups">
<a href="#/group/{{ group }}">{{ group }}</a>
</li>
</ul>
<h4>Quota used</h4>
<div class="row">
<div class="col-md-6">

View File

@ -71,6 +71,12 @@
.table-users {
tbody tr {
cursor: pointer;
&.active {
td {
background-color: #e8e8e8;
}
}
}
}

View File

@ -108,8 +108,31 @@ public class TestGroupResource extends BaseJerseyTest {
groups = json.getJsonArray("groups");
Assert.assertEquals(4, groups.size());
// Remove group1 from g12
json = target().path("/group/g12/group1").request()
// Update group g12
target().path("/group/g12").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form()
.param("name", "g12new")
.param("parent", "g11")), JsonObject.class);
// Check group1 groups with admin (only direct groups)
json = target().path("/user/group1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.get(JsonObject.class);
groups = json.getJsonArray("groups");
Assert.assertEquals(2, groups.size());
Assert.assertEquals("g112", groups.getString(0));
Assert.assertEquals("g12new", groups.getString(1));
// Get group g12new
json = target().path("/group/g12new").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.get(JsonObject.class);
Assert.assertEquals("g12new", json.getString("name"));
Assert.assertEquals("g11", json.getString("parent"));
// Remove group1 from g12new
json = target().path("/group/g12new/group1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.delete(JsonObject.class);
@ -126,5 +149,23 @@ public class TestGroupResource extends BaseJerseyTest {
Assert.assertTrue(groupList.contains("g1"));
Assert.assertTrue(groupList.contains("g11"));
Assert.assertTrue(groupList.contains("g112"));
// Delete group g1
json = target().path("/group/g1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.delete(JsonObject.class);
// Check group1 groups (all computed groups)
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, group1Token)
.get(JsonObject.class);
groups = json.getJsonArray("groups");
groupList = new ArrayList<>();
for (int i = 0; i < groups.size(); i++) {
groupList.add(groups.getString(i));
}
Assert.assertEquals(2, groups.size());
Assert.assertTrue(groupList.contains("g11"));
Assert.assertTrue(groupList.contains("g112"));
}
}