From 61a9e6cc78222239978605ca978ae2cf12038b45 Mon Sep 17 00:00:00 2001 From: Andrey R Date: Mon, 1 Dec 2025 12:59:24 +0100 Subject: [PATCH 1/6] feat(GSGGR-581): per-request permission management (#11) * Working version: visibility for groups and users * Added filtering for completed requests * Added translations for the card title * Fixed session errors in test * Added basic test for the per-request ownership * Move ownership-related parts into an external class * Added db migrations, similar to existing ones --- extract/pom.xml | 8 +- .../ch/asit_asso/extract/domain/Request.java | 111 ++++++++++- .../ch/asit_asso/extract/domain/User.java | 18 +- .../specifications/RequestSpecification.java | 22 +++ .../web/controllers/IndexController.java | 4 +- .../web/controllers/RequestsController.java | 76 +++++++- .../extract/web/model/OwnedObjectModel.java | 182 ++++++++++++++++++ .../extract/web/model/ProcessModel.java | 175 +---------------- .../extract/web/model/RequestModel.java | 11 +- .../src/main/resources/messages_de.properties | 3 +- .../src/main/resources/messages_en.properties | 1 + .../src/main/resources/messages_fr.properties | 24 ++- .../resources/static/js/requestDetails.js | 53 +++++ .../templates/pages/requests/details.html | 59 ++++++ .../requests/RequestModelIntegrationTest.java | 7 + sql/update_db.sql | 32 +++ 16 files changed, 585 insertions(+), 201 deletions(-) create mode 100644 extract/src/main/java/ch/asit_asso/extract/web/model/OwnedObjectModel.java 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 @@ javase 3.5.3 + + org.junit.jupiter + junit-jupiter-params + test + jar + @@ -262,7 +268,7 @@ false target/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/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..365ed21c 100644 --- a/extract/src/main/resources/messages_de.properties +++ b/extract/src/main/resources/messages_de.properties @@ -354,6 +354,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 @@ -862,4 +863,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..961269cb 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 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} +
+
+
+
+ + + + +
+
+ +   - +   + {User group} + +
+
+ +   - +   + {User} + +
+
+
+
+
+
+
+
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); From 9c143c4efa9efe996d697fa9fa0cee4ff1c30af5 Mon Sep 17 00:00:00 2001 From: Andrey R Date: Mon, 1 Dec 2025 16:11:45 +0100 Subject: [PATCH 2/6] Add missing translation for request details (#12) --- extract/src/main/resources/messages_de.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extract/src/main/resources/messages_de.properties b/extract/src/main/resources/messages_de.properties index 365ed21c..b44efe87 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… From db2de35441212dbfbe8d4265ecddaa9a594e5c4d Mon Sep 17 00:00:00 2001 From: Andrey R Date: Wed, 29 Oct 2025 16:26:39 +0100 Subject: [PATCH 3/6] Load email signature from the env variable (#6) --- extract/src/main/resources/messages_de.properties | 2 +- extract/src/main/resources/messages_fr.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extract/src/main/resources/messages_de.properties b/extract/src/main/resources/messages_de.properties index b44efe87..599e6d02 100644 --- a/extract/src/main/resources/messages_de.properties +++ b/extract/src/main/resources/messages_de.properties @@ -788,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: diff --git a/extract/src/main/resources/messages_fr.properties b/extract/src/main/resources/messages_fr.properties index 961269cb..dfe2807c 100644 --- a/extract/src/main/resources/messages_fr.properties +++ b/extract/src/main/resources/messages_fr.properties @@ -787,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: From a428fe3ae0793c3fc9560ebb70b924e6985fa216 Mon Sep 17 00:00:00 2001 From: Andrey R Date: Thu, 30 Oct 2025 16:34:58 +0100 Subject: [PATCH 4/6] Allow using environment variables in the i18n strings. (#7) --- .../EnvResolvingMessageSource.java | 58 +++++++++++++++++++ .../configuration/I18nConfiguration.java | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 extract/src/main/java/ch/asit_asso/extract/configuration/EnvResolvingMessageSource.java 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<>(); From 6f3bf69ed7edc7c84a629eca8e4d76418fbb53ea Mon Sep 17 00:00:00 2001 From: bry Date: Mon, 15 Dec 2025 16:54:08 +0100 Subject: [PATCH 5/6] switch doc from material for mkdocs to zensical --- mkdocs.yml | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 mkdocs.yml diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..bf334625 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,74 @@ +site_name: Extract Documentation +site_url: https://asit-asso.github.io/extract/ +theme: + name: material + palette: + primary: custom + + font: + text: Asap + code: JetBrains Mono + + logo: assets/asit_extract_couleur.jpg + favicon: assets/extract_favicon64.png + + features: + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - search.suggest + - toc.integrate + - navigation.path + - content.code.copy + + icon: + repo: fontawesome/brands/github + +repo_url: https://github.com/asit-asso/extract +repo_name: asit-asso/extract + +markdown_extensions: + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - admonition + - pymdownx.details + - pymdownx.superfences + - tables + - pymdownx.magiclink + +extra_css: + - stylesheets/extra.css + +nav: + - index.md + - Getting Started: + - getting-started/install.md + - getting-started/configure.md + - getting-started/network-integration.md + - getting-started/customize.md + - getting-started/cybersecurity-win.md + - getting-started/cybersecurity-linux.md + - features/user-guide.md + - features/admin-guide.md + - Dev guide: + - features/development.md + - features/architecture.md + - How-To: + - how-to/extract-viageo.md + - how-to/fme-form.md + - how-to/fme-flow.md + - how-to/python.md + - how-to/qgis-server-atlas.md + - Miscellaneous: + - misc/viageo-test.md + +plugins: + - search + - exclude-search: + exclude: + - mitigations/* + - extract-connector-sample/* + - extract-task-sample/* + - changelog/* From 30cc8b136bb79bada9ecfde61dd136bf48fe9414 Mon Sep 17 00:00:00 2001 From: bry Date: Mon, 26 Jan 2026 14:34:04 +0100 Subject: [PATCH 6/6] switch from mkdocs to zensical, move changelog outside of docs, fix internal links --- mkdocs.yml | 74 ------------------------------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 mkdocs.yml diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index bf334625..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,74 +0,0 @@ -site_name: Extract Documentation -site_url: https://asit-asso.github.io/extract/ -theme: - name: material - palette: - primary: custom - - font: - text: Asap - code: JetBrains Mono - - logo: assets/asit_extract_couleur.jpg - favicon: assets/extract_favicon64.png - - features: - - navigation.sections - - navigation.tabs - - navigation.tabs.sticky - - search.suggest - - toc.integrate - - navigation.path - - content.code.copy - - icon: - repo: fontawesome/brands/github - -repo_url: https://github.com/asit-asso/extract -repo_name: asit-asso/extract - -markdown_extensions: - - attr_list - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - admonition - - pymdownx.details - - pymdownx.superfences - - tables - - pymdownx.magiclink - -extra_css: - - stylesheets/extra.css - -nav: - - index.md - - Getting Started: - - getting-started/install.md - - getting-started/configure.md - - getting-started/network-integration.md - - getting-started/customize.md - - getting-started/cybersecurity-win.md - - getting-started/cybersecurity-linux.md - - features/user-guide.md - - features/admin-guide.md - - Dev guide: - - features/development.md - - features/architecture.md - - How-To: - - how-to/extract-viageo.md - - how-to/fme-form.md - - how-to/fme-flow.md - - how-to/python.md - - how-to/qgis-server-atlas.md - - Miscellaneous: - - misc/viageo-test.md - -plugins: - - search - - exclude-search: - exclude: - - mitigations/* - - extract-connector-sample/* - - extract-task-sample/* - - changelog/*