Closes #205: action: remote tag

This commit is contained in:
Benjamin Gamard 2018-03-13 14:09:39 +01:00
parent 995e45d28f
commit 2678ff4477
11 changed files with 204 additions and 34 deletions

View File

@ -9,5 +9,10 @@ public enum ActionType {
/** /**
* Add a tag. * Add a tag.
*/ */
ADD_TAG ADD_TAG,
/**
* Remove a tag.
*/
REMOVE_TAG
} }

View File

@ -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.dao.jpa.dto.DocumentDto;
import com.sismics.docs.core.util.action.Action; import com.sismics.docs.core.util.action.Action;
import com.sismics.docs.core.util.action.AddTagAction; import com.sismics.docs.core.util.action.AddTagAction;
import com.sismics.docs.core.util.action.RemoveTagAction;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.json.JsonObject; import javax.json.JsonObject;
@ -19,6 +20,41 @@ public class ActionUtil {
*/ */
private static final org.slf4j.Logger log = LoggerFactory.getLogger(LuceneUtil.class); 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. * Execute an action.
* *
@ -27,16 +63,7 @@ public class ActionUtil {
* @param documentDto Document DTO * @param documentDto Document DTO
*/ */
public static void executeAction(ActionType actionType, JsonObject actionData, DocumentDto documentDto) { public static void executeAction(ActionType actionType, JsonObject actionData, DocumentDto documentDto) {
Action action; Action action = findAction(actionType);
switch (actionType) {
case ADD_TAG:
action = new AddTagAction();
break;
default:
log.error("Action type not handled: " + actionType);
return;
}
action.execute(documentDto, actionData); action.execute(documentDto, actionData);
} }
} }

View File

@ -17,4 +17,12 @@ public interface Action {
* @param action Action data * @param action Action data
*/ */
void execute(DocumentDto documentDto, JsonObject action); void execute(DocumentDto documentDto, JsonObject action);
/**
* Validate the action.
*
* @param action Action data
* @throws Exception Validation error
*/
void validate(JsonObject action) throws Exception;
} }

View File

@ -15,7 +15,7 @@ import java.util.Set;
* *
* @author bgamard * @author bgamard
*/ */
public class AddTagAction implements Action { public class AddTagAction extends TagAction {
@Override @Override
public void execute(DocumentDto documentDto, JsonObject action) { public void execute(DocumentDto documentDto, JsonObject action) {
if (action.getString("tag") == null) { if (action.getString("tag") == null) {

View File

@ -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<TagDto> tagDtoList = tagDao.findByCriteria(new TagCriteria().setDocumentId(documentDto.getId()), null);
Set<String> tagIdSet = Sets.newHashSet();
for (TagDto tagDto : tagDtoList) {
tagIdSet.add(tagDto.getId());
}
tagIdSet.remove(tagId);
tagDao.updateTagList(documentDto.getId(), tagIdSet);
}
}

View File

@ -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<TagDto> tagDtoList = tagDao.findByCriteria(new TagCriteria().setId(tagId), null);
if (tagDtoList.size() != 1) {
throw new Exception(tagId + " is not a valid tag");
}
}
}

View File

@ -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.TagDao;
import com.sismics.docs.core.dao.jpa.UserDao; 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.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.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.Group;
import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.model.jpa.RouteModel;
import com.sismics.docs.core.model.jpa.User; 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.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException; 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"); throw new ClientException("ValidationError", actionTypeStr + " is not a valid action type");
} }
// Action custom fields // Validate action
if (actionType == ActionType.ADD_TAG) { try {
String tagId = action.getString("tag"); ActionUtil.validateAction(actionType, action);
ValidationUtil.validateRequired(routeStepTransitionStr, "step.transitions.actions.tag"); } catch (Exception e) {
List<TagDto> tagDtoList = tagDao.findByCriteria(new TagCriteria().setId(tagId), null); throw new ClientException("ValidationError", e.getMessage());
if (tagDtoList.size() != 1) {
throw new ClientException("ValidationError", tagId + " is not a valid tag");
}
} }
} }
} }

View File

@ -84,9 +84,17 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial
*/ */
$scope.edit = function () { $scope.edit = function () {
var promise = null; var promise = null;
// Cleanup the workflow data
var workflow = angular.copy($scope.workflow); 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); workflow.steps = JSON.stringify(workflow.steps);
if ($scope.isEdit()) { if ($scope.isEdit()) {
promise = Restangular promise = Restangular
.one('routemodel', $stateParams.id) .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); $scope.workflow.steps.splice($scope.workflow.steps.indexOf(step), 1);
}; };
/**
* Update transitions on a step.
*/
$scope.updateTransitions = function (step) { $scope.updateTransitions = function (step) {
if (step.type === 'VALIDATE') { if (step.type === 'VALIDATE') {
step.transitions = [{ step.transitions = [{
name: 'VALIDATED', name: 'VALIDATED',
actions: [] actions: [],
actionType: 'ADD_TAG'
}]; }];
} else if (step.type === 'APPROVE') { } else if (step.type === 'APPROVE') {
step.transitions = [{ step.transitions = [{
name: 'APPROVED', name: 'APPROVED',
actions: [] actions: [],
actionType: 'ADD_TAG'
}, { }, {
name: 'REJECTED', name: 'REJECTED',
actions: [] actions: [],
actionType: 'ADD_TAG'
}]; }];
} }
}; };
/**
* Add an action.
*/
$scope.addAction = function (transition) { $scope.addAction = function (transition) {
if (_.isUndefined(transition.actionType)) {
return;
}
transition.actions.push({ transition.actions.push({
type: 'ADD_TAG' type: transition.actionType
}); });
}; };
/**
* Remove an action.
*/
$scope.removeAction = function (actions, action) { $scope.removeAction = function (actions, action) {
actions.splice(actions.indexOf(action), 1); actions.splice(actions.indexOf(action), 1);
}; };
// Fetch tags
Restangular.one('tag/list').get().then(function(data) { Restangular.one('tag/list').get().then(function(data) {
$scope.tags = data.tags; $scope.tags = data.tags;
}); });

