diff --git a/README.md b/README.md index 5a09598d..793eb728 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Features -------- - Responsive user interface +- Workflow system ![New!](https://www.sismics.com/public/img/new.png) - Optical character recognition - Support image, PDF, ODT and DOCX files - Flexible search engine diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index 9777151e..f3626b33 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -82,4 +82,9 @@ public class Constants { * Email template for password recovery. */ public static final String EMAIL_TEMPLATE_PASSWORD_RECOVERY = "password_recovery"; + + /** + * Email template for route step validate. + */ + public static final String EMAIL_TEMPLATE_ROUTE_STEP_VALIDATE = "route_step_validate"; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java index 1b44a202..4e0a392b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java @@ -149,7 +149,7 @@ public class AclDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Create audit log - Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type"); + Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type and a.deleteDate is null"); q.setParameter("sourceId", sourceId); q.setParameter("perm", perm); q.setParameter("targetId", targetId); @@ -160,7 +160,7 @@ public class AclDao { } // Soft delete the ACLs - q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type"); + q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.type = :type and a.deleteDate is null"); q.setParameter("sourceId", sourceId); q.setParameter("perm", perm); q.setParameter("targetId", targetId); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java index ac078f9c..59e35681 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java @@ -86,4 +86,23 @@ public class RouteDao { } return dtoList; } + + /** + * Deletes a route and the associated steps. + * + * @param routeId Route ID + */ + public void deleteRoute(String routeId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + em.createNativeQuery("update T_ROUTE_STEP rs set rs.RTP_DELETEDATE_D = :dateNow where rs.RTP_IDROUTE_C = :routeId and rs.RTP_DELETEDATE_D is null") + .setParameter("routeId", routeId) + .setParameter("dateNow", new Date()) + .executeUpdate(); + + em.createNativeQuery("update T_ROUTE r set r.RTE_DELETEDATE_D = :dateNow where r.RTE_ID_C = :routeId and r.RTE_DELETEDATE_D is null") + .setParameter("routeId", routeId) + .setParameter("dateNow", new Date()) + .executeUpdate(); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java index 9424df9c..502256d8 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java @@ -68,7 +68,7 @@ public class RouteStepDao { Map parameterMap = new HashMap<>(); List criteriaList = new ArrayList<>(); - StringBuilder sb = new StringBuilder("select rs.RTP_ID_C, rs.RTP_NAME_C c0, rs.RTP_TYPE_C c1, rs.RTP_TRANSITION_C c2, rs.RTP_COMMENT_C c3, rs.RTP_IDTARGET_C c4, u.USE_USERNAME_C as targetUsername, g.GRP_NAME_C, rs.RTP_ENDDATE_D c5, uv.USE_USERNAME_C as validatorUsername, rs.RTP_ORDER_N c6") + StringBuilder sb = new StringBuilder("select rs.RTP_ID_C, rs.RTP_NAME_C c0, rs.RTP_TYPE_C c1, rs.RTP_TRANSITION_C c2, rs.RTP_COMMENT_C c3, rs.RTP_IDTARGET_C c4, u.USE_USERNAME_C as targetUsername, g.GRP_NAME_C, rs.RTP_ENDDATE_D c5, uv.USE_USERNAME_C as validatorUsername, rs.RTP_IDROUTE_C, rs.RTP_ORDER_N c6") .append(" from T_ROUTE_STEP rs ") .append(" join T_ROUTE r on r.RTE_ID_C = rs.RTP_IDROUTE_C ") .append(" left join T_USER uv on uv.USE_ID_C = rs.RTP_IDVALIDATORUSER_C ") @@ -88,7 +88,7 @@ public class RouteStepDao { if (criteria.getEndDateIsNull() != null) { criteriaList.add("RTP_ENDDATE_D is " + (criteria.getEndDateIsNull() ? "" : "not") + " null"); } - criteriaList.add("r.RTE_DELETEDATE_D is null"); + criteriaList.add("rs.RTP_DELETEDATE_D is null"); if (!criteriaList.isEmpty()) { sb.append(" where "); @@ -123,7 +123,8 @@ public class RouteStepDao { } Timestamp endDateTimestamp = (Timestamp) o[i++]; dto.setEndDateTimestamp(endDateTimestamp == null ? null : endDateTimestamp.getTime()); - dto.setValidatorUserName((String) o[i]); + dto.setValidatorUserName((String) o[i++]); + dto.setRouteId((String) o[i]); dtoList.add(dto); } return dtoList; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java index 4d7924fb..53e0a795 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java @@ -276,7 +276,14 @@ public class UserDao { criteriaList.add("lower(u.USE_USERNAME_C) like lower(:search)"); parameterMap.put("search", "%" + criteria.getSearch() + "%"); } - + if (criteria.getUserId() != null) { + criteriaList.add("u.USE_ID_C = :userId"); + parameterMap.put("userId", criteria.getUserId()); + } + if (criteria.getUserName() != null) { + criteriaList.add("u.USE_USERNAME_C = :userName"); + parameterMap.put("userName", criteria.getUserName()); + } if (criteria.getGroupId() != null) { sb.append(" join T_USER_GROUP ug on ug.UGP_IDUSER_C = u.USE_ID_C and ug.UGP_IDGROUP_C = :groupId and ug.UGP_DELETEDATE_D is null "); parameterMap.put("groupId", criteria.getGroupId()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java index a94ac347..57a5fbb4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java @@ -16,6 +16,16 @@ public class UserCriteria { */ private String groupId; + /** + * User ID. + */ + private String userId; + + /** + * Username. + */ + private String userName; + public String getSearch() { return search; } @@ -33,4 +43,22 @@ public class UserCriteria { this.groupId = groupId; return this; } + + public String getUserId() { + return userId; + } + + public UserCriteria setUserId(String userId) { + this.userId = userId; + return this; + } + + public String getUserName() { + return userName; + } + + public UserCriteria setUserName(String userName) { + this.userName = userName; + return this; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/RouteStepDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/RouteStepDto.java index edeb50c4..70553b2e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/RouteStepDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/RouteStepDto.java @@ -62,6 +62,11 @@ public class RouteStepDto { */ private String validatorUserName; + /** + * Route ID. + */ + private String routeId; + public String getId() { return id; } @@ -152,6 +157,15 @@ public class RouteStepDto { return this; } + public String getRouteId() { + return routeId; + } + + public RouteStepDto setRouteId(String routeId) { + this.routeId = routeId; + return this; + } + /** * Transform in JSON. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java index 9085523c..3a3c48e3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java @@ -1,5 +1,7 @@ package com.sismics.docs.core.dao.jpa.dto; +import com.google.common.base.MoreObjects; + /** * User DTO. * @@ -110,4 +112,13 @@ public class UserDto { public void setTotpKey(String totpKey) { this.totpKey = totpKey; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("username", username) + .add("email", email) + .toString(); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java index 78f39d73..23ce07f9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java @@ -1,8 +1,8 @@ package com.sismics.docs.core.event; import com.google.common.base.MoreObjects; +import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.model.jpa.PasswordRecovery; -import com.sismics.docs.core.model.jpa.User; /** * Event fired on user's password lost event. @@ -13,18 +13,18 @@ public class PasswordLostEvent { /** * User. */ - private User user; + private UserDto user; /** * Password recovery request. */ private PasswordRecovery passwordRecovery; - public User getUser() { + public UserDto getUser() { return user; } - public void setUser(User user) { + public void setUser(UserDto user) { this.user = user; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/RouteStepValidateEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/RouteStepValidateEvent.java new file mode 100644 index 00000000..40496959 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/event/RouteStepValidateEvent.java @@ -0,0 +1,47 @@ +package com.sismics.docs.core.event; + +import com.google.common.base.MoreObjects; +import com.sismics.docs.core.dao.jpa.dto.UserDto; +import com.sismics.docs.core.model.jpa.Document; + +/** + * Event fired on route step validation event. + * + * @author bgamard + */ +public class RouteStepValidateEvent { + /** + * User. + */ + private UserDto user; + + /** + * Document linked to the route. + */ + private Document document; + + public UserDto getUser() { + return user; + } + + public void setUser(UserDto user) { + this.user = user; + } + + public Document getDocument() { + return document; + } + + public RouteStepValidateEvent setDocument(Document document) { + this.document = document; + return this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("user", user) + .add("document", document) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java index 8c4aa48b..a8b02cf4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java @@ -2,9 +2,9 @@ package com.sismics.docs.core.listener.async; import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.event.PasswordLostEvent; import com.sismics.docs.core.model.jpa.PasswordRecovery; -import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.TransactionUtil; import com.sismics.util.EmailUtil; import org.slf4j.Logger; @@ -38,7 +38,7 @@ public class PasswordLostAsyncListener { TransactionUtil.handle(new Runnable() { @Override public void run() { - final User user = passwordLostEvent.getUser(); + final UserDto user = passwordLostEvent.getUser(); final PasswordRecovery passwordRecovery = passwordLostEvent.getPasswordRecovery(); // Send the password recovery email diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java new file mode 100644 index 00000000..cbe55acd --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java @@ -0,0 +1,52 @@ +package com.sismics.docs.core.listener.async; + +import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.jpa.dto.UserDto; +import com.sismics.docs.core.event.RouteStepValidateEvent; +import com.sismics.docs.core.util.TransactionUtil; +import com.sismics.util.EmailUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * Listener for route step validate. + * + * @author bgamard + */ +public class RouteStepValidateAsyncListener { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(RouteStepValidateAsyncListener.class); + + /** + * Handle events. + * + * @param routeStepValidateEvent Event + */ + @Subscribe + public void onRouteStepValidate(final RouteStepValidateEvent routeStepValidateEvent) { + if (log.isInfoEnabled()) { + log.info("Route step validate event: " + routeStepValidateEvent.toString()); + } + + TransactionUtil.handle(new Runnable() { + @Override + public void run() { + final UserDto user = routeStepValidateEvent.getUser(); + + // Send the password recovery email + Map paramRootMap = new HashMap<>(); + paramRootMap.put("user_name", user.getUsername()); + paramRootMap.put("document_id", routeStepValidateEvent.getDocument().getId()); + paramRootMap.put("document_title", routeStepValidateEvent.getDocument().getTitle()); + + EmailUtil.sendEmail(Constants.EMAIL_TEMPLATE_ROUTE_STEP_VALIDATE, user, paramRootMap); + } + }); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index d20f4f34..7da91f14 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -104,6 +104,7 @@ public class AppContext { mailEventBus = newAsyncEventBus(); mailEventBus.register(new PasswordLostAsyncListener()); + mailEventBus.register(new RouteStepValidateAsyncListener()); } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index b06d6d60..30e96274 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -203,6 +203,7 @@ public class User implements Loggable { return MoreObjects.toStringHelper(this) .add("id", id) .add("username", username) + .add("email", email) .toString(); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java index d269cd1c..9e6a7fe7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java @@ -1,10 +1,21 @@ package com.sismics.docs.core.util; +import com.google.common.collect.Lists; +import com.sismics.docs.core.constant.AclTargetType; import com.sismics.docs.core.constant.AclType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.docs.core.dao.jpa.DocumentDao; +import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; import com.sismics.docs.core.dao.jpa.dto.RouteStepDto; +import com.sismics.docs.core.dao.jpa.dto.UserDto; +import com.sismics.docs.core.event.RouteStepValidateEvent; +import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.Acl; +import com.sismics.docs.core.model.jpa.Document; + +import java.util.List; /** * Routing utilities. @@ -38,4 +49,28 @@ public class RoutingUtil { aclDao.create(acl, userId); } } + + public static void sendRouteStepEmail(String documentId, RouteStepDto routeStepDto) { + DocumentDao documentDao = new DocumentDao(); + Document document = documentDao.getById(documentId); + + List userDtoList = Lists.newArrayList(); + UserDao userDao = new UserDao(); + switch (AclTargetType.valueOf(routeStepDto.getTargetType())) { + case USER: + userDtoList.addAll(userDao.findByCriteria(new UserCriteria().setUserId(routeStepDto.getTargetId()), null)); + break; + case GROUP: + userDtoList.addAll(userDao.findByCriteria(new UserCriteria().setGroupId(routeStepDto.getTargetId()), null)); + break; + } + + // Fire route step validate events + for (UserDto userDto : userDtoList) { + RouteStepValidateEvent routeStepValidateEvent = new RouteStepValidateEvent(); + routeStepValidateEvent.setUser(userDto); + routeStepValidateEvent.setDocument(document); + AppContext.getInstance().getMailEventBus().post(routeStepValidateEvent); + } + } } diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index 88269e0d..7a8d699c 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -4,8 +4,8 @@ import com.google.common.base.Strings; import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.ConfigDao; +import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.model.jpa.Config; -import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.ConfigUtil; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapperBuilder; @@ -64,7 +64,7 @@ public class EmailUtil { * @param subject Email subject * @param paramMap Email parameters */ - public static void sendEmail(String templateName, User recipientUser, String subject, Map paramMap) { + public static void sendEmail(String templateName, UserDto recipientUser, String subject, Map paramMap) { if (log.isInfoEnabled()) { log.info("Sending email from template=" + templateName + " to user " + recipientUser); } @@ -154,7 +154,7 @@ public class EmailUtil { * @param recipientUser Recipient user * @param paramMap Email parameters */ - public static void sendEmail(String templateName, User recipientUser, Map paramMap) { + public static void sendEmail(String templateName, UserDto recipientUser, Map paramMap) { java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV)); String subject = MessageUtil.getMessage(userLocale, "email.template." + templateName + ".subject"); sendEmail(templateName, recipientUser, subject, paramMap); diff --git a/docs-core/src/main/resources/email_template/route_step_validate/template.ftl b/docs-core/src/main/resources/email_template/route_step_validate/template.ftl new file mode 100644 index 00000000..1a8ff96d --- /dev/null +++ b/docs-core/src/main/resources/email_template/route_step_validate/template.ftl @@ -0,0 +1,8 @@ +<#import "../layout.ftl" as layout> +<@layout.email> +

${app_name} - ${messages['email.template.route_step_validate.subject']}

+

${messages('email.template.route_step_validate.hello', user_name)}

+

${messages['email.template.route_step_validate.instruction1']}

+

${messages['email.template.route_step_validate.instruction2']}

+ ${document_title} + \ No newline at end of file diff --git a/docs-core/src/main/resources/messages.properties b/docs-core/src/main/resources/messages.properties index 06a43b5a..84510180 100644 --- a/docs-core/src/main/resources/messages.properties +++ b/docs-core/src/main/resources/messages.properties @@ -3,4 +3,8 @@ email.template.password_recovery.hello=Hello {0}. email.template.password_recovery.instruction1=We have received a request to reset your password.
If you did not request help, then feel free to ignore this email. email.template.password_recovery.instruction2=To reset your password, please visit the link below: email.template.password_recovery.click_here=Click here to reset your password +email.template.route_step_validate.subject=A document needs your attention +email.template.route_step_validate.hello=Hello {0}. +email.template.route_step_validate.instruction1=A workflow step has been assigned to you and needs your attention. +email.template.route_step_validate.instruction2=To view the document and validate the workflow, please visit the link below: email.no_html.error=Your email client does not support HTML messages \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java index 96634dbf..ed4e640d 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java @@ -110,11 +110,12 @@ public class RouteResource extends BaseResource { } // Intialize ACLs on the first step - RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId); - RoutingUtil.updateAcl(documentId, routeStep, null, principal.getId()); + RouteStepDto routeStepDto = routeStepDao.getCurrentStep(documentId); + RoutingUtil.updateAcl(documentId, routeStepDto, null, principal.getId()); + RoutingUtil.sendRouteStepEmail(documentId, routeStepDto); JsonObjectBuilder response = Json.createObjectBuilder() - .add("route_step", routeStep.toJson()); + .add("route_step", routeStepDto.toJson()); return Response.ok().entity(response.build()).build(); } @@ -152,13 +153,13 @@ public class RouteResource extends BaseResource { // Get the current step RouteStepDao routeStepDao = new RouteStepDao(); - RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId); - if (routeStep == null) { + RouteStepDto routeStepDto = routeStepDao.getCurrentStep(documentId); + if (routeStepDto == null) { throw new NotFoundException(); } // Check permission to validate this step - if (!getTargetIdList(null).contains(routeStep.getTargetId())) { + if (!getTargetIdList(null).contains(routeStepDto.getTargetId())) { throw new ForbiddenClientException(); } @@ -166,16 +167,16 @@ public class RouteResource extends BaseResource { ValidationUtil.validateRequired(transitionStr, "transition"); comment = ValidationUtil.validateLength(comment, "comment", 1, 500, true); RouteStepTransition transition = RouteStepTransition.valueOf(transitionStr); - if (routeStep.getType() == RouteStepType.VALIDATE && transition != RouteStepTransition.VALIDATED - || routeStep.getType() == RouteStepType.APPROVE && transition != RouteStepTransition.APPROVED && transition != RouteStepTransition.REJECTED) { + if (routeStepDto.getType() == RouteStepType.VALIDATE && transition != RouteStepTransition.VALIDATED + || routeStepDto.getType() == RouteStepType.APPROVE && transition != RouteStepTransition.APPROVED && transition != RouteStepTransition.REJECTED) { throw new ClientException("ValidationError", "Invalid transition for this route step type"); } // Validate the step and update ACLs - routeStepDao.endRouteStep(routeStep.getId(), transition, comment, principal.getId()); + routeStepDao.endRouteStep(routeStepDto.getId(), transition, comment, principal.getId()); RouteStepDto newRouteStep = routeStepDao.getCurrentStep(documentId); - RoutingUtil.updateAcl(documentId, newRouteStep, routeStep, principal.getId()); - // TODO Send an email to the new route step + RoutingUtil.updateAcl(documentId, newRouteStep, routeStepDto, principal.getId()); + RoutingUtil.sendRouteStepEmail(documentId, routeStepDto); JsonObjectBuilder response = Json.createObjectBuilder() .add("readable", aclDao.checkPermission(documentId, PermType.READ, getTargetIdList(null))); @@ -253,5 +254,50 @@ public class RouteResource extends BaseResource { return Response.ok().entity(json.build()).build(); } - // TODO Workflow cancellation + /** + * Cancel a route. + * + * @api {delete} /route Cancel a route + * @apiName DeleteRoute + * @apiRouteModel Route + * @apiParam {String} documentId Document ID + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied + * @apiError (client) NotFound Document or route not found + * @apiPermission user + * @apiVersion 1.5.0 + * + * @return Response + */ + @DELETE + public Response delete(@QueryParam("documentId") String documentId) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Get the document + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.WRITE, getTargetIdList(null))) { + throw new NotFoundException(); + } + + // Get the current step + RouteStepDao routeStepDao = new RouteStepDao(); + RouteStepDto routeStepDto = routeStepDao.getCurrentStep(documentId); + if (routeStepDto == null) { + throw new NotFoundException(); + } + + // Remove the temporary ACLs + RoutingUtil.updateAcl(documentId, null, routeStepDto, principal.getId()); + + // Delete the route and the steps + RouteDao routeDao = new RouteDao(); + routeDao.deleteRoute(routeStepDto.getRouteId()); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 01d643b5..a62c4eb5 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -953,10 +953,11 @@ public class UserResource extends BaseResource { // Check for user existence UserDao userDao = new UserDao(); - User user = userDao.getActiveByUsername(username); - if (user == null) { + List userDtoList = userDao.findByCriteria(new UserCriteria().setUserName(username), null); + if (userDtoList.isEmpty()) { throw new ClientException("UserNotFound", "User not found: " + username); } + UserDto user = userDtoList.get(0); // Create the password recovery key PasswordRecoveryDao passwordRecoveryDao = new PasswordRecoveryDao(); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js index 895d6bfe..9a5fee8b 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js @@ -149,6 +149,11 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta comment: $scope.workflowComment }).then(function (data) { $scope.workflowComment = ''; + var title = $translate.instant('document.view.workflow_validated_title'); + var msg = $translate.instant('document.view.workflow_validated_message'); + var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}]; + $dialog.messageBox(title, msg, btns); + if (data.readable) { $scope.document.route_step = data.route_step; } else { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewWorkflow.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewWorkflow.js index 6b3f7709..7da69002 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewWorkflow.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewWorkflow.js @@ -3,7 +3,10 @@ /** * Document view workflow controller. */ -angular.module('docs').controller('DocumentViewWorkflow', function ($scope, $stateParams, Restangular) { +angular.module('docs').controller('DocumentViewWorkflow', function ($scope, $stateParams, Restangular, $translate, $dialog) { + /** + * Load routes. + */ $scope.loadRoutes = function () { Restangular.one('route').get({ documentId: $stateParams.id @@ -12,12 +15,9 @@ angular.module('docs').controller('DocumentViewWorkflow', function ($scope, $sta }); }; - // Load route models - Restangular.one('routemodel').get().then(function(data) { - $scope.routemodels = data.routemodels; - }); - - // Start the selected workflow + /** + * Start the selected workflow + */ $scope.startWorkflow = function () { Restangular.one('route').post('start', { routeModelId: $scope.routemodel, @@ -28,6 +28,34 @@ angular.module('docs').controller('DocumentViewWorkflow', function ($scope, $sta }); }; + /** + * Cancel the current workflow. + */ + $scope.cancelWorkflow = function () { + var title = $translate.instant('document.view.workflow.cancel_workflow_title'); + var msg = $translate.instant('document.view.workflow.cancel_workflow_message'); + var btns = [ + {result: 'cancel', label: $translate.instant('cancel')}, + {result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'} + ]; + + $dialog.messageBox(title, msg, btns, function (result) { + if (result === 'ok') { + Restangular.one('route').remove({ + documentId: $stateParams.id + }).then(function () { + delete $scope.document.route_step; + $scope.loadRoutes(); + }); + } + }); + }; + + // Load route models + Restangular.one('routemodel').get().then(function(data) { + $scope.routemodels = data.routemodels; + }); + // Load routes $scope.loadRoutes(); }); \ No newline at end of file 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 5d0a3d7a..fe65e37d 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 @@ -117,4 +117,11 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial } }); }; + + /** + * Remove a route step. + */ + $scope.removeStep = function (step) { + $scope.workflow.steps.splice($scope.workflow.steps.indexOf(step), 1); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 6455cec0..c3ed2502 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -94,6 +94,8 @@ "error_loading_comments": "Error loading comments", "workflow_current": "Current workflow step", "workflow_comment": "Add a workflow comment", + "workflow_validated_title": "Workflow step validated", + "workflow_validated_message": "The workflow step has been successfully validated.", "content": { "content": "Content", "delete_file_title": "Delete file", @@ -111,7 +113,10 @@ "workflow_start_label": "Which workflow to start?", "add_more_workflow": "Add more workflows", "start_workflow_submit": "Start workflow", - "full_name": "{{ name }} started on {{ create_date | date }}" + "full_name": "{{ name }} started on {{ create_date | date }}", + "cancel_workflow": "Cancel the current workflow", + "cancel_workflow_title": "Cancel the workflow", + "cancel_workflow_message": "Do you really want to cancel the current workflow?" }, "permissions": { "permissions": "Permissions", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.workflow.html b/docs-web/src/main/webapp/src/partial/docs/document.view.workflow.html index 972cbcc8..a2fb3143 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.workflow.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.workflow.html @@ -1,72 +1,76 @@ +

+ + + {{ 'document.view.workflow.cancel_workflow' | translate }} + +

{{ 'document.view.workflow.message' | translate }}

-
- -
- -
-
- {{ 'validation.required' | translate }} -
+
+ +
+
+
+ {{ 'validation.required' | translate }} +
+
-
-
- -
+
+
+
+
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
TypeNameForValidation
- - - {{ 'workflow_type.' + step.type | translate }} - {{ step.name }} - - - - {{ 'workflow_transition.' + step.transition | translate }} - {{ step.end_date | timeAgo: dateTimeFormat }} - by - {{ step.validator_username }} - -
- {{ step.comment }} -
-
-
TypeNameForValidation
+ + + {{ 'workflow_type.' + step.type | translate }} + {{ step.name }} + + + + {{ 'workflow_transition.' + step.transition | translate }} + {{ step.end_date | timeAgo: dateTimeFormat }} + by + {{ step.validator_username }} + +
{{ step.comment }} +
+
+
\ No newline at end of file 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 68c80f95..6983c8d5 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 @@ -43,7 +43,7 @@
-
+
@@ -63,6 +63,9 @@ typeahead-editable="false" typeahead-wait-ms="200" />
+
+ +
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 9209411d..0b3577da 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 @@ -22,14 +22,23 @@ public class TestRouteResource extends BaseJerseyTest { * Test the route resource. */ @Test - public void testRouteResource() { + public void testRouteResource() throws Exception { // Login route1 clientUtil.createUser("route1"); String route1Token = clientUtil.login("route1"); // Login admin String adminToken = clientUtil.login("admin", "admin", false); - + + // Change SMTP configuration to target Wiser + target().path("/app/config_smtp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("hostname", "localhost") + .param("port", "2500") + .param("from", "contact@sismicsdocs.com") + ), JsonObject.class); + // Get all route models JsonObject json = target().path("/routemodel") .queryParam("sort_column", "2") @@ -59,6 +68,7 @@ public class TestRouteResource extends BaseJerseyTest { .param("routeModelId", routeModels.getJsonObject(0).getString("id"))), JsonObject.class); JsonObject step = json.getJsonObject("route_step"); Assert.assertEquals("Check the document's metadata", step.getString("name")); + Assert.assertTrue(popEmail().contains("workflow step")); // Get the route on document 1 json = target().path("/route") @@ -112,6 +122,7 @@ public class TestRouteResource extends BaseJerseyTest { step = json.getJsonObject("route_step"); Assert.assertEquals("Add relevant files to the document", step.getString("name")); Assert.assertTrue(json.getBoolean("readable")); + Assert.assertTrue(popEmail().contains("workflow step")); // Get the route on document 1 json = target().path("/route") @@ -150,6 +161,7 @@ public class TestRouteResource extends BaseJerseyTest { step = json.getJsonObject("route_step"); Assert.assertEquals("Approve the document", step.getString("name")); Assert.assertTrue(json.getBoolean("readable")); + Assert.assertTrue(popEmail().contains("workflow step")); // Get the route on document 1 json = target().path("/route") @@ -186,6 +198,7 @@ public class TestRouteResource extends BaseJerseyTest { .param("transition", "APPROVED")), JsonObject.class); Assert.assertFalse(json.containsKey("route_step")); Assert.assertFalse(json.getBoolean("readable")); + Assert.assertTrue(popEmail().contains("workflow step")); // Get the route on document 1 json = target().path("/route") @@ -217,5 +230,44 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) .get(JsonObject.class); Assert.assertFalse(json.containsKey("route_step")); + + // Start the default route on document 1 + target().path("/route/start").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) + .post(Entity.form(new Form() + .param("documentId", document1Id) + .param("routeModelId", routeModels.getJsonObject(0).getString("id"))), JsonObject.class); + Assert.assertTrue(popEmail().contains("workflow step")); + + // Get document 1 as route1 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) + .get(JsonObject.class); + Assert.assertTrue(json.containsKey("route_step")); + + // Get document 1 as admin + response = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(); + Assert.assertEquals(Response.Status.OK, Response.Status.fromStatusCode(response.getStatus())); + + // Cancel the route on document 1 + target().path("/route") + .queryParam("documentId", document1Id) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) + .delete(JsonObject.class); + + // Get document 1 as route1 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) + .get(JsonObject.class); + Assert.assertFalse(json.containsKey("route_step")); + + // Get document 1 as admin + response = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(); + Assert.assertEquals(Response.Status.NOT_FOUND, Response.Status.fromStatusCode(response.getStatus())); } } \ No newline at end of file