From 4b1532935c3c937823015a7700d7a0580223fb1e Mon Sep 17 00:00:00 2001 From: Enrico Date: Mon, 2 Dec 2024 08:47:45 -0300 Subject: [PATCH] fix: Implement create note endpoints in tracker [DHIS2-17579] --- .../org/hisp/dhis/note/hibernate/Note.hbm.xml | 8 +- .../tracker/export/event/JdbcEventStore.java | 3 - .../imports/bundle/TrackerObjectsMapper.java | 9 +- .../imports/note/DefaultNoteService.java | 82 ++++++ .../tracker/imports/note/JdbcNoteStore.java | 97 +++++++ .../note/NoteService.java} | 34 +-- .../engine/SupplementaryDataProvider.java | 4 +- ..._42_34__Create_sequence_for_note_table.sql | 5 + .../resources/tracker/simple_metadata.json | 3 - .../tracker/imports/note/NoteServiceTest.java | 208 +++++++++++++++ .../validation/EventImportValidationTest.java | 2 - .../TrackerImportNoteControllerTest.java | 240 ++++++++++++++++++ .../tracker/imports/NoteMapper.java | 8 +- .../imports/TrackerImportController.java | 29 +++ .../webapi/controller/tracker/view/Note.java | 3 +- .../imports/TrackerImportControllerTest.java | 6 +- 16 files changed, 703 insertions(+), 38 deletions(-) create mode 100644 dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java create mode 100644 dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java rename dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/{export/note/HibernateNoteStore.java => imports/note/NoteService.java} (63%) create mode 100644 dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql create mode 100644 dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java create mode 100644 dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml index b68a211e7850..4754e022f602 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml @@ -11,7 +11,13 @@ - &identifiableProperties; + + + + + + diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java index 33e787ce3228..09c6e1c9a542 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java @@ -132,7 +132,6 @@ class JdbcEventStore { n.created as note_created,\ n.creator as note_creator,\ n.uid as note_uid,\ - n.lastupdated as note_lastupdated,\ userinfo.userinfoid as note_user_id,\ userinfo.code as note_user_code,\ userinfo.uid as note_user_uid,\ @@ -467,8 +466,6 @@ private List fetchEvents(EventQueryParams queryParams, PageParams pagePar note.setLastUpdatedBy(noteLastUpdatedBy); } - note.setLastUpdated(resultSet.getTimestamp("note_lastupdated")); - event.getNotes().add(note); notes.add(resultSet.getString("note_id")); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java index 36eb83558601..e6d6c6e6140a 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java @@ -33,6 +33,7 @@ import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.UID; import org.hisp.dhis.event.EventStatus; @@ -302,13 +303,19 @@ private TrackerObjectsMapper() { @Nonnull TrackerPreheat preheat, @Nonnull org.hisp.dhis.tracker.imports.domain.Note note, @Nonnull UserDetails user) { + + return map(note, preheat.getUserByUid(user.getUid()).orElse(null)); + } + + public static @Nonnull Note map( + @Nonnull org.hisp.dhis.tracker.imports.domain.Note note, @Nullable User user) { Date now = new Date(); Note dbNote = new Note(); dbNote.setUid(note.getNote().getValue()); dbNote.setCreated(now); dbNote.setLastUpdated(now); - dbNote.setLastUpdatedBy(preheat.getUserByUid(user.getUid()).orElse(null)); + dbNote.setLastUpdatedBy(user); dbNote.setCreator(note.getStoredBy()); dbNote.setNoteText(note.getValue()); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java new file mode 100644 index 000000000000..8d6dac0d820e --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.tracker.imports.note; + +import static org.apache.commons.lang3.StringUtils.isEmpty; + +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.feedback.ForbiddenException; +import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; +import org.hisp.dhis.tracker.export.event.EventService; +import org.hisp.dhis.tracker.imports.domain.Note; +import org.hisp.dhis.user.CurrentUserUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class DefaultNoteService implements NoteService { + private final EnrollmentService enrollmentService; + + private final EventService eventService; + + private final JdbcNoteStore noteStore; + + @Transactional + public void addNoteForEnrollment(Note note, UID enrollment) + throws ForbiddenException, NotFoundException, BadRequestException { + // Check enrollment existence and access + enrollmentService.getEnrollment(enrollment); + validateNote(note); + + noteStore.saveEnrollmentNote(enrollment, note, CurrentUserUtil.getCurrentUserDetails()); + } + + @Transactional + public void addNoteForEvent(Note note, UID event) + throws ForbiddenException, NotFoundException, BadRequestException { + // Check event existence and access + eventService.getEvent(event); + validateNote(note); + + noteStore.saveEventNote(event, note, CurrentUserUtil.getCurrentUserDetails()); + } + + private void validateNote(Note note) throws BadRequestException { + if (isEmpty(note.getValue())) { + throw new BadRequestException("Value cannot be empty"); + } + + if (noteStore.exists(note.getNote())) { + throw new BadRequestException(String.format("Note `%s` already exists.", note.getNote())); + } + } +} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java new file mode 100644 index 000000000000..41e198a25c29 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.tracker.imports.note; + +import java.util.Date; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.tracker.imports.bundle.persister.PersistenceException; +import org.hisp.dhis.tracker.imports.domain.Note; +import org.hisp.dhis.user.UserDetails; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JdbcNoteStore { + private final NamedParameterJdbcTemplate jdbcTemplate; + + public void saveEnrollmentNote( + @Nonnull UID enrollment, @Nonnull Note note, @Nonnull UserDetails user) { + long noteId = saveNote(note, user); + String sql = + """ + INSERT INTO enrollment_notes(enrollmentid, noteid, sort_order) + VALUES ((select enrollmentid from enrollment where uid = :enrollment), :noteId, coalesce((select max(sort_order) + 1 from enrollment_notes where enrollmentid = (select enrollmentid from enrollment where uid = :enrollment)),1)) + """; + jdbcTemplate.update(sql, Map.of("enrollment", enrollment.getValue(), "noteId", noteId)); + } + + public void saveEventNote(@Nonnull UID event, @Nonnull Note note, @Nonnull UserDetails user) { + long noteId = saveNote(note, user); + String sql = + """ + INSERT INTO event_notes(eventid, noteid, sort_order) + VALUES ((select eventid from event where uid = :event), :noteId, coalesce((select max(sort_order) + 1 from event_notes where eventid = (select eventid from event where uid = :event)),1)) + """; + jdbcTemplate.update(sql, Map.of("event", event.getValue(), "noteId", noteId)); + } + + boolean exists(@Nonnull UID note) { + Integer count = + jdbcTemplate.queryForObject( + "select count(1) from note where uid = :uid", + Map.of("uid", note.getValue()), + Integer.class); + return count == null || count > 0; + } + + private long saveNote(@Nonnull Note note, @Nonnull UserDetails user) { + String sql = + "INSERT INTO public.note(noteid, notetext, creator, lastupdatedby, uid, created) " + + "VALUES (nextVal('note_id_sequence'), :text, :creator, (select userinfoid from userinfo where uid = :lastUpdatedBy), :uid, :created) RETURNING noteid"; + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("text", note.getValue()); + params.addValue("creator", note.getStoredBy()); + params.addValue("lastUpdatedBy", user.getUid()); + params.addValue("uid", note.getNote().getValue()); + params.addValue("created", new Date()); + + Long noteId = jdbcTemplate.queryForObject(sql, params, Long.class); + + if (noteId == null) { + throw new PersistenceException("Note could not be saved"); + } + + return noteId; + } +} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/note/HibernateNoteStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java similarity index 63% rename from dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/note/HibernateNoteStore.java rename to dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java index ed6cb444e6c7..a297b9a0f32d 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/note/HibernateNoteStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2024, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,26 +25,18 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.tracker.export.note; +package org.hisp.dhis.tracker.imports.note; -import jakarta.persistence.EntityManager; -import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; -import org.hisp.dhis.note.Note; -import org.hisp.dhis.security.acl.AclService; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.feedback.ForbiddenException; +import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.tracker.imports.domain.Note; -/** - * @author David Katuscak - */ -@Repository("org.hisp.dhis.tracker.export.note.NoteStore") -class HibernateNoteStore extends HibernateIdentifiableObjectStore { - public HibernateNoteStore( - EntityManager entityManager, - JdbcTemplate jdbcTemplate, - ApplicationEventPublisher publisher, - AclService aclService) { - super(entityManager, jdbcTemplate, publisher, Note.class, aclService, false); - } +public interface NoteService { + void addNoteForEnrollment(Note note, UID enrollment) + throws ForbiddenException, NotFoundException, BadRequestException; + + void addNoteForEvent(Note note, UID event) + throws ForbiddenException, NotFoundException, BadRequestException; } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java index 0cf9b1067088..5ad3e87297df 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java @@ -29,8 +29,10 @@ import com.google.common.collect.Maps; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -58,7 +60,7 @@ public class SupplementaryDataProvider { public Map> getSupplementaryData( List programRules, UserDetails user) { - List orgUnitGroups = new ArrayList<>(); + Set orgUnitGroups = new HashSet<>(); for (ProgramRule programRule : programRules) { Matcher matcher = PATTERN.matcher(StringUtils.defaultIfBlank(programRule.getCondition(), "")); diff --git a/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql new file mode 100644 index 000000000000..4b89b4e8ccf2 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql @@ -0,0 +1,5 @@ +create sequence if not exists note_id_sequence; +select setval('note_id_sequence', coalesce((select max(noteid) from note), 1)) FROM note; + +alter table if exists note drop column code; +alter table if exists note drop column lastupdated; \ No newline at end of file diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json b/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json index ef773fcfb09e..317b94cd9efe 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json +++ b/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json @@ -2362,9 +2362,6 @@ "firstName": "child level 3", "name": "child level 2", "organisationUnits": [ - { - "id": "uoNW0E3xXUy" - }, { "id": "lbDXJBlvtZe" } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java new file mode 100644 index 000000000000..07b703927e8f --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.tracker.imports.note; + +import static org.hisp.dhis.tracker.Assertions.assertNoErrors; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.util.List; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.feedback.ForbiddenException; +import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.program.Enrollment; +import org.hisp.dhis.program.Event; +import org.hisp.dhis.tracker.TrackerTest; +import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; +import org.hisp.dhis.tracker.export.event.EventService; +import org.hisp.dhis.tracker.imports.TrackerImportParams; +import org.hisp.dhis.tracker.imports.TrackerImportService; +import org.hisp.dhis.tracker.imports.domain.Note; +import org.hisp.dhis.user.User; +import org.hisp.dhis.user.UserDetails; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class NoteServiceTest extends TrackerTest { + @Autowired private TrackerImportService trackerImportService; + + @Autowired private EventService eventService; + + @Autowired private EnrollmentService enrollmentService; + + @Autowired private NoteService noteService; + + private UserDetails userDetails; + + @BeforeAll + void setUp() throws IOException { + setUpMetadata("tracker/simple_metadata.json"); + + User importUser = userService.getUser("tTgjgobT1oS"); + userDetails = UserDetails.fromUser(importUser); + injectSecurityContext(userDetails); + + TrackerImportParams params = TrackerImportParams.builder().build(); + assertNoErrors( + trackerImportService.importTracker(params, fromJson("tracker/event_and_enrollment.json"))); + } + + @BeforeEach + void initUser() { + injectSecurityContext(userDetails); + } + + @Test + void shouldCreateEnrollmentNote() + throws ForbiddenException, NotFoundException, BadRequestException { + Note note = note(); + noteService.addNoteForEnrollment(note, UID.of("nxP7UnKhomJ")); + + manager.clear(); + manager.flush(); + + Enrollment dbEnrollment = enrollmentService.getEnrollment(UID.of("nxP7UnKhomJ")); + assertNotes(List.of(note), dbEnrollment.getNotes(), userDetails); + } + + @Test + void shouldFailToCreateEnrollmentNoteWhenNoteValueIsNull() { + Note note = note(); + note.setValue(null); + + assertThrows( + BadRequestException.class, + () -> noteService.addNoteForEnrollment(note, UID.of("nxP7UnKhomJ"))); + } + + @Test + void shouldFailToCreateEnrollmentNoteWhenEnrollmentIsNotPresent() { + Note note = note(); + assertThrows( + NotFoundException.class, + () -> noteService.addNoteForEnrollment(note, UID.of("jPP9AnKh34U"))); + } + + @Test + void shouldFailToCreateDuplicateEnrollmentNote() + throws ForbiddenException, NotFoundException, BadRequestException { + Note note = note(); + noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ")); + assertThrows( + BadRequestException.class, + () -> noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ"))); + } + + @Test + void shouldFailToCreateEnrollmentNoteIfUserHasNoAccessToEnrollment() { + User importUser = userService.getUser("nIidJVYpQQK"); + injectSecurityContext(UserDetails.fromUser(importUser)); + + Note note = note(); + + assertThrows( + ForbiddenException.class, + () -> noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ"))); + } + + @Test + void shouldCreateEventNote() throws ForbiddenException, NotFoundException, BadRequestException { + Note note = note(); + noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72")); + + manager.clear(); + manager.flush(); + + Event dbEvent = eventService.getEvent(UID.of("pTzf9KYMk72")); + assertNotes(List.of(note), dbEvent.getNotes(), userDetails); + } + + @Test + void shouldFailToCreateEventNoteWhenNoteValueIsNull() { + Note note = note(); + note.setValue(null); + + assertThrows( + BadRequestException.class, () -> noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72"))); + } + + @Test + void shouldFailToCreateEventNoteWhenEnrollmentIsNotPresent() { + Note note = note(); + assertThrows( + NotFoundException.class, () -> noteService.addNoteForEvent(note, UID.of("jPP9AnKh34U"))); + } + + @Test + void shouldFailToCreateDuplicateEventNote() + throws ForbiddenException, NotFoundException, BadRequestException { + Note note = note(); + noteService.addNoteForEvent(note, UID.of("D9PbzJY8bJM")); + assertThrows( + BadRequestException.class, () -> noteService.addNoteForEvent(note, UID.of("D9PbzJY8bJM"))); + } + + @Test + void shouldFailToCreateEventNoteIfUserHasNoAccessToEvent() { + User importUser = userService.getUser("nIidJVYpQQK"); + injectSecurityContext(UserDetails.fromUser(importUser)); + + Note note = note(); + + assertThrows( + NotFoundException.class, () -> noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72"))); + } + + private void assertNotes( + List notes, List dbNotes, UserDetails updatedBy) { + for (org.hisp.dhis.tracker.imports.domain.Note note : notes) { + org.hisp.dhis.note.Note dbNote = + dbNotes.stream() + .filter(n -> n.getUid().equals(note.getNote().getValue())) + .findFirst() + .orElse(null); + assertNotNull(dbNote); + assertEquals(note.getValue(), dbNote.getNoteText()); + assertEquals(note.getStoredBy(), dbNote.getCreator()); + assertEquals(updatedBy.getUid(), dbNote.getLastUpdatedBy().getUid()); + } + } + + private Note note() { + return Note.builder() + .note(UID.generate()) + .storedBy("This is the creator") + .value("This is a note") + .build(); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java index 5394088d2d09..d46d77161021 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java @@ -336,7 +336,6 @@ void testValidateAndAddNotesToEvent() throws IOException { Note note = getByNote(event.getNotes(), t); assertTrue(CodeGenerator.isValidUid(note.getUid())); assertTrue(note.getCreated().getTime() > now.getTime()); - assertTrue(note.getLastUpdated().getTime() > now.getTime()); assertNull(note.getCreator()); assertEquals(importUser.getUid(), note.getLastUpdatedBy().getUid()); }); @@ -360,7 +359,6 @@ void testValidateAndAddNotesToUpdatedEvent() throws IOException { Note note = getByNote(event.getNotes(), t); assertTrue(CodeGenerator.isValidUid(note.getUid())); assertTrue(note.getCreated().getTime() > now.getTime()); - assertTrue(note.getLastUpdated().getTime() > now.getTime()); assertNull(note.getCreator()); assertEquals(importUser.getUid(), note.getLastUpdatedBy().getUid()); }); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java new file mode 100644 index 000000000000..3ce3693bb9cf --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller.tracker.imports; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.Set; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.program.Enrollment; +import org.hisp.dhis.program.EnrollmentStatus; +import org.hisp.dhis.program.Event; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; +import org.hisp.dhis.security.acl.AccessStringHelper; +import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; +import org.hisp.dhis.test.webapi.json.domain.JsonWebMessage; +import org.hisp.dhis.trackedentity.TrackedEntity; +import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.hisp.dhis.user.User; +import org.hisp.dhis.user.sharing.UserAccess; +import org.hisp.dhis.webapi.controller.tracker.JsonNote; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TrackerImportNoteControllerTest extends PostgresControllerIntegrationTestBase { + private User importUser; + + private Event event; + + private Enrollment enrollment; + + @BeforeAll + void setUp() { + importUser = makeUser("o"); + manager.save(importUser, false); + + CategoryOptionCombo coc = categoryService.getDefaultCategoryOptionCombo(); + + OrganisationUnit orgUnit = createOrganisationUnit('A'); + manager.save(orgUnit); + + importUser.addOrganisationUnit(orgUnit); + manager.update(importUser); + + Program program = createProgram('A'); + program.getSharing().addUserAccess(new UserAccess(importUser, AccessStringHelper.DATA_READ)); + manager.save(program, false); + + TrackedEntityType trackedEntityType = createTrackedEntityType('A'); + manager.save(trackedEntityType); + + ProgramStage programStage = createProgramStage('A', program); + programStage + .getSharing() + .addUserAccess(new UserAccess(importUser, AccessStringHelper.DATA_READ)); + manager.save(programStage, false); + + TrackedEntity te = createTrackedEntity(orgUnit); + te.setTrackedEntityType(trackedEntityType); + manager.save(te); + + enrollment = enrollment(te, program, orgUnit); + event = event(enrollment, programStage, coc); + enrollment.setEvents(Set.of(event)); + manager.update(enrollment); + } + + @BeforeEach + void injectUser() { + injectSecurityContextUser(importUser); + } + + @Test + void shouldReturnBadRequestWhenValueIsNullForEventNote() { + JsonWebMessage webMessage = + POST( + "/tracker/events/" + event.getUid() + "/note", + """ + + { + "creator": "I am the creator" + } + """) + .content(HttpStatus.BAD_REQUEST) + .as(JsonWebMessage.class); + + assertEquals("Value cannot be empty", webMessage.getMessage()); + } + + @Test + void shouldCreateEventNote() { + JsonNote note = + POST( + "/tracker/events/" + event.getUid() + "/note", + """ + + { + "value": "This is a note" + } + """) + .content(HttpStatus.OK) + .as(JsonNote.class); + + assertEquals("This is a note", note.getValue()); + assertTrue(CodeGenerator.isValidUid(note.getNote())); + } + + @Test + void shouldCreateEventNoteWhenNoteUidIsProvided() { + UID noteUid = UID.generate(); + JsonNote note = + POST( + "/tracker/events/" + event.getUid() + "/note", + """ + + { + "note": "%s", + "value": "This is a note" + } + """ + .formatted(noteUid.getValue())) + .content(HttpStatus.OK) + .as(JsonNote.class); + + assertEquals("This is a note", note.getValue()); + assertEquals(noteUid.getValue(), note.getNote()); + } + + @Test + void shouldReturnBadRequestWhenValueIsNullForEnrollmentNote() { + JsonWebMessage webMessage = + POST( + "/tracker/enrollments/" + enrollment.getUid() + "/note", + """ + + { + "creator": "I am the creator" + } + """) + .content(HttpStatus.BAD_REQUEST) + .as(JsonWebMessage.class); + + assertEquals("Value cannot be empty", webMessage.getMessage()); + } + + @Test + void shouldCreateEnrollmentNote() { + JsonNote note = + POST( + "/tracker/enrollments/" + enrollment.getUid() + "/note", + """ + + { + "value": "This is a note" + } + """) + .content(HttpStatus.OK) + .as(JsonNote.class); + + assertEquals("This is a note", note.getValue()); + assertTrue(CodeGenerator.isValidUid(note.getNote())); + } + + @Test + void shouldCreateEnrollmentNoteWhenNoteUidIsProvided() { + UID noteUid = UID.generate(); + JsonNote note = + POST( + "/tracker/enrollments/" + enrollment.getUid() + "/note", + """ + + { + "note": "%s", + "value": "This is a note" + } + """ + .formatted(noteUid.getValue())) + .content(HttpStatus.OK) + .as(JsonNote.class); + + assertEquals("This is a note", note.getValue()); + assertEquals(noteUid.getValue(), note.getNote()); + } + + private Event event(Enrollment enrollment, ProgramStage programStage, CategoryOptionCombo coc) { + Event eventA = new Event(enrollment, programStage, enrollment.getOrganisationUnit(), coc); + eventA.setAutoFields(); + manager.save(eventA); + return eventA; + } + + private Enrollment enrollment(TrackedEntity te, Program program, OrganisationUnit orgUnit) { + Enrollment enrollmentA = new Enrollment(program, te, orgUnit); + enrollmentA.setAutoFields(); + enrollmentA.setEnrollmentDate(new Date()); + enrollmentA.setOccurredDate(new Date()); + enrollmentA.setStatus(EnrollmentStatus.COMPLETED); + enrollmentA.setFollowup(true); + manager.save(enrollmentA, false); + te.setEnrollments(Set.of(enrollmentA)); + manager.save(te, false); + return enrollmentA; + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java index 058de6068cc4..e4dac5a1d9df 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java @@ -30,12 +30,12 @@ import org.hisp.dhis.tracker.TrackerIdSchemeParams; import org.hisp.dhis.webapi.controller.tracker.view.InstantMapper; import org.hisp.dhis.webapi.controller.tracker.view.Note; -import org.mapstruct.Context; import org.mapstruct.Mapper; @Mapper(uses = {InstantMapper.class, UserMapper.class}) public interface NoteMapper extends DomainMapper { - org.hisp.dhis.tracker.imports.domain.Note from( - org.hisp.dhis.tracker.imports.domain.Note note, - @Context TrackerIdSchemeParams idSchemeParams); + + default org.hisp.dhis.tracker.imports.domain.Note from(Note note) { + return from(note, TrackerIdSchemeParams.builder().build()); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java index 58a76f04a23a..bc02eaef0a50 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java @@ -43,9 +43,12 @@ import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.common.UID; import org.hisp.dhis.commons.util.StreamUtils; import org.hisp.dhis.dxf2.webmessage.WebMessage; +import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.scheduling.JobConfiguration; import org.hisp.dhis.scheduling.JobConfigurationService; @@ -58,16 +61,20 @@ import org.hisp.dhis.tracker.imports.TrackerImportParams; import org.hisp.dhis.tracker.imports.TrackerImportService; import org.hisp.dhis.tracker.imports.domain.TrackerObjects; +import org.hisp.dhis.tracker.imports.note.NoteService; import org.hisp.dhis.tracker.imports.report.ImportReport; import org.hisp.dhis.tracker.imports.report.Status; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.webapi.controller.tracker.export.CsvService; import org.hisp.dhis.webapi.controller.tracker.view.Event; +import org.hisp.dhis.webapi.controller.tracker.view.Note; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.utils.ContextUtils; import org.locationtech.jts.io.ParseException; +import org.mapstruct.factory.Mappers; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.MimeType; import org.springframework.web.bind.annotation.GetMapping; @@ -105,6 +112,10 @@ public class TrackerImportController { private final ObjectMapper jsonMapper; + private final NoteService noteService; + + private final NoteMapper noteMapper = Mappers.getMapper(NoteMapper.class); + @PostMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) @ResponseBody public WebMessage asyncPostJsonTracker( @@ -254,4 +265,22 @@ public ImportReport getJobReport( .map(report -> trackerImportService.buildImportReport((ImportReport) report, reportMode)) .orElseThrow(() -> new NotFoundException("Summary for job " + uid + " does not exist")); } + + @PostMapping(value = "/enrollments/{uid}/note", consumes = APPLICATION_JSON_VALUE) + public ResponseEntity addNoteToEnrollment(@RequestBody Note note, @PathVariable UID uid) + throws ForbiddenException, NotFoundException, BadRequestException { + + noteService.addNoteForEnrollment(noteMapper.from(note), uid); + + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(note); + } + + @PostMapping(value = "/events/{uid}/note", consumes = APPLICATION_JSON_VALUE) + public ResponseEntity addNoteToEvent(@RequestBody Note note, @PathVariable UID uid) + throws ForbiddenException, NotFoundException, BadRequestException { + + noteService.addNoteForEvent(noteMapper.from(note), uid); + + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(note); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java index fed94b0ba405..0b00f6a5ca3e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java @@ -51,7 +51,8 @@ public class Note { @OpenApi.Property({UID.class, org.hisp.dhis.note.Note.class}) @JsonProperty - private UID note; + @Builder.Default + private UID note = UID.generate(); @JsonProperty private Instant storedAt; diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java index a89f4ec1ad81..6d77650330d4 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java @@ -59,6 +59,7 @@ import org.hisp.dhis.system.notification.Notification; import org.hisp.dhis.system.notification.Notifier; import org.hisp.dhis.tracker.imports.DefaultTrackerImportService; +import org.hisp.dhis.tracker.imports.note.NoteService; import org.hisp.dhis.tracker.imports.report.ImportReport; import org.hisp.dhis.tracker.imports.report.PersistenceReport; import org.hisp.dhis.tracker.imports.report.Status; @@ -100,6 +101,8 @@ class TrackerImportControllerTest { @Mock private UserService userService; + @Mock private NoteService noteService; + private RenderService renderService; @BeforeEach @@ -120,7 +123,8 @@ public void setUp() { notifier, jobSchedulerService, jobConfigurationService, - new ObjectMapper()); + new ObjectMapper(), + noteService); mockMvc = MockMvcBuilders.standaloneSetup(controller)