diff --git a/extract/pom.xml b/extract/pom.xml
index d78b05a2..e4349f48 100644
--- a/extract/pom.xml
+++ b/extract/pom.xml
@@ -230,6 +230,12 @@
javase3.5.3
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+ jar
+
@@ -262,7 +268,7 @@
falsetarget/failsafe-reports/failsafe-summary-integration.xml
- ch.asit_asso.extract.integration.**.*IntegrationTest
+ ch.asit_asso.extract.integration.**.*RequestsOwnershipIntegrationTest${project.build.directory}/logs
diff --git a/extract/src/main/java/ch/asit_asso/extract/configuration/EnvResolvingMessageSource.java b/extract/src/main/java/ch/asit_asso/extract/configuration/EnvResolvingMessageSource.java
new file mode 100644
index 00000000..8b12c48a
--- /dev/null
+++ b/extract/src/main/java/ch/asit_asso/extract/configuration/EnvResolvingMessageSource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 arusakov
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package ch.asit_asso.extract.configuration;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.core.env.Environment;
+import org.springframework.util.StringValueResolver;
+
+/**
+ * A MessageSource that resolves ${…} placeholders against the Spring {@link Environment}
+ * after the message has been loaded from the bundle.
+ */
+public class EnvResolvingMessageSource extends ReloadableResourceBundleMessageSource
+ implements EnvironmentAware {
+
+ private StringValueResolver placeholderResolver;
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ // Spring already knows how to resolve ${…} against the environment,
+ // we just delegate to its built‑in resolver.
+ this.placeholderResolver = environment::resolveRequiredPlaceholders;
+ }
+
+ @Override
+ protected String resolveCodeWithoutArguments(String code, Locale locale) {
+ var raw = super.resolveCodeWithoutArguments(code, locale);
+
+ return raw == null || placeholderResolver == null ? raw :
+ placeholderResolver.resolveStringValue(raw);
+ }
+
+ @Override
+ protected MessageFormat resolveCode(String code, Locale locale) {
+ var raw = super.resolveCode(code, locale);
+ if (raw != null && placeholderResolver != null) {
+ raw.applyPattern(placeholderResolver.resolveStringValue(raw.toPattern()));
+ }
+ return raw;
+ }
+}
\ No newline at end of file
diff --git a/extract/src/main/java/ch/asit_asso/extract/configuration/I18nConfiguration.java b/extract/src/main/java/ch/asit_asso/extract/configuration/I18nConfiguration.java
index 1aae10c0..bc46da35 100644
--- a/extract/src/main/java/ch/asit_asso/extract/configuration/I18nConfiguration.java
+++ b/extract/src/main/java/ch/asit_asso/extract/configuration/I18nConfiguration.java
@@ -80,7 +80,7 @@ public class I18nConfiguration {
@Bean
public MessageSource messageSource() {
this.logger.debug("Configuring the message source for languages: {}.", this.language);
- ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
+ EnvResolvingMessageSource messageSource = new EnvResolvingMessageSource();
// la collection des base names
List basenames = new ArrayList<>();
diff --git a/extract/src/main/java/ch/asit_asso/extract/domain/Request.java b/extract/src/main/java/ch/asit_asso/extract/domain/Request.java
index 31704204..d4f1db2b 100644
--- a/extract/src/main/java/ch/asit_asso/extract/domain/Request.java
+++ b/extract/src/main/java/ch/asit_asso/extract/domain/Request.java
@@ -18,6 +18,9 @@
import java.io.Serializable;
import java.util.Calendar;
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -28,6 +31,8 @@
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
@@ -35,6 +40,7 @@
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlTransient;
import org.apache.commons.lang3.StringUtils;
@@ -252,7 +258,40 @@ public class Request implements Serializable {
private Connector connector;
-
+ /**
+ * The operators that supervise this process.
+ */
+ @JoinTable(name = "requests_users",
+ joinColumns = {
+ @JoinColumn(name = "id_request", referencedColumnName = "id_request",
+ foreignKey = @ForeignKey(name = "FK_REQUESTS_USERS_REQUESTS")
+ )
+ },
+ inverseJoinColumns = {
+ @JoinColumn(name = "id_user", referencedColumnName = "id_user",
+ foreignKey = @ForeignKey(name = "FK_REQUESTS_USERS_USER")
+ )
+ }
+ )
+ @ManyToMany
+ private Collection usersCollection;
+
+
+ @JoinTable(name = "requests_usergroups",
+ joinColumns = {
+ @JoinColumn(name = "id_request", referencedColumnName = "id_request",
+ foreignKey = @ForeignKey(name = "FK_REQUESTS_USERGROUPS_REQUESTS")
+ )
+ },
+ inverseJoinColumns = {
+ @JoinColumn(name = "id_usergroup", referencedColumnName = "id_usergroup",
+ foreignKey = @ForeignKey(name = "FK_REQUESTS_USERGROUPS_USERGROUP")
+ )
+ }
+ )
+ @ManyToMany
+ private Collection userGroupsCollection;
+
/**
* The possible states of a data item order processing.
*/
@@ -1006,4 +1045,74 @@ public final void reject(final String rejectionRemark) {
this.setRejected(true);
}
+
+ /**
+ * Obtains the users that supervise this particular task. This only contains the users defined
+ * directly, not those defined through a user group. To get all the operators independently of
+ * how they've been defined, please use the method
+ * {@link #getDistinctOperators()}
+ *
+ * @return a collection that contains the operators
+ */
+ @XmlTransient
+ public Collection getUsersCollection() {
+ return usersCollection;
+ }
+
+
+
+ /**
+ * Defines the users that supervise this particular task.
+ *
+ * @param users a collection that contains the operators for this task
+ */
+ public void setUsersCollection(final Collection users) {
+ this.usersCollection = users;
+ }
+
+
+ /**
+ * Obtains the user groups that supervise this particular task.
+ *
+ * @return a collection that contains the groups of operators
+ */
+ @XmlTransient
+ public Collection getUserGroupsCollection() {
+ return userGroupsCollection;
+ }
+
+
+
+ /**
+ * Defines the user groups that supervise this particular task.
+ *
+ * @param userGroups a collection that contains the groups of operators for this task
+ */
+ public void setUserGroupsCollection(Collection userGroups) {
+ this.userGroupsCollection = userGroups;
+ }
+
+ /**
+ * Obtains a list of all the users allowed to manage this process, including those defined through a user group,
+ * without duplicates.
+ *
+ * @return a collection that contains all the operators for this process
+ */
+ public final Collection getDistinctOperators() {
+ List operators = new ArrayList<>(this.getUsersCollection());
+
+ for (UserGroup operatorsGroup : this.getUserGroupsCollection()) {
+
+ for (User groupOperator : operatorsGroup.getUsersCollection()) {
+
+ if (operators.contains((groupOperator))) {
+ continue;
+ }
+
+ operators.add(groupOperator);
+ }
+ }
+
+ return operators;
+ }
}
diff --git a/extract/src/main/java/ch/asit_asso/extract/domain/User.java b/extract/src/main/java/ch/asit_asso/extract/domain/User.java
index 7e351746..4dc28fbc 100644
--- a/extract/src/main/java/ch/asit_asso/extract/domain/User.java
+++ b/extract/src/main/java/ch/asit_asso/extract/domain/User.java
@@ -73,13 +73,19 @@
@NamedQuery(name = "User.findAllActiveApplicationUsers",
query = "SELECT u FROM User u WHERE u.login != " + "'" + User.SYSTEM_USER_LOGIN + "' and u.active = true"),
@NamedQuery(name = "User.getUserAssociatedRequestsByStatusOrderByEndDate",
- query = "SELECT r FROM Request r WHERE (r.process IN (SELECT p FROM User u JOIN u.processesCollection p WHERE u.id = :userId)"
- + " OR r.process IN (SELECT p FROM User u JOIN u.userGroupsCollection g JOIN g.processesCollection p WHERE u.id = :userId))"
- + " AND r.status = :status ORDER BY r.endDate DESC"),
+ query = "SELECT r FROM Request r WHERE ("
+ + " r.process IN (SELECT p FROM User u JOIN u.processesCollection p WHERE u.id = :userId)"
+ + " OR r.process IN (SELECT p FROM User u JOIN u.userGroupsCollection g JOIN g.processesCollection p WHERE u.id = :userId)"
+ + " OR :userId IN (SELECT uc.id FROM r.usersCollection uc)"
+ + " OR :userId IN (SELECT uc.id FROM r.userGroupsCollection ug LEFT JOIN ug.usersCollection uc)"
+ + ") AND r.status = :status ORDER BY r.endDate DESC"),
@NamedQuery(name = "User.getUserAssociatedRequestsByStatusNot",
- query = "SELECT r FROM Request r WHERE (r.process IN (SELECT p FROM User u JOIN u.processesCollection p WHERE u.id = :userId)"
- + " OR r.process IN (SELECT p FROM User u JOIN u.userGroupsCollection g JOIN g.processesCollection p WHERE u.id = :userId))"
- + " AND r.status != :status")
+ query = "SELECT r FROM Request r WHERE ("
+ + " r.process IN (SELECT p FROM User u JOIN u.processesCollection p WHERE u.id = :userId)"
+ + " OR r.process IN (SELECT p FROM User u JOIN u.userGroupsCollection g JOIN g.processesCollection p WHERE u.id = :userId)"
+ + " OR :userId IN (SELECT uc.id FROM r.usersCollection uc)"
+ + " OR :userId IN (SELECT uc.id FROM r.userGroupsCollection ug LEFT JOIN ug.usersCollection uc)"
+ + ") AND r.status != :status")
})
diff --git a/extract/src/main/java/ch/asit_asso/extract/persistence/specifications/RequestSpecification.java b/extract/src/main/java/ch/asit_asso/extract/persistence/specifications/RequestSpecification.java
index c4ebfa0d..d851f8de 100644
--- a/extract/src/main/java/ch/asit_asso/extract/persistence/specifications/RequestSpecification.java
+++ b/extract/src/main/java/ch/asit_asso/extract/persistence/specifications/RequestSpecification.java
@@ -27,6 +27,7 @@
import ch.asit_asso.extract.domain.Process;
import ch.asit_asso.extract.domain.Request;
import ch.asit_asso.extract.domain.Request_;
+import ch.asit_asso.extract.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.Specification;
@@ -237,7 +238,28 @@ public Predicate toPredicate(final Root root, final CriteriaQuery> qu
};
}
+ /**
+ * Obtains the criteria to filter the request based on the process it is associated with.
+ *
+ * @param processesList a list that contains the process that the request can be associated with
+ * @return the set of criteria to apply the process filter
+ */
+ public static Specification isBoundToUser(final User user) {
+
+ return new Specification() {
+ @Override
+ public Predicate toPredicate(final Root root, final CriteriaQuery> query,
+ final CriteriaBuilder builder) {
+ var result = builder.isMember(user, root.get(Request_.usersCollection));
+ for (var ug: user.getUserGroupsCollection()) {
+ result = builder.or(result, builder.isMember(ug, root.get(Request_.userGroupsCollection)));
+ }
+ return result;
+ }
+
+ };
+ }
/**
* Obtains the criteria to return only the request whose process has completed.
diff --git a/extract/src/main/java/ch/asit_asso/extract/web/controllers/IndexController.java b/extract/src/main/java/ch/asit_asso/extract/web/controllers/IndexController.java
index bf676329..14c7c3c3 100644
--- a/extract/src/main/java/ch/asit_asso/extract/web/controllers/IndexController.java
+++ b/extract/src/main/java/ch/asit_asso/extract/web/controllers/IndexController.java
@@ -501,8 +501,10 @@ private Page getFinishedRequests(final int pageStart, final String sort
String.join(", ",
userProcesses.stream().map((process) ->
process.getId().toString()).toArray(String[]::new)));
+
final Specification userCriteria
- = RequestSpecification.isProcessInList(userProcesses);
+ = RequestSpecification.isProcessInList(userProcesses).or(
+ RequestSpecification.isBoundToUser(currentUser));
return this.requestsRepository.findAll(Specification.where(userCriteria).and(searchCriteria), paging);
}
diff --git a/extract/src/main/java/ch/asit_asso/extract/web/controllers/RequestsController.java b/extract/src/main/java/ch/asit_asso/extract/web/controllers/RequestsController.java
index 769aeb0e..7e62dae5 100644
--- a/extract/src/main/java/ch/asit_asso/extract/web/controllers/RequestsController.java
+++ b/extract/src/main/java/ch/asit_asso/extract/web/controllers/RequestsController.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
@@ -32,6 +33,7 @@
import ch.asit_asso.extract.domain.RequestHistoryRecord;
import ch.asit_asso.extract.domain.Task;
import ch.asit_asso.extract.domain.User;
+import ch.asit_asso.extract.domain.UserGroup;
import ch.asit_asso.extract.orchestrator.OrchestratorSettings;
import ch.asit_asso.extract.persistence.ProcessesRepository;
import ch.asit_asso.extract.persistence.RemarkRepository;
@@ -39,6 +41,7 @@
import ch.asit_asso.extract.persistence.RequestsRepository;
import ch.asit_asso.extract.persistence.SystemParametersRepository;
import ch.asit_asso.extract.persistence.TasksRepository;
+import ch.asit_asso.extract.persistence.UserGroupsRepository;
import ch.asit_asso.extract.persistence.UsersRepository;
import ch.asit_asso.extract.utils.FileSystemUtils;
import ch.asit_asso.extract.utils.FileSystemUtils.RequestDataFolder;
@@ -46,6 +49,10 @@
import ch.asit_asso.extract.web.Message;
import ch.asit_asso.extract.web.Message.MessageType;
import ch.asit_asso.extract.web.model.RequestModel;
+import ch.asit_asso.extract.web.model.UserModel;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@@ -169,7 +176,11 @@ public class RequestsController extends BaseController {
@Autowired
private UsersRepository usersRepository;
-
+ /**
+ * The Spring Data object that links the user data objects with the data source.
+ */
+ @Autowired
+ private UserGroupsRepository userGroupsRepository;
/**
* Processes a request to display detailed information about an order.
@@ -207,6 +218,9 @@ public final String viewItem(@PathVariable final int itemId, final ModelMap mode
this.parametersRepository.getValidationFocusProperties().split(","));
model.addAttribute("request", requestModel);
+ model.addAttribute("allactiveusers", this.getAllActiveUsers());
+ model.addAttribute("allusergroups", this.getAllUserGroups());
+
Task currentTask = this.getCurrentTask(requestModel);
if (currentTask != null) {
@@ -977,7 +991,7 @@ public final synchronized String handleValidateStandbyRequest(@PathVariable fina
this.addStatusMessage(redirectAttributes, "requestDetails.error.request.notAllowed", MessageType.ERROR);
return REDIRECT_TO_ACCESS_DENIED;
- }
+ }
if (!this.canRequestBeValidated(request, currentStep, redirectAttributes)) {
return RequestsController.REDIRECT_TO_LIST;
@@ -996,6 +1010,38 @@ public final synchronized String handleValidateStandbyRequest(@PathVariable fina
}
+ @PostMapping("{requestId}/assign")
+ public final synchronized String handleAssignRequest(
+ @PathVariable final int requestId,
+ @RequestParam List usersIds,
+ @RequestParam List userGroupsIds) {
+ var request = getDomainRequest(requestId);
+ assert request != null : "The request cannot be null.";
+ assert request.getProcess() != null : "The request must be associated with a process.";
+ if (!this.canCurrentUserViewRequestDetails(request)) {
+ this.logger.warn("The user {} tried to assign users to the request {} but is not allowed to.",
+ this.getCurrentUserLogin(), request.getId());
+ return REDIRECT_TO_ACCESS_DENIED;
+ }
+
+ var usersToAdd = usersIds.stream().map(this.usersRepository::findById)
+ .distinct()
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ var groupsToAdd = userGroupsIds.stream().map(this.userGroupsRepository::findById)
+ .distinct()
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ request.setUsersCollection(usersToAdd);
+ request.setUserGroupsCollection(groupsToAdd);
+ this.requestsRepository.save(request);
+
+ return String.format(RequestsController.REDIRECT_TO_DETAILS_FORMAT, requestId);
+ }
/**
* Adds record entries for the remaining tasks when a requests is set to skip to the end of its process.
@@ -1162,8 +1208,11 @@ private boolean canCurrentUserViewRequestDetails(final Request request) {
return false;
}
- Integer[] operatorsIds = process.getDistinctOperators().stream().map(User::getId).toArray(Integer[]::new);
- return ArrayUtils.contains(operatorsIds, this.getCurrentUserId());
+ var currentId = this.getCurrentUserId();
+ return Stream.concat(
+ process.getDistinctOperators().stream(),
+ request.getDistinctOperators().stream()
+ ).map(User::getId).anyMatch((id) -> currentId == id);
}
@@ -1904,5 +1953,24 @@ void validateRequest(final Request request, final String remark) {
private Request getDomainRequest(int requestId) {
return this.requestsRepository.findById(requestId).orElse(null);
}
+
+ /**
+ * Fetches a list of users from the repository and returns a collection of active user objects.
+ *
+ * @return a list of existing active users
+ */
+ private List getAllActiveUsers() {
+ final List usersList = new ArrayList<>();
+
+ for (User domainUser : this.usersRepository.findAllActiveApplicationUsers()) {
+
+ usersList.add(new UserModel(domainUser));
+ }
+ return usersList;
+ }
+
+ private Collection getAllUserGroups() {
+ return this.userGroupsRepository.findAllByOrderByName();
+ }
}
diff --git a/extract/src/main/java/ch/asit_asso/extract/web/model/OwnedObjectModel.java b/extract/src/main/java/ch/asit_asso/extract/web/model/OwnedObjectModel.java
new file mode 100644
index 00000000..41814f88
--- /dev/null
+++ b/extract/src/main/java/ch/asit_asso/extract/web/model/OwnedObjectModel.java
@@ -0,0 +1,182 @@
+package ch.asit_asso.extract.web.model;
+
+import ch.asit_asso.extract.domain.Request;
+import ch.asit_asso.extract.domain.User;
+import ch.asit_asso.extract.domain.UserGroup;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+
+/*
+ * Copyright (C) 2025 arusakov
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ *
+ * @author arusakov
+ */
+public class OwnedObjectModel {
+
+ /**
+ * The operators groups associated to this process.
+ */
+ private final List userGroupsList;
+
+ /**
+ * An array that contains the identifiers of the operators groups associated with this process.
+ */
+ private String[] userGroupsIds;
+
+ /**
+ * The operators associated to this process.
+ */
+ private final List usersList;
+
+ /**
+ * An array that contains the identifiers of the operators associated with this process.
+ */
+ private String[] usersIds;
+
+
+ public OwnedObjectModel() {
+ this.userGroupsList = new ArrayList<>();
+ this.usersList = new ArrayList<>();
+ }
+
+ public OwnedObjectModel(Collection users, Collection userGroups) {
+ this();
+ setUsersFromDomainObject(users);
+ setUserGroupsFromDomainObject(userGroups);
+ }
+ /**
+ * Defines the users of this process.
+ *
+ * @param users an array containing the operators directly attached to this process
+ */
+ public final void setUsers(final UserModel[] users) {
+ this.usersList.clear();
+ this.usersList.addAll(Arrays.asList(users));
+
+ List list = new ArrayList<>();
+ for (UserModel user : this.usersList) {
+ list.add(user.getId().toString());
+ }
+ this.usersIds = list.toArray(String[]::new);
+ }
+
+
+
+ /**
+ * Defines the identifiers of the operators associated with this process.
+ *
+ * @param joinedUsersIds a string with the operator identifiers separated by commas
+ */
+ public final void setUsersIds(final String joinedUsersIds) {
+ this.usersIds = joinedUsersIds.split(",");
+ }
+
+ /**
+ * Obtains the identifiers of the operators associated to this process.
+ *
+ * @return a string with the identifiers separated by commas
+ */
+ public final String getUsersIds() {
+ return StringUtils.join(this.usersIds, ',');
+ }
+
+
+ /**
+ * Obtains the users of this process.
+ *
+ * @return an array containing the users that make up this process
+ */
+ public final UserModel[] getUsers() {
+ return this.usersList.toArray(UserModel[]::new);
+ }
+
+ /**
+ * Defines the process operators in this model based on what is in the data source.
+ *
+ * @param domainProcess the data object for this process
+ */
+ final void setUsersFromDomainObject(final Collection domainObjectUsers) {
+ assert domainObjectUsers != null : "The process data object must not be null.";
+
+ List usersIdsList = new ArrayList<>();
+ for (User user : domainObjectUsers) {
+ this.usersList.add(new UserModel(user));
+ usersIdsList.add(user.getId().toString());
+ }
+ this.usersIds = usersIdsList.toArray(String[]::new);
+ }
+
+ /**
+ * Obtains the identifiers of the operators groups associated to this process.
+ *
+ * @return a string with the identifiers separated by commas
+ */
+ public final String getUserGroupsIds() {
+ return StringUtils.join(this.userGroupsIds, ',');
+ }
+
+ /**
+ * Defines the process operators groups in this model based on what is in the data source.
+ *
+ * @param domainRequest the data object for this process
+ */
+ final void setUserGroupsFromDomainObject(Collection domainObjectUserGroups) {
+ assert domainObjectUserGroups != null : "The process data object must not be null.";
+
+ List userGroupsIdsList = new ArrayList<>();
+
+ for (UserGroup userGroup : domainObjectUserGroups) {
+ this.userGroupsList.add(userGroup);
+ userGroupsIdsList.add(userGroup.getId().toString());
+ }
+ this.userGroupsIds = userGroupsIdsList.toArray(String[]::new);
+ }
+
+ /**
+ * Defines the users groups of this process.
+ *
+ * @param userGroups an array containing the user groups that operate on this process
+ */
+ public final void setUserGroups(final UserGroup[] userGroups) {
+ this.userGroupsList.clear();
+ this.userGroupsList.addAll(List.of(userGroups));
+
+ List list = new ArrayList<>();
+
+ for (UserGroup userGroup : this.userGroupsList) {
+ list.add(userGroup.getId().toString());
+ }
+ this.usersIds = list.toArray(String[]::new);
+ }
+
+
+
+ /**
+ * Defines the identifiers of the operators groups associated with this process.
+ *
+ * @param joinedUserGroupsIds a string with the operator group identifiers separated by commas
+ */
+ public final void setUserGroupsIds(final String joinedUserGroupsIds) {
+ this.userGroupsIds = joinedUserGroupsIds.split(",");
+ }
+
+}
diff --git a/extract/src/main/java/ch/asit_asso/extract/web/model/ProcessModel.java b/extract/src/main/java/ch/asit_asso/extract/web/model/ProcessModel.java
index fbb99c1a..690b9f58 100644
--- a/extract/src/main/java/ch/asit_asso/extract/web/model/ProcessModel.java
+++ b/extract/src/main/java/ch/asit_asso/extract/web/model/ProcessModel.java
@@ -36,7 +36,7 @@
*
* @author Florent Krin
*/
-public class ProcessModel {
+public class ProcessModel extends OwnedObjectModel {
/**
* The number that uniquely identifies this process.
@@ -68,26 +68,6 @@ public class ProcessModel {
*/
private final List tasksList;
- /**
- * The operators groups associated to this process.
- */
- private final List userGroupsList;
-
- /**
- * An array that contains the identifiers of the operators groups associated with this process.
- */
- private String[] userGroupsIds;
-
- /**
- * The operators associated to this process.
- */
- private final List usersList;
-
- /**
- * An array that contains the identifiers of the operators associated with this process.
- */
- private String[] usersIds;
-
/**
* The vertical scroll position of the page.
*/
@@ -191,51 +171,6 @@ public final TaskModel getTask(final int taskId) {
return null;
}
-
-
- /**
- * Obtains the users of this process.
- *
- * @return an array containing the users that make up this process
- */
- public final UserGroup[] getUserGroups() {
- return this.userGroupsList.toArray(new UserGroup[]{});
- }
-
-
- /**
- * Obtains the identifiers of the operators groups associated to this process.
- *
- * @return a string with the identifiers separated by commas
- */
- public final String getUserGroupsIds() {
- return StringUtils.join(this.userGroupsIds, ',');
- }
-
-
-
- /**
- * Obtains the users of this process.
- *
- * @return an array containing the users that make up this process
- */
- public final UserModel[] getUsers() {
- return this.usersList.toArray(new UserModel[]{});
- }
-
-
-
- /**
- * Obtains the identifiers of the operators associated to this process.
- *
- * @return a string with the identifiers separated by commas
- */
- public final String getUsersIds() {
- return StringUtils.join(this.usersIds, ',');
- }
-
-
-
/**
* Gets the data objects for the tasks that compose this process.
*
@@ -265,67 +200,6 @@ public final void setTasks(final TaskModel[] tasks) {
this.tasksList.addAll(Arrays.asList(tasks));
}
-
-
- /**
- * Defines the users groups of this process.
- *
- * @param userGroups an array containing the user groups that operate on this process
- */
- public final void setUserGroups(final UserGroup[] userGroups) {
- this.userGroupsList.clear();
- this.userGroupsList.addAll(Arrays.asList(userGroups));
-
- List list = new ArrayList<>();
-
- for (UserGroup userGroup : this.userGroupsList) {
- list.add(userGroup.getId().toString());
- }
- this.usersIds = list.toArray(new String[]{});
- }
-
-
-
- /**
- * Defines the identifiers of the operators groups associated with this process.
- *
- * @param joinedUserGroupsIds a string with the operator group identifiers separated by commas
- */
- public final void setUserGroupsIds(final String joinedUserGroupsIds) {
- this.userGroupsIds = joinedUserGroupsIds.split(",");
- }
-
-
-
- /**
- * Defines the users of this process.
- *
- * @param users an array containing the operators directly attached to this process
- */
- public final void setUsers(final UserModel[] users) {
- this.usersList.clear();
- this.usersList.addAll(Arrays.asList(users));
-
- List list = new ArrayList<>();
- for (UserModel user : this.usersList) {
- list.add(user.getId().toString());
- }
- this.usersIds = list.toArray(new String[]{});
- }
-
-
-
- /**
- * Defines the identifiers of the operators associated with this process.
- *
- * @param joinedUsersIds a string with the operator identifiers separated by commas
- */
- public final void setUsersIds(final String joinedUsersIds) {
- this.usersIds = joinedUsersIds.split(",");
- }
-
-
-
/**
* Inserts a task in this process.
*
@@ -418,9 +292,8 @@ public final void setDeletable(final boolean canBeDeleted) {
* Creates a new instance of this model.
*/
public ProcessModel() {
+ super();
this.tasksList = new ArrayList<>();
- this.userGroupsList = new ArrayList<>();
- this.usersList = new ArrayList<>();
}
@@ -473,8 +346,8 @@ public ProcessModel(final Process domainProcess,
this.deletable = (requestsRepository != null) ? domainProcess.canBeDeleted(requestsRepository)
: domainProcess.canBeDeleted();
this.setTasksFromDomainObject(domainProcess, taskPluginsDiscoverer);
- this.setUserGroupsFromDomainObject(domainProcess);
- this.setUsersFromDomainObject(domainProcess);
+ setUsersFromDomainObject(domainProcess.getUsersCollection());
+ setUserGroupsFromDomainObject(domainProcess.getUserGroupsCollection());
}
@@ -621,46 +494,6 @@ private void setTasksFromDomainObject(final Process domainProcess,
}
-
-
- /**
- * Defines the process operators groups in this model based on what is in the data source.
- *
- * @param domainProcess the data object for this process
- */
- private void setUserGroupsFromDomainObject(final Process domainProcess) {
- assert domainProcess != null : "The process data object must not be null.";
-
- List userGroupsIdsList = new ArrayList<>();
-
- for (UserGroup userGroup : domainProcess.getUserGroupsCollection()) {
- this.userGroupsList.add(userGroup);
- userGroupsIdsList.add(userGroup.getId().toString());
- }
-
- this.userGroupsIds = userGroupsIdsList.toArray(String[]::new);
- }
-
-
-
- /**
- * Defines the process operators in this model based on what is in the data source.
- *
- * @param domainProcess the data object for this process
- */
- private void setUsersFromDomainObject(final Process domainProcess) {
- assert domainProcess != null : "The process data object must not be null.";
-
- List usersIdsList = new ArrayList<>();
- for (User user : domainProcess.getUsersCollection()) {
- this.usersList.add(new UserModel(user));
- usersIdsList.add(user.getId().toString());
- }
- this.usersIds = usersIdsList.toArray(String[]::new);
- }
-
-
-
/**
* Removes the task that matches the provided identifier. (There should only be one
* anyway.)
diff --git a/extract/src/main/java/ch/asit_asso/extract/web/model/RequestModel.java b/extract/src/main/java/ch/asit_asso/extract/web/model/RequestModel.java
index 2b793f1a..f4b4b0e1 100644
--- a/extract/src/main/java/ch/asit_asso/extract/web/model/RequestModel.java
+++ b/extract/src/main/java/ch/asit_asso/extract/web/model/RequestModel.java
@@ -30,6 +30,8 @@
import ch.asit_asso.extract.domain.Connector;
import ch.asit_asso.extract.domain.Request;
import ch.asit_asso.extract.domain.Task;
+import ch.asit_asso.extract.domain.User;
+import ch.asit_asso.extract.domain.UserGroup;
import ch.asit_asso.extract.domain.comparators.RequestHistoryRecordByStepComparator;
import ch.asit_asso.extract.domain.converters.JsonToParametersValuesConverter;
import ch.asit_asso.extract.exceptions.BaseFolderNotFoundException;
@@ -47,7 +49,7 @@
*
* @author Yves Grasset
*/
-public class RequestModel {
+public class RequestModel extends OwnedObjectModel {
/**
* The string that identifies the localized label of an order export task.
@@ -134,8 +136,6 @@ public class RequestModel {
*/
private final Path outputFolderPath;
-
-
private final List validationFocusProperties;
@@ -176,14 +176,13 @@ public RequestModel(final Request domainRequest, final RequestHistoryRecord[] hi
Arrays.sort(this.fullHistory, new RequestHistoryRecordByStepComparator());
this.currentProcessStep = (!ArrayUtils.isEmpty(this.fullHistory))
? this.fullHistory[this.fullHistory.length - 1].getProcessStep() : -1;
-
this.processHistory = this.buildProcessHistory();
this.validationFocusProperties = List.of(validationFocusProperties);
this.logger.debug("The process history contains {} items.", this.processHistory.length);
+ setUsersFromDomainObject(domainRequest.getUsersCollection());
+ setUserGroupsFromDomainObject(domainRequest.getUserGroupsCollection());
}
-
-
/**
* Obtains the connector that imported this request.
*
diff --git a/extract/src/main/resources/messages_de.properties b/extract/src/main/resources/messages_de.properties
index 98043331..599e6d02 100644
--- a/extract/src/main/resources/messages_de.properties
+++ b/extract/src/main/resources/messages_de.properties
@@ -337,6 +337,9 @@ requestDetails.fields.orderLabel.label=Bezeichnung des Befehls (OrderLabel):
requestDetails.fields.productGuid.label=GUID des Produkts (Product):
requestDetails.fields.requestId.label=Extract-ID der Anfrage (Request):
requestDetails.fields.tiersGuid.label=GUID des Dritten (Tiers):
+requestDetails.fields.operators.label=Zugewiesene Operatoren
+requestDetails.fields.operators.groups.label=Gruppen
+requestDetails.fields.operators.users.label=Benutzer
requestDetails.files.title=Dateien:
requestDetails.files.none=(Keine)
requestDetails.files.add.button.label=Dateien hinzufügen…
@@ -354,6 +357,7 @@ requestDetails.orderDetails.thirdParty.title=Dritter
requestDetails.panels.adminTools.title=Administration
requestDetails.panels.orderDetails.title=Kundenanfrage
requestDetails.panels.response.title=Antwort an den Kunden
+requestDetails.panels.ownership.title=Zusätzliche Eigentümer
requestDetails.process.none=Ohne Übereinstimmung
requestDetails.process.title=Verarbeitung: {0}
requestDetails.processHistory.headers.endDate=Ende
@@ -784,7 +788,7 @@ importTask.message.error.noGeometry=Dieses Element hat keinen geografischen Umfa
#Generic strings
email.general.ending=Mit freundlichen Grüssen,
email.general.greeting=Hallo,
-email.general.signature=Die Anwendung Extract
+email.general.signature=${EMAIL_GENERAL_SIGNATURE_DE:Die Anwendung Extract}
#Orders import through a connector failed
email.connectorImportFailed.action=Bitte konsultieren Sie das Dashboard von Extract für weitere Details:
@@ -862,4 +866,4 @@ usersList.filter.2fa.placeholder=2FA
usersList.card.title=Benutzer und Rechte
#### Temporary strings used during development ####
-development.notImplemented=Noch nicht implementiert
\ No newline at end of file
+development.notImplemented=Noch nicht implementiert
diff --git a/extract/src/main/resources/messages_en.properties b/extract/src/main/resources/messages_en.properties
index dc5b780c..cbd5595c 100644
--- a/extract/src/main/resources/messages_en.properties
+++ b/extract/src/main/resources/messages_en.properties
@@ -352,6 +352,7 @@ requestDetails.orderDetails.thirdParty.title=Third party
requestDetails.panels.adminTools.title=Administration
requestDetails.panels.orderDetails.title=Customer request
requestDetails.panels.response.title=Response to customer
+requestDetails.panels.ownership.title=Additional owners
requestDetails.process.none=No match
requestDetails.process.title=Process: {0}
requestDetails.processHistory.headers.endDate=End
diff --git a/extract/src/main/resources/messages_fr.properties b/extract/src/main/resources/messages_fr.properties
index 72d7685a..dfe2807c 100644
--- a/extract/src/main/resources/messages_fr.properties
+++ b/extract/src/main/resources/messages_fr.properties
@@ -335,6 +335,9 @@ requestDetails.fields.orderLabel.label=Libell\u00e9 de la commande (OrderLabel)\
requestDetails.fields.productGuid.label=GUID du produit (Product)\u00a0:
requestDetails.fields.requestId.label=ID Extract de la requ\u00eate (Request)\u00a0:
requestDetails.fields.tiersGuid.label=GUID du tiers (Tiers)\u00a0:
+requestDetails.fields.operators.label=Op\u00e9rateurs attitr\u00e9s
+requestDetails.fields.operators.groups.label=Groupes
+requestDetails.fields.operators.users.label=Utilisateurs
requestDetails.files.title=Fichiers\u00a0:
requestDetails.files.none=(Aucun)
requestDetails.files.add.button.label=Ajouter des fichiers\u2026
@@ -352,6 +355,7 @@ requestDetails.orderDetails.thirdParty.title=Tiers
requestDetails.panels.adminTools.title=Administration
requestDetails.panels.orderDetails.title=Demande client
requestDetails.panels.response.title=R\u00e9ponse au client
+requestDetails.panels.ownership.title=Propriétaires supplémentaires
requestDetails.process.none=Sans correspondance
requestDetails.process.title=Traitement\u00a0: {0}
requestDetails.processHistory.headers.endDate=Fin
@@ -529,36 +533,36 @@ login.logout.success=Vous avez \u00e9t\u00e9 d\u00e9connect\u00e9 avec succ\u00e
setup.actions.submit=Cr\u00e9er le compte
setup.body.title=Cr\u00e9ation d'un compte administrateur
setup.body.introduction=Veuillez cr\u00e9er un compte administrateur pour acc\u00e9der \u00e0 l'application Extract. Cette \u00e9tape est indispensable avant toute utilisation.
-setup.error.message.one=Le compte n'a pas pu \u00EAtre cr\u00e9\u00e9, car l'erreur suivante est survenue:
-setup.error.message.multiple=Le compte n'a pas pu \u00EAtre cr\u00e9\u00e9, car les erreurs suivantes sont survenues:
+setup.error.message.one=Le compte n'a pas pu \u00eatre cr\u00e9\u00e9, car l'erreur suivante est survenue:
+setup.error.message.multiple=Le compte n'a pas pu \u00eatre cr\u00e9\u00e9, car les erreurs suivantes sont survenues:
setup.fields.name.label=Nom complet
setup.fields.email.label=Courriel
setup.fields.login.label=Identifiant de connexion
-setup.fields.login.reserved=L'identifiant ne doit pas contenir de mots r\u00E9serv\u00E9s
+setup.fields.login.reserved=L'identifiant ne doit pas contenir de mots r\u00e9serv\u00e9s
setup.fields.password1.label=Mot de passe
setup.fields.password2.label=Confirmer le mot de passe
-setup.fields.password.size=Le mot de passe doit avoir entre {0} et {1} caract\u00E8res
+setup.fields.password.size=Le mot de passe doit avoir entre {0} et {1} caract\u00e8res
setup.fields.password.uppercase=Le mot de passe doit contenir au moins une lettre majuscule
setup.fields.password.lowercase=Le mot de passe doit contenir au moins une lettre minuscule
setup.fields.password.digit=Le mot de passe doit contenir au moins un chiffre
-setup.fields.password.special=Le mot de passe doit contenir au moins un caract\u00E8re sp\u00E9cial
+setup.fields.password.special=Le mot de passe doit contenir au moins un caract\u00e8re sp\u00e9cial
setup.fields.password.common=Le mot de passe est trop commun
-setup.fields.password.sequential=Le mot de passe ne doit pas contenir de s\u00E9quences ou de caract\u00E8res r\u00E9p\u00E9t\u00E9s
+setup.fields.password.sequential=Le mot de passe ne doit pas contenir de s\u00e9quences ou de caract\u00e8res r\u00e9p\u00e9t\u00e9s
validation.password.policy=Le mot de passe ne respecte pas la politique
setup.passwords.not.match=Les mots de passe ne correspondent pas
setup.fields.name.constraint.mandatory=Le nom est obligatoire
-setup.fields.name.constraint.size=Le nom doit contenir entre {min} et {max} caract\u00E8res
+setup.fields.name.constraint.size=Le nom doit contenir entre {min} et {max} caract\u00e8res
setup.fields.email.constraint.mandatory=L'adresse de courriel est obligatoire
setup.fields.email.constraint.format=Le format de courriel est incorrect
setup.fields.login.constraint.mandatory=L'identifiant de connexion est obligatoire
-setup.fields.login.constraint.size=Le login doit contenir entre {min} et {max} caract\u00E8res
-setup.fields.login.constraint.pattern=L'identifiant de connexion ne doit contenir que des minuscules, majuscules ou les caract\u00E8res '-' et '_'
+setup.fields.login.constraint.size=Le login doit contenir entre {min} et {max} caract\u00e8res
+setup.fields.login.constraint.pattern=L'identifiant de connexion ne doit contenir que des minuscules, majuscules ou les caract\u00e8res '-' et '_'
setup.fields.password1.constraint.mandatory=Le mot de passe est obligatoire
setup.fields.password1.constraint.policy=Le mot de passe ne respecte pas la politique
setup.fields.password2.constraint.policy=La configuration de mot de passe ne respecte pas la politique
setup.fields.password2.constraint.mandatory=La confirmation de mot de passe est obligatoire
setup.alerts.password.title=Politique de mots de passe
-setup.alerts.password.text=Votre mot de passe doit comporter entre 8 et 24 caract\u00E8res, incluant au moins une lettre minuscule, une lettre majuscule, un chiffre, et un caract\u00E8re sp\u00E9cial. Il ne doit pas contenir de r\u00E9p\u00E9titions de caract\u00E8res et doit \u00EAtre unique, c'est-\u00e0-dire ne pas figurer parmi les mots de passe courants.
+setup.alerts.password.text=Votre mot de passe doit comporter entre 8 et 24 caract\u00e8res, incluant au moins une lettre minuscule, une lettre majuscule, un chiffre, et un caract\u00e8re sp\u00e9cial. Il ne doit pas contenir de r\u00e9p\u00e9titions de caract\u00e8res et doit \u00eatre unique, c'est-\u00e0-dire ne pas figurer parmi les mots de passe courants.
#Parameters page
parameters.about.link.text=Documentation et code
@@ -783,7 +787,7 @@ importTask.message.error.noGeometry=Cet \u00e9l\u00e9ment n'a pas de p\u00e9rim\
#Generic strings
email.general.ending=Cordialement,
email.general.greeting=Bonjour,
-email.general.signature=L'application Extract
+email.general.signature=${EMAIL_GENERAL_SIGNATURE_FR:L'application Extract}
#Orders import through a connector failed
email.connectorImportFailed.action=Veuillez consulter le tableau de bord d'Extract pour plus de d\u00e9tails\u00a0:
diff --git a/extract/src/main/resources/static/js/requestDetails.js b/extract/src/main/resources/static/js/requestDetails.js
index b2272a9f..2a2f2e0a 100644
--- a/extract/src/main/resources/static/js/requestDetails.js
+++ b/extract/src/main/resources/static/js/requestDetails.js
@@ -1850,6 +1850,29 @@ function getRemarkText(remarkId, remarkType, targetControlId) {
});
}
+/**
+ * Sends the data about the current process to the server for adding or updating.
+ */
+function submitUserIds() {
+ //update usersIds in hidden input before saving process
+ var usersListIdsArray = $('#users').select2('val');
+ $('#usersIds').val(usersListIdsArray
+ .filter((value) => value.startsWith('user-'))
+ .map((value) => value.substring('user-'.length)).join(','));
+ $('#userGroupsIds').val(usersListIdsArray
+ .filter((value) => value.startsWith('group-'))
+ .map((value) => value.substring('group-'.length)).join(','));
+
+ $('.parameter-select').each(function (index, item) {
+ var idsArray = $(item).select2('val');
+ var selectId = $(item).attr('id');
+ var valuesFieldId = selectId.substring(0, selectId.length - '-select'.length);
+ var valuesField = document.getElementById(valuesFieldId);
+ $(valuesField).val(idsArray.join(','));
+ });
+
+ $('#requestOwnershipForm').submit();
+}
/********************* EVENT HANDLERS *********************/
@@ -1924,4 +1947,34 @@ $(function () {
var remarkId = parseInt(this.options[this.selectedIndex].value);
var remarkText = getRemarkText(remarkId, 'rejection', 'standbyCancelRemark');
});
+
+ $('#usersSaveButton').on('click', function () {
+ submitUserIds();
+ });
+
+ //set users in the multiple select
+ var usersIdsArray = $("#usersIds").val().split(',').map((value) => `user-${value}`);
+ var userGroupsIdsArray = $("#userGroupsIds").val().split(',').map((value) => `group-${value}`);
+ $('#users').val([...usersIdsArray, ...userGroupsIdsArray]);
+ $('#users').trigger('change');
+
+ function formatUserItem(item) {
+
+ if(!item.id) {
+ return item.text;
+ }
+
+ const icon = (item.id.startsWith('group-')) ? 'fa-users' : 'fa-user';
+ return $(` ${item.text}`);
+ }
+
+ $(".parameter-select.select2").select2({
+ multiple:true
+ });
+
+ $(".user-select.select2").select2({
+ templateSelection: formatUserItem,
+ templateResult: formatUserItem,
+ multiple:true
+ });
});
diff --git a/extract/src/main/resources/templates/pages/requests/details.html b/extract/src/main/resources/templates/pages/requests/details.html
index e074bdc4..41b902b7 100644
--- a/extract/src/main/resources/templates/pages/requests/details.html
+++ b/extract/src/main/resources/templates/pages/requests/details.html
@@ -349,6 +349,65 @@
+
+
+ {Additional owners}
+
+
+
+
+
+
+
+
+
diff --git a/extract/src/test/java/ch/asit_asso/extract/integration/requests/RequestModelIntegrationTest.java b/extract/src/test/java/ch/asit_asso/extract/integration/requests/RequestModelIntegrationTest.java
index 7a685233..7cd06ba3 100644
--- a/extract/src/test/java/ch/asit_asso/extract/integration/requests/RequestModelIntegrationTest.java
+++ b/extract/src/test/java/ch/asit_asso/extract/integration/requests/RequestModelIntegrationTest.java
@@ -15,8 +15,10 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.MessageSource;
import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.GregorianCalendar;
import static org.junit.jupiter.api.Assertions.*;
@@ -28,6 +30,7 @@
@SpringBootTest
@ActiveProfiles("test")
@Tag("integration")
+@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class RequestModelIntegrationTest {
@@ -70,6 +73,8 @@ public void setUpTestData() {
testRequestWithNullFolder.setConnector(testConnector);
testRequestWithNullFolder.setParameters("{}");
testRequestWithNullFolder.setPerimeter("{}");
+ testRequestWithNullFolder.setUsersCollection(new ArrayList<>());
+ testRequestWithNullFolder.setUserGroupsCollection(new ArrayList<>());
testRequestWithNullFolder = requestsRepository.save(testRequestWithNullFolder);
// Create a normal request with folder
@@ -83,6 +88,8 @@ public void setUpTestData() {
testRequestWithFolder.setConnector(testConnector);
testRequestWithFolder.setParameters("{}");
testRequestWithFolder.setPerimeter("{}");
+ testRequestWithFolder.setUsersCollection(new ArrayList<>());
+ testRequestWithFolder.setUserGroupsCollection(new ArrayList<>());
testRequestWithFolder = requestsRepository.save(testRequestWithFolder);
}
diff --git a/sql/update_db.sql b/sql/update_db.sql
index a0a0a1ef..1b5583c6 100644
--- a/sql/update_db.sql
+++ b/sql/update_db.sql
@@ -213,3 +213,35 @@ DROP INDEX IF EXISTS idx_users_usergroups_usergroup;
CREATE INDEX idx_users_usergroups_usergroup
ON users_usergroups (id_usergroup);
+
+-- REQUESTS_USERS Table
+
+ALTER TABLE requests_users
+ DROP CONSTRAINT IF EXISTS fk_processes_users_requests;
+
+ALTER TABLE requests_users
+ DROP CONSTRAINT IF EXISTS fk_processes_users_user;
+
+ALTER TABLE ONLY requests_users
+ ADD CONSTRAINT fk_processes_users_requests FOREIGN KEY (id_request)
+ REFERENCES public.requests(id_request);
+
+ALTER TABLE ONLY requests_users
+ ADD CONSTRAINT fk_processes_users_user FOREIGN KEY (id_user)
+ REFERENCES public.users(id_user);
+
+-- REQUESTS_USERS Table
+
+ALTER TABLE requests_users
+ DROP CONSTRAINT IF EXISTS fk_processes_usergroups_requests;
+
+ALTER TABLE requests_users
+ DROP CONSTRAINT IF EXISTS fk_processes_usergroups_usergroup;
+
+ALTER TABLE ONLY requests_usergroups
+ ADD CONSTRAINT fk_processes_usergroups_requests FOREIGN KEY (id_request)
+ REFERENCES public.requests(id_request);
+
+ALTER TABLE ONLY requests_usergroups
+ ADD CONSTRAINT fk_processes_usergroups_usergroup FOREIGN KEY (id_usergroup)
+ REFERENCES public.usergroups(id_usergroup);