diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/ActionType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/ActionType.java index bb51e590..c58222f5 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/ActionType.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/ActionType.java @@ -9,5 +9,10 @@ public enum ActionType { /** * Add a tag. */ - ADD_TAG + ADD_TAG, + + /** + * Remove a tag. + */ + REMOVE_TAG } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java index ca623bef..9df75c19 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java @@ -4,6 +4,7 @@ import com.sismics.docs.core.constant.ActionType; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.util.action.Action; import com.sismics.docs.core.util.action.AddTagAction; +import com.sismics.docs.core.util.action.RemoveTagAction; import org.slf4j.LoggerFactory; import javax.json.JsonObject; @@ -19,6 +20,41 @@ public class ActionUtil { */ private static final org.slf4j.Logger log = LoggerFactory.getLogger(LuceneUtil.class); + /** + * Find the action associated to an action type. + * + * @param actionType Action type + * @return Action + */ + private static Action findAction(ActionType actionType) { + Action action = null; + switch (actionType) { + case ADD_TAG: + action = new AddTagAction(); + break; + case REMOVE_TAG: + action = new RemoveTagAction(); + break; + default: + log.error("Action type not handled: " + actionType); + break; + } + + return action; + } + + /** + * Validate an action. + * + * @param actionType Action type + * @param actionData Action data + * @throws Exception Validation error + */ + public static void validateAction(ActionType actionType, JsonObject actionData) throws Exception { + Action action = findAction(actionType); + action.validate(actionData); + } + /** * Execute an action. * @@ -27,16 +63,7 @@ public class ActionUtil { * @param documentDto Document DTO */ public static void executeAction(ActionType actionType, JsonObject actionData, DocumentDto documentDto) { - Action action; - switch (actionType) { - case ADD_TAG: - action = new AddTagAction(); - break; - default: - log.error("Action type not handled: " + actionType); - return; - } - + Action action = findAction(actionType); action.execute(documentDto, actionData); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java index 25d6e91d..4add6dc6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java @@ -17,4 +17,12 @@ public interface Action { * @param action Action data */ void execute(DocumentDto documentDto, JsonObject action); + + /** + * Validate the action. + * + * @param action Action data + * @throws Exception Validation error + */ + void validate(JsonObject action) throws Exception; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java index 28c5051f..87816298 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java @@ -15,7 +15,7 @@ import java.util.Set; * * @author bgamard */ -public class AddTagAction implements Action { +public class AddTagAction extends TagAction { @Override public void execute(DocumentDto documentDto, JsonObject action) { if (action.getString("tag") == null) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java new file mode 100644 index 00000000..2063b026 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java @@ -0,0 +1,37 @@ +package com.sismics.docs.core.util.action; + +import com.google.common.collect.Sets; +import com.sismics.docs.core.dao.jpa.TagDao; +import com.sismics.docs.core.dao.jpa.criteria.TagCriteria; +import com.sismics.docs.core.dao.jpa.dto.DocumentDto; +import com.sismics.docs.core.dao.jpa.dto.TagDto; + +import javax.json.JsonObject; +import java.util.List; +import java.util.Set; + +/** + * Action to remove a tag. + * + * @author bgamard + */ +public class RemoveTagAction extends TagAction { + @Override + public void execute(DocumentDto documentDto, JsonObject action) { + if (action.getString("tag") == null) { + return; + } + + + String tagId = action.getString("tag"); + TagDao tagDao = new TagDao(); + List tagDtoList = tagDao.findByCriteria(new TagCriteria().setDocumentId(documentDto.getId()), null); + Set tagIdSet = Sets.newHashSet(); + for (TagDto tagDto : tagDtoList) { + tagIdSet.add(tagDto.getId()); + } + tagIdSet.remove(tagId); + + tagDao.updateTagList(documentDto.getId(), tagIdSet); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java new file mode 100644 index 00000000..3e6cad58 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java @@ -0,0 +1,23 @@ +package com.sismics.docs.core.util.action; + +import com.sismics.docs.core.dao.jpa.TagDao; +import com.sismics.docs.core.dao.jpa.criteria.TagCriteria; +import com.sismics.docs.core.dao.jpa.dto.TagDto; + +import javax.json.JsonObject; +import java.util.List; + +public abstract class TagAction implements Action { + @Override + public void validate(JsonObject action) throws Exception { + TagDao tagDao = new TagDao(); + String tagId = action.getString("tag"); + if (tagId == null) { + throw new Exception("step.transitions.actions.tag is required"); + } + List tagDtoList = tagDao.findByCriteria(new TagCriteria().setId(tagId), null); + if (tagDtoList.size() != 1) { + throw new Exception(tagId + " is not a valid tag"); + } + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java index 51f69ee5..14773b97 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java @@ -10,12 +10,11 @@ import com.sismics.docs.core.dao.jpa.RouteModelDao; import com.sismics.docs.core.dao.jpa.TagDao; import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.dao.jpa.criteria.RouteModelCriteria; -import com.sismics.docs.core.dao.jpa.criteria.TagCriteria; import com.sismics.docs.core.dao.jpa.dto.RouteModelDto; -import com.sismics.docs.core.dao.jpa.dto.TagDto; import com.sismics.docs.core.model.jpa.Group; import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.ActionUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ClientException; @@ -232,14 +231,11 @@ public class RouteModelResource extends BaseResource { throw new ClientException("ValidationError", actionTypeStr + " is not a valid action type"); } - // Action custom fields - if (actionType == ActionType.ADD_TAG) { - String tagId = action.getString("tag"); - ValidationUtil.validateRequired(routeStepTransitionStr, "step.transitions.actions.tag"); - List tagDtoList = tagDao.findByCriteria(new TagCriteria().setId(tagId), null); - if (tagDtoList.size() != 1) { - throw new ClientException("ValidationError", tagId + " is not a valid tag"); - } + // Validate action + try { + ActionUtil.validateAction(actionType, action); + } catch (Exception e) { + throw new ClientException("ValidationError", e.getMessage()); } } } diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js index 783a997f..21b50447 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js @@ -84,9 +84,17 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial */ $scope.edit = function () { var promise = null; + + // Cleanup the workflow data var workflow = angular.copy($scope.workflow); + _.each(workflow.steps, function (step) { + _.each(step.transitions, function (transition) { + delete transition.actionType; + }); + }); workflow.steps = JSON.stringify(workflow.steps); + if ($scope.isEdit()) { promise = Restangular .one('routemodel', $stateParams.id) @@ -133,33 +141,50 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial $scope.workflow.steps.splice($scope.workflow.steps.indexOf(step), 1); }; + /** + * Update transitions on a step. + */ $scope.updateTransitions = function (step) { if (step.type === 'VALIDATE') { step.transitions = [{ name: 'VALIDATED', - actions: [] + actions: [], + actionType: 'ADD_TAG' }]; } else if (step.type === 'APPROVE') { step.transitions = [{ name: 'APPROVED', - actions: [] + actions: [], + actionType: 'ADD_TAG' }, { name: 'REJECTED', - actions: [] + actions: [], + actionType: 'ADD_TAG' }]; } }; + /** + * Add an action. + */ $scope.addAction = function (transition) { + if (_.isUndefined(transition.actionType)) { + return; + } + transition.actions.push({ - type: 'ADD_TAG' + type: transition.actionType }); }; + /** + * Remove an action. + */ $scope.removeAction = function (actions, action) { actions.splice(actions.indexOf(action), 1); }; + // Fetch tags Restangular.one('tag/list').get().then(function(data) { $scope.tags = data.tags; }); diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 4cb52a2e..538a036f 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -510,7 +510,8 @@ "no_space": "Spaces are not allowed" }, "action_type": { - "ADD_TAG": "Add this tag" + "ADD_TAG": "Add a tag", + "REMOVE_TAG": "Remove a tag" }, "pagination": { "previous": "Previous", diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html index b557aad9..950da70c 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html @@ -100,6 +100,11 @@ +
+ +

@@ -108,8 +113,9 @@

- + + diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index 36e2fa61..714b5352 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -352,14 +352,14 @@ public class TestRouteResource extends BaseJerseyTest { } /** - * Test actions on workflow step. + * Test tag actions on workflow step. */ @Test - public void testAction() { + public void testTagActions() { // Login admin String adminToken = clientUtil.login("admin", "admin", false); - // Create a tag + // Create an Approved tag JsonObject json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() @@ -367,12 +367,20 @@ public class TestRouteResource extends BaseJerseyTest { .param("color", "#ff0000")), JsonObject.class); String tagApprovedId = json.getString("id"); + // Create a Pending tag + json = target().path("/tag").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "Approved") + .param("color", "#ff0000")), JsonObject.class); + String tagPendingId = json.getString("id"); + // Create a new route model with actions json = target().path("/routemodel").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() .param("name", "Workflow action 1") - .param("steps", "[{\"type\":\"APPROVE\",\"transitions\":[{\"name\":\"APPROVED\",\"actions\":[{\"type\":\"ADD_TAG\",\"tag\":\"" + tagApprovedId + "\"}]},{\"name\":\"REJECTED\",\"actions\":[]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"}]")), JsonObject.class); + .param("steps", "[{\"type\":\"APPROVE\",\"transitions\":[{\"name\":\"APPROVED\",\"actions\":[{\"type\":\"ADD_TAG\",\"tag\":\"" + tagApprovedId + "\"}]},{\"name\":\"REJECTED\",\"actions\":[]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"},{\"type\":\"VALIDATE\",\"transitions\":[{\"name\":\"VALIDATED\",\"actions\":[{\"type\":\"REMOVE_TAG\",\"tag\":\"" + tagPendingId + "\"}]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"}]")), JsonObject.class); String routeModelId = json.getString("id"); // Create a document @@ -381,6 +389,7 @@ public class TestRouteResource extends BaseJerseyTest { .put(Entity.form(new Form() .param("title", "My super title document 1") .param("description", "My super description for document 1") + .param("tags", tagPendingId) .param("language", "eng")), JsonObject.class); String document1Id = json.getString("id"); @@ -398,7 +407,8 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .get(JsonObject.class); JsonArray tags = json.getJsonArray("tags"); - Assert.assertEquals(0, tags.size()); + Assert.assertEquals(1, tags.size()); + Assert.assertEquals(tagPendingId, tags.getJsonObject(0).getString("id")); // Validate the current step with admin target().path("/route/validate").request() @@ -407,6 +417,22 @@ public class TestRouteResource extends BaseJerseyTest { .param("documentId", document1Id) .param("transition", "APPROVED")), JsonObject.class); + // Check tags on document 1 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + tags = json.getJsonArray("tags"); + Assert.assertEquals(2, tags.size()); + Assert.assertEquals(tagApprovedId, tags.getJsonObject(0).getString("id")); + Assert.assertEquals(tagPendingId, tags.getJsonObject(1).getString("id")); + + // Validate the current step with admin + target().path("/route/validate").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("documentId", document1Id) + .param("transition", "VALIDATED")), JsonObject.class); + // Check tags on document 1 json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) @@ -420,6 +446,7 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() .param("title", "My super title document 2") + .param("tags", tagPendingId) .param("language", "eng")), JsonObject.class); String document2Id = json.getString("id"); @@ -439,6 +466,21 @@ public class TestRouteResource extends BaseJerseyTest { .param("documentId", document2Id) .param("transition", "REJECTED")), JsonObject.class); + // Check tags on document 2 + json = target().path("/document/" + document2Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + tags = json.getJsonArray("tags"); + Assert.assertEquals(1, tags.size()); + Assert.assertEquals(tagPendingId, tags.getJsonObject(0).getString("id")); + + // Validate the current step with admin + target().path("/route/validate").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("documentId", document2Id) + .param("transition", "VALIDATED")), JsonObject.class); + // Check tags on document 2 json = target().path("/document/" + document2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)