From 04be65855d3c00392892ee588bf267f3f3403bbb Mon Sep 17 00:00:00 2001 From: palagdan Date: Wed, 29 Oct 2025 14:31:18 +0100 Subject: [PATCH 01/16] [kbss-cvut/record-manager-ui#302] Add variable for allowing creation records without being assigned to any institution --- .../cz/cvut/kbss/study/rest/RecordController.java | 2 +- .../java/cz/cvut/kbss/study/util/ConfigParam.java | 3 ++- .../java/cz/cvut/kbss/study/util/Configuration.java | 13 +++++++++++++ src/main/resources/application.yml | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/RecordController.java b/src/main/java/cz/cvut/kbss/study/rest/RecordController.java index eaadb2b0..6485a3d3 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/RecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/RecordController.java @@ -168,7 +168,7 @@ private Record findInternal(String key) { @ResponseStatus(HttpStatus.CREATED) public ResponseEntity createRecord(@RequestBody Record record) { - if(userService.getCurrentUser().getInstitution() == null) + if(configReader.getConfig(ConfigParam.RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION).equals("false") && userService.getCurrentUser().getInstitution() == null) throw new ValidationException("record.save-error.user-not-assigned-to-institution", "User is not assigned to any institution."); diff --git a/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java b/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java index 0f257bae..55b5a51a 100644 --- a/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java +++ b/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java @@ -45,7 +45,8 @@ public enum ConfigParam { CORS_ALLOWED_ORIGINS("security.cors.allowedOrigins"), - RECORDS_ALLOWED_REJECT_REASON("records.allowedRejectReason"); + RECORDS_ALLOWED_REJECT_REASON("records.allowedRejectReason"), + RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION("records.allowedCreationWithoutInstitution"); private final String name; diff --git a/src/main/java/cz/cvut/kbss/study/util/Configuration.java b/src/main/java/cz/cvut/kbss/study/util/Configuration.java index 48a6579f..46e86536 100644 --- a/src/main/java/cz/cvut/kbss/study/util/Configuration.java +++ b/src/main/java/cz/cvut/kbss/study/util/Configuration.java @@ -422,12 +422,25 @@ public static class Records { */ boolean allowedRejectReason; + /** + * it indicates functionality allowing users to create records without being assigned to any institution. + */ + boolean allowedCreationWithoutInstitution; + public boolean isAllowedRejectReason() { return allowedRejectReason; } + public boolean isAllowedCreationWithoutInstitution() { + return allowedCreationWithoutInstitution; + } + public void setAllowedRejectReason(boolean allowedRejectReason) { this.allowedRejectReason = allowedRejectReason; } + + public void setAllowedCreationWithoutInstitution(boolean allowedCreationWithoutInstitution) { + this.allowedCreationWithoutInstitution = allowedCreationWithoutInstitution; + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 54fd3e46..9d643401 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,3 +54,4 @@ email: records: allowedRejectReason: true + allowedCreationWithoutInstitution: true \ No newline at end of file From 8f4d0bd6d21c15ed6db6a03ed9e0a66b7970ffee Mon Sep 17 00:00:00 2001 From: palagdan Date: Wed, 29 Oct 2025 19:22:09 +0100 Subject: [PATCH 02/16] [kbss-cvut/record-manager-ui#302] Showing also records without institution --- .../kbss/study/persistence/dao/RecordDao.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java index 4bb70eb8..204651cd 100644 --- a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java +++ b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java @@ -178,6 +178,12 @@ public void requireUniqueNonEmptyLocalName(Record entity) { throw new ValidationException("error.record.localNameOfRecordIsEmpty", "Local name of record is empty for entity " + entity); } + + if(entity.getInstitution() == null){ + em.clear(); + return; + } + boolean unique = findByInstitution(entity.getInstitution()).stream() .filter(pr -> (entity.getFormTemplate() != null) && entity.getFormTemplate() .equals(pr.getFormTemplate())) @@ -305,13 +311,16 @@ private void setQueryParameters(Query query, Map queryParams) { private static String constructWhereClause(RecordFilterParams filters, Map queryParams) { // Could not use Criteria API because it does not support OPTIONAL + String whereClause = "{" + "?r a ?type ; " + "?hasAuthor ?author ; " + - "?hasCreatedDate ?created ; " + - "?hasInstitution ?institution . " + - "?institution ?hasKey ?institutionKey ." + - "?author ?hasUsername ?username ." + + "?hasCreatedDate ?created . " + + "?author ?hasUsername ?username . " + + "OPTIONAL { " + + "?r ?hasInstitution ?institution . " + + "?institution ?hasKey ?institutionKey . " + + "} " + "OPTIONAL { ?r ?hasPhase ?phase . } " + "OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } " + "OPTIONAL { ?r ?hasLastModified ?lastModified . } " + From 0158ab5030e039e14b7923f0d7d68e2adc8c881b Mon Sep 17 00:00:00 2001 From: palagdan Date: Thu, 30 Oct 2025 13:33:50 +0100 Subject: [PATCH 03/16] [kbss-cvut/record-manager-ui#302] Move permission's validation to the separate function and implement record collision validation --- .../repository/RepositoryUserService.java | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index e3245d8a..7ffd93e7 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -1,15 +1,18 @@ package cz.cvut.kbss.study.service.repository; import cz.cvut.kbss.jopa.exceptions.EntityNotFoundException; +import cz.cvut.kbss.study.dto.PatientRecordDto; import cz.cvut.kbss.study.exception.EntityExistsException; import cz.cvut.kbss.study.exception.NotFoundException; import cz.cvut.kbss.study.exception.ValidationException; import cz.cvut.kbss.study.model.Institution; +import cz.cvut.kbss.study.model.PatientRecord; import cz.cvut.kbss.study.model.Role; import cz.cvut.kbss.study.model.User; import cz.cvut.kbss.study.persistence.dao.GenericDao; import cz.cvut.kbss.study.persistence.dao.RecordDao; import cz.cvut.kbss.study.persistence.dao.UserDao; +import cz.cvut.kbss.study.persistence.dao.util.RecordFilterParams; import cz.cvut.kbss.study.service.ConfigReader; import cz.cvut.kbss.study.service.EmailService; import cz.cvut.kbss.study.service.UserService; @@ -26,11 +29,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; @Service public class RepositoryUserService extends BaseRepositoryService implements UserService { @@ -207,23 +206,15 @@ protected void prePersist(User instance) { } } - @Override - protected void preUpdate(User instance) { - final User currentUser = securityUtils.getCurrentUser(); - final User original = userDao.findByUsername(instance.getUsername()); - - if(original == null) { - throw new EntityNotFoundException("User with specified username does not exist."); - } - - boolean differentUser = !Objects.equals(instance.getUsername(), currentUser.getUsername()); + private void validatePermissionToUpdate(User currentUser, User toUpdate, User original) { + boolean differentUser = !Objects.equals(toUpdate.getUsername(), currentUser.getUsername()); boolean hasWriteAllUsers = securityUtils.hasRole(Role.writeAllUsers); boolean lacksPrivilegeOfUpdatedUser = !securityUtils.hasSupersetOfRoles(currentUser, original); - boolean sameInstitution = instance.getInstitution() != null + boolean sameInstitution = toUpdate.getInstitution() != null && currentUser.getInstitution() != null - && instance.getInstitution().getKey().equals(currentUser.getInstitution().getKey()); + && toUpdate.getInstitution().getKey().equals(currentUser.getInstitution().getKey()); boolean hasWriteOrganizationUsers = securityUtils.hasRole(Role.writeOrganizationUsers) && sameInstitution; @@ -243,6 +234,54 @@ protected void preUpdate(User instance) { currentUser.getUsername())); } } + } + + private void validateRecordsAgainstCollisions(User toUpdate, User original) { + + if (toUpdate.getInstitution() == null) return; + + List newInstitutionRecords = patientRecordDao.findByInstitution(toUpdate.getInstitution()); + List originalUserRecords = patientRecordDao.findByAuthor(original); + + Set conflictingRecords = new HashSet<>(); + + originalUserRecords.forEach(originalUserRecord -> { + for (PatientRecordDto newInstitutionRecord : newInstitutionRecords) { + if (newInstitutionRecord.getLocalName().equals(originalUserRecord.getLocalName())) { + conflictingRecords.add(originalUserRecord); + } + } + }); + + if (!conflictingRecords.isEmpty()) { + List localRecordsNames = conflictingRecords.stream() + .map(PatientRecord::getLocalName) + .toList(); + + String message = String.format( + "User cannot be moved to %s institution because of the following conflicting patient records: %s.", + toUpdate.getInstitution().getName(), + String.join(", ", localRecordsNames) + ); + + throw new ValidationException(message); + } + } + + @Override + protected void preUpdate(User instance) { + final User currentUser = securityUtils.getCurrentUser(); + final User original = userDao.findByUsername(instance.getUsername()); + + if (original == null) { + throw new EntityNotFoundException("User with specified username does not exist."); + } + + validatePermissionToUpdate(currentUser, instance, original); + + if (instance.getInstitution() != original.getInstitution()) { + validateRecordsAgainstCollisions(instance, original); + } try { Validator.validateEmail(instance.getEmailAddress()); From a77930794c536b92fed3eed9297186a191e2df95 Mon Sep 17 00:00:00 2001 From: palagdan Date: Thu, 30 Oct 2025 14:19:54 +0100 Subject: [PATCH 04/16] [kbss-cvut/record-manager-ui#302] Fix and implement new tests --- .../kbss/study/rest/RecordControllerTest.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java index 409784e5..08077c7e 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java @@ -203,6 +203,9 @@ public void createRecordReturnsResponseStatusCreated() throws Exception { Record record = Generator.generateRecord(user); when(userService.getCurrentUser()).thenReturn(user); + when(configReaderMock.getConfig(ConfigParam.RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION)) + .thenReturn("false"); + final MvcResult result = mockMvc.perform(post("/records").content(toJson(record)) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andReturn(); @@ -211,12 +214,14 @@ public void createRecordReturnsResponseStatusCreated() throws Exception { } @Test - public void createRecordWithoutInstitutionReturnsResponseStatusBadRequest() throws Exception { + public void createRecordWithoutInstitutionIfItIsNotAllowedReturnsResponseStatusBadRequest() throws Exception { user.setInstitution(null); Record record = Generator.generateRecord(user); when(userService.getCurrentUser()).thenReturn(user); + when(configReaderMock.getConfig(ConfigParam.RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION)) + .thenReturn("false"); final MvcResult result = mockMvc.perform(post("/records").content(toJson(record)) .contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -225,6 +230,22 @@ public void createRecordWithoutInstitutionReturnsResponseStatusBadRequest() thro assertEquals(HttpStatus.CONFLICT, HttpStatus.valueOf(result.getResponse().getStatus())); } + @Test + public void createRecordWithoutInstitutionIfItIsAllowedReturnsResponseStatusBadRequest() throws Exception { + user.setInstitution(null); + + PatientRecord record = Generator.generatePatientRecord(user); + + when(configReaderMock.getConfig(ConfigParam.RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION)) + .thenReturn("true"); + + final MvcResult result = mockMvc.perform(post("/records").content(toJson(record)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn(); + + assertEquals(HttpStatus.CREATED, HttpStatus.valueOf(result.getResponse().getStatus())); + } + @Test public void updateRecordReturnsResponseStatusNoContent() throws Exception { From f4f2224a4aba80fe684da37af22ce301b2586ac8 Mon Sep 17 00:00:00 2001 From: palagdan Date: Fri, 31 Oct 2025 15:49:09 +0100 Subject: [PATCH 05/16] [kbss-cvut/record-manager-ui#302] Change records institution after changing user institution --- .../study/service/repository/RepositoryUserService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index 7ffd93e7..324a4453 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -268,6 +268,14 @@ private void validateRecordsAgainstCollisions(User toUpdate, User original) { } } + void changePatientRecordsInstitution(User original, Institution newInstitution) { + List recordsToUpdate = patientRecordDao.findByAuthor(original); + for (PatientRecord record : recordsToUpdate) { + record.setInstitution(newInstitution); + patientRecordDao.update(record); + } + } + @Override protected void preUpdate(User instance) { final User currentUser = securityUtils.getCurrentUser(); @@ -281,6 +289,7 @@ protected void preUpdate(User instance) { if (instance.getInstitution() != original.getInstitution()) { validateRecordsAgainstCollisions(instance, original); + changePatientRecordsInstitution(original, instance.getInstitution()); } try { From d39b1cf25f197d3396bcf23137849fee5fedc27b Mon Sep 17 00:00:00 2001 From: palagdan Date: Fri, 31 Oct 2025 15:53:51 +0100 Subject: [PATCH 06/16] [kbss-cvut/record-manager-ui#302] Fix modifiers --- .../study/service/repository/RepositoryUserService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index 324a4453..29bc4544 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -206,7 +206,7 @@ protected void prePersist(User instance) { } } - private void validatePermissionToUpdate(User currentUser, User toUpdate, User original) { + protected void validatePermissionToUpdate(User currentUser, User toUpdate, User original) { boolean differentUser = !Objects.equals(toUpdate.getUsername(), currentUser.getUsername()); boolean hasWriteAllUsers = securityUtils.hasRole(Role.writeAllUsers); @@ -236,7 +236,7 @@ private void validatePermissionToUpdate(User currentUser, User toUpdate, User or } } - private void validateRecordsAgainstCollisions(User toUpdate, User original) { + protected void validateRecordsAgainstCollisions(User toUpdate, User original) { if (toUpdate.getInstitution() == null) return; @@ -268,7 +268,7 @@ private void validateRecordsAgainstCollisions(User toUpdate, User original) { } } - void changePatientRecordsInstitution(User original, Institution newInstitution) { + protected void changePatientRecordsInstitution(User original, Institution newInstitution) { List recordsToUpdate = patientRecordDao.findByAuthor(original); for (PatientRecord record : recordsToUpdate) { record.setInstitution(newInstitution); From b31e3c793a2cdd9c19f5835cc926b8936bce85e0 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Fri, 31 Oct 2025 17:28:32 +0100 Subject: [PATCH 07/16] [kbss-cvut/record-manager-ui#302] Improve validation error --- .../repository/RepositoryUserService.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index 29bc4544..c7da7ea0 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -30,6 +30,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.stream.Collectors; @Service public class RepositoryUserService extends BaseRepositoryService implements UserService { @@ -240,28 +241,36 @@ protected void validateRecordsAgainstCollisions(User toUpdate, User original) { if (toUpdate.getInstitution() == null) return; - List newInstitutionRecords = patientRecordDao.findByInstitution(toUpdate.getInstitution()); - List originalUserRecords = patientRecordDao.findByAuthor(original); + Map existingInstitutionRecords = patientRecordDao.findByInstitution(toUpdate.getInstitution()).stream().collect( + Collectors.toMap(PatientRecordDto::getLocalName, r -> r) + ); + Map newInstitutionRecords = patientRecordDao.findByAuthor(original).stream().collect( + Collectors.toMap(PatientRecord::getLocalName, r -> r) + ); - Set conflictingRecords = new HashSet<>(); + Map> conflictingRecords = newInstitutionRecords.keySet().stream() + .filter(existingInstitutionRecords::containsKey) + .collect(Collectors.toMap( + key -> key, + key -> Arrays.asList(existingInstitutionRecords.get(key).getKey(), newInstitutionRecords.get(key).getKey()) + )); - originalUserRecords.forEach(originalUserRecord -> { - for (PatientRecordDto newInstitutionRecord : newInstitutionRecords) { - if (newInstitutionRecord.getLocalName().equals(originalUserRecord.getLocalName())) { - conflictingRecords.add(originalUserRecord); - } - } - }); if (!conflictingRecords.isEmpty()) { - List localRecordsNames = conflictingRecords.stream() - .map(PatientRecord::getLocalName) - .toList(); + + String conflictsFormatted = conflictingRecords.entrySet().stream() + .map(e -> String.format( + "• %s (existing record ID: %s, new record ID: %s)", + e.getKey(), + e.getValue().get(0), + e.getValue().get(1) + )) + .collect(Collectors.joining("\n")); String message = String.format( - "User cannot be moved to %s institution because of the following conflicting patient records: %s.", + "User cannot be moved to institution '%s' because there are conflicting records with the same name:\n%s.", toUpdate.getInstitution().getName(), - String.join(", ", localRecordsNames) + conflictsFormatted ); throw new ValidationException(message); From fd92863dce5ce7b88b386e337634fa32b2af4149 Mon Sep 17 00:00:00 2001 From: palagdan Date: Sat, 1 Nov 2025 11:30:07 +0100 Subject: [PATCH 08/16] [kbss-cvut/record-manager-ui#302] Indent where clauses --- .../kbss/study/persistence/dao/RecordDao.java | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java index 204651cd..717b6850 100644 --- a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java +++ b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java @@ -312,39 +312,52 @@ private void setQueryParameters(Query query, Map queryParams) { private static String constructWhereClause(RecordFilterParams filters, Map queryParams) { // Could not use Criteria API because it does not support OPTIONAL - String whereClause = "{" + - "?r a ?type ; " + - "?hasAuthor ?author ; " + - "?hasCreatedDate ?created . " + - "?author ?hasUsername ?username . " + - "OPTIONAL { " + - "?r ?hasInstitution ?institution . " + - "?institution ?hasKey ?institutionKey . " + - "} " + - "OPTIONAL { ?r ?hasPhase ?phase . } " + - "OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } " + - "OPTIONAL { ?r ?hasLastModified ?lastModified . } " + - "BIND (COALESCE(?lastModified, ?created) AS ?date) "; + String whereClause = """ + { + ?r a ?type ; + ?hasAuthor ?author ; + ?hasCreatedDate ?created . + ?author ?hasUsername ?username . + + OPTIONAL { + ?r ?hasInstitution ?institution . + ?institution ?hasKey ?institutionKey . + } + + OPTIONAL { ?r ?hasPhase ?phase . } + OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } + OPTIONAL { ?r ?hasLastModified ?lastModified . } + + BIND (COALESCE(?lastModified, ?created) AS ?date) + """; + whereClause += mapParamsToQuery(filters, queryParams); whereClause += "}"; + return whereClause; } private static String constructWhereClauseWithGraphs(RecordFilterParams filters, Map queryParams) { // Could not use Criteria API because it does not support OPTIONAL - String whereClause = "{GRAPH ?r{" + - "?r a ?type ; " + - "?hasCreatedDate ?created ; " + - "?hasInstitution ?institution . " + - "OPTIONAL { ?r ?hasPhase ?phase . } " + - "OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } " + - "OPTIONAL { ?r ?hasLastModified ?lastModified . } " + - "BIND (COALESCE(?lastModified, ?created) AS ?date) "; - whereClause += mapParamsToQuery(filters, queryParams); - whereClause += "}" + - "GRAPH ?institutionGraph{" + - "?institution ?hasKey ?institutionKey ." + - "}}"; + String whereClause = """ + {GRAPH ?r { + ?r a ?type ; + ?hasCreatedDate ?created ; + ?hasInstitution ?institution . + + OPTIONAL { ?r ?hasPhase ?phase . } + OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } + OPTIONAL { ?r ?hasLastModified ?lastModified . } + + BIND (COALESCE(?lastModified, ?created) AS ?date) + """ + + mapParamsToQuery(filters, queryParams) + + """ + } + GRAPH ?institutionGraph { + ?institution ?hasKey ?institutionKey . + }} + """; queryParams.put("institutionGraph", URI.create(Vocabulary.s_c_institution + "s")); From ab853a067d55988559b9862bc7f6bcdeafdadb63 Mon Sep 17 00:00:00 2001 From: palagdan Date: Sat, 1 Nov 2025 11:31:57 +0100 Subject: [PATCH 09/16] [kbss-cvut/record-manager-ui#302] Change environment variable description --- src/main/java/cz/cvut/kbss/study/util/Configuration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/cz/cvut/kbss/study/util/Configuration.java b/src/main/java/cz/cvut/kbss/study/util/Configuration.java index 46e86536..032c7d55 100644 --- a/src/main/java/cz/cvut/kbss/study/util/Configuration.java +++ b/src/main/java/cz/cvut/kbss/study/util/Configuration.java @@ -423,7 +423,7 @@ public static class Records { boolean allowedRejectReason; /** - * it indicates functionality allowing users to create records without being assigned to any institution. + * Allow users who are not members of any institution to create records. */ boolean allowedCreationWithoutInstitution; From 89cced0492df73bebb5491fb6a6c274755adb000 Mon Sep 17 00:00:00 2001 From: palagdan Date: Sat, 1 Nov 2025 11:33:46 +0100 Subject: [PATCH 10/16] [kbss-cvut/record-manager-ui#302] Fix test name --- src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java index 08077c7e..6b0c500e 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java @@ -231,7 +231,7 @@ public void createRecordWithoutInstitutionIfItIsNotAllowedReturnsResponseStatusB } @Test - public void createRecordWithoutInstitutionIfItIsAllowedReturnsResponseStatusBadRequest() throws Exception { + public void createRecordWithoutInstitutionIfItIsAllowedReturnsResponseStatusCreated() throws Exception { user.setInstitution(null); PatientRecord record = Generator.generatePatientRecord(user); From acd2dc557e430aeebbfe45237730779f1f966eee Mon Sep 17 00:00:00 2001 From: palagdan Date: Sun, 2 Nov 2025 13:13:01 +0100 Subject: [PATCH 11/16] [kbss-cvut/record-manager-ui#302] Fix Record naming after rebasing --- .../repository/RepositoryUserService.java | 18 +++++++++--------- .../kbss/study/rest/RecordControllerTest.java | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index c7da7ea0..c74ca19b 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -1,12 +1,12 @@ package cz.cvut.kbss.study.service.repository; import cz.cvut.kbss.jopa.exceptions.EntityNotFoundException; -import cz.cvut.kbss.study.dto.PatientRecordDto; +import cz.cvut.kbss.study.dto.RecordDto; import cz.cvut.kbss.study.exception.EntityExistsException; import cz.cvut.kbss.study.exception.NotFoundException; import cz.cvut.kbss.study.exception.ValidationException; import cz.cvut.kbss.study.model.Institution; -import cz.cvut.kbss.study.model.PatientRecord; +import cz.cvut.kbss.study.model.Record; import cz.cvut.kbss.study.model.Role; import cz.cvut.kbss.study.model.User; import cz.cvut.kbss.study.persistence.dao.GenericDao; @@ -241,11 +241,11 @@ protected void validateRecordsAgainstCollisions(User toUpdate, User original) { if (toUpdate.getInstitution() == null) return; - Map existingInstitutionRecords = patientRecordDao.findByInstitution(toUpdate.getInstitution()).stream().collect( - Collectors.toMap(PatientRecordDto::getLocalName, r -> r) + Map existingInstitutionRecords = recordDao.findByInstitution(toUpdate.getInstitution()).stream().collect( + Collectors.toMap(RecordDto::getLocalName, r -> r) ); - Map newInstitutionRecords = patientRecordDao.findByAuthor(original).stream().collect( - Collectors.toMap(PatientRecord::getLocalName, r -> r) + Map newInstitutionRecords = recordDao.findByAuthor(original).stream().collect( + Collectors.toMap(Record::getLocalName, r -> r) ); Map> conflictingRecords = newInstitutionRecords.keySet().stream() @@ -278,10 +278,10 @@ protected void validateRecordsAgainstCollisions(User toUpdate, User original) { } protected void changePatientRecordsInstitution(User original, Institution newInstitution) { - List recordsToUpdate = patientRecordDao.findByAuthor(original); - for (PatientRecord record : recordsToUpdate) { + List recordsToUpdate = recordDao.findByAuthor(original); + for (Record record : recordsToUpdate) { record.setInstitution(newInstitution); - patientRecordDao.update(record); + recordDao.update(record); } } diff --git a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java index 6b0c500e..f9f80772 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/RecordControllerTest.java @@ -234,7 +234,7 @@ public void createRecordWithoutInstitutionIfItIsNotAllowedReturnsResponseStatusB public void createRecordWithoutInstitutionIfItIsAllowedReturnsResponseStatusCreated() throws Exception { user.setInstitution(null); - PatientRecord record = Generator.generatePatientRecord(user); + Record record = Generator.generateRecord(user); when(configReaderMock.getConfig(ConfigParam.RECORDS_ALLOWED_CREATION_WITHOUT_INSTITUTION)) .thenReturn("true"); From c02d903dde1810b8d56ee5fa77e06de86807c837 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Tue, 17 Mar 2026 14:19:49 +0100 Subject: [PATCH 12/16] [kbss-cvut/record-manager-ui#302] Fix record persist/update without institution Records without an institution will throw NPE during save/update instead of being allowed through. --- src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java index 717b6850..84d538c8 100644 --- a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java +++ b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java @@ -173,7 +173,6 @@ public int getNumberOfProcessedRecords() { * @param entity The local name to be checked for uniqueness */ public void requireUniqueNonEmptyLocalName(Record entity) { - Objects.requireNonNull(entity.getInstitution()); if (entity.getLocalName() == null || entity.getLocalName().isEmpty()) { throw new ValidationException("error.record.localNameOfRecordIsEmpty", "Local name of record is empty for entity " + entity); From a287cad5f44e98a6c18f50930e415b1706ba1a59 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Tue, 17 Mar 2026 14:24:27 +0100 Subject: [PATCH 13/16] [kbss-cvut/record-manager-ui#302] Fix pagination results Records without an institution were excluded from paginated results because ?hasInstitution ?institution was a required triple. Made it OPTIONAL and wrapped the GRAPH ?institutionGraph lookup in OPTIONAL as well. --- .../cvut/kbss/study/persistence/dao/RecordDao.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java index 84d538c8..e6fb741b 100644 --- a/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java +++ b/src/main/java/cz/cvut/kbss/study/persistence/dao/RecordDao.java @@ -341,20 +341,22 @@ private static String constructWhereClauseWithGraphs(RecordFilterParams filters, String whereClause = """ {GRAPH ?r { ?r a ?type ; - ?hasCreatedDate ?created ; - ?hasInstitution ?institution . - + ?hasCreatedDate ?created . + + OPTIONAL { ?r ?hasInstitution ?institution . } OPTIONAL { ?r ?hasPhase ?phase . } OPTIONAL { ?r ?hasFormTemplate ?formTemplate . } OPTIONAL { ?r ?hasLastModified ?lastModified . } - + BIND (COALESCE(?lastModified, ?created) AS ?date) """ + mapParamsToQuery(filters, queryParams) + """ } - GRAPH ?institutionGraph { - ?institution ?hasKey ?institutionKey . + OPTIONAL { + GRAPH ?institutionGraph { + ?institution ?hasKey ?institutionKey . + } }} """; From a318a139aeacba87cc8a908d88b7b52236400336 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Tue, 17 Mar 2026 14:29:30 +0100 Subject: [PATCH 14/16] [kbss-cvut/record-manager-ui#302] Fix institution comparison in preUpdate to use URI equality Reference equality (!=) always evaluated to true when comparing deserialized request body institution against a DB-fetched institution, causing records to be unnecessarily migrated on every user update. Compare by URI instead. --- .../java/cz/cvut/kbss/study/model/Institution.java | 13 +++++++++++++ .../service/repository/RepositoryUserService.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/cz/cvut/kbss/study/model/Institution.java b/src/main/java/cz/cvut/kbss/study/model/Institution.java index 580a46bd..1fd7b4cc 100644 --- a/src/main/java/cz/cvut/kbss/study/model/Institution.java +++ b/src/main/java/cz/cvut/kbss/study/model/Institution.java @@ -86,6 +86,19 @@ public void setReferenceId(Integer referenceId) { this.referenceId = referenceId; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Institution institution)) return false; + if (uri == null) return false; + return uri.equals(institution.uri); + } + + @Override + public int hashCode() { + return uri != null ? uri.hashCode() : System.identityHashCode(this); + } + @Override public String toString() { return "Institution{<" + uri + diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index c74ca19b..1b8add5c 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -296,7 +296,7 @@ protected void preUpdate(User instance) { validatePermissionToUpdate(currentUser, instance, original); - if (instance.getInstitution() != original.getInstitution()) { + if (!Objects.equals(instance.getInstitution(), original.getInstitution())) { validateRecordsAgainstCollisions(instance, original); changePatientRecordsInstitution(original, instance.getInstitution()); } From 5fff4149a14a174ffafbb9600698bcc178364565 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Tue, 17 Mar 2026 15:55:13 +0100 Subject: [PATCH 15/16] [kbss-cvut/record-manager-ui#302] Rename changePatientRecordsInstitution to changeRecordsInstitution --- .../kbss/study/service/repository/RepositoryUserService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java index 1b8add5c..a256b15b 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryUserService.java @@ -277,7 +277,7 @@ protected void validateRecordsAgainstCollisions(User toUpdate, User original) { } } - protected void changePatientRecordsInstitution(User original, Institution newInstitution) { + protected void changeRecordsInstitution(User original, Institution newInstitution) { List recordsToUpdate = recordDao.findByAuthor(original); for (Record record : recordsToUpdate) { record.setInstitution(newInstitution); @@ -298,7 +298,7 @@ protected void preUpdate(User instance) { if (!Objects.equals(instance.getInstitution(), original.getInstitution())) { validateRecordsAgainstCollisions(instance, original); - changePatientRecordsInstitution(original, instance.getInstitution()); + changeRecordsInstitution(original, instance.getInstitution()); } try { From 38b926acc8a213488a06389228cd0debc6425132 Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Wed, 18 Mar 2026 13:35:58 +0100 Subject: [PATCH 16/16] [kbss-cvut/record-manager-ui#302] Allow user to view own record without having read permissions --- .../cvut/kbss/study/rest/RecordController.java | 3 ++- .../study/service/security/SecurityUtils.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/RecordController.java b/src/main/java/cz/cvut/kbss/study/rest/RecordController.java index 6485a3d3..9a233d51 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/RecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/RecordController.java @@ -149,7 +149,8 @@ public ResponseEntity exportRecordsExcel(MultiValueMap