diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java index f6881005..ed15c8ee 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java @@ -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; + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java index 4beae0e6..778ba2e6 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java @@ -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(); + } } diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 1d29aee4..839420e3 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -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: { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroup.js b/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroup.js new file mode 100644 index 00000000..2f28b1f9 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroup.js @@ -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 }); + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroupEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroupEdit.js new file mode 100644 index 00000000..3bb2f7f2 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroupEdit.js @@ -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; + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js index 792990c2..44b93ea6 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js @@ -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() { diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index c9623ec0..3793ffa0 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -60,6 +60,8 @@ + + @@ -109,7 +111,7 @@ Tags
  • - Users + Users & Groups
  • diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html new file mode 100644 index 00000000..3e4fde42 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html @@ -0,0 +1,49 @@ + + +
    +

    Edit + "{{ group.name }}" +

    +

    Add + group +

    +
    +
    + + +
    + +
    + +
    + Required + Too short + Too long +
    +
    +
    + + +
    + +
    +
    +
    +
    + + +
    +
    +
    + +

    Members

    + +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.html new file mode 100644 index 00000000..284706ec --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.html @@ -0,0 +1,23 @@ +

    Groups management Add

    + +
    +
    + + + + + + + + + + + +
    Name
    {{ group.name }}
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.html b/docs-web/src/main/webapp/src/partial/docs/settings.html index 4bc1a5d0..ced011b6 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.html @@ -12,6 +12,7 @@
    General settings
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index d0208b85..2b1a3500 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -37,6 +37,15 @@ Too long +
    + + +
    + {{ group }} +
    +
    @@ -90,5 +99,4 @@
    - {{ alert.msg }} \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.html index 0d918214..c4fb3c1b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.html @@ -10,7 +10,8 @@ - + {{ user.username }} {{ user.create_date | date: 'yyyy-MM-dd' }} diff --git a/docs-web/src/main/webapp/src/partial/docs/user.profile.html b/docs-web/src/main/webapp/src/partial/docs/user.profile.html index ab7a2765..faae31de 100644 --- a/docs-web/src/main/webapp/src/partial/docs/user.profile.html +++ b/docs-web/src/main/webapp/src/partial/docs/user.profile.html @@ -2,6 +2,13 @@

    {{ user.username }} {{ user.email }}

    +

    Groups

    + +

    Quota used

    diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 43741919..27a29a4c 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -71,6 +71,12 @@ .table-users { tbody tr { cursor: pointer; + + &.active { + td { + background-color: #e8e8e8; + } + } } } diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index 961d9508..eb7b0ee8 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -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")); } } \ No newline at end of file