View File

@ -510,7 +510,8 @@
"no_space": "Spaces are not allowed" "no_space": "Spaces are not allowed"
}, },
"action_type": { "action_type": {
"ADD_TAG": "Add this tag" "ADD_TAG": "Add a tag",
"REMOVE_TAG": "Remove a tag"
}, },
"pagination": { "pagination": {
"previous": "Previous", "previous": "Previous",

View File

@ -100,6 +100,11 @@
<option ng-repeat="tag in tags" value="{{ tag.id }}">{{ tag.name }}</option> <option ng-repeat="tag in tags" value="{{ tag.id }}">{{ tag.name }}</option>
</select> </select>
</div> </div>
<div ng-switch-when="REMOVE_TAG">
<select title="{{ 'action_type.REMOVE_TAG' | translate }}" ng-model="action.tag" required class="form-control">
<option ng-repeat="tag in tags" value="{{ tag.id }}">{{ tag.name }}</option>
</select>
</div>
</div> </div>
<p class="text-center"> <p class="text-center">
<a href ng-click="removeAction(transition.actions, action)"> <a href ng-click="removeAction(transition.actions, action)">
@ -108,8 +113,9 @@
</p> </p>
</div> </div>
<div class="input-group"> <div class="input-group">
<select title="Action type" class="form-control"> <select title="Action type" class="form-control" ng-model="transition.actionType">
<option name="ADD_TAG">{{ 'action_type.ADD_TAG' | translate }}</option> <option value="ADD_TAG">{{ 'action_type.ADD_TAG' | translate }}</option>
<option value="REMOVE_TAG">{{ 'action_type.REMOVE_TAG' | translate }}</option>
</select> </select>
<span class="input-group-addon btn" ng-click="addAction(transition)"> <span class="input-group-addon btn" ng-click="addAction(transition)">
<span class="fas fa-plus-circle"></span> <span class="fas fa-plus-circle"></span>

View File

@ -352,14 +352,14 @@ public class TestRouteResource extends BaseJerseyTest {
} }
/** /**
* Test actions on workflow step. * Test tag actions on workflow step.
*/ */
@Test @Test
public void testAction() { public void testTagActions() {
// Login admin // Login admin
String adminToken = clientUtil.login("admin", "admin", false); String adminToken = clientUtil.login("admin", "admin", false);
// Create a tag // Create an Approved tag
JsonObject json = target().path("/tag").request() JsonObject json = target().path("/tag").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.put(Entity.form(new Form() .put(Entity.form(new Form()
@ -367,12 +367,20 @@ public class TestRouteResource extends BaseJerseyTest {
.param("color", "#ff0000")), JsonObject.class); .param("color", "#ff0000")), JsonObject.class);
String tagApprovedId = json.getString("id"); 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 // Create a new route model with actions
json = target().path("/routemodel").request() json = target().path("/routemodel").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.put(Entity.form(new Form() .put(Entity.form(new Form()
.param("name", "Workflow action 1") .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"); String routeModelId = json.getString("id");
// Create a document // Create a document
@ -381,6 +389,7 @@ public class TestRouteResource extends BaseJerseyTest {
.put(Entity.form(new Form() .put(Entity.form(new Form()
.param("title", "My super title document 1") .param("title", "My super title document 1")
.param("description", "My super description for document 1") .param("description", "My super description for document 1")
.param("tags", tagPendingId)
.param("language", "eng")), JsonObject.class); .param("language", "eng")), JsonObject.class);
String document1Id = json.getString("id"); String document1Id = json.getString("id");
@ -398,7 +407,8 @@ public class TestRouteResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.get(JsonObject.class); .get(JsonObject.class);
JsonArray tags = json.getJsonArray("tags"); 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 // Validate the current step with admin
target().path("/route/validate").request() target().path("/route/validate").request()
@ -407,6 +417,22 @@ public class TestRouteResource extends BaseJerseyTest {
.param("documentId", document1Id) .param("documentId", document1Id)
.param("transition", "APPROVED")), JsonObject.class); .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 // Check tags on document 1
json = target().path("/document/" + document1Id).request() json = target().path("/document/" + document1Id).request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
@ -420,6 +446,7 @@ public class TestRouteResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.put(Entity.form(new Form() .put(Entity.form(new Form()
.param("title", "My super title document 2") .param("title", "My super title document 2")
.param("tags", tagPendingId)
.param("language", "eng")), JsonObject.class); .param("language", "eng")), JsonObject.class);
String document2Id = json.getString("id"); String document2Id = json.getString("id");
@ -439,6 +466,21 @@ public class TestRouteResource extends BaseJerseyTest {
.param("documentId", document2Id) .param("documentId", document2Id)
.param("transition", "REJECTED")), JsonObject.class); .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 // Check tags on document 2
json = target().path("/document/" + document2Id).request() json = target().path("/document/" + document2Id).request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)