mirror of
https://github.com/sismics/docs.git
synced 2024-11-25 15:17:57 +01:00
#159: validate route steps
This commit is contained in:
parent
c9adff5a25
commit
2b4ddfa072
@ -1,7 +1,6 @@
|
|||||||
package com.sismics.docs.core.dao.jpa;
|
package com.sismics.docs.core.dao.jpa;
|
||||||
|
|
||||||
import com.sismics.docs.core.constant.AclTargetType;
|
import com.sismics.docs.core.constant.AclTargetType;
|
||||||
import com.sismics.docs.core.constant.AclType;
|
|
||||||
import com.sismics.docs.core.constant.AuditLogType;
|
import com.sismics.docs.core.constant.AuditLogType;
|
||||||
import com.sismics.docs.core.constant.PermType;
|
import com.sismics.docs.core.constant.PermType;
|
||||||
import com.sismics.docs.core.dao.jpa.dto.AclDto;
|
import com.sismics.docs.core.dao.jpa.dto.AclDto;
|
||||||
@ -65,7 +64,7 @@ public class AclDao {
|
|||||||
* @return ACL DTO list
|
* @return ACL DTO list
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public List<AclDto> getBySourceId(String sourceId, AclType type) {
|
public List<AclDto> getBySourceId(String sourceId) {
|
||||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||||
StringBuilder sb = new StringBuilder("select a.ACL_ID_C, a.ACL_PERM_C, a.ACL_TARGETID_C, ");
|
StringBuilder sb = new StringBuilder("select a.ACL_ID_C, a.ACL_PERM_C, a.ACL_TARGETID_C, ");
|
||||||
sb.append(" u.USE_USERNAME_C, s.SHA_ID_C, s.SHA_NAME_C, g.GRP_NAME_C ");
|
sb.append(" u.USE_USERNAME_C, s.SHA_ID_C, s.SHA_NAME_C, g.GRP_NAME_C ");
|
||||||
@ -73,12 +72,11 @@ public class AclDao {
|
|||||||
sb.append(" left join T_USER u on u.USE_ID_C = a.ACL_TARGETID_C ");
|
sb.append(" left join T_USER u on u.USE_ID_C = a.ACL_TARGETID_C ");
|
||||||
sb.append(" left join T_SHARE s on s.SHA_ID_C = a.ACL_TARGETID_C ");
|
sb.append(" left join T_SHARE s on s.SHA_ID_C = a.ACL_TARGETID_C ");
|
||||||
sb.append(" left join T_GROUP g on g.GRP_ID_C = a.ACL_TARGETID_C ");
|
sb.append(" left join T_GROUP g on g.GRP_ID_C = a.ACL_TARGETID_C ");
|
||||||
sb.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId and a.ACL_TYPE_C = :type ");
|
sb.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId ");
|
||||||
|
|
||||||
// Perform the query
|
// Perform the query
|
||||||
Query q = em.createNativeQuery(sb.toString());
|
Query q = em.createNativeQuery(sb.toString());
|
||||||
q.setParameter("sourceId", sourceId);
|
q.setParameter("sourceId", sourceId);
|
||||||
q.setParameter("type", type.name());
|
|
||||||
List<Object[]> l = q.getResultList();
|
List<Object[]> l = q.getResultList();
|
||||||
|
|
||||||
// Assemble results
|
// Assemble results
|
||||||
@ -142,29 +140,26 @@ public class AclDao {
|
|||||||
* @param perm Permission
|
* @param perm Permission
|
||||||
* @param targetId Target ID
|
* @param targetId Target ID
|
||||||
* @param userId User ID
|
* @param userId User ID
|
||||||
* @param type Type
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void delete(String sourceId, PermType perm, String targetId, String userId, AclType type) {
|
public void delete(String sourceId, PermType perm, String targetId, String userId) {
|
||||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||||
|
|
||||||
// Create audit log
|
// 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");
|
||||||
q.setParameter("sourceId", sourceId);
|
q.setParameter("sourceId", sourceId);
|
||||||
q.setParameter("perm", perm);
|
q.setParameter("perm", perm);
|
||||||
q.setParameter("targetId", targetId);
|
q.setParameter("targetId", targetId);
|
||||||
q.setParameter("type", type);
|
|
||||||
List<Acl> aclList = q.getResultList();
|
List<Acl> aclList = q.getResultList();
|
||||||
for (Acl acl : aclList) {
|
for (Acl acl : aclList) {
|
||||||
AuditLogUtil.create(acl, AuditLogType.DELETE, userId);
|
AuditLogUtil.create(acl, AuditLogType.DELETE, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft delete the ACLs
|
// 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");
|
||||||
q.setParameter("sourceId", sourceId);
|
q.setParameter("sourceId", sourceId);
|
||||||
q.setParameter("perm", perm);
|
q.setParameter("perm", perm);
|
||||||
q.setParameter("targetId", targetId);
|
q.setParameter("targetId", targetId);
|
||||||
q.setParameter("type", type);
|
|
||||||
q.setParameter("dateNow", new Date());
|
q.setParameter("dateNow", new Date());
|
||||||
q.executeUpdate();
|
q.executeUpdate();
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,7 @@ import com.sismics.docs.core.util.AuditLogUtil;
|
|||||||
import com.sismics.util.context.ThreadLocalContext;
|
import com.sismics.util.context.ThreadLocalContext;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.Query;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,12 +36,4 @@ public class RouteDao {
|
|||||||
|
|
||||||
return route.getId();
|
return route.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public List<Route> getActiveRoutes(String documentId) {
|
|
||||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
|
||||||
Query q = em.createQuery("from Route r where r.documentId = :documentId and r.deleteDate is null order by r.createDate desc");
|
|
||||||
q.setParameter("documentId", documentId);
|
|
||||||
return q.getResultList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package com.sismics.docs.core.dao.jpa;
|
package com.sismics.docs.core.dao.jpa;
|
||||||
|
|
||||||
|
import com.sismics.docs.core.constant.RouteStepTransition;
|
||||||
|
import com.sismics.docs.core.constant.RouteStepType;
|
||||||
|
import com.sismics.docs.core.dao.jpa.dto.RouteStepDto;
|
||||||
import com.sismics.docs.core.model.jpa.RouteStep;
|
import com.sismics.docs.core.model.jpa.RouteStep;
|
||||||
import com.sismics.util.context.ThreadLocalContext;
|
import com.sismics.util.context.ThreadLocalContext;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.Query;
|
import javax.persistence.Query;
|
||||||
|
import java.sql.Timestamp;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -33,11 +37,63 @@ public class RouteStepDao {
|
|||||||
return routeStep.getId();
|
return routeStep.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current route step from a document.
|
||||||
|
*
|
||||||
|
* @param documentId Document ID
|
||||||
|
* @return Current route step
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public List<RouteStep> getRouteSteps(String routeId) {
|
public RouteStepDto getCurrentStep(String documentId) {
|
||||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||||
Query q = em.createQuery("from RouteStep r where r.routeId = :routeId and r.deleteDate is null order by r.order asc");
|
StringBuilder sb = new StringBuilder("select rs.RTP_ID_C, rs.RTP_NAME_C, rs.RTP_TYPE_C, rs.RTP_TRANSITION_C, rs.RTP_COMMENT_C, rs.RTP_IDTARGET_C, rs.RTP_ENDDATE_D");
|
||||||
q.setParameter("routeId", routeId);
|
sb.append(" from T_ROUTE_STEP rs ");
|
||||||
return q.getResultList();
|
sb.append(" join T_ROUTE r on r.RTE_ID_C = rs.RTP_IDROUTE_C ");
|
||||||
|
sb.append(" where r.RTE_IDDOCUMENT_C = :documentId and rs.RTP_ENDDATE_D is null ");
|
||||||
|
sb.append(" order by rs.RTP_ORDER_N asc ");
|
||||||
|
|
||||||
|
Query q = em.createNativeQuery(sb.toString());
|
||||||
|
q.setParameter("documentId", documentId);
|
||||||
|
|
||||||
|
List<Object[]> l = q.getResultList();
|
||||||
|
if (l.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object[] o = l.get(0);
|
||||||
|
int i = 0;
|
||||||
|
RouteStepDto routeStepDto = new RouteStepDto();
|
||||||
|
routeStepDto.setId((String) o[i++]);
|
||||||
|
routeStepDto.setName((String) o[i++]);
|
||||||
|
routeStepDto.setType(RouteStepType.valueOf((String) o[i++]));
|
||||||
|
String transition = (String) o[i++];
|
||||||
|
routeStepDto.setTransition(transition == null ? null : RouteStepTransition.valueOf(transition));
|
||||||
|
routeStepDto.setComment((String) o[i++]);
|
||||||
|
routeStepDto.setTargetId((String) o[i++]);
|
||||||
|
Timestamp endDateTimestamp = (Timestamp) o[i];
|
||||||
|
routeStepDto.setEndDateTimestamp(endDateTimestamp == null ? null : endDateTimestamp.getTime());
|
||||||
|
return routeStepDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End a route step.
|
||||||
|
*
|
||||||
|
* @param id Route step ID
|
||||||
|
* @param transition Transition
|
||||||
|
* @param comment Comment
|
||||||
|
* @param validatorUserId Validator user ID
|
||||||
|
*/
|
||||||
|
public void endRouteStep(String id, RouteStepTransition transition, String comment, String validatorUserId) {
|
||||||
|
StringBuilder sb = new StringBuilder("update T_ROUTE_STEP r ");
|
||||||
|
sb.append(" set r.RTP_ENDDATE_D = :endDate, r.RTP_TRANSITION_C = :transition, r.RTP_COMMENT_C = :comment, r.RTP_IDVALIDATORUSER_C = :validatorUserId ");
|
||||||
|
sb.append(" where r.RTP_ID_C = :id");
|
||||||
|
|
||||||
|
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||||
|
Query q = em.createNativeQuery(sb.toString());
|
||||||
|
q.setParameter("endDate", new Date());
|
||||||
|
q.setParameter("transition", transition.name());
|
||||||
|
q.setParameter("comment", comment);
|
||||||
|
q.setParameter("validatorUserId", validatorUserId);
|
||||||
|
q.setParameter("id", id);
|
||||||
|
q.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ public class UserDao {
|
|||||||
User userDb = (User) q.getSingleResult();
|
User userDb = (User) q.getSingleResult();
|
||||||
|
|
||||||
// Update the user
|
// Update the user
|
||||||
userDb.setStorageQuota(user.getStorageQuota());
|
userDb.setStorageCurrent(user.getStorageCurrent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
package com.sismics.docs.core.dao.jpa.dto;
|
||||||
|
|
||||||
|
import com.sismics.docs.core.constant.RouteStepTransition;
|
||||||
|
import com.sismics.docs.core.constant.RouteStepType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route step DTO.
|
||||||
|
*
|
||||||
|
* @author bgamard
|
||||||
|
*/
|
||||||
|
public class RouteStepDto {
|
||||||
|
/**
|
||||||
|
* Route step ID.
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name.
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type.
|
||||||
|
*/
|
||||||
|
private RouteStepType type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition.
|
||||||
|
*/
|
||||||
|
private RouteStepTransition transition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment.
|
||||||
|
*/
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target ID (user or group).
|
||||||
|
*/
|
||||||
|
private String targetId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End date.
|
||||||
|
*/
|
||||||
|
private Long endDateTimestamp;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setType(RouteStepType type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepTransition getTransition() {
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setTransition(RouteStepTransition transition) {
|
||||||
|
this.transition = transition;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getComment() {
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setComment(String comment) {
|
||||||
|
this.comment = comment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTargetId() {
|
||||||
|
return targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setTargetId(String targetId) {
|
||||||
|
this.targetId = targetId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getEndDateTimestamp() {
|
||||||
|
return endDateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStepDto setEndDateTimestamp(Long endDateTimestamp) {
|
||||||
|
this.endDateTimestamp = endDateTimestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,5 @@
|
|||||||
package com.sismics.docs.core.listener.async;
|
package com.sismics.docs.core.listener.async;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sismics.docs.core.dao.jpa.ContributorDao;
|
import com.sismics.docs.core.dao.jpa.ContributorDao;
|
||||||
import com.sismics.docs.core.dao.jpa.DocumentDao;
|
import com.sismics.docs.core.dao.jpa.DocumentDao;
|
||||||
@ -12,6 +7,10 @@ import com.sismics.docs.core.dao.lucene.LuceneDao;
|
|||||||
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
|
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
|
||||||
import com.sismics.docs.core.model.jpa.Contributor;
|
import com.sismics.docs.core.model.jpa.Contributor;
|
||||||
import com.sismics.docs.core.util.TransactionUtil;
|
import com.sismics.docs.core.util.TransactionUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener on document updated.
|
* Listener on document updated.
|
||||||
@ -31,7 +30,7 @@ public class DocumentUpdatedAsyncListener {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void on(final DocumentUpdatedAsyncEvent event) throws Exception {
|
public void on(final DocumentUpdatedAsyncEvent event) {
|
||||||
if (log.isInfoEnabled()) {
|
if (log.isInfoEnabled()) {
|
||||||
log.info("Document updated event: " + event.toString());
|
log.info("Document updated event: " + event.toString());
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,12 @@ public class RouteStep {
|
|||||||
@Column(name = "RTP_IDTARGET_C", nullable = false, length = 36)
|
@Column(name = "RTP_IDTARGET_C", nullable = false, length = 36)
|
||||||
private String targetId;
|
private String targetId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator user ID.
|
||||||
|
*/
|
||||||
|
@Column(name = "RTP_IDVALIDATORUSER_C", length = 36)
|
||||||
|
private String validatorUserId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order.
|
* Order.
|
||||||
*/
|
*/
|
||||||
@ -156,6 +162,15 @@ public class RouteStep {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getValidatorUserId() {
|
||||||
|
return validatorUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteStep setValidatorUserId(String validatorUserId) {
|
||||||
|
this.validatorUserId = validatorUserId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Date getCreateDate() {
|
public Date getCreateDate() {
|
||||||
return createDate;
|
return createDate;
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,8 @@ package com.sismics.docs.core.util;
|
|||||||
import com.sismics.docs.core.constant.AclType;
|
import com.sismics.docs.core.constant.AclType;
|
||||||
import com.sismics.docs.core.constant.PermType;
|
import com.sismics.docs.core.constant.PermType;
|
||||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||||
import com.sismics.docs.core.dao.jpa.RouteDao;
|
import com.sismics.docs.core.dao.jpa.dto.RouteStepDto;
|
||||||
import com.sismics.docs.core.dao.jpa.RouteStepDao;
|
|
||||||
import com.sismics.docs.core.model.jpa.Acl;
|
import com.sismics.docs.core.model.jpa.Acl;
|
||||||
import com.sismics.docs.core.model.jpa.Route;
|
|
||||||
import com.sismics.docs.core.model.jpa.RouteStep;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing utilities.
|
* Routing utilities.
|
||||||
@ -17,31 +12,6 @@ import java.util.List;
|
|||||||
* @author bgamard
|
* @author bgamard
|
||||||
*/
|
*/
|
||||||
public class RoutingUtil {
|
public class RoutingUtil {
|
||||||
/**
|
|
||||||
* Get the current route step from a document.
|
|
||||||
*
|
|
||||||
* @param documentId Document ID
|
|
||||||
* @return Active route step
|
|
||||||
*/
|
|
||||||
public static RouteStep getCurrentStep(String documentId) {
|
|
||||||
// TODO Optimize
|
|
||||||
RouteDao routeDao = new RouteDao();
|
|
||||||
List<Route> routeList = routeDao.getActiveRoutes(documentId);
|
|
||||||
if (routeList.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Route route = routeList.get(0);
|
|
||||||
RouteStepDao routeStepDao = new RouteStepDao();
|
|
||||||
List<RouteStep> routeStepList = routeStepDao.getRouteSteps(route.getId());
|
|
||||||
for (RouteStep routeStep : routeStepList) {
|
|
||||||
if (routeStep.getEndDate() == null) {
|
|
||||||
return routeStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update routing ACLs according to the current route step.
|
* Update routing ACLs according to the current route step.
|
||||||
@ -51,7 +21,7 @@ public class RoutingUtil {
|
|||||||
* @param previousStep Previous route step
|
* @param previousStep Previous route step
|
||||||
* @param userId User ID
|
* @param userId User ID
|
||||||
*/
|
*/
|
||||||
public static void updateAcl(String sourceId, RouteStep currentStep, RouteStep previousStep, String userId) {
|
public static void updateAcl(String sourceId, RouteStepDto currentStep, RouteStepDto previousStep, String userId) {
|
||||||
AclDao aclDao = new AclDao();
|
AclDao aclDao = new AclDao();
|
||||||
|
|
||||||
if (previousStep != null) {
|
if (previousStep != null) {
|
||||||
@ -59,12 +29,14 @@ public class RoutingUtil {
|
|||||||
aclDao.delete(sourceId, PermType.READ, previousStep.getTargetId(), userId, AclType.ROUTING);
|
aclDao.delete(sourceId, PermType.READ, previousStep.getTargetId(), userId, AclType.ROUTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary READ ACL
|
if (currentStep != null) {
|
||||||
Acl acl = new Acl();
|
// Create a temporary READ ACL
|
||||||
acl.setPerm(PermType.READ);
|
Acl acl = new Acl();
|
||||||
acl.setType(AclType.ROUTING);
|
acl.setPerm(PermType.READ);
|
||||||
acl.setSourceId(sourceId);
|
acl.setType(AclType.ROUTING);
|
||||||
acl.setTargetId(currentStep.getTargetId());
|
acl.setSourceId(sourceId);
|
||||||
aclDao.create(acl, userId);
|
acl.setTargetId(currentStep.getTargetId());
|
||||||
|
aclDao.create(acl, userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,11 @@ public class ThreadLocalContext {
|
|||||||
* @return entityManager
|
* @return entityManager
|
||||||
*/
|
*/
|
||||||
public EntityManager getEntityManager() {
|
public EntityManager getEntityManager() {
|
||||||
|
if (entityManager != null && entityManager.isOpen()) {
|
||||||
|
// This disables the L1 cache
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
}
|
||||||
return entityManager;
|
return entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
create table T_ROUTE_MODEL ( RTM_ID_C varchar(36) not null, RTM_NAME_C varchar(50) not null, RTM_STEPS_C varchar(5000) not null, RTM_CREATEDATE_D datetime not null, RTM_DELETEDATE_D datetime, primary key (RTM_ID_C) );
|
create table T_ROUTE_MODEL ( RTM_ID_C varchar(36) not null, RTM_NAME_C varchar(50) not null, RTM_STEPS_C varchar(5000) not null, RTM_CREATEDATE_D datetime not null, RTM_DELETEDATE_D datetime, primary key (RTM_ID_C) );
|
||||||
create cached table T_ROUTE ( RTE_ID_C varchar(36) not null, RTE_IDDOCUMENT_C varchar(36) not null, RTE_NAME_C varchar(50) not null, RTE_CREATEDATE_D datetime not null, RTE_DELETEDATE_D datetime, primary key (RTE_ID_C) );
|
create cached table T_ROUTE ( RTE_ID_C varchar(36) not null, RTE_IDDOCUMENT_C varchar(36) not null, RTE_NAME_C varchar(50) not null, RTE_CREATEDATE_D datetime not null, RTE_DELETEDATE_D datetime, primary key (RTE_ID_C) );
|
||||||
create cached table T_ROUTE_STEP ( RTP_ID_C varchar(36) not null, RTP_IDROUTE_C varchar(36) not null, RTP_NAME_C varchar(200) not null, RTP_TYPE_C varchar(50) not null, RTP_TRANSITION_C varchar(50), RTP_COMMENT_C varchar(500), RTP_IDTARGET_C varchar(36) not null, RTP_ORDER_N int not null, RTP_CREATEDATE_D datetime not null, RTP_ENDDATE_D datetime, RTP_DELETEDATE_D datetime, primary key (RTP_ID_C) );
|
create cached table T_ROUTE_STEP ( RTP_ID_C varchar(36) not null, RTP_IDROUTE_C varchar(36) not null, RTP_NAME_C varchar(200) not null, RTP_TYPE_C varchar(50) not null, RTP_TRANSITION_C varchar(50), RTP_COMMENT_C varchar(500), RTP_IDTARGET_C varchar(36) not null, RTP_IDVALIDATORUSER_C varchar(36), RTP_ORDER_N int not null, RTP_CREATEDATE_D datetime not null, RTP_ENDDATE_D datetime, RTP_DELETEDATE_D datetime, primary key (RTP_ID_C) );
|
||||||
alter table T_ACL add column ACL_TYPE_C varchar(30) not null default 'USER';
|
alter table T_ACL add column ACL_TYPE_C varchar(30) not null default 'USER';
|
||||||
alter table T_ROUTE add constraint FK_RTE_IDDOCUMENT_C foreign key (RTE_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict;
|
alter table T_ROUTE add constraint FK_RTE_IDDOCUMENT_C foreign key (RTE_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict;
|
||||||
alter table T_ROUTE_STEP add constraint FK_RTP_IDROUTE_C foreign key (RTP_IDROUTE_C) references T_ROUTE (RTE_ID_C) on delete restrict on update restrict;
|
alter table T_ROUTE_STEP add constraint FK_RTP_IDROUTE_C foreign key (RTP_IDROUTE_C) references T_ROUTE (RTE_ID_C) on delete restrict on update restrict;
|
||||||
|
alter table T_ROUTE_STEP add constraint FK_RTP_IDVALIDATORUSER_C foreign key (RTP_IDVALIDATORUSER_C) references T_USER (USE_ID_C) on delete restrict on update restrict;
|
||||||
|
|
||||||
insert into T_ROUTE_MODEL (RTM_ID_C, RTM_NAME_C, RTM_STEPS_C, RTM_CREATEDATE_D) values ('default-document-review', 'Document review', '[{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Check the document''s metadata"},{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Add relevant files to the document"},{"type":"APPROVE","target":{"name":"administrators","type":"GROUP"},"name":"Approve the document"}]', now());
|
insert into T_ROUTE_MODEL (RTM_ID_C, RTM_NAME_C, RTM_STEPS_C, RTM_CREATEDATE_D) values ('default-document-review', 'Document review', '[{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Check the document''s metadata"},{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Add relevant files to the document"},{"type":"APPROVE","target":{"name":"administrators","type":"GROUP"},"name":"Approve the document"}]', now());
|
||||||
|
|
||||||
|
@ -13,9 +13,11 @@ import com.sismics.docs.core.event.DocumentCreatedAsyncEvent;
|
|||||||
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
|
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
|
||||||
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
|
import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent;
|
||||||
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
|
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
|
||||||
import com.sismics.docs.core.model.jpa.*;
|
import com.sismics.docs.core.model.jpa.Acl;
|
||||||
|
import com.sismics.docs.core.model.jpa.Document;
|
||||||
|
import com.sismics.docs.core.model.jpa.File;
|
||||||
|
import com.sismics.docs.core.model.jpa.User;
|
||||||
import com.sismics.docs.core.util.PdfUtil;
|
import com.sismics.docs.core.util.PdfUtil;
|
||||||
import com.sismics.docs.core.util.RoutingUtil;
|
|
||||||
import com.sismics.docs.core.util.jpa.PaginatedList;
|
import com.sismics.docs.core.util.jpa.PaginatedList;
|
||||||
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
import com.sismics.docs.core.util.jpa.PaginatedLists;
|
||||||
import com.sismics.docs.core.util.jpa.SortCriteria;
|
import com.sismics.docs.core.util.jpa.SortCriteria;
|
||||||
@ -212,12 +214,12 @@ public class DocumentResource extends BaseResource {
|
|||||||
document.add("relations", relationList);
|
document.add("relations", relationList);
|
||||||
|
|
||||||
// Add current route step
|
// Add current route step
|
||||||
RouteStep routeStep = RoutingUtil.getCurrentStep(documentId);
|
RouteStepDto routeStepDto = new RouteStepDao().getCurrentStep(documentId);
|
||||||
if (routeStep != null && !principal.isAnonymous()) {
|
if (routeStepDto != null && !principal.isAnonymous()) {
|
||||||
document.add("route_step", Json.createObjectBuilder()
|
document.add("route_step", Json.createObjectBuilder()
|
||||||
.add("name", routeStep.getName())
|
.add("name", routeStepDto.getName())
|
||||||
.add("type", routeStep.getType().name())
|
.add("type", routeStepDto.getType().name())
|
||||||
.add("transitionable", getTargetIdList(null).contains(routeStep.getTargetId())));
|
.add("transitionable", getTargetIdList(null).contains(routeStepDto.getTargetId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.ok().entity(document.build()).build();
|
return Response.ok().entity(document.build()).build();
|
||||||
|
@ -2,11 +2,13 @@ package com.sismics.docs.rest.resource;
|
|||||||
|
|
||||||
import com.sismics.docs.core.constant.AclTargetType;
|
import com.sismics.docs.core.constant.AclTargetType;
|
||||||
import com.sismics.docs.core.constant.PermType;
|
import com.sismics.docs.core.constant.PermType;
|
||||||
|
import com.sismics.docs.core.constant.RouteStepTransition;
|
||||||
import com.sismics.docs.core.constant.RouteStepType;
|
import com.sismics.docs.core.constant.RouteStepType;
|
||||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||||
import com.sismics.docs.core.dao.jpa.RouteDao;
|
import com.sismics.docs.core.dao.jpa.RouteDao;
|
||||||
import com.sismics.docs.core.dao.jpa.RouteModelDao;
|
import com.sismics.docs.core.dao.jpa.RouteModelDao;
|
||||||
import com.sismics.docs.core.dao.jpa.RouteStepDao;
|
import com.sismics.docs.core.dao.jpa.RouteStepDao;
|
||||||
|
import com.sismics.docs.core.dao.jpa.dto.RouteStepDto;
|
||||||
import com.sismics.docs.core.model.jpa.Route;
|
import com.sismics.docs.core.model.jpa.Route;
|
||||||
import com.sismics.docs.core.model.jpa.RouteModel;
|
import com.sismics.docs.core.model.jpa.RouteModel;
|
||||||
import com.sismics.docs.core.model.jpa.RouteStep;
|
import com.sismics.docs.core.model.jpa.RouteStep;
|
||||||
@ -14,6 +16,7 @@ import com.sismics.docs.core.util.RoutingUtil;
|
|||||||
import com.sismics.docs.core.util.SecurityUtil;
|
import com.sismics.docs.core.util.SecurityUtil;
|
||||||
import com.sismics.rest.exception.ClientException;
|
import com.sismics.rest.exception.ClientException;
|
||||||
import com.sismics.rest.exception.ForbiddenClientException;
|
import com.sismics.rest.exception.ForbiddenClientException;
|
||||||
|
import com.sismics.rest.util.ValidationUtil;
|
||||||
|
|
||||||
import javax.json.*;
|
import javax.json.*;
|
||||||
import javax.ws.rs.FormParam;
|
import javax.ws.rs.FormParam;
|
||||||
@ -79,9 +82,8 @@ public class RouteResource extends BaseResource {
|
|||||||
RouteStepDao routeStepDao = new RouteStepDao();
|
RouteStepDao routeStepDao = new RouteStepDao();
|
||||||
try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
|
try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
|
||||||
JsonArray stepsJson = reader.readArray();
|
JsonArray stepsJson = reader.readArray();
|
||||||
int order = 0;
|
for (int order = 0; order < stepsJson.size(); order++) {
|
||||||
for (int i = 0; i < stepsJson.size(); i++) {
|
JsonObject step = stepsJson.getJsonObject(order);
|
||||||
JsonObject step = stepsJson.getJsonObject(i);
|
|
||||||
JsonObject target = step.getJsonObject("target");
|
JsonObject target = step.getJsonObject("target");
|
||||||
AclTargetType targetType = AclTargetType.valueOf(target.getString("type"));
|
AclTargetType targetType = AclTargetType.valueOf(target.getString("type"));
|
||||||
String targetName = target.getString("name");
|
String targetName = target.getString("name");
|
||||||
@ -89,7 +91,7 @@ public class RouteResource extends BaseResource {
|
|||||||
RouteStep routeStep = new RouteStep()
|
RouteStep routeStep = new RouteStep()
|
||||||
.setRouteId(route.getId())
|
.setRouteId(route.getId())
|
||||||
.setName(step.getString("name"))
|
.setName(step.getString("name"))
|
||||||
.setOrder(order++)
|
.setOrder(order)
|
||||||
.setType(RouteStepType.valueOf(step.getString("type")))
|
.setType(RouteStepType.valueOf(step.getString("type")))
|
||||||
.setTargetId(SecurityUtil.getTargetIdFromName(targetName, targetType));
|
.setTargetId(SecurityUtil.getTargetIdFromName(targetName, targetType));
|
||||||
|
|
||||||
@ -98,18 +100,82 @@ public class RouteResource extends BaseResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routeStepDao.create(routeStep);
|
routeStepDao.create(routeStep);
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
// Initialize ACL on the first step
|
|
||||||
RoutingUtil.updateAcl(documentId, routeStep, null, principal.getId());
|
|
||||||
// TODO Send an email to the targetId users
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intialize ACLs on the first step
|
||||||
|
RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId);
|
||||||
|
RoutingUtil.updateAcl(documentId, routeStep, null, principal.getId());
|
||||||
|
|
||||||
// Always return OK
|
// Always return OK
|
||||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||||
.add("status", "ok");
|
.add("status", "ok");
|
||||||
return Response.ok().entity(response.build()).build();
|
return Response.ok().entity(response.build()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the current step of a route.
|
||||||
|
*
|
||||||
|
* @api {post} /route/validate Validate the current step of a route
|
||||||
|
* @apiName PostRouteValidate
|
||||||
|
* @apiRouteModel Route
|
||||||
|
* @apiParam {String} documentId Document ID
|
||||||
|
* @apiParam {String} transition Route step transition
|
||||||
|
* @apiParam {String} comment Route step comment
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("validate")
|
||||||
|
public Response validate(@FormParam("documentId") String documentId,
|
||||||
|
@FormParam("transition") String transitionStr,
|
||||||
|
@FormParam("comment") String comment) {
|
||||||
|
if (!authenticate()) {
|
||||||
|
throw new ForbiddenClientException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the document
|
||||||
|
AclDao aclDao = new AclDao();
|
||||||
|
if (!aclDao.checkPermission(documentId, PermType.READ, getTargetIdList(null))) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current step
|
||||||
|
RouteStepDao routeStepDao = new RouteStepDao();
|
||||||
|
RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId);
|
||||||
|
if (routeStep == null) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission to validate this step
|
||||||
|
if (!getTargetIdList(null).contains(routeStep.getTargetId())) {
|
||||||
|
throw new ForbiddenClientException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data
|
||||||
|
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) {
|
||||||
|
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());
|
||||||
|
RouteStepDto newRouteStep = routeStepDao.getCurrentStep(documentId);
|
||||||
|
RoutingUtil.updateAcl(documentId, newRouteStep, routeStep, principal.getId());
|
||||||
|
// TODO Send an email to the new route step
|
||||||
|
|
||||||
|
// Always return OK
|
||||||
|
// TODO Return if the document is still readable and return the new current step if any
|
||||||
|
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||||
|
.add("status", "ok");
|
||||||
|
return Response.ok().entity(response.build()).build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package com.sismics.docs.rest.resource;
|
|||||||
|
|
||||||
|
|
||||||
import com.sismics.docs.core.constant.AclTargetType;
|
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.constant.PermType;
|
||||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||||
import com.sismics.docs.core.dao.jpa.ShareDao;
|
import com.sismics.docs.core.dao.jpa.ShareDao;
|
||||||
@ -77,7 +76,6 @@ public class ShareResource extends BaseResource {
|
|||||||
Acl acl = new Acl();
|
Acl acl = new Acl();
|
||||||
acl.setSourceId(documentId);
|
acl.setSourceId(documentId);
|
||||||
acl.setPerm(PermType.READ);
|
acl.setPerm(PermType.READ);
|
||||||
acl.setType(AclType.USER);
|
|
||||||
acl.setTargetId(share.getId());
|
acl.setTargetId(share.getId());
|
||||||
aclDao.create(acl, principal.getId());
|
aclDao.create(acl, principal.getId());
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import javax.json.JsonArray;
|
|||||||
import javax.json.JsonObject;
|
import javax.json.JsonObject;
|
||||||
import javax.ws.rs.client.Entity;
|
import javax.ws.rs.client.Entity;
|
||||||
import javax.ws.rs.core.Form;
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ public class TestRouteResource extends BaseJerseyTest {
|
|||||||
JsonObject routeStep = json.getJsonObject("route_step");
|
JsonObject routeStep = json.getJsonObject("route_step");
|
||||||
Assert.assertNotNull(routeStep);
|
Assert.assertNotNull(routeStep);
|
||||||
Assert.assertFalse(routeStep.getBoolean("transitionable"));
|
Assert.assertFalse(routeStep.getBoolean("transitionable"));
|
||||||
|
Assert.assertEquals("Check the document's metadata", routeStep.getString("name"));
|
||||||
|
|
||||||
// Get document 1 as admin
|
// Get document 1 as admin
|
||||||
json = target().path("/document/" + document1Id).request()
|
json = target().path("/document/" + document1Id).request()
|
||||||
@ -72,5 +74,56 @@ public class TestRouteResource extends BaseJerseyTest {
|
|||||||
routeStep = json.getJsonObject("route_step");
|
routeStep = json.getJsonObject("route_step");
|
||||||
Assert.assertNotNull(routeStep);
|
Assert.assertNotNull(routeStep);
|
||||||
Assert.assertTrue(routeStep.getBoolean("transitionable"));
|
Assert.assertTrue(routeStep.getBoolean("transitionable"));
|
||||||
|
Assert.assertEquals("Check the document's metadata", routeStep.getString("name"));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Get document 1 as admin
|
||||||
|
json = target().path("/document/" + document1Id).request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
routeStep = json.getJsonObject("route_step");
|
||||||
|
Assert.assertNotNull(routeStep);
|
||||||
|
Assert.assertEquals("Add relevant files to the document", routeStep.getString("name"));
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
.param("comment", "OK")), JsonObject.class);
|
||||||
|
|
||||||
|
// Get document 1 as admin
|
||||||
|
json = target().path("/document/" + document1Id).request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
routeStep = json.getJsonObject("route_step");
|
||||||
|
Assert.assertNotNull(routeStep);
|
||||||
|
Assert.assertEquals("Approve the document", routeStep.getString("name"));
|
||||||
|
|
||||||
|
// 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", "APPROVED")), JsonObject.class);
|
||||||
|
|
||||||
|
// Get document 1 as admin
|
||||||
|
Response response = target().path("/document/" + document1Id).request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||||
|
.get();
|
||||||
|
Assert.assertEquals(Response.Status.NOT_FOUND, Response.Status.fromStatusCode(response.getStatus()));
|
||||||
|
|
||||||
|
// Get document 1 as admin
|
||||||
|
json = target().path("/document/" + document1Id).request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
Assert.assertFalse(json.containsKey("route_step"));
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user