diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datadimensionitem/DataDimensionItemStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datadimensionitem/DataDimensionItemStore.java index bbbb547add92..f7636fae3bb7 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datadimensionitem/DataDimensionItemStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datadimensionitem/DataDimensionItemStore.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.datadimensionitem; +import java.util.Collection; import java.util.List; import org.hisp.dhis.common.DataDimensionItem; import org.hisp.dhis.common.GenericStore; @@ -47,4 +48,14 @@ public interface DataDimensionItemStore extends GenericStore List getIndicatorDataDimensionItems(List indicators); List getDataElementDataDimensionItems(List dataElements); + + /** + * Update the entities with refs to the category option combo ids passed in, with the new category + * option combo id passed in. + * + * @param cocIds category option combo ids to be used to update linked data dimension items + * @param newCocId new category option combo id to use + * @return number of entities updated + */ + int updateDeoCategoryOptionCombo(Collection cocIds, long newCocId); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java new file mode 100644 index 000000000000..93d67f171241 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java @@ -0,0 +1,46 @@ +/* + * 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.expression; + +import org.hisp.dhis.common.GenericStore; + +/** + * @author david mackessy + */ +public interface ExpressionStore extends GenericStore { + + /** + * Update all expressions whose expression property contains the 'find' value. When updating, it + * replaces all occurrences of 'find' with 'replace'. + * + * @param find text to search for + * @param replace text used to replace 'find' + * @return number of entities updated + */ + int updateExpressionContaining(String find, String replace); +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java index 7d5a89310035..3d647af0cb46 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java @@ -47,4 +47,15 @@ public interface IndicatorStore extends IdentifiableObjectStore { List getIndicatorsWithNumeratorContaining(String search); List getIndicatorsWithDenominatorContaining(String search); + + /** + * Updates any indicator that has the 'find' param in either its numerator or denominator. The + * update involves updating numerator and denominator, replacing all occurrences of 'find' with + * 'replace'. + * + * @param find text to search for + * @param replace text used to replace + * @return number of rows updated + */ + int updateNumeratorDenominatorContaining(String find, String replace); } 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..5e1f45e3b2b5 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,13 @@ import java.util.List; import org.hisp.dhis.common.AnalyticalObjectStore; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; +import org.hisp.dhis.program.Program; /** * @author Morten Olav Hansen */ public interface MapViewStore extends AnalyticalObjectStore { List getByOrganisationUnitGroupSet(OrganisationUnitGroupSet groupSet); + + List findByProgram(Program program); } 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..62104a5743c3 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,7 @@ import java.util.List; import org.hisp.dhis.common.AnalyticalObjectService; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; +import org.hisp.dhis.program.Program; /** * @author Jan Henrik Overland @@ -91,6 +92,8 @@ public interface MappingService extends AnalyticalObjectService { int countMapViewMaps(MapView mapView); + List findByProgram(Program program); + // ------------------------------------------------------------------------- // ExternalMapLayer // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-administration/pom.xml b/dhis-2/dhis-services/dhis-service-administration/pom.xml index f2100f1a647e..fe03be0f7a1d 100644 --- a/dhis-2/dhis-services/dhis-service-administration/pom.xml +++ b/dhis-2/dhis-services/dhis-service-administration/pom.xml @@ -131,7 +131,7 @@ com.networknt json-schema-validator - 1.5.4 + 1.5.5 org.apache.commons diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java index bbaa4928b09f..c99ad4f42131 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java @@ -119,7 +119,9 @@ private void initMergeHandlers() { metadataMergeHandler::handlePredictors, metadataMergeHandler::handleDataElementOperands, metadataMergeHandler::handleMinMaxDataElements, - metadataMergeHandler::handleSmsCodes); + metadataMergeHandler::handleSmsCodes, + metadataMergeHandler::handleIndicators, + metadataMergeHandler::handleExpressions); dataMergeHandlers = List.of( diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java index 0c9bf1c40a5a..b32b36aa5fde 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java @@ -28,6 +28,7 @@ package org.hisp.dhis.merge.category.optioncombo; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.category.CategoryCombo; @@ -37,8 +38,11 @@ import org.hisp.dhis.category.CategoryOptionStore; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.UID; +import org.hisp.dhis.datadimensionitem.DataDimensionItemStore; import org.hisp.dhis.dataelement.DataElementOperand; import org.hisp.dhis.dataelement.DataElementOperandStore; +import org.hisp.dhis.expression.ExpressionStore; +import org.hisp.dhis.indicator.IndicatorStore; import org.hisp.dhis.minmax.MinMaxDataElement; import org.hisp.dhis.minmax.MinMaxDataElementStore; import org.hisp.dhis.predictor.Predictor; @@ -60,9 +64,12 @@ public class MetadataCategoryOptionComboMergeHandler { private final CategoryOptionStore categoryOptionStore; private final CategoryComboStore categoryComboStore; private final DataElementOperandStore dataElementOperandStore; + private final DataDimensionItemStore dataDimensionItemStore; private final MinMaxDataElementStore minMaxDataElementStore; private final PredictorStore predictorStore; private final SMSCommandStore smsCommandStore; + private final IndicatorStore indicatorStore; + private final ExpressionStore expressionStore; /** * Remove sources from {@link CategoryOption} and add target to {@link CategoryOption} @@ -116,6 +123,17 @@ public void handleDataElementOperands( UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); dataElementOperands.forEach(deo -> deo.setCategoryOptionCombo(target)); + + // A data element operand is also a data dimension item. + // The above update does not cascade the reference change though. + // The Data dimension item table also needs updating + int dataDimensionItemsUpdated = + dataDimensionItemStore.updateDeoCategoryOptionCombo( + sources.stream().map(BaseIdentifiableObject::getId).collect(Collectors.toSet()), + target.getId()); + log.info( + "{} data dimension items updated as part of category option combo merge", + dataDimensionItemsUpdated); } /** @@ -163,4 +181,38 @@ public void handleSmsCodes(List sources, CategoryOptionComb smsCodes.forEach(smsCode -> smsCode.setOptionId(target)); } + + /** + * Update each Indicator numerator and denominator values, replacing any source ref with the + * target ref. + * + * @param sources to be replaced + * @param target to replace source refs + */ + public void handleIndicators(List sources, CategoryOptionCombo target) { + log.info("Merging source indicators"); + int totalUpdates = 0; + for (CategoryOptionCombo source : sources) { + totalUpdates += + indicatorStore.updateNumeratorDenominatorContaining(source.getUid(), target.getUid()); + } + + log.info("{} indicators updated", totalUpdates); + } + + /** + * Update each Expression expression value, replacing any source ref with the target ref. + * + * @param sources to be replaced + * @param target to replace source refs + */ + public void handleExpressions(List sources, CategoryOptionCombo target) { + log.info("Merging source expressions"); + int totalUpdates = 0; + for (CategoryOptionCombo source : sources) { + totalUpdates += expressionStore.updateExpressionContaining(source.getUid(), target.getUid()); + } + + log.info("{} expressions updated", totalUpdates); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datadimensionitem/hibernate/HibernateDataDimensionItemStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datadimensionitem/hibernate/HibernateDataDimensionItemStore.java index a190c0a0a037..9f55a7909385 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datadimensionitem/hibernate/HibernateDataDimensionItemStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datadimensionitem/hibernate/HibernateDataDimensionItemStore.java @@ -28,7 +28,10 @@ package org.hisp.dhis.datadimensionitem.hibernate; import jakarta.persistence.EntityManager; +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.hisp.dhis.common.DataDimensionItem; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.hibernate.HibernateGenericStore; @@ -69,4 +72,19 @@ public List getDataElementDataDimensionItems(List cocIds, long newCocId) { + if (cocIds.isEmpty()) return 0; + + String sql = + """ + update datadimensionitem + set dataelementoperand_categoryoptioncomboid = %s + where dataelementoperand_categoryoptioncomboid in (%s) + """ + .formatted( + newCocId, cocIds.stream().map(String::valueOf).collect(Collectors.joining(","))); + return jdbcTemplate.update(sql); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DhisHttpSessionEventListener.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java similarity index 56% rename from dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DhisHttpSessionEventListener.java rename to dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java index 03b9851effc1..3ee7d3e67eee 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DhisHttpSessionEventListener.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2023, 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,36 +25,35 @@ * (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.security; +package org.hisp.dhis.expression; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.hisp.dhis.external.conf.ConfigurationKey; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; -import org.springframework.context.event.EventListener; -import org.springframework.security.web.session.HttpSessionCreatedEvent; -import org.springframework.stereotype.Component; +import jakarta.persistence.EntityManager; +import org.hisp.dhis.hibernate.HibernateGenericStore; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; /** - * @author Morten Svanæs + * @author david mackessy */ -@Component -@Slf4j -@RequiredArgsConstructor -public class DhisHttpSessionEventListener { - private final DhisConfigurationProvider config; +@Repository +public class HibernateExpressionStore extends HibernateGenericStore + implements org.hisp.dhis.expression.ExpressionStore { - @EventListener - public void sessionCreated(HttpSessionCreatedEvent event) { - HttpSession session = event.getSession(); - try { - String property = config.getProperty(ConfigurationKey.SYSTEM_SESSION_TIMEOUT); - session.setMaxInactiveInterval(Integer.parseInt(property)); - } catch (Exception e) { - session.setMaxInactiveInterval( - Integer.parseInt(ConfigurationKey.SYSTEM_SESSION_TIMEOUT.getDefaultValue())); - log.error("Could not read session timeout value from config", e); - } + public HibernateExpressionStore( + EntityManager entityManager, JdbcTemplate jdbcTemplate, ApplicationEventPublisher publisher) { + super(entityManager, jdbcTemplate, publisher, Expression.class, false); + } + + @Override + public int updateExpressionContaining(String find, String replace) { + String sql = + """ + update expression + set expression = replace(expression, '%s', '%s') + where expression like '%s'; + """ + .formatted(find, replace, "%" + find + "%"); + return jdbcTemplate.update(sql); } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java index 55ba2e8551c6..6382a9403944 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java @@ -100,4 +100,18 @@ public List getIndicatorsWithDenominatorContaining(String search) { .setParameter("search", "%" + search + "%") .getResultList(); } + + @Override + public int updateNumeratorDenominatorContaining(String find, String replace) { + String sql = + """ + update indicator + set numerator = replace(numerator, '%s', '%s'), + denominator = replace(denominator, '%s', '%s') + where numerator like '%s' + or denominator like '%s'; + """ + .formatted(find, replace, find, replace, "%" + find + "%", "%" + find + "%"); + return jdbcTemplate.update(sql); + } } 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..86f77d0ed971 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,7 @@ 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.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -185,6 +186,12 @@ public int countMapViewMaps(MapView mapView) { return mapStore.countMapViewMaps(mapView); } + @Override + @Transactional(readOnly = true) + public List findByProgram(Program program) { + return mapViewStore.findByProgram(program); + } + // ------------------------------------------------------------------------- // 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..a57338d0eb12 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 @@ -32,6 +32,7 @@ import java.util.List; import org.hisp.dhis.common.GenericAnalyticalObjectDeletionHandler; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.expressiondimensionitem.ExpressionDimensionItem; @@ -41,6 +42,7 @@ 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.system.deletion.DeletionVeto; import org.springframework.stereotype.Component; @@ -72,6 +74,7 @@ protected void registerHandler() { whenDeleting(OrganisationUnitGroupSet.class, this::deleteOrganisationUnitGroupSetSpecial); whenDeleting(ExpressionDimensionItem.class, this::deleteExpressionDimensionItem); whenVetoing(MapView.class, this::allowDeleteMapView); + whenDeleting(Program.class, this::deleteProgram); } private void deleteLegendSet(LegendSet legendSet) { @@ -83,6 +86,21 @@ private void deleteLegendSet(LegendSet legendSet) { } } + private void deleteProgram(Program program) { + List mapViews = service.findByProgram(program); + for (MapView mapView : mapViews) { + mapView.setProgram(null); + if (mapView.getProgramStage() != null + && program.getProgramStages().stream() + .map(IdentifiableObject::getUid) + .toList() + .contains(mapView.getProgramStage().getUid())) { + mapView.setProgramStage(null); + service.update(mapView); + } + } + } + public void deleteOrganisationUnitGroupSetSpecial(OrganisationUnitGroupSet groupSet) { List mapViews = service.getMapViewsByOrganisationUnitGroupSet(groupSet); 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 686969428a61..0ea3e36cec08 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,7 @@ 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.security.acl.AclService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.jdbc.core.JdbcTemplate; @@ -45,6 +46,7 @@ @Repository("org.hisp.dhis.mapping.MapViewStore") public class HibernateMapViewStore extends HibernateAnalyticalObjectStore implements MapViewStore { + public HibernateMapViewStore( EntityManager entityManager, JdbcTemplate jdbcTemplate, @@ -62,4 +64,12 @@ 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))); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/pom.xml b/dhis-2/dhis-services/dhis-service-tracker/pom.xml index e4067be79ef4..80f3cf952c24 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/pom.xml +++ b/dhis-2/dhis-services/dhis-service-tracker/pom.xml @@ -167,10 +167,6 @@ org.geotools gt-main - - org.springframework.security - spring-security-core - diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java index 068d619ce5d0..12f50fbf7687 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java @@ -32,6 +32,7 @@ import java.util.Optional; import java.util.function.LongSupplier; import java.util.function.Supplier; +import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; import org.hibernate.Hibernate; import org.hisp.dhis.cache.Cache; @@ -53,7 +54,6 @@ import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserService; -import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -149,71 +149,54 @@ public void transferOwnership( @Override @Transactional - public void assignOwnership( - TrackedEntity trackedEntity, - Program program, - OrganisationUnit organisationUnit, - boolean skipAccessValidation, - boolean overwriteIfExists) { - if (trackedEntity == null || program == null || organisationUnit == null) { - return; + public void grantTemporaryOwnership( + @Nonnull TrackedEntity trackedEntity, Program program, UserDetails user, String reason) + throws ForbiddenException { + validateGrantTemporaryOwnershipInputs(trackedEntity, program, user); + + if (config.isEnabled(CHANGELOG_TRACKER)) { + programTempOwnershipAuditService.addProgramTempOwnershipAudit( + new ProgramTempOwnershipAudit(program, trackedEntity, reason, user.getUsername())); } - UserDetails currentUser = CurrentUserUtil.getCurrentUserDetails(); + ProgramTempOwner programTempOwner = + new ProgramTempOwner( + program, + trackedEntity, + reason, + userService.getUser(user.getUid()), + TEMPORARY_OWNERSHIP_VALIDITY_IN_HOURS); + programTempOwnerService.addProgramTempOwner(programTempOwner); + tempOwnerCache.invalidate( + getTempOwnershipCacheKey(trackedEntity.getUid(), program.getUid(), user.getUid())); + } - if (hasAccess(currentUser, trackedEntity, program) || skipAccessValidation) { - TrackedEntityProgramOwner teProgramOwner = - trackedEntityProgramOwnerService.getTrackedEntityProgramOwner(trackedEntity, program); + private void validateGrantTemporaryOwnershipInputs( + TrackedEntity trackedEntity, Program program, UserDetails user) throws ForbiddenException { + if (program == null) { + throw new ForbiddenException( + "Temporary ownership not created. Program supplied does not exist."); + } - if (teProgramOwner != null) { - if (overwriteIfExists && !teProgramOwner.getOrganisationUnit().equals(organisationUnit)) { - ProgramOwnershipHistory programOwnershipHistory = - new ProgramOwnershipHistory( - program, - trackedEntity, - teProgramOwner.getOrganisationUnit(), - teProgramOwner.getLastUpdated(), - teProgramOwner.getCreatedBy()); - programOwnershipHistoryService.addProgramOwnershipHistory(programOwnershipHistory); - trackedEntityProgramOwnerService.updateTrackedEntityProgramOwner( - trackedEntity, program, organisationUnit); - } - } else { - trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( - trackedEntity, program, organisationUnit); - } + if (user.isSuper()) { + throw new ForbiddenException("Temporary ownership not created. Current user is a superuser."); + } - ownerCache.invalidate(getOwnershipCacheKey(trackedEntity::getId, program)); - } else { - log.error("Unauthorized attempt to assign ownership"); - throw new AccessDeniedException( - "User does not have access to assign ownership for the entity-program combination"); + if (ProgramType.WITHOUT_REGISTRATION == program.getProgramType()) { + throw new ForbiddenException( + "Temporary ownership not created. Program supplied is not a tracker program."); } - } - @Override - @Transactional - public void grantTemporaryOwnership( - TrackedEntity trackedEntity, Program program, UserDetails user, String reason) { - if (canSkipOwnershipCheck(user, program) || trackedEntity == null) { - return; + if (!program.isProtected()) { + throw new ForbiddenException( + String.format( + "Temporary ownership can only be granted to protected programs. %s access level is %s.", + program.getUid(), program.getAccessLevel().name())); } - if (program.isProtected()) { - if (config.isEnabled(CHANGELOG_TRACKER)) { - programTempOwnershipAuditService.addProgramTempOwnershipAudit( - new ProgramTempOwnershipAudit(program, trackedEntity, reason, user.getUsername())); - } - ProgramTempOwner programTempOwner = - new ProgramTempOwner( - program, - trackedEntity, - reason, - userService.getUser(user.getUid()), - TEMPORARY_OWNERSHIP_VALIDITY_IN_HOURS); - programTempOwnerService.addProgramTempOwner(programTempOwner); - tempOwnerCache.invalidate( - getTempOwnershipCacheKey(trackedEntity.getUid(), program.getUid(), user.getUid())); + if (!isOwnerInUserSearchScope(user, trackedEntity, program)) { + throw new ForbiddenException( + "The owner of the entity-program combination is not in the user's search scope."); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/TrackerOwnershipManager.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/TrackerOwnershipManager.java index bd1a930bbfb7..ac0a7ac65c16 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/TrackerOwnershipManager.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/TrackerOwnershipManager.java @@ -52,18 +52,6 @@ public interface TrackerOwnershipManager { void transferOwnership(TrackedEntity trackedEntity, Program program, OrganisationUnit orgUnit) throws ForbiddenException; - /** - * @param trackedEntity The tracked entity object - * @param program The program object - * @param organisationUnit The org unit that has to become the owner - */ - void assignOwnership( - TrackedEntity trackedEntity, - Program program, - OrganisationUnit organisationUnit, - boolean skipAccessValidation, - boolean overwriteIfExists); - /** * Check whether the user has access (as owner or has temporarily broken the glass) to the tracked * entity - program combination. @@ -87,7 +75,8 @@ boolean hasAccess( * @param reason The reason for requesting temporary ownership */ void grantTemporaryOwnership( - TrackedEntity trackedEntity, Program program, UserDetails user, String reason); + TrackedEntity trackedEntity, Program program, UserDetails user, String reason) + throws ForbiddenException; /** * Ownership check can be skipped if the user is superuser or if the program type is without diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/DefaultTrackedEntityAuditService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/DefaultTrackedEntityAuditService.java index ac78c77f92c5..90f363a9acbd 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/DefaultTrackedEntityAuditService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/DefaultTrackedEntityAuditService.java @@ -28,6 +28,7 @@ package org.hisp.dhis.tracker.audit; import java.util.List; +import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import org.hisp.dhis.audit.AuditOperationType; import org.hisp.dhis.common.IdentifiableObjectManager; @@ -57,13 +58,13 @@ public class DefaultTrackedEntityAuditService implements TrackedEntityAuditServi @Async @Transactional public void addTrackedEntityAudit( - TrackedEntity trackedEntity, String username, AuditOperationType auditOperationType) { + AuditOperationType type, String username, TrackedEntity trackedEntity) { if (username != null && trackedEntity != null && trackedEntity.getTrackedEntityType() != null && trackedEntity.getTrackedEntityType().isAllowAuditLog()) { TrackedEntityAudit trackedEntityAudit = - new TrackedEntityAudit(trackedEntity.getUid(), username, auditOperationType); + new TrackedEntityAudit(trackedEntity.getUid(), username, type); trackedEntityAuditStore.addTrackedEntityAudit(trackedEntityAudit); } } @@ -71,8 +72,19 @@ public void addTrackedEntityAudit( @Override @Async @Transactional - public void addTrackedEntityAudit(List trackedEntityAudits) { - trackedEntityAuditStore.addTrackedEntityAudit(trackedEntityAudits); + public void addTrackedEntityAudit( + AuditOperationType type, String username, @Nonnull List trackedEntities) { + List audits = + trackedEntities.stream() + .filter(te -> te.getTrackedEntityType().isAllowAuditLog()) + .map(te -> new TrackedEntityAudit(te.getUid(), username, type)) + .toList(); + + if (audits.isEmpty()) { + return; + } + + trackedEntityAuditStore.addTrackedEntityAudit(audits); } @Override diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/TrackedEntityAuditService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/TrackedEntityAuditService.java index b8ae73a1983e..72aa24a0f8d8 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/TrackedEntityAuditService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/audit/TrackedEntityAuditService.java @@ -28,6 +28,7 @@ package org.hisp.dhis.tracker.audit; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.audit.AuditOperationType; import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.trackedentity.TrackedEntityAudit; @@ -40,11 +41,11 @@ public interface TrackedEntityAuditService { String ID = TrackedEntityAuditService.class.getName(); - void addTrackedEntityAudit( - TrackedEntity trackedEntity, String username, AuditOperationType auditOperationType); + void addTrackedEntityAudit(AuditOperationType type, String username, TrackedEntity trackedEntity); /** Adds multiple tracked entity audit */ - void addTrackedEntityAudit(List trackedEntityAudits); + void addTrackedEntityAudit( + AuditOperationType type, String username, @Nonnull List trackedEntity); /** * Returns tracked entity audits matching query params diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java index 03ebb2305160..a4b20a86b208 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java @@ -186,7 +186,7 @@ public TrackedEntity validateTrackedEntity(UID uid, UserDetails user) if (trackedEntity == null) { throw new BadRequestException("Tracked entity is specified but does not exist: " + uid); } - trackedEntityAuditService.addTrackedEntityAudit(trackedEntity, user.getUsername(), READ); + trackedEntityAuditService.addTrackedEntityAudit(READ, user.getUsername(), trackedEntity); if (trackedEntity.getTrackedEntityType() != null && !aclService.canDataRead(user, trackedEntity.getTrackedEntityType())) { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java index c0e191468601..3fa151ed0979 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java @@ -33,8 +33,6 @@ import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; @@ -60,7 +58,6 @@ import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; -import org.hisp.dhis.trackedentity.TrackedEntityAudit; import org.hisp.dhis.trackedentity.TrackedEntityProgramOwner; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; @@ -76,7 +73,6 @@ import org.hisp.dhis.tracker.export.event.EventParams; import org.hisp.dhis.tracker.export.event.EventService; import org.hisp.dhis.tracker.export.trackedentity.aggregates.TrackedEntityAggregate; -import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -236,9 +232,7 @@ public TrackedEntity getTrackedEntity( } } - UserDetails user = getCurrentUserDetails(); - TrackedEntity trackedEntity = getTrackedEntity(trackedEntityUid, program, params, user, false); - return trackedEntity; + return getTrackedEntity(trackedEntityUid, program, params, getCurrentUserDetails()); } /** @@ -249,18 +243,14 @@ public TrackedEntity getTrackedEntity( * @throws ForbiddenException if TE owner is not in user's scope or not enough sharing access */ private TrackedEntity getTrackedEntity( - UID uid, - Program program, - TrackedEntityParams params, - UserDetails user, - boolean includeDeleted) + UID uid, Program program, TrackedEntityParams params, UserDetails user) throws NotFoundException, ForbiddenException { TrackedEntity trackedEntity = trackedEntityStore.getByUid(uid.getValue()); if (trackedEntity == null) { throw new NotFoundException(TrackedEntity.class, uid); } - trackedEntityAuditService.addTrackedEntityAudit(trackedEntity, user.getUsername(), READ); + trackedEntityAuditService.addTrackedEntityAudit(READ, user.getUsername(), trackedEntity); if (program != null) { List errors = @@ -280,32 +270,16 @@ private TrackedEntity getTrackedEntity( } } - TrackedEntity result = new TrackedEntity(); - result.setId(trackedEntity.getId()); - result.setUid(trackedEntity.getUid()); - result.setOrganisationUnit(trackedEntity.getOrganisationUnit()); - result.setTrackedEntityType(trackedEntity.getTrackedEntityType()); - result.setCreated(trackedEntity.getCreated()); - result.setCreatedAtClient(trackedEntity.getCreatedAtClient()); - result.setLastUpdated(trackedEntity.getLastUpdated()); - result.setLastUpdatedAtClient(trackedEntity.getLastUpdatedAtClient()); - result.setInactive(trackedEntity.isInactive()); - result.setGeometry(trackedEntity.getGeometry()); - result.setDeleted(trackedEntity.isDeleted()); - result.setPotentialDuplicate(trackedEntity.isPotentialDuplicate()); - result.setStoredBy(trackedEntity.getStoredBy()); - result.setCreatedByUserInfo(trackedEntity.getCreatedByUserInfo()); - result.setLastUpdatedByUserInfo(trackedEntity.getLastUpdatedByUserInfo()); - result.setGeometry(trackedEntity.getGeometry()); if (params.isIncludeEnrollments()) { - result.setEnrollments(getEnrollments(trackedEntity, user, includeDeleted, program)); + trackedEntity.setEnrollments(getEnrollments(trackedEntity, user, false, program)); } - setRelationshipItems(result, trackedEntity, params, includeDeleted); + setRelationshipItems(trackedEntity, trackedEntity, params, false); if (params.isIncludeProgramOwners()) { - result.setProgramOwners(getTrackedEntityProgramOwners(trackedEntity, program)); + trackedEntity.setProgramOwners(getTrackedEntityProgramOwners(trackedEntity, program)); } - result.setTrackedEntityAttributeValues(getTrackedEntityAttributeValues(trackedEntity, program)); - return result; + trackedEntity.setTrackedEntityAttributeValues( + getTrackedEntityAttributeValues(trackedEntity, program)); + return trackedEntity; } private Set getEnrollments( @@ -375,24 +349,7 @@ public List getTrackedEntities( TrackedEntityQueryParams queryParams = mapper.map(operationParams, user); final List ids = trackedEntityStore.getTrackedEntityIds(queryParams); - List trackedEntities = - this.trackedEntityAggregate.find( - ids, - operationParams.getTrackedEntityParams(), - queryParams, - operationParams.getOrgUnitMode()); - setRelationshipItems( - trackedEntities, - operationParams.getTrackedEntityParams(), - operationParams.isIncludeDeleted()); - for (TrackedEntity trackedEntity : trackedEntities) { - trackedEntity.setTrackedEntityAttributeValues( - getTrackedEntityAttributeValues(trackedEntity, queryParams.getProgram())); - } - - addSearchAudit(trackedEntities); - - return trackedEntities; + return getTrackedEntities(ids, operationParams, queryParams, user); } @Override @@ -403,24 +360,38 @@ public List getTrackedEntities( TrackedEntityQueryParams queryParams = mapper.map(operationParams, user); final Page ids = trackedEntityStore.getTrackedEntityIds(queryParams, pageParams); + List trackedEntities = + getTrackedEntities(ids.getItems(), operationParams, queryParams, user); + return ids.withItems(trackedEntities); + } + + private List getTrackedEntities( + List ids, + TrackedEntityOperationParams operationParams, + TrackedEntityQueryParams queryParams, + UserDetails user) + throws NotFoundException { + List trackedEntities = this.trackedEntityAggregate.find( - ids.getItems(), + ids, operationParams.getTrackedEntityParams(), queryParams, - operationParams.getOrgUnitMode()); - + queryParams.getOrgUnitMode()); setRelationshipItems( trackedEntities, operationParams.getTrackedEntityParams(), operationParams.isIncludeDeleted()); for (TrackedEntity trackedEntity : trackedEntities) { - getTrackedEntityAttributeValues(trackedEntity, queryParams.getProgram()); + if (operationParams.getTrackedEntityParams().isIncludeProgramOwners()) { + trackedEntity.setProgramOwners( + getTrackedEntityProgramOwners(trackedEntity, queryParams.getProgram())); + } + trackedEntity.setTrackedEntityAttributeValues( + getTrackedEntityAttributeValues(trackedEntity, queryParams.getProgram())); } - - addSearchAudit(trackedEntities); - - return ids.withItems(trackedEntities); + trackedEntityAuditService.addTrackedEntityAudit(SEARCH, user.getUsername(), trackedEntities); + return trackedEntities; } /** @@ -594,40 +565,16 @@ private RelationshipItem getTrackedEntityInRelationshipItem(String uid) throws N } UserDetails user = getCurrentUserDetails(); - trackedEntityAuditService.addTrackedEntityAudit(trackedEntity, user.getUsername(), READ); - if (!trackerAccessManager.canRead(user, trackedEntity).isEmpty()) { return null; } + trackedEntityAuditService.addTrackedEntityAudit(SEARCH, user.getUsername(), trackedEntity); + relationshipItem.setTrackedEntity(trackedEntity); return relationshipItem; } - private void addSearchAudit(List trackedEntities) { - if (trackedEntities.isEmpty()) { - return; - } - Map tetMap = - trackedEntityTypeService.getAllTrackedEntityType().stream() - .collect(Collectors.toMap(TrackedEntityType::getUid, t -> t)); - - List auditable = - trackedEntities.stream() - .filter(Objects::nonNull) - .filter(te -> te.getTrackedEntityType() != null) - .filter(te -> tetMap.get(te.getTrackedEntityType().getUid()).isAllowAuditLog()) - .map( - te -> - new TrackedEntityAudit( - te.getUid(), CurrentUserUtil.getCurrentUsername(), SEARCH)) - .toList(); - - if (!auditable.isEmpty()) { - trackedEntityAuditService.addTrackedEntityAudit(auditable); - } - } - @Override public Set getOrderableFields() { return trackedEntityStore.getOrderableFields(); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityOperationParams.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityOperationParams.java index 5d4a34da6513..3c5d23e77c57 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityOperationParams.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityOperationParams.java @@ -105,7 +105,8 @@ public class TrackedEntityOperationParams { /** Tracked entity type to fetch. */ private UID trackedEntityType; - private OrganisationUnitSelectionMode orgUnitMode; + @Builder.Default + private OrganisationUnitSelectionMode orgUnitMode = OrganisationUnitSelectionMode.ACCESSIBLE; @Getter @Builder.Default private AssignedUserQueryParam assignedUserQueryParam = AssignedUserQueryParam.ALL; diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AclStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AclStore.java index 8c37e05eed18..9dbd006ea521 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AclStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AclStore.java @@ -28,16 +28,101 @@ package org.hisp.dhis.tracker.export.trackedentity.aggregates; import java.util.List; +import org.hisp.dhis.common.collection.CollectionUtils; +import org.hisp.dhis.hibernate.jsonb.type.JsonbFunctions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; /** * @author Luciano Fiandesio */ -public interface AclStore { - List getAccessibleTrackedEntityTypes(String userUID, List userGroupUIDs); +@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.AclStore") +class AclStore { + private final NamedParameterJdbcTemplate jdbcTemplate; - List getAccessiblePrograms(String userUID, List userGroupUIDs); + private static final String USER_SQL_PARAM_NAME = "userId"; - List getAccessibleProgramStages(String userUID, List userGroupUIDs); + private static final String USER_GROUP_SQL_PARAM_NAME = "userGroupUIDs"; - List getAccessibleRelationshipTypes(String userUID, List userGroupUIDs); + private static final String PUBLIC_ACCESS_CONDITION = + "sharing->>'public' LIKE '__r%' OR sharing->>'public' IS NULL"; + + private static final String USERACCESS_CONDITION = + "sharing->'users'->:" + USER_SQL_PARAM_NAME + "->>'access' LIKE '__r%'"; + + private static final String USERGROUPACCESS_CONDITION = + JsonbFunctions.HAS_USER_GROUP_IDS + + "( sharing, :" + + USER_GROUP_SQL_PARAM_NAME + + ") = true " + + "and " + + JsonbFunctions.CHECK_USER_GROUPS_ACCESS + + "(sharing, '__r%', :" + + USER_GROUP_SQL_PARAM_NAME + + ") = true"; + + private static final String GET_TE_TYPE_ACL = + "SELECT trackedentitytypeid FROM trackedentitytype " + + " WHERE " + + PUBLIC_ACCESS_CONDITION + + " OR " + + USERACCESS_CONDITION; + + static final String GET_PROGRAM_ACL = + "SELECT p.programid FROM program p " + + " WHERE " + + PUBLIC_ACCESS_CONDITION + + " OR " + + USERACCESS_CONDITION; + + static final String GET_PROGRAMSTAGE_ACL = + "SELECT ps.programstageid FROM programstage ps " + + " WHERE " + + PUBLIC_ACCESS_CONDITION + + " OR " + + USERACCESS_CONDITION; + + private static final String GET_RELATIONSHIPTYPE_ACL = + "SELECT rs.relationshiptypeid " + + "FROM relationshiptype rs" + + " WHERE " + + PUBLIC_ACCESS_CONDITION + + " OR " + + USERACCESS_CONDITION; + + AclStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + + List getAccessibleTrackedEntityTypes(String userUID, List userGroupUIDs) { + return executeAclQuery(userUID, userGroupUIDs, GET_TE_TYPE_ACL, "trackedentitytypeid"); + } + + List getAccessiblePrograms(String userUID, List userGroupUIDs) { + return executeAclQuery(userUID, userGroupUIDs, GET_PROGRAM_ACL, "programid"); + } + + List getAccessibleProgramStages(String userUID, List userGroupUIDs) { + return executeAclQuery(userUID, userGroupUIDs, GET_PROGRAMSTAGE_ACL, "programstageid"); + } + + List getAccessibleRelationshipTypes(String userUID, List userGroupUIDs) { + return executeAclQuery(userUID, userGroupUIDs, GET_RELATIONSHIPTYPE_ACL, "relationshiptypeid"); + } + + private List executeAclQuery( + String userUID, List userGroupUIDs, String sql, String primaryKey) { + MapSqlParameterSource parameterMap = new MapSqlParameterSource(); + parameterMap.addValue(USER_SQL_PARAM_NAME, userUID); + + if (!CollectionUtils.isEmpty(userGroupUIDs)) { + sql += " OR " + USERGROUPACCESS_CONDITION; + parameterMap.addValue(USER_GROUP_SQL_PARAM_NAME, "{" + String.join(",", userGroupUIDs) + "}"); + } + + return jdbcTemplate.query(sql, parameterMap, (rs, i) -> rs.getLong(primaryKey)); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/Aggregate.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AsyncUtils.java similarity index 92% rename from dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/Aggregate.java rename to dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AsyncUtils.java index e8c819d8bfe7..fb3eefbdb919 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/Aggregate.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/AsyncUtils.java @@ -38,7 +38,11 @@ /** * @author Luciano Fiandesio */ -interface Aggregate { +class AsyncUtils { + AsyncUtils() { + throw new IllegalStateException("Utility class"); + } + /** * Executes the Supplier asynchronously using the thread pool from the provided {@see Executor} * @@ -48,7 +52,7 @@ interface Aggregate { * @param executor an Executor instance * @return A CompletableFuture with the result of the Supplier */ - default CompletableFuture> conditionalAsyncFetch( + static CompletableFuture> conditionalAsyncFetch( boolean condition, Supplier> supplier, Executor executor) { return (condition ? supplyAsync(supplier, executor) @@ -61,7 +65,7 @@ default CompletableFuture> conditionalAsyncFetch( * @param supplier The Supplier to execute * @return A CompletableFuture with the result of the Supplier */ - default CompletableFuture> asyncFetch( + static CompletableFuture> asyncFetch( Supplier> supplier, Executor executor) { return supplyAsync(supplier, executor); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultAclStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultAclStore.java deleted file mode 100644 index 51dda051a6b3..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultAclStore.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.tracker.export.trackedentity.aggregates; - -import java.util.List; -import org.hisp.dhis.common.collection.CollectionUtils; -import org.hisp.dhis.hibernate.jsonb.type.JsonbFunctions; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -/** - * @author Luciano Fiandesio - */ -@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.AclStore") -public class DefaultAclStore implements AclStore { - private final NamedParameterJdbcTemplate jdbcTemplate; - - private static final String USER_SQL_PARAM_NAME = "userId"; - - private static final String USER_GROUP_SQL_PARAM_NAME = "userGroupUIDs"; - - private static final String PUBLIC_ACCESS_CONDITION = - "sharing->>'public' LIKE '__r%' OR sharing->>'public' IS NULL"; - - private static final String USERACCESS_CONDITION = - "sharing->'users'->:" + USER_SQL_PARAM_NAME + "->>'access' LIKE '__r%'"; - - private static final String USERGROUPACCESS_CONDITION = - JsonbFunctions.HAS_USER_GROUP_IDS - + "( sharing, :" - + USER_GROUP_SQL_PARAM_NAME - + ") = true " - + "and " - + JsonbFunctions.CHECK_USER_GROUPS_ACCESS - + "(sharing, '__r%', :" - + USER_GROUP_SQL_PARAM_NAME - + ") = true"; - - private static final String GET_TE_TYPE_ACL = - "SELECT trackedentitytypeid FROM trackedentitytype " - + " WHERE " - + PUBLIC_ACCESS_CONDITION - + " OR " - + USERACCESS_CONDITION; - - static final String GET_PROGRAM_ACL = - "SELECT p.programid FROM program p " - + " WHERE " - + PUBLIC_ACCESS_CONDITION - + " OR " - + USERACCESS_CONDITION; - - static final String GET_PROGRAMSTAGE_ACL = - "SELECT ps.programstageid FROM programstage ps " - + " WHERE " - + PUBLIC_ACCESS_CONDITION - + " OR " - + USERACCESS_CONDITION; - - private static final String GET_RELATIONSHIPTYPE_ACL = - "SELECT rs.relationshiptypeid " - + "FROM relationshiptype rs" - + " WHERE " - + PUBLIC_ACCESS_CONDITION - + " OR " - + USERACCESS_CONDITION; - - public DefaultAclStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); - } - - @Override - public List getAccessibleTrackedEntityTypes(String userUID, List userGroupUIDs) { - return executeAclQuery(userUID, userGroupUIDs, GET_TE_TYPE_ACL, "trackedentitytypeid"); - } - - @Override - public List getAccessiblePrograms(String userUID, List userGroupUIDs) { - return executeAclQuery(userUID, userGroupUIDs, GET_PROGRAM_ACL, "programid"); - } - - @Override - public List getAccessibleProgramStages(String userUID, List userGroupUIDs) { - return executeAclQuery(userUID, userGroupUIDs, GET_PROGRAMSTAGE_ACL, "programstageid"); - } - - @Override - public List getAccessibleRelationshipTypes(String userUID, List userGroupUIDs) { - return executeAclQuery(userUID, userGroupUIDs, GET_RELATIONSHIPTYPE_ACL, "relationshiptypeid"); - } - - private List executeAclQuery( - String userUID, List userGroupUIDs, String sql, String primaryKey) { - MapSqlParameterSource parameterMap = new MapSqlParameterSource(); - parameterMap.addValue(USER_SQL_PARAM_NAME, userUID); - - if (!CollectionUtils.isEmpty(userGroupUIDs)) { - sql += " OR " + USERGROUPACCESS_CONDITION; - parameterMap.addValue(USER_GROUP_SQL_PARAM_NAME, "{" + String.join(",", userGroupUIDs) + "}"); - } - - return jdbcTemplate.query(sql, parameterMap, (rs, i) -> rs.getLong(primaryKey)); - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEnrollmentStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEnrollmentStore.java deleted file mode 100644 index dd06a176a9a9..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEnrollmentStore.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.tracker.export.trackedentity.aggregates; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import java.util.List; -import org.hisp.dhis.note.Note; -import org.hisp.dhis.program.Enrollment; -import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EnrollmentRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.NoteRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.ProgramAttributeRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.EnrollmentQuery; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.ProgramAttributeQuery; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -/** - * @author Luciano Fiandesio - */ -@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.EnrollmentStore") -public class DefaultEnrollmentStore extends AbstractStore implements EnrollmentStore { - private static final String GET_ENROLLMENT_SQL_BY_TE = EnrollmentQuery.getQuery(); - - private static final String GET_ATTRIBUTES = ProgramAttributeQuery.getQuery(); - - private static final String GET_NOTES_SQL = - "select en.uid as key, n.uid, n.notetext, " - + "n.creator, n.created " - + "from note n join enrollment_notes enn " - + "on n.noteid = enn.noteid " - + "join enrollment en on enn.enrollmentid = en.enrollmentid " - + "where enn.enrollmentid in (:ids)"; - - private static final String FILTER_OUT_DELETED_ENROLLMENTS = "en.deleted=false"; - - public DefaultEnrollmentStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - @Override - public Multimap getEnrollmentsByTrackedEntityIds( - List ids, Context ctx) { - List> teIds = Lists.partition(ids, PARITITION_SIZE); - - Multimap enrollmentMultimap = ArrayListMultimap.create(); - - teIds.forEach( - partition -> - enrollmentMultimap.putAll(getEnrollmentsByTrackedEntityIdsPartitioned(partition, ctx))); - - return enrollmentMultimap; - } - - private Multimap getEnrollmentsByTrackedEntityIdsPartitioned( - List ids, Context ctx) { - EnrollmentRowCallbackHandler handler = new EnrollmentRowCallbackHandler(); - - jdbcTemplate.query( - getQuery( - GET_ENROLLMENT_SQL_BY_TE, - ctx, - " en.programid IN (:programIds)", - FILTER_OUT_DELETED_ENROLLMENTS), - createIdsParam(ids).addValue("programIds", ctx.getPrograms()), - handler); - - return handler.getItems(); - } - - @Override - public Multimap getNotes(List ids) { - return fetch(GET_NOTES_SQL, new NoteRowCallbackHandler(), ids); - } - - @Override - public Multimap getAttributes(List ids, Context ctx) { - ProgramAttributeRowCallbackHandler handler = new ProgramAttributeRowCallbackHandler(); - - jdbcTemplate.query( - getQuery( - GET_ATTRIBUTES, ctx, " pa.programid IN (:programIds)", FILTER_OUT_DELETED_ENROLLMENTS), - createIdsParam(ids).addValue("programIds", ctx.getPrograms()), - handler); - - return handler.getItems(); - } - - @Override - String getRelationshipEntityColumn() { - return "enrollmentid"; - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEventStore.java deleted file mode 100644 index 9c3b05d2ddfe..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultEventStore.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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.tracker.export.trackedentity.aggregates; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.hisp.dhis.eventdatavalue.EventDataValue; -import org.hisp.dhis.note.Note; -import org.hisp.dhis.program.Event; -import org.hisp.dhis.query.JpaQueryUtils; -import org.hisp.dhis.security.acl.AclService; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EventDataValueRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EventRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.NoteRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.EventQuery; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -/** - * @author Luciano Fiandesio - */ -@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.EventStore") -public class DefaultEventStore extends AbstractStore implements EventStore { - private static final String GET_EVENTS_SQL = EventQuery.getQuery(); - - private static final String GET_DATAVALUES_SQL = - "select ev.uid as key, " - + "ev.eventdatavalues " - + "from event ev " - + "where ev.eventid in (:ids)"; - - private static final String GET_NOTES_SQL = - "select ev.uid as key, n.uid, n.notetext, " - + "n.creator, n.created " - + "from note n " - + "join event_notes evn " - + "on n.noteid = evn.noteid " - + "join event ev on evn.eventid = ev.eventid " - + "where evn.eventid in (:ids)"; - - private static final String ACL_FILTER_SQL = - "CASE WHEN p.type = 'WITH_REGISTRATION' THEN " - + "p.trackedentitytypeid in (:trackedEntityTypeIds) else true END " - + "AND ev.programstageid in (:programStageIds) AND en.programid IN (:programIds)"; - - private static final String ACL_FILTER_SQL_NO_PROGRAM_STAGE = - "CASE WHEN p.type = 'WITH_REGISTRATION' THEN " - + "p.trackedentitytypeid in (:trackedEntityTypeIds) else true END " - + "AND en.programid IN (:programIds)"; - - private static final String FILTER_OUT_DELETED_EVENTS = "ev.deleted=false"; - - public DefaultEventStore(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - @Override - String getRelationshipEntityColumn() { - return "eventid"; - } - - @Override - public Multimap getEventsByEnrollmentIds(List enrollmentsId, Context ctx) { - List> enrollmentIdsPartitions = Lists.partition(enrollmentsId, PARITITION_SIZE); - - Multimap eventMultimap = ArrayListMultimap.create(); - - enrollmentIdsPartitions.forEach( - partition -> eventMultimap.putAll(getEventsByEnrollmentIdsPartitioned(partition, ctx))); - - return eventMultimap; - } - - private String getAttributeOptionComboClause(Context ctx) { - return " and ev.attributeoptioncomboid not in (" - + "select distinct(cocco.categoryoptioncomboid) " - + "from categoryoptioncombos_categoryoptions as cocco " - + - // Get inaccessible category options - "where cocco.categoryoptionid not in ( " - + "select co.categoryoptionid " - + "from categoryoption co " - + " where " - + JpaQueryUtils.generateSQlQueryForSharingCheck( - "co.sharing", ctx.getUserUid(), ctx.getUserGroups(), AclService.LIKE_READ_DATA) - + ") )"; - } - - private Multimap getEventsByEnrollmentIdsPartitioned( - List enrollmentsId, Context ctx) { - EventRowCallbackHandler handler = new EventRowCallbackHandler(); - - List programStages = ctx.getProgramStages(); - - String aocSql = ctx.isSuperUser() ? "" : getAttributeOptionComboClause(ctx); - - if (programStages.isEmpty()) { - jdbcTemplate.query( - getQuery( - GET_EVENTS_SQL, - ctx, - ACL_FILTER_SQL_NO_PROGRAM_STAGE + aocSql, - FILTER_OUT_DELETED_EVENTS), - createIdsParam(enrollmentsId) - .addValue("trackedEntityTypeIds", ctx.getTrackedEntityTypes()) - .addValue("programIds", ctx.getPrograms()), - handler); - } else { - jdbcTemplate.query( - getQuery(GET_EVENTS_SQL, ctx, ACL_FILTER_SQL + aocSql, FILTER_OUT_DELETED_EVENTS), - createIdsParam(enrollmentsId) - .addValue("trackedEntityTypeIds", ctx.getTrackedEntityTypes()) - .addValue("programStageIds", programStages) - .addValue("programIds", ctx.getPrograms()), - handler); - } - - return handler.getItems(); - } - - @Override - public Map> getDataValues(List eventIds) { - Map> dataValueListMultimap = new HashMap<>(); - - Lists.partition(eventIds, PARITITION_SIZE) - .forEach(partition -> dataValueListMultimap.putAll(getDataValuesPartitioned(partition))); - - return dataValueListMultimap; - } - - private Map> getDataValuesPartitioned(List eventIds) { - EventDataValueRowCallbackHandler handler = new EventDataValueRowCallbackHandler(); - - jdbcTemplate.query(GET_DATAVALUES_SQL, createIdsParam(eventIds), handler); - - return handler.getItems(); - } - - @Override - public Multimap getNotes(List eventIds) { - return fetch(GET_NOTES_SQL, new NoteRowCallbackHandler(), eventIds); - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultTrackedEntityStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultTrackedEntityStore.java deleted file mode 100644 index 8a51666f0c9f..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/DefaultTrackedEntityStore.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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.tracker.export.trackedentity.aggregates; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.hisp.dhis.trackedentity.TrackedEntity; -import org.hisp.dhis.trackedentity.TrackedEntityProgramOwner; -import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.OwnedTeMapper; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.ProgramOwnerRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.TrackedEntityAttributeRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.TrackedEntityRowCallbackHandler; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.TeAttributeQuery; -import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.TrackedEntityQuery; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.stereotype.Repository; - -/** - * @author Luciano Fiandesio - * @author Ameen Mohamed - */ -@Repository -public class DefaultTrackedEntityStore extends AbstractStore implements TrackedEntityStore { - private static final String GET_TE_SQL = TrackedEntityQuery.getQuery(); - - private static final String GET_TE_ATTRIBUTES = TeAttributeQuery.getQuery(); - - private static final String GET_PROGRAM_OWNERS = - "select te.uid as key, p.uid as prguid, o.uid as ouuid " - + "from trackedentityprogramowner teop " - + "join program p on teop.programid = p.programid " - + "join organisationunit o on teop.organisationunitid = o.organisationunitid " - + "join trackedentity te on teop.trackedentityid = te.trackedentityid " - + "where teop.trackedentityid in (:ids)"; - - private static final String FILTER_OUT_DELETED_TE = "te.deleted=false"; - - private String getTrackedEntitiesOwnershipSqlForAllPrograms(boolean skipUserScopeValidation) { - String sql = - "SELECT te.uid as te_uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, p.accesslevel,p.uid as pgm_uid " - + "FROM trackedentityprogramowner TPO " - + "LEFT JOIN program P on P.programid = TPO.programid " - + "LEFT JOIN organisationunit OU on OU.organisationunitid = TPO.organisationunitid " - + "LEFT JOIN trackedentity TE on TE.trackedentityid = tpo.trackedentityid " - + "WHERE TPO.trackedentityid in (:ids) " - + "AND p.programid in (SELECT programid FROM program) "; - - if (!skipUserScopeValidation) - sql += - "GROUP BY te.uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, ou.path, p.accesslevel,p.uid " - + "HAVING (P.accesslevel in ('OPEN', 'AUDITED') AND (EXISTS(SELECT SS.organisationunitid FROM userteisearchorgunits SS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = SS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')) OR EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')))) " - + "OR (P.accesslevel in ('CLOSED', 'PROTECTED') AND EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')));"; - - return sql; - } - - private String getTrackedEntitiesOwnershipSqlForSpecificProgram(boolean skipUserScopeValidation) { - String sql = - "SELECT te.uid as te_uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, p.accesslevel,p.uid as pgm_uid " - + "FROM trackedentityprogramowner TPO " - + "LEFT JOIN program P on P.programid = TPO.programid " - + "LEFT JOIN organisationunit OU on OU.organisationunitid = TPO.organisationunitid " - + "LEFT JOIN trackedentity TE on TE.trackedentityid = tpo.trackedentityid " - + "WHERE TPO.trackedentityid in (:ids) " - + "AND p.uid = :programUid "; - - if (!skipUserScopeValidation) { - sql += - "GROUP BY te.uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, ou.path, p.accesslevel,p.uid " - + "HAVING (P.accesslevel in ('OPEN', 'AUDITED') AND (EXISTS(SELECT SS.organisationunitid FROM userteisearchorgunits SS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = SS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')) OR EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')))) " - + "OR (P.accesslevel in ('CLOSED', 'PROTECTED') AND EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')));"; - } - - return sql; - } - - public DefaultTrackedEntityStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - @Override - String getRelationshipEntityColumn() { - return "trackedentityid"; - } - - @Override - public Map getTrackedEntities(List ids, Context ctx) { - List> idPartitions = Lists.partition(ids, PARITITION_SIZE); - - Map trackedEntityMap = new LinkedHashMap<>(); - - idPartitions.forEach( - partition -> trackedEntityMap.putAll(getTrackedEntitiesPartitioned(partition, ctx))); - return trackedEntityMap; - } - - private Map getTrackedEntitiesPartitioned(List ids, Context ctx) { - TrackedEntityRowCallbackHandler handler = new TrackedEntityRowCallbackHandler(); - - if (!ctx.isSuperUser() && ctx.getTrackedEntityTypes().isEmpty()) { - // If not super user and no tets are accessible. then simply return - // empty list. - return new HashMap<>(); - } - - String sql = - getQuery(GET_TE_SQL, ctx, "te.trackedentitytypeid in (:teTypeIds)", FILTER_OUT_DELETED_TE); - jdbcTemplate.query( - applySortOrder(sql, StringUtils.join(ids, ",")), - createIdsParam(ids).addValue("teTypeIds", ctx.getTrackedEntityTypes()), - handler); - - return handler.getItems(); - } - - @Override - public Multimap getAttributes(List ids) { - return fetch(GET_TE_ATTRIBUTES, new TrackedEntityAttributeRowCallbackHandler(), ids); - } - - @Override - public Multimap getProgramOwners(List ids) { - return fetch(GET_PROGRAM_OWNERS, new ProgramOwnerRowCallbackHandler(), ids); - } - - @Override - public Multimap getOwnedTrackedEntities( - List ids, Context ctx, boolean skipUserScopeValidation) { - List> teds = Lists.partition(ids, PARITITION_SIZE); - - Multimap ownedTeisMultiMap = ArrayListMultimap.create(); - - teds.forEach( - partition -> - ownedTeisMultiMap.putAll( - getOwnedTeisPartitioned(partition, ctx, skipUserScopeValidation))); - - return ownedTeisMultiMap; - } - - private Multimap getOwnedTeisPartitioned( - List ids, Context ctx, boolean skipUserScopeValidation) { - OwnedTeMapper handler = new OwnedTeMapper(); - - MapSqlParameterSource paramSource = createIdsParam(ids).addValue("userInfoId", ctx.getUserId()); - - boolean checkForOwnership = - ctx.getParams().isIncludeEnrollments() - || ctx.getParams().getTeEnrollmentParams().isIncludeEvents(); - - String sql; - - if (ctx.getQueryParams().hasProgram()) { - sql = getTrackedEntitiesOwnershipSqlForSpecificProgram(skipUserScopeValidation); - paramSource.addValue("programUid", ctx.getQueryParams().getProgram().getUid()); - } else if (checkForOwnership) { - sql = getTrackedEntitiesOwnershipSqlForAllPrograms(skipUserScopeValidation); - } else { - return ArrayListMultimap.create(); - } - - jdbcTemplate.query(sql, paramSource, handler); - - return handler.getItems(); - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentAggregate.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentAggregate.java index 1ec4e4c736b9..6fd4b38a9ba3 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentAggregate.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentAggregate.java @@ -28,6 +28,8 @@ package org.hisp.dhis.tracker.export.trackedentity.aggregates; import static java.util.concurrent.CompletableFuture.allOf; +import static org.hisp.dhis.tracker.export.trackedentity.aggregates.AsyncUtils.asyncFetch; +import static org.hisp.dhis.tracker.export.trackedentity.aggregates.AsyncUtils.conditionalAsyncFetch; import static org.hisp.dhis.tracker.export.trackedentity.aggregates.ThreadPoolManager.getPool; import com.google.common.collect.Multimap; @@ -51,7 +53,7 @@ */ @Component("org.hisp.dhis.tracker.trackedentity.aggregates.EnrollmentAggregate") @RequiredArgsConstructor -public class EnrollmentAggregate implements Aggregate { +class EnrollmentAggregate { @Qualifier("org.hisp.dhis.tracker.trackedentity.aggregates.EnrollmentStore") @Nonnull private final EnrollmentStore enrollmentStore; diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentStore.java index 1ebefba64aa4..c4171c88cb3a 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EnrollmentStore.java @@ -27,47 +27,91 @@ */ package org.hisp.dhis.tracker.export.trackedentity.aggregates; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import java.util.List; import org.hisp.dhis.note.Note; import org.hisp.dhis.program.Enrollment; -import org.hisp.dhis.relationship.RelationshipItem; import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EnrollmentRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.NoteRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.ProgramAttributeRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.EnrollmentQuery; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.ProgramAttributeQuery; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; /** * @author Luciano Fiandesio */ -public interface EnrollmentStore { - /** - * @param ids a list of {@see TrackedEntity} Primary Keys - * @return a MultiMap where key is a {@see TrackedEntity} uid and the key a List of {@see - * Enrollment} objects - */ - Multimap getEnrollmentsByTrackedEntityIds(List ids, Context ctx); - - /** - * @param ids a list of {@see Enrollment} Primary Keys - * @return a MultiMap where key is a {@see Enrollment} uid and the key a List of {@see Note} - * objects - */ - Multimap getNotes(List ids); - - /** - * Fetches all the relationships having the enrollment id specified in the arg as "left" or - * "right" relationship - * - * @param ids a list of {@see Enrollment} Primary Keys - * @return a MultiMap where key is a {@see Enrollment} uid and the key a List of {@see - * Relationship} objects - */ - Multimap getRelationships(List ids, Context ctx); - - /** - * Fetches all the attributes - * - * @param ids a list of enrollment ids - * @return a MultiMap where key is a {@see Enrollment} uid and the key a List of {@see Attribute} - * objects - */ - Multimap getAttributes(List ids, Context ctx); +@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.EnrollmentStore") +class EnrollmentStore extends AbstractStore { + private static final String GET_ENROLLMENT_SQL_BY_TE = EnrollmentQuery.getQuery(); + + private static final String GET_ATTRIBUTES = ProgramAttributeQuery.getQuery(); + + private static final String GET_NOTES_SQL = + "select en.uid as key, n.uid, n.notetext, " + + "n.creator, n.created " + + "from note n join enrollment_notes enn " + + "on n.noteid = enn.noteid " + + "join enrollment en on enn.enrollmentid = en.enrollmentid " + + "where enn.enrollmentid in (:ids)"; + + private static final String FILTER_OUT_DELETED_ENROLLMENTS = "en.deleted=false"; + + EnrollmentStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + Multimap getEnrollmentsByTrackedEntityIds(List ids, Context ctx) { + List> teIds = Lists.partition(ids, PARITITION_SIZE); + + Multimap enrollmentMultimap = ArrayListMultimap.create(); + + teIds.forEach( + partition -> + enrollmentMultimap.putAll(getEnrollmentsByTrackedEntityIdsPartitioned(partition, ctx))); + + return enrollmentMultimap; + } + + private Multimap getEnrollmentsByTrackedEntityIdsPartitioned( + List ids, Context ctx) { + EnrollmentRowCallbackHandler handler = new EnrollmentRowCallbackHandler(); + + jdbcTemplate.query( + getQuery( + GET_ENROLLMENT_SQL_BY_TE, + ctx, + " en.programid IN (:programIds)", + FILTER_OUT_DELETED_ENROLLMENTS), + createIdsParam(ids).addValue("programIds", ctx.getPrograms()), + handler); + + return handler.getItems(); + } + + Multimap getNotes(List ids) { + return fetch(GET_NOTES_SQL, new NoteRowCallbackHandler(), ids); + } + + Multimap getAttributes(List ids, Context ctx) { + ProgramAttributeRowCallbackHandler handler = new ProgramAttributeRowCallbackHandler(); + + jdbcTemplate.query( + getQuery( + GET_ATTRIBUTES, ctx, " pa.programid IN (:programIds)", FILTER_OUT_DELETED_ENROLLMENTS), + createIdsParam(ids).addValue("programIds", ctx.getPrograms()), + handler); + + return handler.getItems(); + } + + @Override + String getRelationshipEntityColumn() { + return "enrollmentid"; + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventAggregate.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventAggregate.java index ac934a9dc894..29fcb93fc89d 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventAggregate.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventAggregate.java @@ -29,6 +29,8 @@ import static java.util.concurrent.CompletableFuture.allOf; import static java.util.concurrent.CompletableFuture.supplyAsync; +import static org.hisp.dhis.tracker.export.trackedentity.aggregates.AsyncUtils.asyncFetch; +import static org.hisp.dhis.tracker.export.trackedentity.aggregates.AsyncUtils.conditionalAsyncFetch; import static org.hisp.dhis.tracker.export.trackedentity.aggregates.ThreadPoolManager.getPool; import com.google.common.collect.Multimap; @@ -51,7 +53,7 @@ */ @Component("org.hisp.dhis.tracker.trackedentity.aggregates.EventAggregate") @RequiredArgsConstructor -public class EventAggregate implements Aggregate { +class EventAggregate { @Qualifier("org.hisp.dhis.tracker.trackedentity.aggregates.EventStore") @Nonnull private final EventStore eventStore; diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventStore.java index fbe20026c42d..925c3aa14067 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/EventStore.java @@ -27,46 +27,142 @@ */ package org.hisp.dhis.tracker.export.trackedentity.aggregates; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.hisp.dhis.eventdatavalue.EventDataValue; import org.hisp.dhis.note.Note; import org.hisp.dhis.program.Event; -import org.hisp.dhis.relationship.RelationshipItem; +import org.hisp.dhis.query.JpaQueryUtils; +import org.hisp.dhis.security.acl.AclService; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EventDataValueRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.EventRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.NoteRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.EventQuery; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; /** * @author Luciano Fiandesio */ -public interface EventStore { - /** - * Key: enrollment uid -> Value: Event - * - * @param enrollmentsId a List of Enrollment Primary Keys - * @param ctx the {@see Context} - * @return A Map, where the key is a Enrollment Primary Key, and the value is a List of {@see - * Event} - */ - Multimap getEventsByEnrollmentIds(List enrollmentsId, Context ctx); - - /** - * Key: event uid -> Value: List - * - * @param eventIds a List of event primary keys - * @return A Map, where the key is an event primary key, and the value is a List of {@see - * DataValue} - */ - Map> getDataValues(List eventIds); - - /** - * Fetches all the relationships having the event id specified in the arg as "left" or "right" - * relationship - * - * @param ids a list of {@see Enrollment} Primary Keys - * @return a MultiMap where key is a {@see Enrollment} uid and the value a List of {@see - * Relationship} objects - */ - Multimap getRelationships(List ids, Context ctx); - - Multimap getNotes(List eventIds); +@Repository("org.hisp.dhis.tracker.trackedentity.aggregates.EventStore") +class EventStore extends AbstractStore { + private static final String GET_EVENTS_SQL = EventQuery.getQuery(); + + private static final String GET_DATAVALUES_SQL = + "select ev.uid as key, " + + "ev.eventdatavalues " + + "from event ev " + + "where ev.eventid in (:ids)"; + + private static final String GET_NOTES_SQL = + "select ev.uid as key, n.uid, n.notetext, " + + "n.creator, n.created " + + "from note n " + + "join event_notes evn " + + "on n.noteid = evn.noteid " + + "join event ev on evn.eventid = ev.eventid " + + "where evn.eventid in (:ids)"; + + private static final String ACL_FILTER_SQL = + "CASE WHEN p.type = 'WITH_REGISTRATION' THEN " + + "p.trackedentitytypeid in (:trackedEntityTypeIds) else true END " + + "AND ev.programstageid in (:programStageIds) AND en.programid IN (:programIds)"; + + private static final String ACL_FILTER_SQL_NO_PROGRAM_STAGE = + "CASE WHEN p.type = 'WITH_REGISTRATION' THEN " + + "p.trackedentitytypeid in (:trackedEntityTypeIds) else true END " + + "AND en.programid IN (:programIds)"; + + private static final String FILTER_OUT_DELETED_EVENTS = "ev.deleted=false"; + + EventStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + String getRelationshipEntityColumn() { + return "eventid"; + } + + public Multimap getEventsByEnrollmentIds(List enrollmentsId, Context ctx) { + List> enrollmentIdsPartitions = Lists.partition(enrollmentsId, PARITITION_SIZE); + + Multimap eventMultimap = ArrayListMultimap.create(); + + enrollmentIdsPartitions.forEach( + partition -> eventMultimap.putAll(getEventsByEnrollmentIdsPartitioned(partition, ctx))); + + return eventMultimap; + } + + private String getAttributeOptionComboClause(Context ctx) { + return " and ev.attributeoptioncomboid not in (" + + "select distinct(cocco.categoryoptioncomboid) " + + "from categoryoptioncombos_categoryoptions as cocco " + + + // Get inaccessible category options + "where cocco.categoryoptionid not in ( " + + "select co.categoryoptionid " + + "from categoryoption co " + + " where " + + JpaQueryUtils.generateSQlQueryForSharingCheck( + "co.sharing", ctx.getUserUid(), ctx.getUserGroups(), AclService.LIKE_READ_DATA) + + ") )"; + } + + private Multimap getEventsByEnrollmentIdsPartitioned( + List enrollmentsId, Context ctx) { + EventRowCallbackHandler handler = new EventRowCallbackHandler(); + + List programStages = ctx.getProgramStages(); + + String aocSql = ctx.isSuperUser() ? "" : getAttributeOptionComboClause(ctx); + + if (programStages.isEmpty()) { + jdbcTemplate.query( + getQuery( + GET_EVENTS_SQL, + ctx, + ACL_FILTER_SQL_NO_PROGRAM_STAGE + aocSql, + FILTER_OUT_DELETED_EVENTS), + createIdsParam(enrollmentsId) + .addValue("trackedEntityTypeIds", ctx.getTrackedEntityTypes()) + .addValue("programIds", ctx.getPrograms()), + handler); + } else { + jdbcTemplate.query( + getQuery(GET_EVENTS_SQL, ctx, ACL_FILTER_SQL + aocSql, FILTER_OUT_DELETED_EVENTS), + createIdsParam(enrollmentsId) + .addValue("trackedEntityTypeIds", ctx.getTrackedEntityTypes()) + .addValue("programStageIds", programStages) + .addValue("programIds", ctx.getPrograms()), + handler); + } + + return handler.getItems(); + } + + Map> getDataValues(List eventIds) { + Map> dataValueListMultimap = new HashMap<>(); + + Lists.partition(eventIds, PARITITION_SIZE) + .forEach(partition -> dataValueListMultimap.putAll(getDataValuesPartitioned(partition))); + + return dataValueListMultimap; + } + + private Map> getDataValuesPartitioned(List eventIds) { + EventDataValueRowCallbackHandler handler = new EventDataValueRowCallbackHandler(); + + jdbcTemplate.query(GET_DATAVALUES_SQL, createIdsParam(eventIds), handler); + + return handler.getItems(); + } + + Multimap getNotes(List eventIds) { + return fetch(GET_NOTES_SQL, new NoteRowCallbackHandler(), eventIds); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/ThreadPoolManager.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/ThreadPoolManager.java index 3253d96a791c..d5ebd702625b 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/ThreadPoolManager.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/ThreadPoolManager.java @@ -37,7 +37,7 @@ * * @author Luciano Fiandesio */ -public class ThreadPoolManager { +class ThreadPoolManager { // Thread factory that sets a user-defined thread name (useful for debugging // purposes) private ThreadPoolManager() { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityAggregate.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityAggregate.java index 3053b96e0445..d8c25e914519 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityAggregate.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityAggregate.java @@ -30,6 +30,7 @@ import static java.util.concurrent.CompletableFuture.allOf; import static java.util.concurrent.CompletableFuture.supplyAsync; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ALL; +import static org.hisp.dhis.tracker.export.trackedentity.aggregates.AsyncUtils.conditionalAsyncFetch; import static org.hisp.dhis.tracker.export.trackedentity.aggregates.ThreadPoolManager.getPool; import com.google.common.collect.Lists; @@ -74,7 +75,7 @@ */ @Component @RequiredArgsConstructor -public class TrackedEntityAggregate implements Aggregate { +public class TrackedEntityAggregate { @Nonnull private final TrackedEntityStore trackedEntityStore; @Qualifier("org.hisp.dhis.tracker.trackedentity.aggregates.EnrollmentAggregate") @@ -335,7 +336,7 @@ private Set filterAttributes( * @return an instance of {@see Context} populated with ACL-related info */ private Context getSecurityContext(String userUID, List userGroupUIDs) { - final CompletableFuture> getTeiTypes = + final CompletableFuture> getTrackedEntityTypes = supplyAsync( () -> aclStore.getAccessibleTrackedEntityTypes(userUID, userGroupUIDs), getPool()); @@ -349,11 +350,11 @@ private Context getSecurityContext(String userUID, List userGroupUIDs) { supplyAsync( () -> aclStore.getAccessibleRelationshipTypes(userUID, userGroupUIDs), getPool()); - return allOf(getTeiTypes, getPrograms, getProgramStages, getRelationshipTypes) + return allOf(getTrackedEntityTypes, getPrograms, getProgramStages, getRelationshipTypes) .thenApplyAsync( fn -> Context.builder() - .trackedEntityTypes(getTeiTypes.join()) + .trackedEntityTypes(getTrackedEntityTypes.join()) .programs(getPrograms.join()) .programStages(getProgramStages.join()) .relationshipTypes(getRelationshipTypes.join()) diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityStore.java index 027b335e7f34..07f2a442b8a8 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/TrackedEntityStore.java @@ -27,58 +27,170 @@ */ package org.hisp.dhis.tracker.export.trackedentity.aggregates; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.hisp.dhis.relationship.RelationshipItem; +import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.trackedentity.TrackedEntityProgramOwner; import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.OwnedTeMapper; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.ProgramOwnerRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.TrackedEntityAttributeRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.mapper.TrackedEntityRowCallbackHandler; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.TeAttributeQuery; +import org.hisp.dhis.tracker.export.trackedentity.aggregates.query.TrackedEntityQuery; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.stereotype.Repository; /** * @author Luciano Fiandesio + * @author Ameen Mohamed */ -public interface TrackedEntityStore { - /** - * Get a Map of {@see TrackedEntity} by Primary Keys - * - * @param ids a list of Tracked Entity Primary Keys - * @return a Map where key is a {@see TrackedEntity} uid and the key is the corresponding {@see - * TrackedEntity} - */ - Map getTrackedEntities(List ids, Context ctx); - - /** - * Fetches all the relationships having the TE id specified in the arg as "left" or "right" - * relationship - * - * @param ids a list of Tracked Entity Primary Keys - * @return a MultiMap where key is a {@see TrackedEntity} uid and the key a List of {@see - * Relationship} objects - */ - Multimap getRelationships(List ids, Context ctx); - - /** - * @param ids @param ids a list of Tracked Entity Primary Keys - * @return a MultiMap where key is a {@see TrackedEntity} uid and the key a List of {@see - * Attribute} objects - */ - Multimap getAttributes(List ids); - - /** - * @param ids a list of Tracked Entity Primary Keys - * @return a MultiMap where key is a {@see TrackedEntity} uid and the * key a List of {@see - * ProgramOwner} objects - */ - Multimap getProgramOwners(List ids); - - /** - * For each te, get the list of programs for which the user has ownership. - * - * @param ids a list of Tracked Entity primary keys - * @param ctx aggregate context - * @return Tei uids mapped to a list of program uids to which user has ownership - */ +@Repository +class TrackedEntityStore extends AbstractStore { + private static final String GET_TE_SQL = TrackedEntityQuery.getQuery(); + + private static final String GET_TE_ATTRIBUTES = TeAttributeQuery.getQuery(); + + private static final String GET_PROGRAM_OWNERS = + "select te.uid as key, p.uid as prguid, o.uid as ouuid " + + "from trackedentityprogramowner teop " + + "join program p on teop.programid = p.programid " + + "join organisationunit o on teop.organisationunitid = o.organisationunitid " + + "join trackedentity te on teop.trackedentityid = te.trackedentityid " + + "where teop.trackedentityid in (:ids)"; + + private static final String FILTER_OUT_DELETED_TE = "te.deleted=false"; + + private String getTrackedEntitiesOwnershipSqlForAllPrograms(boolean skipUserScopeValidation) { + String sql = + "SELECT te.uid as te_uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, p.accesslevel,p.uid as pgm_uid " + + "FROM trackedentityprogramowner TPO " + + "LEFT JOIN program P on P.programid = TPO.programid " + + "LEFT JOIN organisationunit OU on OU.organisationunitid = TPO.organisationunitid " + + "LEFT JOIN trackedentity TE on TE.trackedentityid = tpo.trackedentityid " + + "WHERE TPO.trackedentityid in (:ids) " + + "AND p.programid in (SELECT programid FROM program) "; + + if (!skipUserScopeValidation) + sql += + "GROUP BY te.uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, ou.path, p.accesslevel,p.uid " + + "HAVING (P.accesslevel in ('OPEN', 'AUDITED') AND (EXISTS(SELECT SS.organisationunitid FROM userteisearchorgunits SS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = SS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')) OR EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')))) " + + "OR (P.accesslevel in ('CLOSED', 'PROTECTED') AND EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')));"; + + return sql; + } + + private String getTrackedEntitiesOwnershipSqlForSpecificProgram(boolean skipUserScopeValidation) { + String sql = + "SELECT te.uid as te_uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, p.accesslevel,p.uid as pgm_uid " + + "FROM trackedentityprogramowner TPO " + + "LEFT JOIN program P on P.programid = TPO.programid " + + "LEFT JOIN organisationunit OU on OU.organisationunitid = TPO.organisationunitid " + + "LEFT JOIN trackedentity TE on TE.trackedentityid = tpo.trackedentityid " + + "WHERE TPO.trackedentityid in (:ids) " + + "AND p.uid = :programUid "; + + if (!skipUserScopeValidation) { + sql += + "GROUP BY te.uid,tpo.trackedentityid, tpo.programid, tpo.organisationunitid, ou.path, p.accesslevel,p.uid " + + "HAVING (P.accesslevel in ('OPEN', 'AUDITED') AND (EXISTS(SELECT SS.organisationunitid FROM userteisearchorgunits SS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = SS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')) OR EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')))) " + + "OR (P.accesslevel in ('CLOSED', 'PROTECTED') AND EXISTS(SELECT CS.organisationunitid FROM usermembership CS LEFT JOIN organisationunit OU2 ON OU2.organisationunitid = CS.organisationunitid WHERE userinfoid = :userInfoId AND OU.path LIKE CONCAT(OU2.path, '%')));"; + } + + return sql; + } + + TrackedEntityStore(@Qualifier("readOnlyJdbcTemplate") JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + String getRelationshipEntityColumn() { + return "trackedentityid"; + } + + Map getTrackedEntities(List ids, Context ctx) { + List> idPartitions = Lists.partition(ids, PARITITION_SIZE); + + Map trackedEntityMap = new LinkedHashMap<>(); + + idPartitions.forEach( + partition -> trackedEntityMap.putAll(getTrackedEntitiesPartitioned(partition, ctx))); + return trackedEntityMap; + } + + private Map getTrackedEntitiesPartitioned(List ids, Context ctx) { + TrackedEntityRowCallbackHandler handler = new TrackedEntityRowCallbackHandler(); + + if (!ctx.isSuperUser() && ctx.getTrackedEntityTypes().isEmpty()) { + // If not super user and no tets are accessible. then simply return + // empty list. + return new HashMap<>(); + } + + String sql = + getQuery(GET_TE_SQL, ctx, "te.trackedentitytypeid in (:teTypeIds)", FILTER_OUT_DELETED_TE); + jdbcTemplate.query( + applySortOrder(sql, StringUtils.join(ids, ",")), + createIdsParam(ids).addValue("teTypeIds", ctx.getTrackedEntityTypes()), + handler); + + return handler.getItems(); + } + + Multimap getAttributes(List ids) { + return fetch(GET_TE_ATTRIBUTES, new TrackedEntityAttributeRowCallbackHandler(), ids); + } + + Multimap getProgramOwners(List ids) { + return fetch(GET_PROGRAM_OWNERS, new ProgramOwnerRowCallbackHandler(), ids); + } + Multimap getOwnedTrackedEntities( - List ids, Context ctx, boolean skipUserScopeValidation); + List ids, Context ctx, boolean skipUserScopeValidation) { + List> teds = Lists.partition(ids, PARITITION_SIZE); + + Multimap ownedTeisMultiMap = ArrayListMultimap.create(); + + teds.forEach( + partition -> + ownedTeisMultiMap.putAll( + getOwnedTeisPartitioned(partition, ctx, skipUserScopeValidation))); + + return ownedTeisMultiMap; + } + + private Multimap getOwnedTeisPartitioned( + List ids, Context ctx, boolean skipUserScopeValidation) { + OwnedTeMapper handler = new OwnedTeMapper(); + + MapSqlParameterSource paramSource = createIdsParam(ids).addValue("userInfoId", ctx.getUserId()); + + boolean checkForOwnership = + ctx.getParams().isIncludeEnrollments() + || ctx.getParams().getTeEnrollmentParams().isIncludeEvents(); + + String sql; + + if (ctx.getQueryParams().hasProgram()) { + sql = getTrackedEntitiesOwnershipSqlForSpecificProgram(skipUserScopeValidation); + paramSource.addValue("programUid", ctx.getQueryParams().getProgram().getUid()); + } else if (checkForOwnership) { + sql = getTrackedEntitiesOwnershipSqlForAllPrograms(skipUserScopeValidation); + } else { + return ArrayListMultimap.create(); + } + + jdbcTemplate.query(sql, paramSource, handler); + + return handler.getItems(); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/mapper/TrackedEntityRowCallbackHandler.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/mapper/TrackedEntityRowCallbackHandler.java index 59c4131fbc59..912fafaea61b 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/mapper/TrackedEntityRowCallbackHandler.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/mapper/TrackedEntityRowCallbackHandler.java @@ -55,6 +55,8 @@ private TrackedEntity getTrackedEntity(ResultSet rs) throws SQLException { te.setUid(rs.getString(TrackedEntityQuery.getColumnName(COLUMNS.UID))); TrackedEntityType trackedEntityType = new TrackedEntityType(); trackedEntityType.setUid(rs.getString(TrackedEntityQuery.getColumnName(COLUMNS.TYPE_UID))); + trackedEntityType.setAllowAuditLog( + rs.getBoolean(TrackedEntityQuery.getColumnName(COLUMNS.TYPE_ALLOW_AUDITLOG))); te.setTrackedEntityType(trackedEntityType); OrganisationUnit orgUnit = new OrganisationUnit(); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/query/TrackedEntityQuery.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/query/TrackedEntityQuery.java index 5bddc7689e97..90ba79835983 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/query/TrackedEntityQuery.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/aggregates/query/TrackedEntityQuery.java @@ -46,6 +46,7 @@ public enum COLUMNS { DELETED, GEOMETRY, TYPE_UID, + TYPE_ALLOW_AUDITLOG, ORGUNIT_UID, TRACKEDENTITYID, @@ -65,6 +66,9 @@ public enum COLUMNS { .put(COLUMNS.DELETED, new TableColumn("te", "deleted")) .put(COLUMNS.GEOMETRY, new Function("ST_AsBinary", "te", "geometry", "geometry")) .put(COLUMNS.TYPE_UID, new TableColumn("tet", "uid", "type_uid")) + .put( + COLUMNS.TYPE_ALLOW_AUDITLOG, + new TableColumn("tet", "allowauditlog", "type_allowauditlog")) .put(COLUMNS.ORGUNIT_UID, new TableColumn("o", "uid", "ou_uid")) .put(COLUMNS.TRACKEDENTITYID, new TableColumn("te", "trackedentityid", "trackedentityid")) .put( diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/DefaultTrackerObjectsDeletionService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/DefaultTrackerObjectsDeletionService.java index 4132fc0fbe86..99e2ff14daf0 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/DefaultTrackerObjectsDeletionService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/DefaultTrackerObjectsDeletionService.java @@ -27,7 +27,7 @@ */ package org.hisp.dhis.tracker.imports.bundle.persister; -import static org.hisp.dhis.audit.AuditOperationType.READ; +import static org.hisp.dhis.audit.AuditOperationType.DELETE; import static org.hisp.dhis.user.CurrentUserUtil.getCurrentUserDetails; import static org.hisp.dhis.user.CurrentUserUtil.getCurrentUsername; @@ -180,7 +180,7 @@ public TrackerTypeReport deleteTrackedEntities(List trackedEntities) if (entity == null) { throw new NotFoundException(TrackedEntity.class, uid); } - trackedEntityAuditService.addTrackedEntityAudit(entity, getCurrentUsername(), READ); + trackedEntityAuditService.addTrackedEntityAudit(DELETE, getCurrentUsername(), entity); entity.setLastUpdatedByUserInfo(userInfoSnapshot); diff --git a/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/config/ServiceConfig.java b/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/config/ServiceConfig.java index 9fcfaeedbcbf..fa358b1e7288 100644 --- a/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/config/ServiceConfig.java +++ b/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/config/ServiceConfig.java @@ -29,7 +29,6 @@ import static org.hisp.dhis.external.conf.ConfigurationKey.META_DATA_SYNC_RETRY; import static org.hisp.dhis.external.conf.ConfigurationKey.META_DATA_SYNC_RETRY_TIME_FREQUENCY_MILLISEC; -import static org.hisp.dhis.external.conf.ConfigurationKey.SYSTEM_SESSION_TIMEOUT; import org.hisp.dhis.external.conf.ConfigurationPropertyFactoryBean; import org.hisp.dhis.external.location.DefaultLocationManager; @@ -60,9 +59,4 @@ public ConfigurationPropertyFactoryBean maxAttempts() { public ConfigurationPropertyFactoryBean initialInterval() { return new ConfigurationPropertyFactoryBean(META_DATA_SYNC_RETRY_TIME_FREQUENCY_MILLISEC); } - - @Bean - public ConfigurationPropertyFactoryBean sessionTimeout() { - return new ConfigurationPropertyFactoryBean(SYSTEM_SESSION_TIMEOUT); - } } diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java index b072a24aa164..763e989f1a05 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java @@ -191,24 +191,59 @@ public static void assertNotEmpty(Collection actual, String message) { assertFalse(actual.isEmpty(), message); } + /** + * Asserts that the given collection is not null and not empty. + * + * @param actual the collection. + * @param messageSupplier fails with this supplied message + */ + public static void assertNotEmpty(Collection actual, Supplier messageSupplier) { + assertNotNull(actual, messageSupplier); + assertFalse(actual.isEmpty(), messageSupplier); + } + /** * Asserts that the given collection contains the expected number of elements. * * @param actual the collection. */ public static void assertHasSize(int expected, Collection actual) { - assert expected > 0 : "use assertIsEmpty"; - - assertNotEmpty(actual); - assertEquals( + assertHasSize( expected, - actual.size(), + actual, () -> String.format( "expected collection to contain %d elements, it has %d instead: '%s'", expected, actual.size(), actual)); } + /** + * Asserts that the given collection contains the expected number of elements. + * + * @param actual the collection. + * @param messageSupplier fails with this supplied message + */ + public static void assertHasSize( + int expected, Collection actual, Supplier messageSupplier) { + assert expected > 0 : "use assertIsEmpty"; + + assertNotEmpty(actual); + assertEquals(expected, actual.size(), messageSupplier); + } + + /** + * Asserts that the given collection contains the expected number of elements. + * + * @param actual the collection. + * @param message fails with this message + */ + public static void assertHasSize(int expected, Collection actual, String message) { + assert expected > 0 : "use assertIsEmpty"; + + assertNotEmpty(actual); + assertEquals(expected, actual.size(), message); + } + /** * Asserts that the given string starts with the expected prefix. * 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 2b61abbb750d..24a0f27d194f 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 @@ -1070,7 +1070,7 @@ "attribute": { "id": "j45AR9cBQKc" }, - "value": "programStage qLZC0lvvxQH" + "value": "multi-program-stage-attribute" } ], "autoGenerateEvent": true, @@ -1126,15 +1126,7 @@ } ], "validationStrategy": "ON_UPDATE_AND_INSERT", - "featureType": "POINT", - "attributeValues": [ - { - "attribute": { - "id": "j45AR9cBQKc" - }, - "value": "multi-program-stage-attribute" - } - ] + "featureType": "POINT" }, { "id": "SKNvpoLioON", @@ -1851,6 +1843,29 @@ }, "relationshipEntity": "PROGRAM_STAGE_INSTANCE" } + }, + { + "id": "m1575931405", + "displayFromToName": "Parent Of", + "displayName": "Parent to Child", + "fromConstraint": { + "relationshipEntity": "TRACKED_ENTITY_INSTANCE", + "trackedEntityType": { + "id": "ja8NY4PW7Xm" + } + }, + "fromToName": "Child Of", + "name": "Tracked entity to tracked entity", + "sharing": { + "owner": null, + "public": "r-------" + }, + "toConstraint": { + "relationshipEntity": "TRACKED_ENTITY_INSTANCE", + "trackedEntityType": { + "id": "ja8NY4PW7Xm" + } + } } ], "trackedEntityAttributes": [ @@ -2230,6 +2245,41 @@ ], "username": "trackeradmin" }, + { + "id": "Z7870757a75", + "access": { + "delete": true, + "externalize": true, + "manage": true, + "read": true, + "update": true, + "write": true + }, + "dataViewOrganisationUnits": [ + { + "id": "h4w96yEMlzO" + } + ], + "displayName": "basicuser", + "email": "basic@dhis2.org", + "firstName": "basic", + "name": "user", + "organisationUnits": [ + { + "id": "h4w96yEMlzO" + } + ], + "password": "Test123###...", + "passwordLastUpdated": "2020-05-31T08:57:59.060", + "phoneNumber": "+4740332255", + "surname": "basic", + "userRoles": [ + { + "id": "UbhT3bXWUyb" + } + ], + "username": "basicuser" + }, { "id": "o1HMTIzBGo7", "access": { diff --git a/dhis-2/dhis-test-e2e/pom.xml b/dhis-2/dhis-test-e2e/pom.xml index d9f11bee9c91..6c571f67dd15 100644 --- a/dhis-2/dhis-test-e2e/pom.xml +++ b/dhis-2/dhis-test-e2e/pom.xml @@ -36,7 +36,7 @@ 4.5.14 5.4.1 2.18.0 - 6.2.1 + 6.2.2 1.2 1.0.0 diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java index 5c440982bbee..2d6b53290a39 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java @@ -69,8 +69,12 @@ class CategoryOptionComboMergeTest extends ApiTest { private RestApiActions dataElementApiActions; private RestApiActions minMaxActions; private MetadataActions metadataActions; + private RestApiActions visualizationActions; private RestApiActions maintenanceApiActions; private RestApiActions dataValueSetActions; + private RestApiActions indicatorActions; + private RestApiActions indicatorTypeActions; + private RestApiActions validationRuleActions; private UserActions userActions; private LoginActions loginActions; private String sourceUid1; @@ -90,6 +94,10 @@ public void before() { metadataActions = new MetadataActions(); maintenanceApiActions = new RestApiActions("maintenance"); dataValueSetActions = new RestApiActions("dataValueSets"); + visualizationActions = new RestApiActions("visualizations"); + indicatorActions = new RestApiActions("indicators"); + indicatorTypeActions = new RestApiActions("indicatorTypes"); + validationRuleActions = new RestApiActions("validationRules"); loginActions.loginAsSuperUser(); // add user with required merge auth @@ -122,13 +130,9 @@ public void resetSuperUserOrgUnit() { void validCategoryOptionComboMergeTest() { // given // generate category option combos - String emptyParams = new QueryParamsBuilder().build(); maintenanceApiActions - .post("categoryOptionComboUpdate/categoryCombo/CatComUid01", emptyParams) - .validateStatus(200); - maintenanceApiActions - .post("categoryOptionComboUpdate/categoryCombo/CatComUid02", emptyParams) - .validateStatus(200); + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); // get cat opt combo uids for sources and target, after generating sourceUid1 = getCocWithOptions("1A", "2A"); @@ -145,6 +149,10 @@ void validCategoryOptionComboMergeTest() { .body("categoryOptions", hasItem(hasEntry("id", "CatOptUid4B"))) .body("categoryOptions", hasItem(hasEntry("id", "CatOptUid3A"))); + String dataElement = setupDataElement("test de 1"); + // import visualization to persist data dimension item which has ref to source coc + visualizationActions.post(getViz(dataElement, sourceUid1)).validateStatus(201); + // login as merge user loginActions.loginAsUser("userWithMergeAuth", "Test1234!"); @@ -410,6 +418,49 @@ private void updateDataValuesAoc() { .body("response.importCount.updated", equalTo(4)); } + private String getViz(String dataElement, String coc) { + return """ + + { + "name": "Test viz with data dimension item - DE operand", + "displayName": "Test 1", + "type": "PIVOT_TABLE", + "filters": [ + { + "dimension": "ou", + "items": [ + { + "id": "USER_ORGUNIT" + } + ] + } + ], + "columns": [ + { + "dimension": "dx", + "items": [ + { + "id": "%s.%s", + "dimensionItemType": "DATA_ELEMENT_OPERAND" + } + ] + } + ], + "rows": [ + { + "dimension": "pe", + "items": [ + { + "id": "LAST_10_YEARS" + } + ] + } + ] + } + """ + .formatted(dataElement, coc); + } + private QueryParamsBuilder getDataValueQueryParams() { return new QueryParamsBuilder() .add("async=false") @@ -494,7 +545,7 @@ void dbConstraintMinMaxTest() { sourceUid2 = getCocWithOptions("1B", "2B"); targetUid = getCocWithOptions("3A", "4B"); - String dataElement = setupDataElement(); + String dataElement = setupDataElement("DE test 2"); setupMinMaxDataElements(sourceUid1, sourceUid2, targetUid, dataElement); @@ -515,8 +566,131 @@ void dbConstraintMinMaxTest() { .body("message", containsString("minmaxdataelement_unique_key")); } + @Test + @DisplayName( + "Indicators with COC source refs in their numerator or denominator are updated with target COC ref") + void indicatorsNumeratorDenominatorTest() { + // given + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); + + // get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1B", "2B"); + targetUid = getCocWithOptions("3A", "4B"); + + // indicators with mix of source COC in numerator, denominator + String indicatorType = setupIndicatorType("1"); + String indicator1 = setupIndicator("1", indicatorType, sourceUid1, "num2", "denom1", "denom2"); + String indicator2 = setupIndicator("2", indicatorType, "num1", "num2", sourceUid2, sourceUid2); + String indicator3 = + setupIndicator("3", indicatorType, sourceUid1, sourceUid2, sourceUid1, sourceUid2); + String indicator4 = setupIndicator("4", indicatorType, targetUid, "num2", targetUid, "denom2"); + String indicator5 = + setupIndicator("5", indicatorType, "randomUID1", "randomUID2", "randomUID3", "randomUID4"); + + // when + ValidatableResponse response = + categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validate(); + + // then + response + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo")) + .body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2)); + + // and source COC refs have been replaced + checkIndicatorValues(1, indicator1, targetUid, "num2", "denom1", "denom2"); + checkIndicatorValues(2, indicator2, "num1", "num2", targetUid, targetUid); + checkIndicatorValues(3, indicator3, targetUid, targetUid, targetUid, targetUid); + checkIndicatorValues(4, indicator4, targetUid, "num2", targetUid, "denom2"); + checkIndicatorValues(5, indicator5, "randomUID1", "randomUID2", "randomUID3", "randomUID4"); + } + + @Test + @DisplayName( + "Expressions with COC source refs in their expression are updated with target COC ref") + void expressionTest() { + // given + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); + + // get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1B", "2B"); + targetUid = getCocWithOptions("3A", "4B"); + + // indicators with mix of source COC in numerator, denominator + String validationRule1 = + setupExpressionInValidationRule("1", sourceUid1, "leftSide2", "rightSide1", "rightSide2"); + String validationRule2 = + setupExpressionInValidationRule("2", "leftSide1", "leftSide2", "rightSide1", sourceUid2); + String validationRule3 = + setupExpressionInValidationRule("3", sourceUid1, sourceUid1, sourceUid2, "rightSide2"); + String validationRule4 = + setupExpressionInValidationRule("4", targetUid, "leftSide2", "rightSide1", "rightSide2"); + String validationRule5 = + setupExpressionInValidationRule("5", "leftSide1", "leftSide2", "rightSide1", "rightSide2"); + + // when + ValidatableResponse response = + categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validate(); + + // then + response + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo")) + .body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2)); + + // and source COC refs have been replaced with target COC refs + checkExpressionValues(1, validationRule1, targetUid, "leftSide2", "rightSide1", "rightSide2"); + checkExpressionValues(2, validationRule2, "leftSide1", "leftSide2", "rightSide1", targetUid); + checkExpressionValues(3, validationRule3, targetUid, targetUid, targetUid, "rightSide2"); + checkExpressionValues(4, validationRule4, targetUid, "leftSide2", "rightSide1", "rightSide2"); + checkExpressionValues(5, validationRule5, "leftSide1", "leftSide2", "rightSide1", "rightSide2"); + } + + private void checkIndicatorValues( + int name, String indicator, String num1, String num2, String denom1, String denom2) { + indicatorActions + .get("/" + indicator) + .validate() + .statusCode(200) + .body("numerator", equalTo("#{%s.RanDOmUID01}.%s".formatted(num1, num2))) + .body("denominator", equalTo("#{h0xKKjijTdI.%s}.%s".formatted(denom1, denom2))) + .body("name", equalTo("test indicator %d".formatted(name))); + } + + private void checkExpressionValues( + int name, + String rule, + String leftSide1, + String leftSide2, + String rightSide1, + String rightSide2) { + validationRuleActions + .get("/" + rule) + .validate() + .statusCode(200) + .body( + "leftSide.expression", + equalTo("#{%s.RandomUid01}+#{RandomUid02.%s}".formatted(leftSide1, leftSide2))) + .body( + "rightSide.expression", + equalTo("#{%s.RandomUid03}+#{RandomUid04.%s}".formatted(rightSide1, rightSide2))) + .body("name", equalTo("test val rule %d".formatted(name))); + } + private void setupMetadata() { - metadataActions.post(metadata()).validateStatus(200); + metadataActions.importMetadata(metadata()).validateStatus(200); } private void setupMinMaxDataElements( @@ -546,19 +720,79 @@ private String minMaxDataElements(String coc, String de) { .formatted(de, coc); } - private String setupDataElement() { + private String setupDataElement(String name) { return dataElementApiActions .post( """ { "aggregationType": "DEFAULT", "domainType": "AGGREGATE", - "name": "source 19", - "shortName": "source 19", - "displayName": "source 19", + "name": "%s", + "shortName": "%s", "valueType": "TEXT" } - """) + """ + .formatted(name, name)) + .validateStatus(201) + .extractUid(); + } + + private String setupIndicator( + String name, String indType, String num1, String num2, String denom1, String denom2) { + return indicatorActions + .post( + """ + { + "name": "test indicator %s", + "shortName": "test short %s", + "dimensionItemType": "INDICATOR", + "numerator": "#{%s.RanDOmUID01}.%s", + "denominator": "#{h0xKKjijTdI.%s}.%s", + "indicatorType": { + "id": "%s" + } + } + """ + .formatted(name, name, num1, num2, denom1, denom2, indType)) + .validateStatus(201) + .extractUid(); + } + + private String setupExpressionInValidationRule( + String name, String leftSide1, String leftSide2, String rightSide1, String rightSide2) { + return validationRuleActions + .post( + """ + { + "name": "test val rule %s", + "leftSide": { + "expression": "#{%s.RandomUid01}+#{RandomUid02.%s}", + "description": "expression 1" + }, + "rightSide": { + "expression": "#{%s.RandomUid03}+#{RandomUid04.%s}", + "description": "expression 2" + }, + "operator": "less_than_or_equal_to", + "periodType": "Monthly" + } + """ + .formatted(name, leftSide1, leftSide2, rightSide1, rightSide2)) + .validateStatus(201) + .extractUid(); + } + + private String setupIndicatorType(String name) { + return indicatorTypeActions + .post( + """ + { + "name": "test indicator %s", + "factor": 12, + "number": true + } + """ + .formatted(name)) .validateStatus(201) .extractUid(); } diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionMergeTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionMergeTest.java index da8981f7d958..2470a7c574fa 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionMergeTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionMergeTest.java @@ -46,7 +46,6 @@ import org.hisp.dhis.test.e2e.helpers.QueryParamsBuilder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -57,9 +56,9 @@ class CategoryOptionMergeTest extends ApiTest { private RestApiActions maintenanceApiActions; private UserActions userActions; private LoginActions loginActions; - private final String sourceUid1 = "CatOptUid1A"; - private final String sourceUid2 = "CatOptUid2B"; - private final String targetUid = "CatOptUid3A"; + private final String sourceUid1 = "UIDCatOpt1A"; + private final String sourceUid2 = "UIDCatOpt2B"; + private final String targetUid = "UIDCatOpt3A"; @BeforeAll public void before() { @@ -88,7 +87,6 @@ public void setup() { } @Test - @Disabled("Started failing on Jenkins only, will investigate.") @DisplayName( "Valid CategoryOption merge completes successfully with all source CategoryOption refs replaced with target CategoryOption") void validCategoryOptionMergeTest() { @@ -106,7 +104,7 @@ void validCategoryOptionMergeTest() { .body("organisationUnits", hasSize(equalTo(1))) .body("organisationUnits", hasItem(hasEntry("id", "OrgUnitUid3"))) .body("categories", hasSize(equalTo(1))) - .body("categories", hasItem(hasEntry("id", "CategoUid03"))) + .body("categories", hasItem(hasEntry("id", "UIDCatego03"))) .body("categoryOptionCombos", hasSize(equalTo(2))) .body("categoryOptionGroups", hasSize(equalTo(1))) .body("categoryOptionGroups", hasItem(hasEntry("id", "CatOptGrp03"))); @@ -143,9 +141,9 @@ void validCategoryOptionMergeTest() { .body( "categories", hasItems( - hasEntry("id", "CategoUid01"), - hasEntry("id", "CategoUid02"), - hasEntry("id", "CategoUid03"))) + hasEntry("id", "UIDCatego01"), + hasEntry("id", "UIDCatego02"), + hasEntry("id", "UIDCatego03"))) .body("categoryOptionCombos", hasSize(equalTo(5))) .body( "categoryOptionGroups", @@ -156,7 +154,7 @@ void validCategoryOptionMergeTest() { } private void setupMetadata() { - metadataActions.post(metadata()).validateStatus(200); + metadataActions.importMetadata(metadata()).validateStatus(200); } @Test @@ -196,9 +194,9 @@ private String metadata() { { "categoryOptions": [ { - "id": "CatOptUid1A", - "name": "cat opt 1A", - "shortName": "cat opt 1A", + "id": "UIDCatOpt1A", + "name": "cat option 1A", + "shortName": "cat option 1A", "organisationUnits": [ { "id": "OrgUnitUid1" @@ -206,9 +204,9 @@ private String metadata() { ] }, { - "id": "CatOptUid1B", - "name": "cat opt 1B", - "shortName": "cat opt 1B", + "id": "UIDCatOpt1B", + "name": "cat option 1B", + "shortName": "cat option 1B", "organisationUnits": [ { "id": "OrgUnitUid1" @@ -216,9 +214,9 @@ private String metadata() { ] }, { - "id": "CatOptUid2A", - "name": "cat opt 2A", - "shortName": "cat opt 2A", + "id": "UIDCatOpt2A", + "name": "cat option 2A", + "shortName": "cat option 2A", "organisationUnits": [ { "id": "OrgUnitUid2" @@ -226,9 +224,9 @@ private String metadata() { ] }, { - "id": "CatOptUid2B", - "name": "cat opt 2B", - "shortName": "cat opt 2B", + "id": "UIDCatOpt2B", + "name": "cat option 2B", + "shortName": "cat option 2B", "organisationUnits": [ { "id": "OrgUnitUid2" @@ -236,9 +234,9 @@ private String metadata() { ] }, { - "id": "CatOptUid3A", - "name": "cat opt 3A", - "shortName": "cat opt 3A", + "id": "UIDCatOpt3A", + "name": "cat option 3A", + "shortName": "cat option 3A", "organisationUnits": [ { "id": "OrgUnitUid3" @@ -246,9 +244,9 @@ private String metadata() { ] }, { - "id": "CatOptUid3B", - "name": "cat opt 3B", - "shortName": "cat opt 3B", + "id": "UIDCatOpt3B", + "name": "cat option 3B", + "shortName": "cat option 3B", "organisationUnits": [ { "id": "OrgUnitUid3" @@ -256,9 +254,9 @@ private String metadata() { ] }, { - "id": "CatOptUid4A", - "name": "cat opt 4A", - "shortName": "cat opt 4A", + "id": "UIDCatOpt4A", + "name": "cat option 4A", + "shortName": "cat option 4A", "organisationUnits": [ { "id": "OrgUnitUid4" @@ -266,9 +264,9 @@ private String metadata() { ] }, { - "id": "CatOptUid4B", - "name": "cat opt 4B", - "shortName": "cat opt 4B", + "id": "UIDCatOpt4B", + "name": "cat option 4B", + "shortName": "cat option 4B", "organisationUnits": [ { "id": "OrgUnitUid4" @@ -278,58 +276,58 @@ private String metadata() { ], "categories": [ { - "id": "CategoUid01", - "name": "cat 1", - "shortName": "cat 1", + "id": "UIDCatego01", + "name": "category 1", + "shortName": "category 1", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid1A" + "id": "UIDCatOpt1A" }, { - "id": "CatOptUid1B" + "id": "UIDCatOpt1B" } ] }, { - "id": "CategoUid02", - "name": "cat 2", - "shortName": "cat 2", + "id": "UIDCatego02", + "name": "category 2", + "shortName": "category 2", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid2A" + "id": "UIDCatOpt2A" }, { - "id": "CatOptUid2B" + "id": "UIDCatOpt2B" } ] }, { - "id": "CategoUid03", - "name": "cat 3", - "shortName": "cat 3", + "id": "UIDCatego03", + "name": "category 3", + "shortName": "category 3", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid3A" + "id": "UIDCatOpt3A" }, { - "id": "CatOptUid3B" + "id": "UIDCatOpt3B" } ] }, { - "id": "CategoUid04", - "name": "cat 4", - "shortName": "cat 4", + "id": "UIDCatego04", + "name": "category 4", + "shortName": "category 4", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid4A" + "id": "UIDCatOpt4A" }, { - "id": "CatOptUid4B" + "id": "UIDCatOpt4B" } ] } @@ -363,85 +361,85 @@ private String metadata() { "categoryOptionGroups": [ { "id": "CatOptGrp01", - "name": "cog 1", - "shortName": "cog 1", + "name": "co group 1", + "shortName": "co group 1", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid1A" + "id": "UIDCatOpt1A" }, { - "id": "CatOptUid1B" + "id": "UIDCatOpt1B" } ] }, { "id": "CatOptGrp02", - "name": "cog 2", - "shortName": "cog 2", + "name": "co group 2", + "shortName": "co group 2", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid2A" + "id": "UIDCatOpt2A" }, { - "id": "CatOptUid2B" + "id": "UIDCatOpt2B" } ] }, { "id": "CatOptGrp03", - "name": "cog 3", - "shortName": "cog 3", + "name": "co group 3", + "shortName": "co group 3", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid3A" + "id": "UIDCatOpt3A" }, { - "id": "CatOptUid3B" + "id": "UIDCatOpt3B" } ] }, { "id": "CatOptGrp04", - "name": "cog 4", - "shortName": "cog 4", + "name": "co group 4", + "shortName": "co group 4", "dataDimensionType": "DISAGGREGATION", "categoryOptions": [ { - "id": "CatOptUid4A" + "id": "UIDCatOpt4A" }, { - "id": "CatOptUid4B" + "id": "UIDCatOpt4B" } ] } ], "categoryCombos": [ { - "id": "CatComUid01", - "name": "cat combo 1", + "id": "UIDCatCom01", + "name": "category combo 1", "dataDimensionType": "DISAGGREGATION", "categories": [ { - "id": "CategoUid01" + "id": "UIDCatego01" }, { - "id": "CategoUid02" + "id": "UIDCatego02" } ] }, { - "id": "CatComUid02", - "name": "cat combo 2", + "id": "UIDCatCom02", + "name": "category combo 2", "dataDimensionType": "DISAGGREGATION", "categories": [ { - "id": "CategoUid03" + "id": "UIDCatego03" }, { - "id": "CategoUid04" + "id": "UIDCatego04" } ] } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java index c76f5f8ae5c7..fce4dc7bee1d 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java @@ -377,15 +377,13 @@ void dataElementOperandRefsReplacedSourcesDeletedTest() throws ConflictException List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); - assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); - // then + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + assertFalse(report.hasErrorMessages(), "there should be no merge errors"); List deoSourcesAfter = dataElementOperandStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); List deoTargetAfter = dataElementOperandStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); - - assertFalse(report.hasErrorMessages()); assertEquals( 0, deoSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); assertEquals( diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramServiceTest.java index 235e222d0220..db92adbd0812 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramServiceTest.java @@ -34,6 +34,7 @@ import java.util.HashSet; import java.util.List; +import org.hisp.dhis.mapping.MappingService; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; @@ -50,8 +51,12 @@ class ProgramServiceTest extends PostgresIntegrationTestBase { @Autowired private ProgramService programService; + @Autowired private ProgramStageService programStageService; + @Autowired private OrganisationUnitService organisationUnitService; + @Autowired private MappingService mappingService; + private OrganisationUnit organisationUnitA; private OrganisationUnit organisationUnitB; diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerAccessManagerTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerAccessManagerTest.java index 4fb09bcbeb84..ba633bdbfbd7 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerAccessManagerTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerAccessManagerTest.java @@ -62,6 +62,7 @@ import org.hisp.dhis.program.ProgramType; import org.hisp.dhis.security.acl.AccessStringHelper; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.hisp.dhis.tracker.acl.TrackedEntityProgramOwnerService; import org.hisp.dhis.tracker.acl.TrackerAccessManager; import org.hisp.dhis.tracker.acl.TrackerOwnershipManager; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityService; @@ -82,6 +83,8 @@ class TrackerAccessManagerTest extends PostgresIntegrationTestBase { @Autowired private TrackerOwnershipManager trackerOwnershipManager; + @Autowired private TrackedEntityProgramOwnerService trackedEntityProgramOwnerService; + @Autowired private TrackedEntityTypeService trackedEntityTypeService; @Autowired private TrackedEntityService trackedEntityService; @@ -271,7 +274,8 @@ void checkAccessPermissionForEnrollmentInClosedProgram() throws ForbiddenExcepti "User has no create access to organisation unit:"); enrollment.setOrganisationUnit(orgUnitA); // Transferring ownership to orgUnitB. user is no longer owner - trackerOwnershipManager.assignOwnership(trackedEntity, programA, orgUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntity, programA, orgUnitA); trackerOwnershipManager.transferOwnership(trackedEntity, programA, orgUnitB); // Cannot create enrollment if not owner assertHasError( @@ -388,7 +392,8 @@ void checkAccessPermissionsForEventInClosedProgram() throws ForbiddenException { assertNoErrors(trackerAccessManager.canUpdate(userDetails, eventB, false)); // Can delete events if user is owner irrespective of eventOU assertNoErrors(trackerAccessManager.canDelete(userDetails, eventB, false)); - trackerOwnershipManager.assignOwnership(trackedEntityA, programA, orgUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA, programA, orgUnitA); trackerOwnershipManager.transferOwnership(trackedEntityA, programA, orgUnitB); // Cannot create events anywhere if user is not owner assertHasErrors(2, trackerAccessManager.canCreate(userDetails, eventB, false)); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerOwnershipManagerTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerOwnershipManagerTest.java index c85e2dcacd0b..6440cb149a0d 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerOwnershipManagerTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/trackedentity/TrackerOwnershipManagerTest.java @@ -27,7 +27,12 @@ */ package org.hisp.dhis.trackedentity; +import static org.hisp.dhis.common.AccessLevel.AUDITED; +import static org.hisp.dhis.common.AccessLevel.CLOSED; +import static org.hisp.dhis.common.AccessLevel.OPEN; +import static org.hisp.dhis.common.AccessLevel.PROTECTED; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ACCESSIBLE; +import static org.hisp.dhis.test.utils.Assertions.assertContains; import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.TrackerTestUtils.uids; @@ -39,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import org.hisp.dhis.common.AccessLevel; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.UID; @@ -50,9 +56,11 @@ import org.hisp.dhis.program.Enrollment; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramService; +import org.hisp.dhis.program.ProgramType; import org.hisp.dhis.security.Authorities; import org.hisp.dhis.security.acl.AccessStringHelper; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.hisp.dhis.tracker.acl.TrackedEntityProgramOwnerService; import org.hisp.dhis.tracker.acl.TrackerOwnershipManager; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityEnrollmentParams; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityOperationParams; @@ -61,8 +69,11 @@ import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.sharing.Sharing; import org.hisp.dhis.user.sharing.UserAccess; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; /** @@ -83,6 +94,8 @@ class TrackerOwnershipManagerTest extends PostgresIntegrationTestBase { @Autowired private TrackedEntityTypeService trackedEntityTypeService; + @Autowired private TrackedEntityProgramOwnerService trackedEntityProgramOwnerService; + private TrackedEntity trackedEntityA1; private TrackedEntity trackedEntityB1; @@ -133,7 +146,7 @@ void setUp() { createAndAddUserWithAuth("trackertestownership", organisationUnitA, Authorities.ALL); programA = createProgram('A'); - programA.setAccessLevel(AccessLevel.PROTECTED); + programA.setAccessLevel(PROTECTED); programA.setTrackedEntityType(trackedEntityType); programA.setOrganisationUnits(Set.of(organisationUnitA, organisationUnitB)); programService.addProgram(programA); @@ -162,25 +175,19 @@ void setUp() { } @Test - void testAssignOwnership() { + void shouldFailWhenGrantingTemporaryOwnershipAndUserNotInSearchScope() { assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsA, trackedEntityA1, programA)); assertFalse(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programA)); - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityB1, programA)); - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitB, false, true); - assertFalse(trackerOwnershipAccessManager.hasAccess(userDetailsA, trackedEntityA1, programA)); - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programA)); - } + Exception exception = + Assertions.assertThrows( + ForbiddenException.class, + () -> + trackerOwnershipAccessManager.grantTemporaryOwnership( + trackedEntityA1, programA, userDetailsB, "testing reason")); - @Test - void testGrantTemporaryOwnershipWithAudit() { - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsA, trackedEntityA1, programA)); - assertFalse(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programA)); - trackerOwnershipAccessManager.grantTemporaryOwnership( - trackedEntityA1, programA, userDetailsB, "testing reason"); - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsA, trackedEntityA1, programA)); - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsA, trackedEntityA1, programA)); - assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programA)); + assertEquals( + "The owner of the entity-program combination is not in the user's search scope.", + exception.getMessage()); } @Test @@ -188,8 +195,8 @@ void shouldNotHaveAccessToEnrollmentWithUserAWhenTransferredToAnotherOrgUnit() throws ForbiddenException { userA.setTeiSearchOrganisationUnits(Set.of(organisationUnitB)); userService.updateUser(userA); - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityA1, programA, organisationUnitB); injectSecurityContextUser(userA); @@ -205,8 +212,8 @@ void shouldNotHaveAccessToEnrollmentWithUserAWhenTransferredToAnotherOrgUnit() @Test void shouldHaveAccessToEnrollmentWithUserBWhenTransferredToOwnOrgUnit() throws ForbiddenException, NotFoundException, BadRequestException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityA1, programA, organisationUnitB); injectSecurityContextUser(userB); @@ -219,8 +226,8 @@ void shouldHaveAccessToEnrollmentWithUserBWhenTransferredToOwnOrgUnit() @Test void shouldHaveAccessToEnrollmentWithSuperUserWhenTransferredToOwnOrgUnit() throws ForbiddenException, NotFoundException, BadRequestException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityA1, programA, organisationUnitB); superUser.setOrganisationUnits(Set.of(organisationUnitB)); userService.updateUser(superUser); @@ -245,8 +252,8 @@ void shouldHaveAccessToTEWhenProgramNotProvidedButUserHasAccessToAtLeastOneProgr @Test void shouldNotHaveAccessToTEWhenProgramNotProvidedAndUserHasNoAccessToAnyProgram() { injectSecurityContextUser(userA); - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitB, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitB); ForbiddenException exception = assertThrows( @@ -272,9 +279,14 @@ void shouldHaveAccessWhenProgramProtectedAndUserInCaptureScope() { } @Test - void shouldHaveAccessWhenProgramProtectedAndHasTemporaryAccess() { + void shouldHaveAccessWhenProgramProtectedAndHasTemporaryAccess() throws ForbiddenException { + userB.setTeiSearchOrganisationUnits(Set.of(organisationUnitA)); + userService.updateUser(userB); + userDetailsB = UserDetails.fromUser(userB); + trackerOwnershipAccessManager.grantTemporaryOwnership( trackedEntityA1, programA, userDetailsB, "test protected program"); + assertTrue(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programA)); assertTrue( trackerOwnershipAccessManager.hasAccess( @@ -338,32 +350,76 @@ void shouldHaveAccessWhenProgramClosedAndUserInCaptureScope() { programB)); } + private static Stream providePrograms() { + return Stream.of(createProgram(OPEN), createProgram(AUDITED), createProgram(CLOSED)); + } + + @ParameterizedTest + @MethodSource("providePrograms") + void shouldFailWhenGrantingTemporaryOwnershipToProgramWithAccessLevelOtherThanProtected( + Program program) { + Exception exception = + Assertions.assertThrows( + ForbiddenException.class, + () -> + trackerOwnershipAccessManager.grantTemporaryOwnership( + trackedEntityA1, program, userDetailsB, "test temporary ownership")); + + assertContains( + "Temporary ownership can only be granted to protected programs.", exception.getMessage()); + } + @Test - void shouldNotHaveAccessWhenProgramClosedAndUserHasTemporaryAccess() { - trackerOwnershipAccessManager.grantTemporaryOwnership( - trackedEntityA1, programB, userDetailsB, "test closed program"); - assertFalse(trackerOwnershipAccessManager.hasAccess(userDetailsB, trackedEntityA1, programB)); - assertFalse( - trackerOwnershipAccessManager.hasAccess( - UserDetails.fromUser(userB), - trackedEntityA1.getUid(), - trackedEntityA1.getOrganisationUnit(), - programB)); + void shouldFailWhenGrantingTemporaryAccessIfUserIsSuperuser() { + Exception exception = + Assertions.assertThrows( + ForbiddenException.class, + () -> + trackerOwnershipAccessManager.grantTemporaryOwnership( + trackedEntityA1, + programA, + UserDetails.fromUser(superUser), + "test temporary ownership")); - injectSecurityContextUser(userB); - ForbiddenException exception = - assertThrows( + assertEquals( + "Temporary ownership not created. Current user is a superuser.", exception.getMessage()); + } + + @Test + void shouldFailWhenGrantingTemporaryAccessIfProgramIsNull() { + Exception exception = + Assertions.assertThrows( ForbiddenException.class, () -> - trackedEntityService.getTrackedEntity( - UID.of(trackedEntityA1), UID.of(programB), defaultParams)); - assertEquals(TrackerOwnershipManager.PROGRAM_ACCESS_CLOSED, exception.getMessage()); + trackerOwnershipAccessManager.grantTemporaryOwnership( + trackedEntityA1, null, userDetailsB, "test temporary ownership")); + + assertEquals( + "Temporary ownership not created. Program supplied does not exist.", + exception.getMessage()); + } + + @Test + void shouldFailWhenGrantingTemporaryAccessIfProgramIsNotTrackerProgram() { + Program eventProgram = createProgram(PROTECTED); + eventProgram.setProgramType(ProgramType.WITHOUT_REGISTRATION); + + Exception exception = + Assertions.assertThrows( + ForbiddenException.class, + () -> + trackerOwnershipAccessManager.grantTemporaryOwnership( + trackedEntityA1, eventProgram, userDetailsB, "test temporary ownership")); + + assertEquals( + "Temporary ownership not created. Program supplied is not a tracker program.", + exception.getMessage()); } @Test void shouldHaveAccessWhenProgramOpenAndUserInScope() throws ForbiddenException, NotFoundException, BadRequestException { - programA.setAccessLevel(AccessLevel.OPEN); + programA.setAccessLevel(OPEN); programService.updateProgram(programA); assertEquals( @@ -374,11 +430,11 @@ void shouldHaveAccessWhenProgramOpenAndUserInScope() @Test void shouldNotHaveAccessWhenProgramOpenAndUserNotInSearchScope() throws ForbiddenException { - programA.setAccessLevel(AccessLevel.OPEN); + programA.setAccessLevel(OPEN); programService.updateProgram(programA); - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityA1, programA, organisationUnitB); injectSecurityContextUser(userA); @@ -394,8 +450,8 @@ void shouldNotHaveAccessWhenProgramOpenAndUserNotInSearchScope() throws Forbidde @Test void shouldHaveAccessWhenProgramNotProvidedAndTEEnrolledButHaveAccessToTEOwner() throws ForbiddenException, NotFoundException, BadRequestException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityA1, programA, organisationUnitB); injectSecurityContextUser(userB); @@ -447,8 +503,8 @@ void shouldNotHaveAccessWhenProgramNotProvidedAndTENotEnrolledAndNoAccessToTeReg @Test void shouldFindTrackedEntityWhenTransferredToAccessibleOrgUnit() throws ForbiddenException, BadRequestException, NotFoundException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); transferOwnership(trackedEntityA1, programA, organisationUnitB); TrackedEntityOperationParams operationParams = createOperationParams(null); injectSecurityContext(userDetailsB); @@ -461,8 +517,8 @@ void shouldFindTrackedEntityWhenTransferredToAccessibleOrgUnit() @Test void shouldFindTrackedEntityWhenTransferredToAccessibleOrgUnitAndSuperUser() throws ForbiddenException, BadRequestException, NotFoundException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); transferOwnership(trackedEntityA1, programA, organisationUnitB); superUser.setOrganisationUnits(Set.of(organisationUnitB)); userService.updateUser(superUser); @@ -477,8 +533,8 @@ void shouldFindTrackedEntityWhenTransferredToAccessibleOrgUnitAndSuperUser() @Test void shouldNotFindTrackedEntityWhenTransferredToInaccessibleOrgUnit() throws ForbiddenException, BadRequestException, NotFoundException { - trackerOwnershipAccessManager.assignOwnership( - trackedEntityA1, programA, organisationUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); transferOwnership(trackedEntityA1, programA, organisationUnitB); TrackedEntityOperationParams operationParams = createOperationParams(null); @@ -531,7 +587,8 @@ void shouldNotTransferOwnershipWhenOrgUnitNotAssociatedToProgram() { @Test void shouldFindTrackedEntityWhenProgramSuppliedAndUserIsOwner() throws ForbiddenException, BadRequestException, NotFoundException { - assignOwnership(trackedEntityA1, programA, organisationUnitA); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); TrackedEntityOperationParams operationParams = createOperationParams(UID.of(programA)); injectSecurityContext(userDetailsA); @@ -543,7 +600,8 @@ void shouldFindTrackedEntityWhenProgramSuppliedAndUserIsOwner() @Test void shouldNotFindTrackedEntityWhenProgramSuppliedAndUserIsNotOwner() throws ForbiddenException, BadRequestException, NotFoundException { - assignOwnership(trackedEntityA1, programA, organisationUnitA); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA1, programA, organisationUnitA); TrackedEntityOperationParams operationParams = createOperationParams(UID.of(programA)); injectSecurityContext(userDetailsB); @@ -556,11 +614,6 @@ private void transferOwnership( trackerOwnershipAccessManager.transferOwnership(trackedEntity, program, orgUnit); } - private void assignOwnership( - TrackedEntity trackedEntity, Program program, OrganisationUnit orgUnit) { - trackerOwnershipAccessManager.assignOwnership(trackedEntity, program, orgUnit, false, true); - } - private TrackedEntityOperationParams createOperationParams(UID programUid) { return TrackedEntityOperationParams.builder() .trackedEntityType(trackedEntityA1.getTrackedEntityType()) @@ -573,4 +626,11 @@ private List getTrackedEntities(TrackedEntityOperationParams params) throws ForbiddenException, BadRequestException, NotFoundException { return uids(trackedEntityService.getTrackedEntities(params)); } + + private static Program createProgram(AccessLevel accessLevel) { + Program program = new Program(); + program.setAccessLevel(accessLevel); + + return program; + } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/relationship/RelationshipServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/relationship/RelationshipServiceTest.java index 07af5aace287..7c470ced7901 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/relationship/RelationshipServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/relationship/RelationshipServiceTest.java @@ -58,6 +58,7 @@ import org.hisp.dhis.test.utils.RelationshipUtils; import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.hisp.dhis.tracker.acl.TrackedEntityProgramOwnerService; import org.hisp.dhis.tracker.acl.TrackerOwnershipManager; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserService; @@ -79,6 +80,8 @@ class RelationshipServiceTest extends PostgresIntegrationTestBase { @Autowired private TrackerOwnershipManager trackerOwnershipAccessManager; + @Autowired private TrackedEntityProgramOwnerService trackedEntityProgramOwnerService; + private Date enrollmentDate; private TrackedEntity teA; @@ -301,8 +304,8 @@ void shouldNotReturnRelationshipWhenTeIsTransferredAndUserHasNoAccessToAtLeastOn manager.save(createEnrollment(program, trackedEntityFrom, orgUnitA)); - trackerOwnershipAccessManager.assignOwnership( - trackedEntityFrom, program, orgUnitA, false, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityFrom, program, orgUnitA); trackerOwnershipAccessManager.transferOwnership(trackedEntityFrom, program, orgUnitB); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java index 1ead5c13e1b2..c02670ca455e 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java @@ -100,7 +100,7 @@ import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.trackedentity.TrackedEntityTypeAttribute; import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; -import org.hisp.dhis.tracker.acl.TrackerOwnershipManager; +import org.hisp.dhis.tracker.acl.TrackedEntityProgramOwnerService; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityOperationParams.TrackedEntityOperationParamsBuilder; import org.hisp.dhis.tracker.trackedentityattributevalue.TrackedEntityAttributeValueService; import org.hisp.dhis.user.User; @@ -123,7 +123,7 @@ class TrackedEntityServiceTest extends PostgresIntegrationTestBase { @Autowired private TrackedEntityAttributeValueService attributeValueService; - @Autowired private TrackerOwnershipManager trackerOwnershipManager; + @Autowired private TrackedEntityProgramOwnerService trackedEntityProgramOwnerService; private User user; @@ -402,8 +402,10 @@ void setUp() { trackedEntityC.setTrackedEntityType(trackedEntityTypeA); manager.save(trackedEntityC, false); - trackerOwnershipManager.assignOwnership(trackedEntityA, programA, orgUnitA, true, true); - trackerOwnershipManager.assignOwnership(trackedEntityA, programB, orgUnitA, true, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA, programA, orgUnitA); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityA, programB, orgUnitA); attributeValueService.addTrackedEntityAttributeValue( new TrackedEntityAttributeValue(teaA, trackedEntityA, "A")); @@ -595,7 +597,8 @@ void shouldReturnTrackedEntitiesGivenUserHasDataReadAccessToTrackedEntityType() void shouldReturnTrackedEntityIncludingAllAttributesEnrollmentsEventsRelationshipsOwners() throws ForbiddenException, NotFoundException, BadRequestException { // this was declared as "remove ownership"; unclear to me how this is removing ownership - trackerOwnershipManager.assignOwnership(trackedEntityA, programB, orgUnitB, true, true); + trackedEntityProgramOwnerService.updateTrackedEntityProgramOwner( + trackedEntityA, programB, orgUnitB); TrackedEntityOperationParams operationParams = TrackedEntityOperationParams.builder() @@ -724,7 +727,7 @@ void shouldReturnEnrollmentsFromSpecifiedProgramWhenRequestingSingleTrackedEntit trackedEntityService.getTrackedEntity( UID.of(trackedEntityA), UID.of(programA), TrackedEntityParams.TRUE); - assertContainsOnly(Set.of(enrollmentA), trackedEntity.getEnrollments()); + assertContainsOnly(Set.of(enrollmentA), trackedEntity.getEnrollments(), Enrollment::getUid); } @Test @@ -740,7 +743,9 @@ void shouldReturnAllEnrollmentsWhenRequestingSingleTrackedEntityAndNoProgramSpec UID.of(trackedEntityA), null, TrackedEntityParams.TRUE); assertContainsOnly( - Set.of(enrollmentA, enrollmentB, enrollmentProgramB), trackedEntity.getEnrollments()); + Set.of(enrollmentA, enrollmentB, enrollmentProgramB), + trackedEntity.getEnrollments(), + Enrollment::getUid); } @Test @@ -1530,8 +1535,8 @@ void shouldNotReturnTrackedEntityRelationshipWhenTEFromItemNotAccessible() manager.save(inaccessibleOrgUnit); makeProgramMetadataInaccessible(programB); makeProgramMetadataInaccessible(programC); - trackerOwnershipManager.assignOwnership( - trackedEntityA, programA, inaccessibleOrgUnit, true, true); + trackedEntityProgramOwnerService.updateTrackedEntityProgramOwner( + trackedEntityA, programA, inaccessibleOrgUnit); TrackedEntityOperationParams operationParams = createOperationParams(orgUnitB, trackedEntityB); injectSecurityContextUser(user); @@ -1553,8 +1558,8 @@ void shouldNotReturnTrackedEntityRelationshipWhenTEToItemNotAccessible() manager.save(inaccessibleOrgUnit); makeProgramMetadataInaccessible(programB); makeProgramMetadataInaccessible(programC); - trackerOwnershipManager.assignOwnership( - trackedEntityB, programA, inaccessibleOrgUnit, true, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityB, programA, inaccessibleOrgUnit); TrackedEntityOperationParams operationParams = createOperationParams(orgUnitA, trackedEntityA); injectSecurityContextUser(user); @@ -1593,8 +1598,8 @@ void shouldNotReturnTrackedEntityRelationshipWhenEnrollmentItemNotAccessible() OrganisationUnit inaccessibleOrgUnit = createOrganisationUnit('D'); manager.save(inaccessibleOrgUnit); makeProgramMetadataInaccessible(programC); - trackerOwnershipManager.assignOwnership( - trackedEntityB, programB, inaccessibleOrgUnit, true, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityB, programB, inaccessibleOrgUnit); TrackedEntityOperationParams operationParams = createOperationParams(orgUnitA, trackedEntityA); injectSecurityContextUser(user); @@ -1633,8 +1638,8 @@ void shouldReturnTrackedEntityRelationshipWhenEventItemNotAccessible() OrganisationUnit inaccessibleOrgUnit = createOrganisationUnit('D'); manager.save(inaccessibleOrgUnit); makeProgramMetadataInaccessible(programC); - trackerOwnershipManager.assignOwnership( - trackedEntityB, programB, inaccessibleOrgUnit, true, true); + trackedEntityProgramOwnerService.createTrackedEntityProgramOwner( + trackedEntityB, programB, inaccessibleOrgUnit); TrackedEntityOperationParams operationParams = createOperationParams(orgUnitA, trackedEntityA); injectSecurityContextUser(user); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventSecurityImportValidationTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventSecurityImportValidationTest.java index 76695043d1dd..e649f3633fb1 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventSecurityImportValidationTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventSecurityImportValidationTest.java @@ -204,7 +204,8 @@ void setUp() throws IOException { manager.save(enrollmentA); maleA.getEnrollments().add(enrollmentA); manager.update(maleA); - trackerOwnershipAccessManager.assignOwnership(maleA, programA, organisationUnitA, false, false); + trackedEntityProgramOwnerService.updateTrackedEntityProgramOwner( + maleA, programA, organisationUnitA); trackedEntityProgramOwnerService.updateTrackedEntityProgramOwner( maleA, programA, organisationUnitA); 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 index 812364bc3b41..95e1ac424b4b 100644 --- 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 @@ -32,6 +32,7 @@ import static org.hisp.dhis.test.webapi.Assertions.assertWebMessage; import static org.hisp.dhis.test.webapi.TestUtils.getMatchingGroupFromPattern; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.JsonProcessingException; @@ -43,6 +44,7 @@ import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; import org.hisp.dhis.test.webapi.json.domain.JsonErrorReport; import org.hisp.dhis.test.webapi.json.domain.JsonObjectReport; @@ -364,4 +366,31 @@ void testCopyProgramWithNoPublicSharing() { assertStatus(HttpStatus.NOT_FOUND, POST("/programs/%s/copy".formatted(PROGRAM_UID))); } + + @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("/programs/%s".formatted(PROGRAM_UID))); + assertStatus(HttpStatus.NOT_FOUND, GET("/programs/%s".formatted(PROGRAM_UID))); + JsonMixed mapview = GET("/mapViews/mVIVRd23Jm9").content().as(JsonMixed.class); + assertFalse(mapview.has("program")); + assertFalse(mapview.has("programStage")); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/JsonAssertions.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/JsonAssertions.java index 44dce4e051b1..b006553f812e 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/JsonAssertions.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/JsonAssertions.java @@ -35,8 +35,10 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import org.hisp.dhis.jsontree.JsonArray; import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; @@ -171,6 +173,24 @@ public static void assertHasMember(JsonObject json, String name) { assertTrue(json.has(name), String.format("member \"%s\" should be in %s", name, json)); } + /** + * Asserts that the actual list contains an element matching the predicate. If the element is + * found, it is returned. + * + * @param actual the list to search + * @param predicate the predicate to match the element + * @param messageSubject the subject of the message in case the element is not found + * @return the element that matches the predicate + * @param the type of the elements in the list + */ + public static T assertContains( + JsonList actual, Predicate predicate, String messageSubject) { + Optional element = actual.stream().filter(predicate).findFirst(); + assertTrue( + element.isPresent(), () -> String.format("%s not found in %s", messageSubject, actual)); + return element.get(); + } + public static void assertContainsAll( Collection expected, JsonList actual, Function toValue) { assertFalse( diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/enrollment/EnrollmentsExportControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/enrollment/EnrollmentsExportControllerTest.java index 75299997d412..c1db357afa70 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/enrollment/EnrollmentsExportControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/enrollment/EnrollmentsExportControllerTest.java @@ -27,254 +27,269 @@ */ package org.hisp.dhis.webapi.controller.tracker.export.enrollment; +import static org.hisp.dhis.test.utils.Assertions.assertNotEmpty; import static org.hisp.dhis.test.utils.Assertions.assertStartsWith; +import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertContains; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasMember; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasNoMember; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasOnlyMembers; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Date; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.Set; -import org.hisp.dhis.category.CategoryOptionCombo; -import org.hisp.dhis.category.CategoryService; -import org.hisp.dhis.common.CodeGenerator; +import java.util.Map; +import java.util.function.Supplier; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.ValueType; -import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleMode; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleParams; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleService; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleValidationService; +import org.hisp.dhis.dxf2.metadata.objectbundle.feedback.ObjectBundleValidationReport; import org.hisp.dhis.eventdatavalue.EventDataValue; import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.importexport.ImportStrategy; import org.hisp.dhis.jsontree.JsonList; -import org.hisp.dhis.note.Note; -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.relationship.Relationship; -import org.hisp.dhis.relationship.RelationshipEntity; -import org.hisp.dhis.relationship.RelationshipItem; -import org.hisp.dhis.relationship.RelationshipType; -import org.hisp.dhis.security.acl.AccessStringHelper; -import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; -import org.hisp.dhis.trackedentity.TrackedEntity; +import org.hisp.dhis.render.RenderFormat; +import org.hisp.dhis.render.RenderService; +import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; import org.hisp.dhis.trackedentity.TrackedEntityAttribute; -import org.hisp.dhis.trackedentity.TrackedEntityType; -import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +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.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Status; +import org.hisp.dhis.tracker.imports.report.ValidationReport; import org.hisp.dhis.user.User; import org.hisp.dhis.webapi.controller.tracker.JsonAttribute; +import org.hisp.dhis.webapi.controller.tracker.JsonDataValue; import org.hisp.dhis.webapi.controller.tracker.JsonEnrollment; import org.hisp.dhis.webapi.controller.tracker.JsonEvent; import org.hisp.dhis.webapi.controller.tracker.JsonNote; import org.hisp.dhis.webapi.controller.tracker.JsonRelationship; -import org.hisp.dhis.webapi.controller.tracker.JsonRelationshipItem; +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.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; import org.springframework.transaction.annotation.Transactional; @Transactional -class EnrollmentsExportControllerTest extends H2ControllerIntegrationTestBase { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class EnrollmentsExportControllerTest extends PostgresControllerIntegrationTestBase { - private static final String ATTRIBUTE_VALUE = "value"; + @Autowired private RenderService renderService; - @Autowired private IdentifiableObjectManager manager; - - @Autowired private CategoryService categoryService; - - private CategoryOptionCombo coc; - - private OrganisationUnit orgUnit; - - private User owner; + @Autowired private ObjectBundleService objectBundleService; - private Program program; + @Autowired private ObjectBundleValidationService objectBundleValidationService; - private TrackedEntity te; + @Autowired private TrackerImportService trackerImportService; - private Enrollment enrollment; - - private TrackedEntityAttribute tea; + @Autowired private IdentifiableObjectManager manager; - private Relationship relationship; + private User importUser; + + protected ObjectBundle setUpMetadata(String path) throws IOException { + Map, List> metadata = + renderService.fromMetadata(new ClassPathResource(path).getInputStream(), RenderFormat.JSON); + ObjectBundleParams params = new ObjectBundleParams(); + params.setObjectBundleMode(ObjectBundleMode.COMMIT); + params.setImportStrategy(ImportStrategy.CREATE); + params.setObjects(metadata); + ObjectBundle bundle = objectBundleService.create(params); + assertNoErrors(objectBundleValidationService.validate(bundle)); + objectBundleService.commit(bundle); + return bundle; + } - private ProgramStage programStage; + protected TrackerObjects fromJson(String path) throws IOException { + return renderService.fromJson( + new ClassPathResource(path).getInputStream(), TrackerObjects.class); + } - private Event event; + @BeforeAll + void setUp() throws IOException { + setUpMetadata("tracker/simple_metadata.json"); - private DataElement dataElement; + importUser = userService.getUser("tTgjgobT1oS"); + injectSecurityContextUser(importUser); - private TrackedEntityAttributeValue trackedEntityAttributeValue; + TrackerImportParams params = TrackerImportParams.builder().build(); + assertNoErrors( + trackerImportService.importTracker(params, fromJson("tracker/event_and_enrollment.json"))); - private EventDataValue eventDataValue; + manager.flush(); + manager.clear(); + } @BeforeEach - void setUp() { - owner = makeUser("o"); - manager.save(owner, false); - - coc = categoryService.getDefaultCategoryOptionCombo(); - - orgUnit = createOrganisationUnit('A'); - manager.save(orgUnit); - - User user = createUserWithId("tester", CodeGenerator.generateUid()); - user.addOrganisationUnit(orgUnit); - user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); - this.userService.updateUser(user); - - program = createProgram('A'); - manager.save(program); - - TrackedEntityType trackedEntityType = createTrackedEntityType('A'); - manager.save(trackedEntityType); - - tea = createTrackedEntityAttribute('A'); - tea.getSharing().setOwner(owner); - manager.save(tea, false); - - te = createTrackedEntity(orgUnit); - te.setTrackedEntityType(trackedEntityType); - manager.save(te); - - trackedEntityAttributeValue = new TrackedEntityAttributeValue(); - trackedEntityAttributeValue.setAttribute(tea); - trackedEntityAttributeValue.setTrackedEntity(te); - trackedEntityAttributeValue.setStoredBy("user"); - trackedEntityAttributeValue.setValue(ATTRIBUTE_VALUE); - te.setTrackedEntityAttributeValues(Set.of(trackedEntityAttributeValue)); - manager.update(te); - - program.setProgramAttributes(List.of(createProgramTrackedEntityAttribute(program, tea))); - - programStage = createProgramStage('A', program); - manager.save(programStage); - - enrollment = enrollment(te); - event = event(); - enrollment.setEvents(Set.of(event)); - manager.update(enrollment); - - manager.save(relationship(enrollment, te)); + void setUpUser() { + switchContextToUser(importUser); } @Test void getEnrollmentById() { - JsonEnrollment enrollment = - GET("/tracker/enrollments/{id}", this.enrollment.getUid()) + Enrollment enrollment = get(Enrollment.class, "TvctPPhpD8z"); + + JsonEnrollment jsonEnrollment = + GET("/tracker/enrollments/{id}", enrollment.getUid()) .content(HttpStatus.OK) .as(JsonEnrollment.class); - assertDefaultResponse(enrollment); + assertDefaultResponse(enrollment, jsonEnrollment); } @Test void getEnrollmentByIdWithFields() { - JsonEnrollment enrollment = - GET("/tracker/enrollments/{id}?fields=orgUnit,status", this.enrollment.getUid()) + Enrollment enrollment = get(Enrollment.class, "TvctPPhpD8z"); + + JsonEnrollment jsonEnrollment = + GET("/tracker/enrollments/{id}?fields=orgUnit,status", enrollment.getUid()) .content(HttpStatus.OK) .as(JsonEnrollment.class); - assertHasOnlyMembers(enrollment, "orgUnit", "status"); - assertEquals(this.enrollment.getOrganisationUnit().getUid(), enrollment.getOrgUnit()); - assertEquals(this.enrollment.getStatus().toString(), enrollment.getStatus()); + assertHasOnlyMembers(jsonEnrollment, "orgUnit", "status"); + assertEquals(enrollment.getOrganisationUnit().getUid(), jsonEnrollment.getOrgUnit()); + assertEquals(enrollment.getStatus().toString(), jsonEnrollment.getStatus()); } @Test void getEnrollmentByIdWithNotes() { - enrollment.setNotes(List.of(note("oqXG28h988k", "my notes", owner.getUid()))); + Enrollment enrollment = get(Enrollment.class, "TvctPPhpD8z"); + assertNotEmpty(enrollment.getNotes(), "test expects an enrollment with notes"); - JsonEnrollment enrollment = - GET("/tracker/enrollments/{uid}?fields=notes", this.enrollment.getUid()) + JsonEnrollment jsonEnrollment = + GET("/tracker/enrollments/{uid}?fields=notes", enrollment.getUid()) .content(HttpStatus.OK) .as(JsonEnrollment.class); - JsonNote note = enrollment.getNotes().get(0); - assertEquals("oqXG28h988k", note.getNote()); - assertEquals("my notes", note.getValue()); - assertEquals(owner.getUid(), note.getStoredBy()); + JsonNote note = jsonEnrollment.getNotes().get(0); + assertEquals("f9423652692", note.getNote()); + assertEquals("enrollment comment value", note.getValue()); } @Test void getEnrollmentByIdWithAttributes() { - JsonEnrollment enrollment = - GET("/tracker/enrollments/{id}?fields=attributes", this.enrollment.getUid()) + Enrollment enrollment = get(Enrollment.class, "TvctPPhpD8z"); + assertNotEmpty( + enrollment.getTrackedEntity().getTrackedEntityAttributeValues(), + "test expects an enrollment with attribute values"); + TrackedEntityAttribute ptea = get(TrackedEntityAttribute.class, "dIVt4l5vIOa"); + + JsonEnrollment jsonEnrollment = + GET("/tracker/enrollments/{id}?fields=attributes", enrollment.getUid()) .content(HttpStatus.OK) .as(JsonEnrollment.class); - assertHasOnlyMembers(enrollment, "attributes"); - JsonAttribute attribute = enrollment.getAttributes().get(0); - assertEquals(tea.getUid(), attribute.getAttribute()); - TrackedEntityAttribute expected = trackedEntityAttributeValue.getAttribute(); - assertEquals(trackedEntityAttributeValue.getValue(), attribute.getValue()); - assertEquals(expected.getValueType().toString(), attribute.getValueType()); + assertHasOnlyMembers(jsonEnrollment, "attributes"); + JsonAttribute attribute = jsonEnrollment.getAttributes().get(0); + assertEquals(ptea.getUid(), attribute.getAttribute()); + assertEquals("Frank PTEA", attribute.getValue()); + assertEquals(ValueType.TEXT.name(), attribute.getValueType()); assertHasMember(attribute, "createdAt"); assertHasMember(attribute, "updatedAt"); assertHasMember(attribute, "displayName"); assertHasMember(attribute, "code"); - assertHasMember(attribute, "storedBy"); } @Test void getEnrollmentByIdWithRelationshipsFields() { - JsonList relationships = + Relationship relationship = get(Relationship.class, "p53a6314631"); + assertNotNull( + relationship.getTo().getEnrollment(), + "test expects relationship to have a 'to' enrollment"); + Enrollment enrollment = relationship.getTo().getEnrollment(); + + JsonList jsonRelationships = GET("/tracker/enrollments/{id}?fields=relationships", enrollment.getUid()) .content(HttpStatus.OK) .getList("relationships", JsonRelationship.class); - JsonRelationship jsonRelationship = relationships.get(0); - assertEquals(relationship.getUid(), jsonRelationship.getRelationship()); - - JsonRelationshipItem.JsonEnrollment enrollment = jsonRelationship.getFrom().getEnrollment(); - assertEquals(relationship.getFrom().getEnrollment().getUid(), enrollment.getEnrollment()); - assertEquals( - relationship.getFrom().getEnrollment().getTrackedEntity().getUid(), - enrollment.getTrackedEntity()); - - JsonRelationshipItem.JsonTrackedEntity trackedEntity = - jsonRelationship.getTo().getTrackedEntity(); - assertEquals( - relationship.getTo().getTrackedEntity().getUid(), trackedEntity.getTrackedEntity()); - - assertHasMember(jsonRelationship, "relationshipName"); - assertHasMember(jsonRelationship, "relationshipType"); - assertHasMember(jsonRelationship, "createdAt"); - assertHasMember(jsonRelationship, "updatedAt"); - assertHasMember(jsonRelationship, "bidirectional"); + JsonRelationship jsonRelationship = + assertContains( + jsonRelationships, + re -> relationship.getUid().equals(re.getRelationship()), + relationship.getUid()); + + assertAll( + "relationship JSON", + () -> + assertEquals( + relationship.getFrom().getTrackedEntity().getUid(), + jsonRelationship.getFrom().getTrackedEntity().getTrackedEntity()), + () -> + assertEquals( + relationship.getTo().getEnrollment().getUid(), + jsonRelationship.getTo().getEnrollment().getEnrollment()), + () -> assertHasMember(jsonRelationship, "relationshipName"), + () -> assertHasMember(jsonRelationship, "relationshipType"), + () -> assertHasMember(jsonRelationship, "createdAt"), + () -> assertHasMember(jsonRelationship, "updatedAt"), + () -> assertHasMember(jsonRelationship, "bidirectional")); } @Test void getEnrollmentByIdWithEventsFields() { - JsonList events = - GET("/tracker/enrollments/{id}?fields=events", enrollment.getUid()) + Event event = get(Event.class, "pTzf9KYMk72"); + assertNotNull(event.getEnrollment(), "test expects an event with an enrollment"); + assertNotEmpty(event.getEventDataValues(), "test expects an event with data values"); + EventDataValue eventDataValue = event.getEventDataValues().iterator().next(); + + JsonList jsonEvents = + GET("/tracker/enrollments/{id}?fields=events", event.getEnrollment().getUid()) .content(HttpStatus.OK) .getList("events", JsonEvent.class); - JsonEvent event = events.get(0); - assertEquals(this.event.getUid(), event.getEvent()); - assertEquals(enrollment.getUid(), event.getEnrollment()); - assertEquals(te.getUid(), event.getTrackedEntity()); - assertEquals(dataElement.getUid(), event.getDataValues().get(0).getDataElement()); - assertEquals(eventDataValue.getValue(), event.getDataValues().get(0).getValue()); - assertEquals(program.getUid(), event.getProgram()); - - assertHasMember(event, "status"); - assertHasMember(event, "followUp"); - assertHasMember(event, "followup"); - assertEquals(program.getUid(), event.getProgram()); - assertEquals(orgUnit.getUid(), event.getOrgUnit()); - assertFalse(event.getDeleted()); + JsonEvent jsonEvent = jsonEvents.get(0); + assertAll( + "event JSON", + () -> assertEquals(event.getUid(), jsonEvent.getEvent()), + () -> assertEquals(event.getEnrollment().getUid(), jsonEvent.getEnrollment()), + () -> + assertEquals( + event.getEnrollment().getTrackedEntity().getUid(), jsonEvent.getTrackedEntity()), + () -> assertEquals(event.getProgramStage().getProgram().getUid(), jsonEvent.getProgram()), + () -> assertEquals(event.getOrganisationUnit().getUid(), jsonEvent.getOrgUnit()), + () -> { + JsonDataValue jsonDataValue = + assertContains( + jsonEvent.getDataValues(), + dv -> eventDataValue.getDataElement().equals(dv.getDataElement()), + eventDataValue.getDataElement()); + assertEquals( + eventDataValue.getValue(), + jsonDataValue.getValue(), + "data value for data element " + eventDataValue.getDataElement()); + }, + () -> assertHasMember(jsonEvent, "status"), + () -> assertHasMember(jsonEvent, "followUp"), + () -> assertHasMember(jsonEvent, "followup"), + () -> assertEquals(event.isDeleted(), jsonEvent.getDeleted())); } @Test void getEnrollmentByIdWithExcludedFields() { + Event event = get(Event.class, "pTzf9KYMk72"); + assertNotNull(event.getEnrollment(), "test expects an event with an enrollment"); + assertNotNull( + event.getRelationshipItems(), "test expects an event with at least one relationship"); + assertTrue( (GET( "/tracker/enrollments/{id}?fields=!attributes,!relationships,!events", - enrollment.getUid()) + event.getEnrollment().getUid()) .content(HttpStatus.OK)) .isEmpty()); } @@ -295,89 +310,66 @@ void getEnrollmentsFailsIfGivenEnrollmentAndEnrollmentsParameters() { .getMessage()); } - private Event event() { - Event eventA = new Event(enrollment, programStage, enrollment.getOrganisationUnit(), coc); - eventA.setAutoFields(); - - eventDataValue = new EventDataValue(); - eventDataValue.setValue("value"); - dataElement = createDataElement('A'); - dataElement.setValueType(ValueType.TEXT); - manager.save(dataElement); - eventDataValue.setDataElement(dataElement.getUid()); - Set eventDataValues = Set.of(eventDataValue); - eventA.setEventDataValues(eventDataValues); - manager.save(eventA); - return eventA; + private void assertDefaultResponse(Enrollment expected, JsonEnrollment actual) { + assertFalse(actual.isEmpty()); + assertEquals(expected.getUid(), actual.getEnrollment()); + assertEquals(expected.getTrackedEntity().getUid(), actual.getTrackedEntity()); + assertEquals(expected.getProgram().getUid(), actual.getProgram()); + assertEquals(expected.getStatus().name(), actual.getStatus()); + assertEquals(expected.getOrganisationUnit().getUid(), actual.getOrgUnit()); + assertEquals(expected.getFollowup(), actual.getBoolean("followUp").bool()); + assertEquals(expected.isDeleted(), actual.getBoolean("deleted").bool()); + assertHasMember(actual, "enrolledAt"); + assertHasMember(actual, "occurredAt"); + assertHasMember(actual, "createdAt"); + assertHasMember(actual, "createdAtClient"); + assertHasMember(actual, "updatedAt"); + assertHasMember(actual, "notes"); + assertHasNoMember(actual, "relationships"); + assertHasNoMember(actual, "events"); + assertHasNoMember(actual, "attributes"); } - private Relationship relationship(Enrollment from, TrackedEntity to) { - relationship = new Relationship(); - - RelationshipItem fromItem = new RelationshipItem(); - fromItem.setEnrollment(from); - from.getRelationshipItems().add(fromItem); - relationship.setFrom(fromItem); - fromItem.setRelationship(relationship); - - RelationshipItem toItem = new RelationshipItem(); - toItem.setTrackedEntity(to); - to.getRelationshipItems().add(toItem); - relationship.setTo(toItem); - toItem.setRelationship(relationship); - - RelationshipType type = createRelationshipType('A'); - type.getFromConstraint().setRelationshipEntity(RelationshipEntity.PROGRAM_INSTANCE); - type.getToConstraint().setRelationshipEntity(RelationshipEntity.TRACKED_ENTITY_INSTANCE); - type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); - manager.save(type, false); - - relationship.setRelationshipType(type); - relationship.setKey(type.getUid()); - relationship.setInvertedKey(type.getUid()); - relationship.setAutoFields(); - - manager.save(relationship, false); - return relationship; + private T get(Class type, String uid) { + T t = manager.get(type, uid); + assertNotNull( + t, + () -> + String.format( + "'%s' with uid '%s' should have been created", type.getSimpleName(), uid)); + return t; } - private void assertDefaultResponse(JsonEnrollment enrollment) { - assertFalse(enrollment.isEmpty()); - assertEquals(this.enrollment.getUid(), enrollment.getEnrollment()); - assertEquals(te.getUid(), enrollment.getTrackedEntity()); - assertEquals(program.getUid(), enrollment.getProgram()); - assertEquals("COMPLETED", enrollment.getStatus()); - assertEquals(orgUnit.getUid(), enrollment.getOrgUnit()); - assertTrue(enrollment.getBoolean("followUp").bool()); - assertFalse(enrollment.getBoolean("deleted").bool()); - assertHasMember(enrollment, "enrolledAt"); - assertHasMember(enrollment, "occurredAt"); - assertHasMember(enrollment, "createdAt"); - assertHasMember(enrollment, "createdAtClient"); - assertHasMember(enrollment, "updatedAt"); - assertHasMember(enrollment, "notes"); - assertHasNoMember(enrollment, "relationships"); - assertHasNoMember(enrollment, "events"); - assertHasNoMember(enrollment, "attributes"); + public static void assertNoErrors(ImportReport report) { + assertNotNull(report); + assertEquals( + Status.OK, + report.getStatus(), + errorMessage( + "Expected import with status OK, instead got:%n", report.getValidationReport())); } - private Enrollment enrollment(TrackedEntity te) { - Enrollment enrollment = new Enrollment(program, te, orgUnit); - enrollment.setAutoFields(); - enrollment.setEnrollmentDate(new Date()); - enrollment.setOccurredDate(new Date()); - enrollment.setStatus(EnrollmentStatus.COMPLETED); - enrollment.setFollowup(true); - manager.save(enrollment, false); - te.setEnrollments(Set.of(enrollment)); - manager.save(te, false); - return enrollment; + private static Supplier errorMessage(String errorTitle, ValidationReport report) { + return () -> { + StringBuilder msg = new StringBuilder(errorTitle); + report + .getErrors() + .forEach( + e -> { + msg.append(e.getErrorCode()); + msg.append(": "); + msg.append(e.getMessage()); + msg.append('\n'); + }); + return msg.toString(); + }; } - private Note note(String uid, String value, String storedBy) { - Note note = new Note(value, storedBy); - note.setUid(uid); - manager.save(note, false); - return note; + public static void assertNoErrors(ObjectBundleValidationReport report) { + assertNotNull(report); + List errors = new ArrayList<>(); + report.forEachErrorReport(err -> errors.add(err.toString())); + assertFalse( + report.hasErrorReports(), String.format("Expected no errors, instead got: %s%n", errors)); } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/relationship/RelationshipsExportControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/relationship/RelationshipsExportControllerTest.java index b10ce8864dcd..9261a496081b 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/relationship/RelationshipsExportControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/relationship/RelationshipsExportControllerTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.webapi.controller.tracker.export.relationship; +import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.test.utils.Assertions.assertStartsWith; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertContainsAll; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertEnrollmentWithinRelationship; @@ -652,7 +653,11 @@ void getRelationshipsByTrackedEntityWithEnrollments() { } @Test - void getRelationshipsByTrackedEntityAndEnrollmentWithAttributes() { + void getRelationshipsByTrackedEntityAndEnrollmentWithAttributesIsEmpty() { + // Tracked entity attribute values are owned by the tracked entity and only mapped onto the + // enrollment on export. Program tracked entity attributes are only returned by the underlying + // TE service if a program is + // provided which is not possible on the relationship endpoint. TrackedEntity to = trackedEntity(orgUnit); to.setTrackedEntityAttributeValues( Set.of(attributeValue(tea, to, "12"), attributeValue(tea2, to, "24"))); @@ -689,13 +694,13 @@ void getRelationshipsByTrackedEntityAndEnrollmentWithAttributes() { JsonList enrollmentAttr = relationships.get(0).getFrom().getEnrollment().getAttributes(); - assertContainsAll(List.of(tea2.getUid()), enrollmentAttr, JsonAttribute::getAttribute); - assertContainsAll(List.of("24"), enrollmentAttr, JsonAttribute::getValue); + assertIsEmpty( + enrollmentAttr.toList(JsonAttribute::getAttribute), + "program attributes should not be returned as no program can be provided"); JsonList teAttributes = relationships.get(0).getTo().getTrackedEntity().getAttributes(); - assertContainsAll( - List.of(tea.getUid(), tea2.getUid()), teAttributes, JsonAttribute::getAttribute); - assertContainsAll(List.of("12", "24"), teAttributes, JsonAttribute::getValue); + assertContainsAll(List.of(tea.getUid()), teAttributes, JsonAttribute::getAttribute); + assertContainsAll(List.of("12"), teAttributes, JsonAttribute::getValue); } @Test diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntitiesExportControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntitiesExportControllerTest.java index 2491f75e691d..89cea37bf476 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntitiesExportControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntitiesExportControllerTest.java @@ -29,6 +29,10 @@ import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ACCESSIBLE; import static org.hisp.dhis.http.HttpClientAdapter.Accept; +import static org.hisp.dhis.test.utils.Assertions.assertContains; +import static org.hisp.dhis.test.utils.Assertions.assertHasSize; +import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; +import static org.hisp.dhis.test.utils.Assertions.assertNotEmpty; import static org.hisp.dhis.test.utils.Assertions.assertStartsWith; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertContainsAll; import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertFirstRelationship; @@ -41,69 +45,129 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; -import org.hisp.dhis.category.CategoryOptionCombo; -import org.hisp.dhis.category.CategoryService; +import java.util.function.Supplier; import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.ValueType; -import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.eventdatavalue.EventDataValue; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleMode; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleParams; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleService; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleValidationService; +import org.hisp.dhis.dxf2.metadata.objectbundle.feedback.ObjectBundleValidationReport; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.fileresource.FileResource; import org.hisp.dhis.fileresource.FileResourceService; import org.hisp.dhis.fileresource.FileResourceStorageStatus; import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.importexport.ImportStrategy; import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Enrollment; import org.hisp.dhis.program.Event; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.UserInfoSnapshot; import org.hisp.dhis.relationship.Relationship; import org.hisp.dhis.relationship.RelationshipEntity; import org.hisp.dhis.relationship.RelationshipItem; import org.hisp.dhis.relationship.RelationshipType; +import org.hisp.dhis.render.RenderFormat; +import org.hisp.dhis.render.RenderService; import org.hisp.dhis.security.acl.AccessStringHelper; -import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; +import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.trackedentity.TrackedEntityTypeAttribute; import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +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.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Status; +import org.hisp.dhis.tracker.imports.report.ValidationReport; import org.hisp.dhis.user.User; import org.hisp.dhis.user.sharing.UserAccess; import org.hisp.dhis.util.DateUtils; import org.hisp.dhis.webapi.controller.tracker.JsonAttribute; -import org.hisp.dhis.webapi.controller.tracker.JsonDataValue; import org.hisp.dhis.webapi.controller.tracker.JsonEnrollment; import org.hisp.dhis.webapi.controller.tracker.JsonEvent; import org.hisp.dhis.webapi.controller.tracker.JsonRelationship; import org.hisp.dhis.webapi.controller.tracker.JsonRelationshipItem; import org.hisp.dhis.webapi.controller.tracker.JsonTrackedEntity; import org.hisp.dhis.webapi.utils.ContextUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; import org.springframework.transaction.annotation.Transactional; @Transactional -class TrackedEntitiesExportControllerTest extends H2ControllerIntegrationTestBase { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TrackedEntitiesExportControllerTest extends PostgresControllerIntegrationTestBase { // Used to generate unique chars for creating test objects like TEA, ... private static final String UNIQUE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private static final String EVENT_OCCURRED_AT = "2023-03-23T12:23:00.000"; + + @Autowired private RenderService renderService; + + @Autowired private ObjectBundleService objectBundleService; + + @Autowired private ObjectBundleValidationService objectBundleValidationService; + + @Autowired private TrackerImportService trackerImportService; @Autowired private IdentifiableObjectManager manager; - @Autowired private FileResourceService fileResourceService; + private User importUser; + + protected ObjectBundle setUpMetadata(String path) throws IOException { + Map, List> metadata = + renderService.fromMetadata(new ClassPathResource(path).getInputStream(), RenderFormat.JSON); + ObjectBundleParams params = new ObjectBundleParams(); + params.setObjectBundleMode(ObjectBundleMode.COMMIT); + params.setImportStrategy(ImportStrategy.CREATE); + params.setObjects(metadata); + ObjectBundle bundle = objectBundleService.create(params); + assertNoErrors(objectBundleValidationService.validate(bundle)); + objectBundleService.commit(bundle); + return bundle; + } + + protected TrackerObjects fromJson(String path) throws IOException { + return renderService.fromJson( + new ClassPathResource(path).getInputStream(), TrackerObjects.class); + } + + @BeforeAll + void setUp() throws IOException { + setUpMetadata("tracker/simple_metadata.json"); - @Autowired private CategoryService categoryService; + importUser = userService.getUser("tTgjgobT1oS"); + injectSecurityContextUser(importUser); - private CategoryOptionCombo coc; + TrackerImportParams params = TrackerImportParams.builder().build(); + assertNoErrors( + trackerImportService.importTracker(params, fromJson("tracker/event_and_enrollment.json"))); + + manager.flush(); + manager.clear(); + } + + @BeforeEach + void setUpUser() { + switchContextToUser(importUser); + } + + @Autowired private FileResourceService fileResourceService; private OrganisationUnit orgUnit; @@ -115,8 +179,6 @@ class TrackedEntitiesExportControllerTest extends H2ControllerIntegrationTestBas private TrackedEntityType trackedEntityType; - private DataElement dataElement; - private User owner; private User user; @@ -127,11 +189,9 @@ class TrackedEntitiesExportControllerTest extends H2ControllerIntegrationTestBas private int uniqueAttributeCharCounter = 0; @BeforeEach - void setUp() { + void setUpToBeMigrated() { owner = makeUser("owner"); - coc = categoryService.getDefaultCategoryOptionCombo(); - orgUnit = createOrganisationUnit('A'); orgUnit.getSharing().setOwner(owner); manager.save(orgUnit, false); @@ -167,8 +227,6 @@ void setUp() { @Test void getTrackedEntitiesNeedsProgramOrType() { - injectSecurityContextUser(user); - assertEquals( "Either `program`, `trackedEntityType` or `trackedEntities` should be specified", GET("/tracker/trackedEntities").error(HttpStatus.BAD_REQUEST).getMessage()); @@ -176,8 +234,6 @@ void getTrackedEntitiesNeedsProgramOrType() { @Test void getTrackedEntitiesNeedsProgramOrTrackedEntityType() { - this.switchContextToUser(user); - assertEquals( "Either `program`, `trackedEntityType` or `trackedEntities` should be specified", GET("/tracker/trackedEntities?orgUnit={ou}", orgUnit.getUid()) @@ -188,7 +244,7 @@ void getTrackedEntitiesNeedsProgramOrTrackedEntityType() { @Test void shouldReturnEmptyListWhenGettingTrackedEntitiesWithNoMatchingParams() { LocalDate futureDate = LocalDate.now().plusYears(1); - JsonList instances = + JsonList trackedEntities = GET("/tracker/trackedEntities?trackedEntityType=" + trackedEntityType.getUid() + "&ouMode=ALL" @@ -198,13 +254,12 @@ void shouldReturnEmptyListWhenGettingTrackedEntitiesWithNoMatchingParams() { .content(HttpStatus.OK) .getList("trackedEntities", JsonTrackedEntity.class); - assertEquals(0, instances.size()); + assertEquals(0, trackedEntities.size()); } @Test void getTrackedEntityById() { - TrackedEntity te = trackedEntity(); - this.switchContextToUser(user); + TrackedEntity te = get(TrackedEntity.class, "QS6w44flWAf"); JsonTrackedEntity json = GET("/tracker/trackedEntities/{id}", te.getUid()) @@ -226,8 +281,7 @@ void getTrackedEntityById() { @Test void getTrackedEntityByIdWithFields() { - TrackedEntity te = trackedEntity(); - this.switchContextToUser(user); + TrackedEntity te = get(TrackedEntity.class, "QS6w44flWAf"); JsonTrackedEntity json = GET("/tracker/trackedEntities/{id}?fields=trackedEntityType,orgUnit", te.getUid()) @@ -241,77 +295,75 @@ void getTrackedEntityByIdWithFields() { @Test void getTrackedEntityByIdWithAttributesReturnsTrackedEntityTypeAttributesOnly() { - TrackedEntity trackedEntity = trackedEntity(); - enroll(trackedEntity, program, orgUnit); - - TrackedEntityAttribute tea = - addTrackedEntityTypeAttributeValue(trackedEntity, ValueType.NUMBER, "12"); - addProgramAttributeValue(trackedEntity, program, ValueType.NUMBER, "24"); + TrackedEntity te = get(TrackedEntity.class, "dUE514NMOlo"); + // TETA + TrackedEntityAttribute tea1 = get(TrackedEntityAttribute.class, "numericAttr"); + TrackedEntityAttribute tea2 = get(TrackedEntityAttribute.class, "toUpdate000"); JsonList attributes = - GET( - "/tracker/trackedEntities/{id}?fields=attributes[attribute,value]", - trackedEntity.getUid()) + GET("/tracker/trackedEntities/{id}?fields=attributes[attribute,value]", te.getUid()) .content(HttpStatus.OK) .getList("attributes", JsonAttribute.class); - assertAll( - "include tracked entity type attributes only if no program query param is given", - () -> - assertEquals( - 1, - attributes.size(), - () -> String.format("expected 1 attribute instead got %s", attributes)), - () -> assertEquals(tea.getUid(), attributes.get(0).getAttribute()), - () -> assertEquals("12", attributes.get(0).getValue())); + assertContainsAll( + List.of(tea1.getUid(), tea2.getUid()), attributes, JsonAttribute::getAttribute); + assertContainsAll(List.of("rainy day", "70"), attributes, JsonAttribute::getValue); } @Test void getTrackedEntityByIdWithAttributesReturnsAllAttributes() { - TrackedEntity trackedEntity = trackedEntity(); - enroll(trackedEntity, program, orgUnit); - - TrackedEntityAttribute tea = - addTrackedEntityTypeAttributeValue(trackedEntity, ValueType.NUMBER, "12"); - TrackedEntityAttribute tea2 = - addProgramAttributeValue(trackedEntity, program, ValueType.NUMBER, "24"); + TrackedEntity te = get(TrackedEntity.class, "dUE514NMOlo"); + assertNotEmpty(te.getEnrollments(), "test expects a tracked entity with an enrollment"); + String program = te.getEnrollments().iterator().next().getProgram().getUid(); + // TETA + TrackedEntityAttribute tea1 = get(TrackedEntityAttribute.class, "numericAttr"); + TrackedEntityAttribute tea2 = get(TrackedEntityAttribute.class, "toUpdate000"); + // PTEA + TrackedEntityAttribute tea3 = get(TrackedEntityAttribute.class, "dIVt4l5vIOa"); JsonList attributes = GET( "/tracker/trackedEntities/{id}?program={id}&fields=attributes[attribute,value]", - trackedEntity.getUid(), - program.getUid()) + te.getUid(), + program) .content(HttpStatus.OK) .getList("attributes", JsonAttribute.class); assertContainsAll( - List.of(tea.getUid(), tea2.getUid()), attributes, JsonAttribute::getAttribute); - assertContainsAll(List.of("12", "24"), attributes, JsonAttribute::getValue); + List.of(tea1.getUid(), tea2.getUid(), tea3.getUid()), + attributes, + JsonAttribute::getAttribute); + assertContainsAll( + List.of("rainy day", "70", "Frank PTEA"), attributes, JsonAttribute::getValue); } @Test void getTrackedEntityByIdWithFieldsRelationships() { - TrackedEntity from = trackedEntity(); - TrackedEntity to = trackedEntity(); - Relationship r = relationship(from, to); - this.switchContextToUser(user); + TrackedEntity from = get(TrackedEntity.class, "mHWCacsGYYn"); + assertHasSize( + 1, from.getRelationshipItems(), "test expects a tracked entity with one relationship"); + RelationshipItem relItem = from.getRelationshipItems().iterator().next(); + Relationship r = get(Relationship.class, relItem.getRelationship().getUid()); + Event to = r.getTo().getEvent(); JsonList rels = GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) .content(HttpStatus.OK) .getList("relationships", JsonRelationship.class); - assertEquals(1, rels.size()); JsonRelationship relationship = assertFirstRelationship(r, rels); assertTrackedEntityWithinRelationship(from, relationship.getFrom()); assertTrackedEntityWithinRelationship(to, relationship.getTo()); } @Test - void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipType() { - TrackedEntity from = trackedEntity(); - TrackedEntity to = trackedEntity(); - relationship(relationshipTypeNotAccessible(), fromTrackedEntity(from), toTrackedEntity(to)); + void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipItemTo() { + TrackedEntity from = get(TrackedEntity.class, "mHWCacsGYYn"); + assertNotEmpty( + from.getRelationshipItems(), + "test expects a tracked entity with at least one relationship"); + + User user = userService.getUser("Z7870757a75"); this.switchContextToUser(user); JsonList relationships = @@ -322,18 +374,20 @@ void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipType() { assertEquals( 0, relationships.size(), - "user needs access to relationship type to access the relationship"); + "user needs access to from and to items to access the relationship"); } @Test - void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipItemTo() { - TrackedEntity from = trackedEntity(); - TrackedEntity to = trackedEntityNotInSearchScope(); - relationship(from, to); + void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipItemFrom() { + TrackedEntity to = get(TrackedEntity.class, "QesgJkTyTCk"); + assertNotEmpty( + to.getRelationshipItems(), "test expects a tracked entity with at least one relationship"); + + User user = userService.getUser("Z7870757a75"); this.switchContextToUser(user); JsonList relationships = - GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) + GET("/tracker/trackedEntities/{id}?fields=relationships", to.getUid()) .content(HttpStatus.OK) .getList("relationships", JsonRelationship.class); @@ -343,34 +397,6 @@ void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipItemTo() { "user needs access to from and to items to access the relationship"); } - @Test - void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToBothRelationshipItems() { - TrackedEntity from = trackedEntityNotInSearchScope(); - TrackedEntity to = trackedEntityNotInSearchScope(); - relationship(from, to); - this.switchContextToUser(user); - - assertTrue( - GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) - .error(HttpStatus.FORBIDDEN) - .getMessage() - .contains("User has no access to TrackedEntity")); - } - - @Test - void getTrackedEntityByIdWithFieldsRelationshipsNoAccessToRelationshipItemFrom() { - TrackedEntity from = trackedEntityNotInSearchScope(); - TrackedEntity to = trackedEntity(); - relationship(from, to); - this.switchContextToUser(user); - - assertTrue( - GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) - .error(HttpStatus.FORBIDDEN) - .getMessage() - .contains("User has no access to TrackedEntity")); - } - @Test void getTrackedEntityByIdyWithFieldsRelationshipsNoAccessToTrackedEntityType() { TrackedEntityType type = trackedEntityTypeNotAccessible(); @@ -379,11 +405,8 @@ void getTrackedEntityByIdyWithFieldsRelationshipsNoAccessToTrackedEntityType() { relationship(from, to); this.switchContextToUser(user); - assertTrue( - GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) - .error(HttpStatus.FORBIDDEN) - .getMessage() - .contains("User has no access to TrackedEntity")); + GET("/tracker/trackedEntities/{id}?fields=relationships", from.getUid()) + .error(HttpStatus.FORBIDDEN); } @Test @@ -402,7 +425,7 @@ void shouldReturnNotFoundWhenGettingASoftDeletedTrackedEntityById() { @Test void getTrackedEntityReturnsCsvFormat() { - injectSecurityContextUser(user); + Program program = get(Program.class, "BFcipDERJnf"); HttpResponse response = GET( @@ -423,8 +446,15 @@ void getTrackedEntityReturnsCsvFormat() { @Test void getTrackedEntityCsvById() { - TrackedEntity te = trackedEntity(); - this.switchContextToUser(user); + TrackedEntity te = get(TrackedEntity.class, "QS6w44flWAf"); + List trackedEntityTypeAttributeValues = + te.getTrackedEntityAttributeValues().stream() + .filter(teav -> !"toDelete000".equals(teav.getAttribute().getUid())) + .toList(); + assertHasSize( + 2, + trackedEntityTypeAttributeValues, + "test expects the tracked entity to have 2 tracked entity type attribute values"); HttpResponse response = GET("/tracker/trackedEntities/{id}", te.getUid(), Accept(ContextUtils.CONTENT_TYPE_CSV)); @@ -433,27 +463,45 @@ void getTrackedEntityCsvById() { assertTrue(response.header("content-type").contains(ContextUtils.CONTENT_TYPE_CSV)); assertTrue(response.header("content-disposition").contains("filename=trackedEntity.csv")); - assertEquals(trackedEntityToCsv(te), csvResponse); - } - - String trackedEntityToCsv(TrackedEntity te) { - return """ - trackedEntity,trackedEntityType,createdAt,createdAtClient,updatedAt,updatedAtClient,orgUnit,inactive,deleted,potentialDuplicate,geometry,latitude,longitude,storedBy,createdBy,updatedBy,attrCreatedAt,attrUpdatedAt,attribute,displayName,value,valueType - """ - .concat( - String.join( - ",", - te.getUid(), - te.getTrackedEntityType().getUid(), - DateUtils.instantFromDate(te.getCreated()).toString(), - DateUtils.instantFromDate(te.getCreatedAtClient()).toString(), - DateUtils.instantFromDate(te.getLastUpdated()).toString(), - DateUtils.instantFromDate(te.getLastUpdatedAtClient()).toString(), - te.getOrganisationUnit().getUid(), - Boolean.toString(te.isInactive()), - Boolean.toString(te.isDeleted()), - Boolean.toString(te.isPotentialDuplicate()), - ",,,,,,,,,,," + "\n")); + assertStartsWith( + """ +trackedEntity,trackedEntityType,createdAt,createdAtClient,updatedAt,updatedAtClient,orgUnit,inactive,deleted,potentialDuplicate,geometry,latitude,longitude,storedBy,createdBy,updatedBy,attrCreatedAt,attrUpdatedAt,attribute,displayName,value,valueType +""", + csvResponse); + // TEAV order is not deterministic + assertContains(trackedEntityToCsv(te, trackedEntityTypeAttributeValues.get(0)), csvResponse); + assertContains(trackedEntityToCsv(te, trackedEntityTypeAttributeValues.get(1)), csvResponse); + } + + String trackedEntityToCsv(TrackedEntity te, TrackedEntityAttributeValue attributeValue) { + String value = attributeValue.getValue(); + if (attributeValue.getAttribute().getValueType() == ValueType.TEXT) { + value = "\"" + value + "\""; + } + return String.join( + ",", + te.getUid(), + te.getTrackedEntityType().getUid(), + DateUtils.instantFromDate(te.getCreated()).toString(), + DateUtils.instantFromDate(te.getCreatedAtClient()).toString(), + DateUtils.instantFromDate(te.getLastUpdated()).toString(), + DateUtils.instantFromDate(te.getLastUpdatedAtClient()).toString(), + te.getOrganisationUnit().getUid(), + Boolean.toString(te.isInactive()), + Boolean.toString(te.isDeleted()), + Boolean.toString(te.isPotentialDuplicate()), + ",,,", + importUser.getUsername(), + importUser.getUsername()) + + "," + + String.join( + ",", + DateUtils.instantFromDate(attributeValue.getCreated()).toString(), + DateUtils.instantFromDate(attributeValue.getLastUpdated()).toString(), + attributeValue.getAttribute().getUid(), + attributeValue.getAttribute().getDisplayName(), + value, + attributeValue.getAttribute().getValueType().name()); } @Test @@ -503,105 +551,97 @@ void getTrackedEntityReturnsCsvGZipFormat() { @Test void shouldGetEnrollmentWhenFieldsHasEnrollments() { - TrackedEntity trackedEntity = trackedEntity(); - Enrollment enrollment = enroll(trackedEntity, program, orgUnit); + TrackedEntity te = get(TrackedEntity.class, "dUE514NMOlo"); + assertHasSize(1, te.getEnrollments(), "test expects a tracked entity with one enrollment"); + Enrollment enrollment = te.getEnrollments().iterator().next(); JsonList json = - GET("/tracker/trackedEntities/{id}?fields=enrollments", trackedEntity.getUid()) + GET("/tracker/trackedEntities/{id}?fields=enrollments", te.getUid()) .content(HttpStatus.OK) .getList("enrollments", JsonEnrollment.class); - JsonEnrollment jsonEnrollment = assertDefaultEnrollmentResponse(json, enrollment); - - assertTrue(jsonEnrollment.getArray("relationships").isEmpty()); - assertTrue(jsonEnrollment.getAttributes().isEmpty()); - assertTrue(jsonEnrollment.getEvents().isEmpty()); + assertDefaultEnrollmentResponse(json, enrollment); } @Test void shouldGetNoEventRelationshipsWhenEventsHasNoRelationshipsAndFieldsIncludeAll() { - TrackedEntity trackedEntity = trackedEntity(); - - Enrollment enrollment = enroll(trackedEntity, program, orgUnit); - - Event event = eventWithDataValue(enrollment); - - enrollment.getEvents().add(event); - manager.update(enrollment); + TrackedEntity te = get(TrackedEntity.class, "mHWCacsGYYn"); + assertHasSize(1, te.getEnrollments(), "test expects a tracked entity with one enrollment"); + Enrollment enrollment = te.getEnrollments().iterator().next(); + assertHasSize(1, enrollment.getEvents(), "test expects an enrollment with one event"); + Event event = enrollment.getEvents().iterator().next(); + assertIsEmpty(event.getRelationshipItems(), "test expects an event with no relationships"); JsonList json = - GET("/tracker/trackedEntities/{id}?fields=enrollments", trackedEntity.getUid()) + GET("/tracker/trackedEntities/{id}?fields=enrollments", te.getUid()) .content(HttpStatus.OK) .getList("enrollments", JsonEnrollment.class); JsonEnrollment jsonEnrollment = assertDefaultEnrollmentResponse(json, enrollment); - assertTrue(jsonEnrollment.getArray("relationships").isEmpty()); - assertTrue(jsonEnrollment.getAttributes().isEmpty()); - JsonEvent jsonEvent = assertDefaultEventResponse(jsonEnrollment, event); - assertTrue(jsonEvent.getRelationships().isEmpty()); } - @Disabled( - "TODO(DHIS2-18541) test fixtures will be fixed in next PR: org.hisp.dhis.feedback.ForbiddenException: User needs to be assigned either search or data capture org units") @Test void shouldGetEventRelationshipsWhenEventHasRelationshipsAndFieldsIncludeEventRelationships() { - TrackedEntity trackedEntity = trackedEntity(); - - Enrollment enrollment = enroll(trackedEntity, program, orgUnit); - - Event event = eventWithDataValue(enrollment); - enrollment.getEvents().add(event); - manager.update(enrollment); - - Relationship teToEventRelationship = relationship(trackedEntity, event); + TrackedEntity te = get(TrackedEntity.class, "QS6w44flWAf"); + Enrollment enrollment = get(Enrollment.class, "nxP7UnKhomJ"); + assertHasSize(1, enrollment.getEvents(), "test expects an enrollment with one event"); + Event event = enrollment.getEvents().iterator().next(); + assertNotEmpty( + event.getRelationshipItems(), "test expects an event with at least one relationship"); + RelationshipItem relItem = te.getRelationshipItems().iterator().next(); + Relationship r = get(Relationship.class, relItem.getRelationship().getUid()); JsonList json = - GET("/tracker/trackedEntities/{id}?fields=enrollments", trackedEntity.getUid()) + GET("/tracker/trackedEntities/{id}?fields=enrollments", te.getUid()) .content(HttpStatus.OK) .getList("enrollments", JsonEnrollment.class); - JsonEnrollment jsonEnrollment = assertDefaultEnrollmentResponse(json, enrollment); - assertTrue(jsonEnrollment.getAttributes().isEmpty()); - assertTrue(jsonEnrollment.getArray("relationships").isEmpty()); + List enrollments = + json.stream().filter(en -> "nxP7UnKhomJ".equals(en.getEnrollment())).toList(); + assertNotEmpty( + enrollments, + () -> String.format("Expected enrollment \"nxP7UnKhomJ\" instead got %s", enrollments)); + JsonEnrollment jsonEnrollment = enrollments.get(0); + assertDefaultEnrollmentResponse(enrollment, jsonEnrollment); JsonEvent jsonEvent = assertDefaultEventResponse(jsonEnrollment, event); - - JsonRelationship relationship = jsonEvent.getRelationships().get(0); - - assertEquals(teToEventRelationship.getUid(), relationship.getRelationship()); - assertEquals( - trackedEntity.getUid(), relationship.getFrom().getTrackedEntity().getTrackedEntity()); - assertEquals(event.getUid(), relationship.getTo().getEvent().getEvent()); + assertTrue( + jsonEvent + .getRelationships() + .contains(JsonRelationship::getRelationship, actual -> r.getUid().equals(actual)), + () -> + String.format( + "Expected event to have relationship to TE instead got %s", + jsonEvent.getRelationships().toJson())); } @Test void shouldGetNoEventRelationshipsWhenEventHasRelationshipsAndFieldsExcludeEventRelationships() { - TrackedEntity trackedEntity = trackedEntity(); - - Enrollment enrollment = enroll(trackedEntity, program, orgUnit); - - Event event = eventWithDataValue(enrollment); - - enrollment.getEvents().add(event); - manager.update(enrollment); - - relationship(trackedEntity, event); + TrackedEntity te = get(TrackedEntity.class, "QS6w44flWAf"); + Enrollment enrollment = get(Enrollment.class, "nxP7UnKhomJ"); + assertHasSize(1, enrollment.getEvents(), "test expects an enrollment with one event"); + Event event = enrollment.getEvents().iterator().next(); + assertNotEmpty( + event.getRelationshipItems(), "test expects an event with at least one relationship"); JsonList json = GET( "/tracker/trackedEntities/{id}?fields=enrollments[*,events[!relationships]]", - trackedEntity.getUid()) + te.getUid()) .content(HttpStatus.OK) .getList("enrollments", JsonEnrollment.class); - JsonEnrollment jsonEnrollment = assertDefaultEnrollmentResponse(json, enrollment); - assertTrue(jsonEnrollment.getAttributes().isEmpty()); - assertTrue(jsonEnrollment.getArray("relationships").isEmpty()); + List enrollments = + json.stream().filter(en -> "nxP7UnKhomJ".equals(en.getEnrollment())).toList(); + assertNotEmpty( + enrollments, + () -> String.format("Expected enrollment \"nxP7UnKhomJ\" instead got %s", enrollments)); + JsonEnrollment jsonEnrollment = enrollments.get(0); + assertDefaultEnrollmentResponse(enrollment, jsonEnrollment); JsonEvent jsonEvent = assertDefaultEventResponse(jsonEnrollment, event); - assertHasNoMember(jsonEvent, "relationships"); } @@ -946,39 +986,24 @@ void getAttributeValuesImageByProgramAttribute() throws ConflictException { assertEquals("file content", response.content("image/png")); } - private Event eventWithDataValue(Enrollment enrollment) { - Event event = new Event(enrollment, programStage, enrollment.getOrganisationUnit(), coc); - event.setAutoFields(); - event.setOccurredDate(DateUtils.parseDate(EVENT_OCCURRED_AT)); - - dataElement = createDataElement('A'); - dataElement.setValueType(ValueType.TEXT); - manager.save(dataElement); - - EventDataValue eventDataValue = new EventDataValue(); - eventDataValue.setValue("value"); - eventDataValue.setDataElement(dataElement.getUid()); - eventDataValue.setCreatedByUserInfo(UserInfoSnapshot.from(user)); - eventDataValue.setLastUpdatedByUserInfo(UserInfoSnapshot.from(user)); - Set eventDataValues = Set.of(eventDataValue); - event.setEventDataValues(eventDataValues); - - manager.save(event); - return event; - } - private JsonEnrollment assertDefaultEnrollmentResponse( JsonList enrollments, Enrollment enrollment) { assertFalse(enrollments.isEmpty()); JsonEnrollment jsonEnrollment = enrollments.get(0); - assertHasMember(jsonEnrollment, "enrollment"); + assertDefaultEnrollmentResponse(enrollment, jsonEnrollment); + return jsonEnrollment; + } + + private static void assertDefaultEnrollmentResponse( + Enrollment enrollment, JsonEnrollment jsonEnrollment) { + assertHasMember(jsonEnrollment, "enrollment"); assertEquals(enrollment.getUid(), jsonEnrollment.getEnrollment()); assertEquals(enrollment.getTrackedEntity().getUid(), jsonEnrollment.getTrackedEntity()); - assertEquals(program.getUid(), jsonEnrollment.getProgram()); - assertEquals("ACTIVE", jsonEnrollment.getStatus()); - assertEquals(orgUnit.getUid(), jsonEnrollment.getOrgUnit()); + assertEquals(enrollment.getProgram().getUid(), jsonEnrollment.getProgram()); + assertEquals(enrollment.getStatus().name(), jsonEnrollment.getStatus()); + assertEquals(enrollment.getOrganisationUnit().getUid(), jsonEnrollment.getOrgUnit()); assertFalse(jsonEnrollment.getBoolean("deleted").booleanValue()); assertHasMember(jsonEnrollment, "enrolledAt"); assertHasMember(jsonEnrollment, "occurredAt"); @@ -987,8 +1012,6 @@ private JsonEnrollment assertDefaultEnrollmentResponse( assertHasMember(jsonEnrollment, "updatedAt"); assertHasMember(jsonEnrollment, "notes"); assertHasMember(jsonEnrollment, "followUp"); - - return jsonEnrollment; } private JsonEvent assertDefaultEventResponse(JsonEnrollment enrollment, Event event) { @@ -1000,27 +1023,15 @@ private JsonEvent assertDefaultEventResponse(JsonEnrollment enrollment, Event ev assertEquals(event.getUid(), jsonEvent.getEvent()); assertEquals(event.getProgramStage().getUid(), jsonEvent.getProgramStage()); assertEquals(event.getEnrollment().getUid(), jsonEvent.getEnrollment()); - assertEquals(program.getUid(), jsonEvent.getProgram()); - assertEquals("ACTIVE", jsonEvent.getStatus()); - assertEquals(orgUnit.getUid(), jsonEvent.getOrgUnit()); + assertEquals(event.getProgramStage().getProgram().getUid(), jsonEvent.getProgram()); + assertEquals(event.getStatus().name(), jsonEvent.getStatus()); + assertEquals(event.getOrganisationUnit().getUid(), jsonEvent.getOrgUnit()); assertFalse(jsonEvent.getDeleted()); assertHasMember(jsonEvent, "createdAt"); assertHasMember(jsonEvent, "occurredAt"); - assertEquals(EVENT_OCCURRED_AT, jsonEvent.getString("occurredAt").string()); - assertHasMember(jsonEvent, "createdAtClient"); assertHasMember(jsonEvent, "updatedAt"); assertHasMember(jsonEvent, "notes"); assertHasMember(jsonEvent, "followUp"); - assertHasMember(jsonEvent, "followup"); - - JsonDataValue dataValue = jsonEvent.getDataValues().get(0); - - assertEquals(dataElement.getUid(), dataValue.getDataElement()); - assertEquals(event.getEventDataValues().iterator().next().getValue(), dataValue.getValue()); - assertHasMember(dataValue, "createdAt"); - assertHasMember(dataValue, "updatedAt"); - assertHasMember(dataValue, "createdBy"); - assertHasMember(dataValue, "updatedBy"); return jsonEvent; } @@ -1053,12 +1064,6 @@ private TrackedEntity trackedEntity() { return te; } - private TrackedEntity trackedEntityNotInSearchScope() { - TrackedEntity te = trackedEntity(anotherOrgUnit); - manager.save(te, false); - return te; - } - private TrackedEntity trackedEntity(TrackedEntityType trackedEntityType) { TrackedEntity te = trackedEntity(orgUnit, trackedEntityType); manager.save(te, false); @@ -1103,11 +1108,6 @@ private RelationshipType relationshipTypeAccessible( return type; } - private RelationshipType relationshipTypeNotAccessible() { - return relationshipType( - RelationshipEntity.TRACKED_ENTITY_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE); - } - private RelationshipType relationshipType(RelationshipEntity from, RelationshipEntity to) { RelationshipType type = createRelationshipType('A'); type.getFromConstraint().setRelationshipEntity(from); @@ -1125,21 +1125,6 @@ private Relationship relationship(TrackedEntity from, TrackedEntity to) { return relationship(type, fromTrackedEntity(from), toTrackedEntity(to)); } - private Relationship relationship(TrackedEntity from, Event to) { - RelationshipType type = - relationshipTypeAccessible( - RelationshipEntity.TRACKED_ENTITY_INSTANCE, RelationshipEntity.PROGRAM_STAGE_INSTANCE); - RelationshipItem fromItem = fromTrackedEntity(from); - RelationshipItem toItem = toEvent(to); - Relationship relationship = relationship(type, fromItem, toItem); - fromItem.setRelationship(relationship); - toItem.setRelationship(relationship); - to.getRelationshipItems().add(toItem); - manager.save(to, false); - manager.save(relationship, false); - return relationship; - } - private Relationship relationship( RelationshipType type, RelationshipItem fromItem, RelationshipItem toItem) { Relationship r = new Relationship(); @@ -1173,13 +1158,6 @@ private RelationshipItem toTrackedEntity(TrackedEntity to) { return toItem; } - private RelationshipItem toEvent(Event to) { - RelationshipItem toItem = new RelationshipItem(); - toItem.setEvent(to); - to.getRelationshipItems().add(toItem); - return toItem; - } - private void assertTrackedEntityWithinRelationship( TrackedEntity expected, JsonRelationshipItem json) { JsonRelationshipItem.JsonTrackedEntity jsonTe = json.getTrackedEntity(); @@ -1190,7 +1168,20 @@ private void assertTrackedEntityWithinRelationship( assertHasNoMember(json, "relationships"); // relationships are not // returned within // relationships - assertTrue(jsonTe.getArray("attributes").isEmpty()); + assertEquals( + expected.getTrackedEntityAttributeValues().isEmpty(), + jsonTe.getArray("attributes").isEmpty()); + } + + private void assertTrackedEntityWithinRelationship(Event expected, JsonRelationshipItem json) { + JsonRelationshipItem.JsonEvent jsonEvent = json.getEvent(); + assertFalse(jsonEvent.isEmpty(), "event should not be empty"); + assertEquals(expected.getUid(), jsonEvent.getEvent()); + assertHasNoMember(json, "trackedEntityType"); + assertHasNoMember(json, "orgUnit"); + assertHasNoMember(json, "relationships"); // relationships are not + // returned within + // relationships } private TrackedEntityAttribute addTrackedEntityTypeAttributeValue( @@ -1253,4 +1244,47 @@ private FileResource storeFile(String contentType, String content, char uniqueCh fr.setStorageStatus(FileResourceStorageStatus.STORED); return fr; } + + private T get(Class type, String uid) { + T t = manager.get(type, uid); + assertNotNull( + t, + () -> + String.format( + "'%s' with uid '%s' should have been created", type.getSimpleName(), uid)); + return t; + } + + public static void assertNoErrors(ImportReport report) { + assertNotNull(report); + assertEquals( + Status.OK, + report.getStatus(), + errorMessage( + "Expected import with status OK, instead got:%n", report.getValidationReport())); + } + + private static Supplier errorMessage(String errorTitle, ValidationReport report) { + return () -> { + StringBuilder msg = new StringBuilder(errorTitle); + report + .getErrors() + .forEach( + e -> { + msg.append(e.getErrorCode()); + msg.append(": "); + msg.append(e.getMessage()); + msg.append('\n'); + }); + return msg.toString(); + }; + } + + public static void assertNoErrors(ObjectBundleValidationReport report) { + assertNotNull(report); + List errors = new ArrayList<>(); + report.forEachErrorReport(err -> errors.add(err.toString())); + assertFalse( + report.hasErrorReports(), String.format("Expected no errors, instead got: %s%n", errors)); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntityFieldsParamMapperTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntityFieldsParamMapperTest.java index d1ede1162508..82f631b469c8 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntityFieldsParamMapperTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/trackedentity/TrackedEntityFieldsParamMapperTest.java @@ -129,11 +129,12 @@ void mapWithExcludedSubFields() { map("enrollments[!uid,!relationships],relationships[relationship]"); assertTrue(params.isIncludeRelationships()); + assertFalse(params.isIncludeProgramOwners()); + assertTrue(params.isIncludeEnrollments()); assertTrue(params.getEnrollmentParams().isIncludeEvents()); assertTrue(params.getEnrollmentParams().isIncludeAttributes()); assertFalse(params.getEnrollmentParams().isIncludeRelationships()); - assertFalse(params.isIncludeProgramOwners()); } @Test @@ -141,9 +142,12 @@ void mapOnlyIncludeIfFieldIsRoot() { TrackedEntityParams params = map("enrollments[events,relationships]"); assertFalse(params.isIncludeRelationships()); + assertFalse(params.isIncludeProgramOwners()); + assertTrue(params.isIncludeEnrollments()); assertTrue(params.getTeEnrollmentParams().isIncludeEvents()); - assertFalse(params.isIncludeProgramOwners()); + assertTrue(params.getTeEnrollmentParams().isIncludeRelationships()); + assertFalse(params.getTeEnrollmentParams().isIncludeAttributes()); } @Test @@ -152,9 +156,10 @@ void mapOnlyIncludeIfNotAlsoExcluded() { TrackedEntityParams params = map("relationships,!relationships"); assertFalse(params.isIncludeRelationships()); + assertFalse(params.isIncludeProgramOwners()); + assertFalse(params.isIncludeEnrollments()); assertFalse(params.getTeEnrollmentParams().isIncludeEvents()); - assertFalse(params.isIncludeProgramOwners()); params = map("!relationships,relationships"); @@ -169,9 +174,12 @@ void mapRootInclusionPrecedesSubfieldExclusion() { TrackedEntityParams params = map("enrollments,enrollments[!status]"); assertFalse(params.isIncludeRelationships()); + assertFalse(params.isIncludeProgramOwners()); + assertTrue(params.isIncludeEnrollments()); assertTrue(params.getTeEnrollmentParams().isIncludeEvents()); - assertFalse(params.isIncludeProgramOwners()); + assertTrue(params.getTeEnrollmentParams().isIncludeAttributes()); + assertTrue(params.getTeEnrollmentParams().isIncludeRelationships()); } static Stream mapEnrollmentsAndEvents() { diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipControllerTest.java index ad99714e7c4b..db48f507bc2c 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipControllerTest.java @@ -57,6 +57,8 @@ class TrackerOwnershipControllerTest extends PostgresControllerIntegrationTestBa private String pId; + private User regularUser; + @BeforeEach void setUp() { orgUnitAUid = @@ -74,11 +76,19 @@ void setUp() { OrganisationUnit orgUnitA = manager.get(OrganisationUnit.class, orgUnitAUid); OrganisationUnit orgUnitB = manager.get(OrganisationUnit.class, orgUnitBUid); - User user = - createAndAddUser(true, "user", Set.of(orgUnitA, orgUnitB), Set.of(orgUnitA, orgUnitB)); - injectSecurityContextUser(user); - - String tetId = assertStatus(HttpStatus.CREATED, POST("/trackedEntityTypes/", "{'name': 'A'}")); + regularUser = + createAndAddUser( + false, "regular-user", Set.of(orgUnitA, orgUnitB), Set.of(orgUnitA, orgUnitB)); + User superuser = + createAndAddUser(true, "superuser", Set.of(orgUnitA, orgUnitB), Set.of(orgUnitA, orgUnitB)); + injectSecurityContextUser(superuser); + + String tetId = + assertStatus( + HttpStatus.CREATED, + POST( + "/trackedEntityTypes/", + "{'name': 'A', 'sharing':{'external':false,'public':'rwrw----'}}")); teUid = CodeGenerator.generateUid(); assertStatus( @@ -107,11 +117,14 @@ void setUp() { { 'name':'P1', 'shortName':'P1', - 'programType':'WITHOUT_REGISTRATION', - 'organisationUnits': [{'id':'%s'},{'id':'%s'}] + 'programType':'WITH_REGISTRATION', + 'accessLevel':'PROTECTED', + 'trackedEntityType': {'id': '%s'}, + 'organisationUnits': [{'id':'%s'},{'id':'%s'}], + 'sharing':{'external':false,'public':'rwrw----'} } """ - .formatted(orgUnitAUid, orgUnitBUid))); + .formatted(tetId, orgUnitAUid, orgUnitBUid))); } @Test @@ -170,7 +183,8 @@ void shouldFailToUpdateWhenNoTrackedEntityOrTrackedEntityInstanceParametersArePr } @Test - void shouldOverrideOwnershipAccessWhenUsingDeprecateTrackedEntityInstanceParam() { + void shouldGrantTemporaryAccessWhenUsingDeprecateTrackedEntityInstanceParam() { + injectSecurityContextUser(regularUser); assertWebMessage( "OK", 200, @@ -184,7 +198,8 @@ void shouldOverrideOwnershipAccessWhenUsingDeprecateTrackedEntityInstanceParam() } @Test - void shouldOverrideOwnershipAccess() { + void shouldGrantTemporaryAccess() { + injectSecurityContextUser(regularUser); assertWebMessage( "OK", 200, diff --git a/dhis-2/dhis-test-web-api/src/test/resources/tracker/event_and_enrollment.json b/dhis-2/dhis-test-web-api/src/test/resources/tracker/event_and_enrollment.json index 12a75f2722c6..c43114728de9 100644 --- a/dhis-2/dhis-test-web-api/src/test/resources/tracker/event_and_enrollment.json +++ b/dhis-2/dhis-test-web-api/src/test/resources/tracker/event_and_enrollment.json @@ -42,10 +42,8 @@ "identifier": "h4w96yEMlzO" }, "inactive": true, - "deleted": false, - "potentialDuplicate": false, "createdAtClient": "2018-10-01T12:17:30.163", - "relationships": [], + "updatedAtClient": "2018-10-07T12:17:30.163", "attributes": [ { "valueType": "TEXT", @@ -71,8 +69,7 @@ }, "value": "88" } - ], - "enrollments": [] + ] }, { "trackedEntity": "dUE514NMOlo", @@ -84,11 +81,7 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, "createdAtClient": "2018-11-01T12:17:30.163", - "relationships": [], "attributes": [ { "valueType": "TEXT", @@ -122,8 +115,7 @@ }, "value": "70" } - ], - "enrollments": [] + ] }, { "trackedEntity": "mHWCacsGYYn", @@ -135,10 +127,6 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, - "relationships": [], "attributes": [ { "valueType": "TEXT", @@ -164,8 +152,7 @@ }, "value": "72" } - ], - "enrollments": [] + ] }, { "trackedEntity": "QesgJkTyTCk", @@ -177,10 +164,6 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, - "relationships": [], "attributes": [ { "valueType": "TEXT", @@ -206,8 +189,7 @@ }, "value": "89" } - ], - "enrollments": [] + ] }, { "trackedEntity": "guVNoAerxWo", @@ -219,10 +201,6 @@ "idScheme": "UID", "identifier": "tSsGrtfRzjY" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, - "relationships": [], "attributes": [ { "valueType": "TEXT", @@ -248,8 +226,7 @@ }, "value": "91" } - ], - "enrollments": [] + ] }, { "trackedEntity": "woitxQbWYNq", @@ -261,10 +238,6 @@ "idScheme": "UID", "identifier": "RojfDTBhoGC" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, - "relationships": [], "attributes": [ { "valueType": "TEXT", @@ -290,8 +263,7 @@ }, "value": "90" } - ], - "enrollments": [] + ] }, { "trackedEntity": "XUitxQbWYNq", @@ -302,12 +274,18 @@ "orgUnit": { "idScheme": "UID", "identifier": "DiszpKrYNg8" + } + }, + { + "trackedEntity": "H8732208127", + "trackedEntityType": { + "idScheme": "UID", + "identifier": "ja8NY4PW7Xm" }, - "inactive": false, - "deleted": false, - "potentialDuplicate": false, - "relationships": [], - "enrollments": [] + "orgUnit": { + "idScheme": "UID", + "identifier": "DiszpKrYNg8" + } } ], "enrollments": [ @@ -324,16 +302,9 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "orgUnitName": "Mbokie CHP", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "nxP8UnKhomJ", @@ -350,13 +321,7 @@ }, "enrolledAt": "2021-04-28T12:05:00.000", "occurredAt": "2021-04-28T12:05:00.000", - "scheduledAt": "2021-04-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-04-28T12:05:00.000" }, { "enrollment": "TvctPPhpD8z", @@ -371,16 +336,24 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "orgUnitName": "Mbokie CHP", "enrolledAt": "2021-03-28T12:05:00.000", "occurredAt": "2021-03-28T12:05:00.000", "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "attributes": [ + { + "attribute": { + "idScheme": "UID", + "identifier": "dIVt4l5vIOa" + }, + "value": "Frank PTEA" + } + ], + "notes": [ + { + "note": "f9423652692", + "value": "enrollment comment value" + } + ] }, { "enrollment": "JuioKiICQqI", @@ -395,16 +368,9 @@ "idScheme": "UID", "identifier": "uoNW0E3xXUy" }, - "orgUnitName": "test-orgunit-2", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "iHFHfPKTSYP", @@ -419,16 +385,9 @@ "idScheme": "UID", "identifier": "uoNW0E3xXUy" }, - "orgUnitName": "test-orgunit-2", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "ipBifypAQTo", @@ -443,16 +402,9 @@ "idScheme": "UID", "identifier": "tSsGrtfRzjY" }, - "orgUnitName": "test-orgunit-3", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "qxOSXoEZkOA", @@ -467,16 +419,9 @@ "idScheme": "UID", "identifier": "RojfDTBhoGC" }, - "orgUnitName": "test-orgunit-3", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "HDWTYSYkICe", @@ -491,16 +436,9 @@ "idScheme": "UID", "identifier": "DiszpKrYNg8" }, - "orgUnitName": "test-orgunit-3", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "FXWSSZunTLk", @@ -515,16 +453,9 @@ "idScheme": "UID", "identifier": "lbDXJBlvtZe" }, - "orgUnitName": "test-orgunit-3", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" }, { "enrollment": "GYWSSZunTLk", @@ -539,16 +470,9 @@ "idScheme": "UID", "identifier": "DiszpKrYNg8" }, - "orgUnitName": "test-orgunit-3", "enrolledAt": "2021-02-28T12:05:00.000", "occurredAt": "2021-02-28T12:05:00.000", - "scheduledAt": "2021-02-28T12:05:00.000", - "followUp": false, - "deleted": false, - "events": [], - "relationships": [], - "attributes": [], - "notes": [] + "scheduledAt": "2021-02-28T12:05:00.000" } ], "events": [ @@ -568,14 +492,12 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "relationships": [], "occurredAt": "2019-01-25T12:10:38.100", "scheduledAt": "2019-01-28T12:32:38.100", "createdAtClient": "2020-01-25T12:10:38.100", "updatedAtClient": "2020-01-26T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" @@ -595,8 +517,7 @@ }, "value": "value00001", "createdAt": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -605,8 +526,7 @@ }, "value": "option1", "createdAt": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -615,8 +535,7 @@ }, "value": "88", "createdAt": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null } ], "notes": [ @@ -648,14 +567,12 @@ "idScheme": "UID", "identifier": "h4w96yEMlzO" }, - "relationships": [], "occurredAt": "2020-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "createdAtClient": "2019-01-25T12:10:38.100", "updatedAtClient": "2021-01-26T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" @@ -674,8 +591,7 @@ }, "value": "value00002", "created": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -684,8 +600,7 @@ }, "value": "value00002", "created": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -694,8 +609,7 @@ }, "value": "option2", "created": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -704,8 +618,7 @@ }, "value": "70", "created": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null }, { "dataElement": { @@ -714,11 +627,9 @@ }, "value": "70", "created": "2021-07-01T12:05:00", - "storedBy": null, - "providedElsewhere": false + "storedBy": null } ], - "notes": [], "assignedUser": { "uid": "xE7jOejl9FI", "firstName": "John", @@ -747,7 +658,6 @@ "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" @@ -757,8 +667,7 @@ "idScheme": "UID", "identifier": "xYerKDKCefk" } - ], - "notes": [] + ] }, { "event": "JaRDIvcEcEx", @@ -781,7 +690,6 @@ "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" @@ -791,8 +699,7 @@ "idScheme": "UID", "identifier": "xYerKDKCefk" } - ], - "notes": [] + ] }, { "event": "QRYjLTiJTrA", @@ -827,7 +734,6 @@ "identifier": "DiszpKrYNg8" }, "status": "ACTIVE", - "deleted": false, "dataValues": [ { "created": "2022-04-22T06:00:38.339", @@ -835,20 +741,16 @@ "idScheme": "UID", "identifier": "GieVkTxp4HH" }, - "value": "15", - "providedElsewhere": false + "value": "15" }, { "dataElement": { "idScheme": "UID", "identifier": "GieVkTxp4HG" }, - "value": "1.5", - "providedElsewhere": false + "value": "1.5" } - ], - "notes": [], - "relationships": [] + ] }, { "event": "kWjSezkXHVp", @@ -883,7 +785,6 @@ "identifier": "DiszpKrYNg8" }, "status": "ACTIVE", - "deleted": false, "dataValues": [ { "created": "2022-04-22T06:00:34.319", @@ -891,12 +792,9 @@ "idScheme": "UID", "identifier": "GieVkTxp4HH" }, - "value": "14", - "providedElsewhere": false + "value": "14" } - ], - "notes": [], - "relationships": [] + ] }, { "event": "OTmjvJDn0Fu", @@ -931,7 +829,6 @@ "identifier": "DiszpKrYNg8" }, "status": "ACTIVE", - "deleted": false, "dataValues": [ { "created": "2022-04-22T06:00:30.559", @@ -939,12 +836,9 @@ "idScheme": "UID", "identifier": "GieVkTxp4HH" }, - "value": "13", - "providedElsewhere": false + "value": "13" } - ], - "notes": [], - "relationships": [] + ] }, { "event": "ck7DzdxqLqA", @@ -979,7 +873,6 @@ "identifier": "DiszpKrYNg8" }, "status": "ACTIVE", - "deleted": false, "dataValues": [ { "created": "2022-04-22T06:00:14.224", @@ -987,12 +880,9 @@ "idScheme": "UID", "identifier": "GieVkTxp4HH" }, - "value": "12", - "providedElsewhere": false + "value": "12" } - ], - "notes": [], - "relationships": [] + ] }, { "event": "lumVtWwwy0O", @@ -1078,17 +968,14 @@ "idScheme": "UID", "identifier": "tSsGrtfRzjY" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" - }, - "notes": [] + } }, { "event": "SbUJzkxKYAG", @@ -1106,12 +993,10 @@ "idScheme": "UID", "identifier": "RojfDTBhoGC" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T11:10:38.100", "storedBy": "tracker", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" @@ -1133,17 +1018,14 @@ "idScheme": "UID", "identifier": "DiszpKrYNg8" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" - }, - "notes": [] + } }, { "event": "YKmfzHdjUDL", @@ -1161,17 +1043,14 @@ "idScheme": "UID", "identifier": "lbDXJBlvtZe" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" - }, - "notes": [] + } }, { "event": "G9PbzJY8bJG", @@ -1188,17 +1067,14 @@ "idScheme": "UID", "identifier": "g4w96yEMlzO" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" - }, - "notes": [] + } }, { "event": "H0PbzJY8bJG", @@ -1216,17 +1092,14 @@ "idScheme": "UID", "identifier": "DiszpKrYNg8" }, - "relationships": [], "occurredAt": "2019-01-28T00:00:00.000", "scheduledAt": "2019-01-28T12:10:38.100", "storedBy": "admin", "followUp": true, - "deleted": false, "attributeOptionCombo": { "idScheme": "UID", "identifier": "HllvX50cXC0" - }, - "notes": [] + } } ], "relationships": [ @@ -1237,8 +1110,6 @@ "identifier": "TV9oB9LT3sh" }, "createdAtClient": "2018-10-01T12:17:30.163", - "bidirectional": false, - "deleted": false, "from": { "trackedEntity": "QS6w44flWAf" }, @@ -1253,14 +1124,51 @@ "identifier": "TV9oB9LT3sh" }, "createdAtClient": "2018-11-01T13:24:37.118", - "bidirectional": false, - "deleted": false, "from": { "trackedEntity": "dUE514NMOlo" }, "to": { "event": "pTzf9KYMk72" } + }, + { + "relationship": "x8919212736", + "relationshipType": { + "idScheme": "UID", + "identifier": "TV9oB9LT3sh" + }, + "from": { + "trackedEntity": "mHWCacsGYYn" + }, + "to": { + "event": "QRYjLTiJTrA" + } + }, + { + "relationship": "N8800829a58", + "relationshipType": { + "idScheme": "UID", + "identifier": "m1575931405" + }, + "from": { + "trackedEntity": "H8732208127" + }, + "to": { + "trackedEntity": "QesgJkTyTCk" + } + }, + { + "relationship": "p53a6314631", + "relationshipType": { + "idScheme": "UID", + "identifier": "xLmPUYJX8Ks" + }, + "from": { + "trackedEntity": "dUE514NMOlo" + }, + "to": { + "enrollment": "nxP7UnKhomJ" + } } ], "username": "system-process" diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipController.java index 1a4d3d5fdd2d..8fea04d2b93a 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/ownership/TrackerOwnershipController.java @@ -43,6 +43,7 @@ import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramService; import org.hisp.dhis.tracker.acl.TrackerOwnershipManager; +import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityParams; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityService; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; @@ -97,7 +98,8 @@ public WebMessage updateTrackerProgramOwner( "trackedEntityInstance", trackedEntityInstance, "trackedEntity", trackedEntity); trackerOwnershipAccessManager.transferOwnership( - trackedEntityService.getTrackedEntity(trackedEntityUid), + trackedEntityService.getTrackedEntity( + trackedEntityUid, UID.of(program), TrackedEntityParams.FALSE), programService.getProgram(program), organisationUnitService.getOrganisationUnit(ou)); return ok("Ownership transferred"); @@ -105,7 +107,7 @@ public WebMessage updateTrackerProgramOwner( @PostMapping(value = "/override", produces = APPLICATION_JSON_VALUE) @ResponseBody - public WebMessage overrideOwnershipAccess( + public WebMessage grantTemporaryAccess( @Deprecated(since = "2.41") @RequestParam(required = false) UID trackedEntityInstance, @RequestParam(required = false) UID trackedEntity, @RequestParam String reason, diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/NonRedisSessionConfig.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/NonRedisSessionConfig.java index da0883deefaa..285f00473516 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/NonRedisSessionConfig.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/NonRedisSessionConfig.java @@ -28,13 +28,19 @@ package org.hisp.dhis.webapi.security.session; import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpSession; import org.hisp.dhis.condition.RedisDisabledCondition; +import org.hisp.dhis.external.conf.ConfigurationKey; +import org.hisp.dhis.external.conf.DhisConfigurationProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.web.session.HttpSessionCreatedEvent; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.web.filter.CharacterEncodingFilter; @@ -53,6 +59,8 @@ @Conditional(RedisDisabledCondition.class) public class NonRedisSessionConfig { + @Autowired private DhisConfigurationProvider config; + @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); @@ -67,4 +75,11 @@ public HttpSessionEventPublisher httpSessionEventPublisher() { public Filter springSessionRepositoryFilter() { return new CharacterEncodingFilter(); } + + @EventListener + public void sessionCreated(HttpSessionCreatedEvent event) { + HttpSession session = event.getSession(); + int sessionTimeout = config.getIntProperty(ConfigurationKey.SYSTEM_SESSION_TIMEOUT); + session.setMaxInactiveInterval(sessionTimeout); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/RedisSpringSessionConfig.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/RedisSpringSessionConfig.java index 8d32db09ae53..8f5def3919ac 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/RedisSpringSessionConfig.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/session/RedisSpringSessionConfig.java @@ -28,6 +28,8 @@ package org.hisp.dhis.webapi.security.session; import org.hisp.dhis.condition.RedisEnabledCondition; +import org.hisp.dhis.external.conf.ConfigurationKey; +import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -56,6 +58,7 @@ @Conditional(RedisEnabledCondition.class) @EnableRedisHttpSession public class RedisSpringSessionConfig { + @Autowired private DhisConfigurationProvider config; @Bean public RedisIndexedSessionRepository sessionRepository( @@ -69,6 +72,9 @@ public RedisIndexedSessionRepository sessionRepository( redisTemplate.afterPropertiesSet(); RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(redisTemplate); repository.setDefaultSerializer(new JdkSerializationRedisSerializer()); + + int sessionTimeout = config.getIntProperty(ConfigurationKey.SYSTEM_SESSION_TIMEOUT); + repository.setDefaultMaxInactiveInterval(sessionTimeout); return repository; } diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 58fcf3e4f786..d8d6da3b32f5 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -85,7 +85,7 @@ - 3.2.2 + 3.3.0 0.6.1 @@ -115,7 +115,7 @@ 6.5.2.RELEASE - 11.1.1 + 11.2.0 5.6.15.Final 3.10.8 4.0.5 @@ -169,7 +169,7 @@ 2.38.0 - 4.1.116.Final + 4.1.117.Final 4.8.179 @@ -1985,7 +1985,7 @@ com.github.spotbugs spotbugs - 4.8.6 + 4.9.0