Skip to content

Commit

Permalink
Process parent link (#5869)
Browse files Browse the repository at this point in the history
* allow linking of processes to parents in different project

* restore delete test file

* added Unit test for process sorting by projectId

* create new list in sort method to avoid side effects

* Add authority to link to processes of unassigned projects

* Update name of SQL migration file

* Show multiple potential parent processes in titleRecordLinkTab

* Fix test

* Move filtering of processes from ImportService to ProcessService

* Use correct metadata file for test

* Fix ProcessServiceIT

* Fix test

* Update message

* Adjust new ruleset restrictions to fix test

* Fix test and revert test ruleset changes

* Rename SQL migration file

* Use Junit version 5 instead of 4 in test

---------

Co-authored-by: BartChris <[email protected]>
  • Loading branch information
solth and BartChris authored Jul 19, 2024
1 parent 144b93f commit b26bcfc
Show file tree
Hide file tree
Showing 23 changed files with 370 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ public Authority getByTitle(String title) throws DAOException {
if (!authorities.isEmpty()) {
return authorities.get(0);
}
throw new DAOException("Object cannot be found in database");
throw new DAOException(String.format("Authority '%s' cannot be found in database", title));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--
-- (c) Kitodo. Key to digital objects e. V. <[email protected]>
--
-- This file is part of the Kitodo project.
--
-- It is licensed under GNU General Public License version 3 or later.
--
-- For the full copyright and license information, please read the
-- GPL3-License.txt file that was distributed with this source code.
--

SET SQL_SAFE_UPDATES = 0;

-- add authorities/permission for linking to parent processes of unassigned projects
INSERT IGNORE INTO authority (title) VALUES ('linkToProcessesOfUnassignedProjects_clientAssignable');

SET SQL_SAFE_UPDATES = 1;
Original file line number Diff line number Diff line change
Expand Up @@ -1108,4 +1108,13 @@ public boolean hasAuthorityToRenameMediaFiles() {
public boolean hasAuthorityToAssignImportConfigurationToClient() {
return securityAccessService.hasAuthorityToAssignImportConfigurationToClient();
}

/**
* Check if the current user has the permission to link processes to parent processes of unassigned projects.
*
* @return true if the current user has the permission to link processes to parent processes of unassigned projects.
*/
public boolean hasAuthorityToLinkToProcessesOfUnassignedProjects() {
return securityAccessService.hasAuthorityToLinkToProcessesOfUnassignedProjects();
}
}
21 changes: 21 additions & 0 deletions Kitodo/src/main/java/org/kitodo/production/forms/ProcessForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.annotation.PostConstruct;
Expand Down Expand Up @@ -60,6 +62,7 @@
import org.kitodo.production.process.ProcessValidator;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.command.KitodoScriptService;
import org.kitodo.production.services.data.ImportService;
import org.kitodo.production.services.data.ProcessService;
import org.kitodo.production.services.file.FileService;
import org.kitodo.production.services.workflow.WorkflowControllerService;
Expand Down Expand Up @@ -96,6 +99,7 @@ public class ProcessForm extends TemplateBaseForm {
private static final String CREATE_PROCESS_PATH = "/pages/processFromTemplate.jsf?faces-redirect=true";
private static final String PROCESS_TABLE_VIEW_ID = "/pages/processes.xhtml";
private static final String PROCESS_TABLE_ID = "processesTabView:processesForm:processesTable";
private final Map<Integer, Boolean> assignedProcesses = new HashMap<>();

@Inject
private CustomListColumnInitializer initializer;
Expand Down Expand Up @@ -1169,4 +1173,21 @@ public String getMediaRenamingConfirmMessage() {
public String getErrorMessage() {
return errorMessage;
}

/**
* Check and return whether process with ID 'processId' belongs to a project assigned to the current user or not.
* @param processId ID of process to check
* @return whether process belongs to project assigned to current user or not
*/
public boolean processInAssignedProject(int processId) {
try {
if (!assignedProcesses.containsKey(processId)) {
assignedProcesses.put(processId, ImportService.processInAssignedProject(processId));
}
return assignedProcesses.get(processId);
} catch (DAOException e) {
Helper.setErrorMessage(e);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;
import java.util.Objects;
import java.util.Optional;

import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;

import org.apache.commons.lang3.StringUtils;
Expand All @@ -40,6 +42,7 @@
import org.kitodo.production.helper.Helper;
import org.kitodo.production.metadata.MetadataEditor;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.data.ImportService;
import org.kitodo.production.services.data.ProcessService;
import org.kitodo.production.services.dataformat.MetsService;
import org.omnifaces.util.Ajax;
Expand All @@ -53,6 +56,7 @@ public class TitleRecordLinkTab {
private static final Logger logger = LogManager.getLogger(TitleRecordLinkTab.class);

static final String INSERTION_TREE = "editForm:processFromTemplateTabView:insertionTree";
static final String PARENT_PROCESS_SELECTION = "editForm:processFromTemplateTabView:chooseParentGroup";
private static final MetsService metsService = ServiceManager.getMetsService();
private static final ProcessService processService = ServiceManager.getProcessService();

Expand Down Expand Up @@ -160,12 +164,12 @@ public void createInsertionPositionSelectionTree() throws DAOException, DataExce
priorityList);
logicalStructure.setExpanded(true);

if (!selectableInsertionPositions.isEmpty()) {
selectedInsertionPosition = (String) ((LinkedList<SelectItem>) selectableInsertionPositions).getLast()
.getValue();
} else {
if (selectableInsertionPositions.isEmpty()) {
selectedInsertionPosition = null;
Helper.setErrorMessage("createProcessForm.titleRecordLinkTab.noInsertionPosition");
} else {
selectedInsertionPosition = (String) ((LinkedList<SelectItem>) selectableInsertionPositions).getLast()
.getValue();
}
}

Expand Down Expand Up @@ -308,21 +312,31 @@ public void searchForParentProcesses() {
}
try {
List<ProcessDTO> processes = ServiceManager.getProcessService().findLinkableParentProcesses(searchQuery,
createProcessForm.getProject().getId(), createProcessForm.getTemplate().getRuleset().getId());
createProcessForm.getTemplate().getRuleset().getId());
if (processes.isEmpty()) {
Helper.setMessage("createProcessForm.titleRecordLinkTab.searchButtonClick.noHits");
}
indicationOfMoreHitsVisible = processes.size() > MAXIMUM_NUMBER_OF_HITS;
possibleParentProcesses = new ArrayList<>();
for (ProcessDTO process : processes.subList(0, Math.min(processes.size(), MAXIMUM_NUMBER_OF_HITS))) {
possibleParentProcesses.add(new SelectItem(process.getId().toString(), process.getTitle()));
}
} catch (DataException e) {
possibleParentProcesses = ServiceManager.getImportService()
.getPotentialParentProcesses(processes, MAXIMUM_NUMBER_OF_HITS);
} catch (DataException | DAOException | IOException e) {
Helper.setErrorMessage("createProcessForm.titleRecordLinkTab.searchButtonClick.error", e.getMessage(),
logger, e);
logger, e);
indicationOfMoreHitsVisible = false;
possibleParentProcesses = Collections.emptyList();
}
possibleParentProcesses.sort(Comparator.comparing(SelectItem::getLabel));
for (SelectItem selectItem : possibleParentProcesses) {
if (!selectItem.isDisabled()) {
try {
int processId = Integer.parseInt(selectItem.getValue().toString());
setParentAsTitleRecord(ServiceManager.getProcessService().getById(processId));
break;
} catch (DAOException | NumberFormatException e) {
logger.error(e);
}
}
}
}

/**
Expand Down Expand Up @@ -429,15 +443,25 @@ public void setTitleRecordProcess(Process titleRecordProcess) {

/**
* Set given process "parentProcess" as parent title record of new process.
*
* @param parentProcess process to set as parent title record
*/
public void setParentAsTitleRecord(Process parentProcess) {
createProcessForm.setEditActiveTabIndex(CreateProcessForm.TITLE_RECORD_LINK_TAB_INDEX);
ArrayList<SelectItem> parentCandidates = new ArrayList<>();
parentCandidates.add(new SelectItem(parentProcess.getId().toString(), parentProcess.getTitle()));
createProcessForm.getTitleRecordLinkTab().setPossibleParentProcesses(parentCandidates);
createProcessForm.getTitleRecordLinkTab().setChosenParentProcess((String)parentCandidates.get(0).getValue());
createProcessForm.getTitleRecordLinkTab().chooseParentProcess();
Ajax.update(INSERTION_TREE);
try {
if (ImportService.userMayLinkToParent(parentProcess.getId())) {
setChosenParentProcess(String.valueOf(parentProcess.getId()));
} else {
setChosenParentProcess(null);
}
} catch (DAOException e) {
Helper.setErrorMessage(e);
}
chooseParentProcess();
// only update UI components if FacesContext exists (not the case during integration tests, for example)
if (Objects.nonNull(FacesContext.getCurrentInstance())) {
Ajax.update(PARENT_PROCESS_SELECTION);
Ajax.update(INSERTION_TREE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
Expand All @@ -32,6 +33,7 @@
import java.util.Objects;
import java.util.stream.Collectors;

import javax.faces.model.SelectItem;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
Expand Down Expand Up @@ -956,14 +958,14 @@ private Process loadParentProcess(Ruleset ruleset, int projectId, String parentI
HashMap<String, String> parentIDMetadata = new HashMap<>();
parentIDMetadata.put(identifierMetadata, parentId);
try {
for (ProcessDTO processDTO : ServiceManager.getProcessService().findByMetadata(parentIDMetadata, true)) {
for (ProcessDTO processDTO : sortProcessesByProjectID(ServiceManager.getProcessService()
.findByMetadataInAllProjects(parentIDMetadata, true), projectId)) {
Process process = ServiceManager.getProcessService().getById(processDTO.getId());
if (Objects.isNull(process.getRuleset()) || Objects.isNull(process.getRuleset().getId())) {
throw new ProcessGenerationException("Ruleset or ruleset ID of potential parent process "
+ process.getId() + " is null!");
}
if (process.getProject().getId() == projectId
&& process.getRuleset().getId().equals(ruleset.getId())) {
if (process.getRuleset().getId().equals(ruleset.getId())) {
parentProcess = process;
break;
}
Expand All @@ -976,6 +978,25 @@ private Process loadParentProcess(Ruleset ruleset, int projectId, String parentI
return parentProcess;
}

/**
* Sorts a list of process DTOs based on a provided projectId.
* Processes which match the provided projectId should come first.
* @param processDTOs list of process DTOs
* @param projectId projectId by which the list gets sorted
*/
public List<ProcessDTO> sortProcessesByProjectID(List<ProcessDTO> processDTOs, int projectId) {
List<ProcessDTO> sortedList = new ArrayList<>(processDTOs);
Comparator<ProcessDTO> comparator = Comparator.comparingInt(obj -> {
if (obj.getProject().getId() == projectId) {
return 0; // Matching value should come first
} else {
return 1; // Non-matching value comes later
}
});
sortedList.sort(comparator);
return sortedList;
}

/**
* Check and return whether the "parentIdSearchField" is configured in the current ImportConfiguration.
*
Expand Down Expand Up @@ -1396,4 +1417,63 @@ public boolean isRecordIdentifierMetadataConfigured(RulesetManagementInterface r
public Collection<RecordIdentifierMissingDetail> getDetailsOfRecordIdentifierMissingError() {
return recordIdentifierMissingDetails;
}

/**
* Create and return list of "SelectItem" objects for given list of "ProcessDTO"s. For each "ProcessDTO" object
* check whether current user can link to it as a parent process.
* - If a specific process belongs to a project that is not assigned to the current user, add a hint to message to
* the corresponding "SelectItem"
* - If the user cannot link to a specific process, because the process belongs to a project which is not assigned
* to him, and he also lacks the special permission to link to processes in unassigned projects, the corresponding
* "SelectItem" is disabled.
* @param parentCandidates list of "ProcessDTO"s
* @param maxNumber limit
* @return list of "SelectItem" objects corresponding to given "ProcessDTO" objects.
* @throws DAOException when checking whether user can link to given "ProcessDTO"s fails
*/
public ArrayList<SelectItem> getPotentialParentProcesses(List<ProcessDTO> parentCandidates, int maxNumber)
throws DAOException {
ArrayList<SelectItem> possibleParentProcesses = new ArrayList<>();
for (ProcessDTO process : parentCandidates.subList(0, Math.min(parentCandidates.size(), maxNumber))) {
SelectItem selectItem = new SelectItem(process.getId().toString(), process.getTitle());
selectItem.setDisabled(!userMayLinkToParent(process.getId()));
if (!processInAssignedProject(process.getId())) {
String problem = Helper.getTranslation("projectNotAssignedToCurrentUser", process.getProject()
.getTitle());
selectItem.setDescription(problem);
selectItem.setLabel(selectItem.getLabel() + " (" + problem + ")");
}
possibleParentProcesses.add(selectItem);
}
return possibleParentProcesses;
}

/**
* Check and return whether the process with the provided ID "processId" belongs to a project that is assigned to
* the current user or not.
* @param processId ID of the process to check
* @return whether the process with the provided ID belongs to a project assigned to the current user or not
* @throws DAOException when retrieving the process with the ID "processId" from the database fails
*/
public static boolean processInAssignedProject(int processId) throws DAOException {
Process process = ServiceManager.getProcessService().getById(processId);
if (Objects.nonNull(process)) {
return ServiceManager.getUserService().getCurrentUser().getProjects().contains(process.getProject());
}
return false;
}

/**
* Check and return whether current user is allowed to link to process with provided ID "processId". For this
* method to return "true", one of the following two conditions has to be met:
* 1. the project of the process with the provided ID is assigned to the current user OR
* 2. the current user has the special permission to link to parent processes of unassigned projects
* @param processId the ID of the process to which a link is to be established
* @return whether the current user can link to the process with the provided ID or not
* @throws DAOException when checking whether the process with provided ID fails
*/
public static boolean userMayLinkToParent(int processId) throws DAOException {
return processInAssignedProject(processId)
|| ServiceManager.getSecurityAccessService().hasAuthorityToLinkToProcessesOfUnassignedProjects();
}
}
Loading

0 comments on commit b26bcfc

Please sign in to comment.