From e3bb5d2805fd95bfb528f5802adac3d9e301cab3 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Thu, 30 Jan 2025 08:52:52 -0600 Subject: [PATCH] fix: remove Program fk when deleting MapView (#19810) --- .../org/hisp/dhis/mapping/MapViewStore.java | 6 + .../org/hisp/dhis/mapping/MappingService.java | 6 + .../dhis/mapping/DefaultMappingService.java | 12 ++ .../dhis/mapping/MapViewDeletionHandler.java | 22 ++ .../hibernate/HibernateMapViewStore.java | 21 ++ .../controller/ProgramControllerTest.java | 93 ++++++++ .../resources/program/create_program.json | 199 ++++++++++++++++++ 7 files changed, 359 insertions(+) create mode 100644 dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/ProgramControllerTest.java create mode 100644 dhis-2/dhis-test-web-api/src/test/resources/program/create_program.json diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MapViewStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MapViewStore.java index 037cbb7aab9d..f78e6d360700 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MapViewStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MapViewStore.java @@ -30,10 +30,16 @@ import java.util.List; import org.hisp.dhis.common.AnalyticalObjectStore; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; /** * @author Morten Olav Hansen */ public interface MapViewStore extends AnalyticalObjectStore { List getByOrganisationUnitGroupSet(OrganisationUnitGroupSet groupSet); + + List findByProgram(Program program); + + List findByProgramStage(ProgramStage programStage); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MappingService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MappingService.java index 426add4add6e..70ec069587c1 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MappingService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/mapping/MappingService.java @@ -30,6 +30,8 @@ import java.util.List; import org.hisp.dhis.common.AnalyticalObjectService; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; /** * @author Jan Henrik Overland @@ -91,6 +93,10 @@ public interface MappingService extends AnalyticalObjectService { int countMapViewMaps(MapView mapView); + List findByProgram(Program program); + + List findByProgramStage(ProgramStage programStage); + // ------------------------------------------------------------------------- // ExternalMapLayer // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/DefaultMappingService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/DefaultMappingService.java index 7db899cc3207..5674a650d290 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/DefaultMappingService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/DefaultMappingService.java @@ -39,6 +39,8 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodService; import org.hisp.dhis.period.RelativePeriods; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -185,6 +187,16 @@ public int countMapViewMaps(MapView mapView) { return mapStore.countMapViewMaps(mapView); } + @Override + public List findByProgram(Program program) { + return mapViewStore.findByProgram(program); + } + + @Override + public List findByProgramStage(ProgramStage programStage) { + return mapViewStore.findByProgramStage(programStage); + } + // ------------------------------------------------------------------------- // ExternalMapLayer // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/MapViewDeletionHandler.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/MapViewDeletionHandler.java index 7033801ff531..2803937ef934 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/MapViewDeletionHandler.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/MapViewDeletionHandler.java @@ -41,7 +41,9 @@ import org.hisp.dhis.organisationunit.OrganisationUnitGroup; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; import org.hisp.dhis.period.Period; +import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramIndicator; +import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.system.deletion.DeletionVeto; import org.springframework.stereotype.Component; @@ -67,6 +69,8 @@ protected void registerHandler() { whenVetoing(Period.class, this::allowDeletePeriod); whenDeleting(OrganisationUnit.class, this::deleteOrganisationUnit); whenDeleting(OrganisationUnitGroup.class, this::deleteOrganisationUnitGroup); + whenDeleting(Program.class, this::deleteProgram); + whenDeleting(ProgramStage.class, this::deleteProgramStage); // special whenDeleting(LegendSet.class, this::deleteLegendSet); whenDeleting(OrganisationUnitGroupSet.class, this::deleteOrganisationUnitGroupSetSpecial); @@ -74,6 +78,24 @@ protected void registerHandler() { whenVetoing(MapView.class, this::allowDeleteMapView); } + private void deleteProgramStage(ProgramStage programStage) { + List mapViews = service.findByProgramStage(programStage); + mapViews.forEach( + mapView -> { + mapView.setProgramStage(null); + service.updateMapView(mapView); + }); + } + + private void deleteProgram(Program program) { + List mapViews = service.findByProgram(program); + mapViews.forEach( + mapView -> { + mapView.setProgram(null); + service.update(mapView); + }); + } + private void deleteLegendSet(LegendSet legendSet) { List mapViews = service.getAnalyticalObjects(legendSet); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/hibernate/HibernateMapViewStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/hibernate/HibernateMapViewStore.java index b17deb41c1b3..52f74ff85e1b 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/hibernate/HibernateMapViewStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/mapping/hibernate/HibernateMapViewStore.java @@ -34,6 +34,8 @@ import org.hisp.dhis.mapping.MapView; import org.hisp.dhis.mapping.MapViewStore; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.user.CurrentUserService; import org.springframework.context.ApplicationEventPublisher; @@ -71,4 +73,23 @@ public List getByOrganisationUnitGroupSet(OrganisationUnitGroupSet grou newJpaParameters() .addPredicate(root -> builder.equal(root.get("organisationUnitGroupSet"), groupSet))); } + + @Override + public List findByProgram(Program program) { + CriteriaBuilder builder = getCriteriaBuilder(); + + return getList( + builder, + newJpaParameters().addPredicate(root -> builder.equal(root.get("program"), program))); + } + + @Override + public List findByProgramStage(ProgramStage programStage) { + CriteriaBuilder builder = getCriteriaBuilder(); + + return getList( + builder, + newJpaParameters() + .addPredicate(root -> builder.equal(root.get("programStage"), programStage))); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/ProgramControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/ProgramControllerTest.java new file mode 100644 index 000000000000..97903028cfd0 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/ProgramControllerTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2004-2025, 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; + +import static org.hisp.dhis.web.WebClientUtils.assertStatus; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.jsontree.JsonResponse; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; +import org.hisp.dhis.web.HttpStatus; +import org.hisp.dhis.webapi.DhisControllerConvenienceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ProgramControllerTest extends DhisControllerConvenienceTest { + + @Autowired private ObjectMapper jsonMapper; + + public static final String PROGRAM_UID = "PrZMWi7rBga"; + + @BeforeEach + public void testSetup() throws JsonProcessingException { + DataElement dataElement1 = createDataElement('a'); + DataElement dataElement2 = createDataElement('b'); + dataElement1.setUid("deabcdefgha"); + dataElement2.setUid("deabcdefghb"); + TrackedEntityAttribute tea1 = createTrackedEntityAttribute('a'); + TrackedEntityAttribute tea2 = createTrackedEntityAttribute('b'); + tea1.setUid("TEA1nnnnnaa"); + tea2.setUid("TEA1nnnnnab"); + POST("/dataElements", jsonMapper.writeValueAsString(dataElement1)).content(HttpStatus.CREATED); + POST("/dataElements", jsonMapper.writeValueAsString(dataElement2)).content(HttpStatus.CREATED); + POST("/trackedEntityAttributes", jsonMapper.writeValueAsString(tea1)) + .content(HttpStatus.CREATED); + POST("/trackedEntityAttributes", jsonMapper.writeValueAsString(tea2)) + .content(HttpStatus.CREATED); + + POST("/metadata", org.hisp.dhis.web.WebClient.Body("program/create_program.json")) + .content(HttpStatus.OK); + } + + @Test + void testDeleteWithMapView() { + String mapViewJson = + "{" + + "\"name\": \"test mapview\"," + + "\"id\": \"mVIVRd23Jm9\"," + + "\"organisationUnitLevels\": []," + + "\"maps\": []," + + "\"layer\": \"event\"," + + "\"program\": {" + + "\"id\": \"PrZMWi7rBga\"" + + "}," + + "\"programStage\": {" + + "\"id\": \"PSzMWi7rBga\"" + + "}" + + "}"; + POST("/mapViews", mapViewJson).content(HttpStatus.CREATED); + assertStatus(HttpStatus.OK, DELETE(String.format("/programs/%s", PROGRAM_UID))); + assertStatus(HttpStatus.NOT_FOUND, GET(String.format("/programs/%s", PROGRAM_UID))); + JsonResponse mapview = GET("/mapViews/mVIVRd23Jm9").content(); + assertFalse(mapview.has("program")); + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/resources/program/create_program.json b/dhis-2/dhis-test-web-api/src/test/resources/program/create_program.json new file mode 100644 index 000000000000..2b4a007f84cf --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/resources/program/create_program.json @@ -0,0 +1,199 @@ +{ + "programs": [ + { + "name": "test program", + "id": "PrZMWi7rBga", + "shortName": "test program", + "description": "program description", + "programType": "WITH_REGISTRATION", + "sharing": { + "public": "rw------", + "external": false, + "owner": "M5zQapPyTZI" + }, + "organisationUnits": [ + { + "id": "Orgunit1000" + } + ], + "programStages": [ + { + "id": "PSzMWi7rBga" + } + ], + "programSections": [ + { + "id": "PSeMWi7rBga" + }, + { + "id": "PSeMWi7rBgb" + } + ], + "programRuleVariables": [ + { + "id": "PRVmWi7rBga" + }, + { + "id": "PRVmWi7rBgb" + } + ], + "programIndicators": [ + { + "id": "PInmWi7rBga" + }, + { + "id": "PInmWi7rBgb" + } + ], + "programTrackedEntityAttributes": [ + { + "id": "PTEAmWi7rBa", + "name": "attribute 1", + "trackedEntityAttribute": { + "id": "TEA1nnnnnaa" + }, + "program": { + "id": "PrZMWi7rBga" + } + }, + { + "id": "PTEAmWi7rBb", + "name": "attribute 2", + "trackedEntityAttribute": { + "id": "TEA1nnnnnab" + }, + "program": { + "id": "PrZMWi7rBga" + } + } + ], + "enrollmentDateLabel": "Label for Enrollment Date", + "enrollmentLabel": "Label for Enrollment", + "followUpLabel": "Label for Follow Up", + "orgUnitLabel": "Label for Org Unit", + "relationshipLabel": "Label for Relationship", + "noteLabel": "Label for Note", + "trackedEntityAttributeLabel": "Label for Tracked Entity Attribute", + "programStageLabel": "Label for Program Stage", + "eventLabel": "Label for Event" + } + ], + "programStages": [ + { + "id": "PSzMWi7rBga", + "name": "programStage 1", + "programStageSections": [ + { + "id": "PSSzMWi7rBa" + }, + { + "id": "PSSzMWi7rBb" + } + ], + "programStageDataElements": [ + { + "id": "PSDEWi7rBga", + "name": "programStageDataElement1e", + "dataElement": { + "id": "deabcdefgha" + } + }, + { + "id": "PSDEWi7rBgb", + "name": "programStageDataElement2e", + "dataElement": { + "id": "deabcdefghb" + } + } + ] + } + ], + "programStageSections": [ + { + "id": "PSSzMWi7rBa", + "name": "programStageSection1s", + "sortOrder": 1 + }, + { + "id": "PSSzMWi7rBb", + "name": "programStageSection2s", + "sortOrder": 2 + } + ], + "programSections": [ + { + "id": "PSeMWi7rBga", + "name": "program section 1", + "sortOrder": 1, + "program": { + "id": "PrZMWi7rBga" + } + }, + { + "id": "PSeMWi7rBgb", + "name": "program section 2", + "sortOrder": 2, + "program": { + "id": "PrZMWi7rBga" + } + } + ], + "programRuleVariables": [ + { + "id": "PRVmWi7rBga", + "name": "RuleVariable 1", + "program": { + "id": "PrZMWi7rBga" + }, + "programRuleVariableSourceType": "CALCULATED_VALUE" + }, + { + "id": "PRVmWi7rBgb", + "name": "RuleVariable 2", + "program": { + "id": "PrZMWi7rBga" + }, + "programRuleVariableSourceType": "CALCULATED_VALUE" + } + ], + "programIndicators": [ + { + "id": "PInmWi7rBga", + "name": "Indicator 1", + "shortName": "short name1", + "analyticsType": "ENROLLMENT", + "program": { + "id": "PrZMWi7rBga" + } + }, + { + "id": "PInmWi7rBgb", + "name": "Indicator 2", + "shortName": "short name2", + "analyticsType": "ENROLLMENT", + "program": { + "id": "PrZMWi7rBga" + } + } + ], + "organisationUnits": [ + { + "featureType": "NONE", + "uuid": "eb380b08-6cc6-4aca-9b67-e1247e02db33", + "lastUpdated": "2016-03-17T01:51:39.609+0000", + "shortName": "Country", + "attributeValues": [], + "openingDate": "2016-03-17", + "id": "Orgunit1000", + "code": "Orgunit1000", + "description": "org desc", + "programs": [ + { + "id": "PrZMWi7rBga" + } + ], + "name": "Country", + "created": "2016-03-17T01:51:39.597+0000" + } + ] +} \ No newline at end of file